# 更新日志 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/), 版本号参考 [语义化版本](https://semver.org/lang/zh-CN/)。 ## [Unreleased] ### Added - 仓库骨架:`src/mcm_toolkit/` 7 个包(core / data / stats / models / solve / decision / viz) - 顶层 `__init__.py` 只导出 7 个子包 + 8 个快捷函数,`__all__` 即公开 API 清单 - `core.Result`:唯一的结果对象(替代原建议里的 5 个 Result 类) - `docs/01-suggestion-review.md`:对 ChatGPT 建库建议的审查与精简(14 包 → 7 包,~250 函数 → ~40) - `docs/02-architecture.md`:包边界、`Result` / RNG / `fit-predict-score` 协议、依赖分层 - `docs/03-roadmap.md`:L0/L1/L2 分级 + **永久不做清单** - `docs/04-environment.md`:conda / venv 配置与三个已踩的坑 - `docs/dev/function-card-template.md`:S 档算法的函数卡片模板 - `.venv`(基于 Anaconda base,`--system-site-packages`)+ 可编辑安装 —— 10.5 MB,离线可用 - conda 环境 `mcm-toolkit`(`D:\anaconda\envs\mcm-toolkit`)—— Python 3.13.15,含 SALib / pymoo / pyright - `scripts/setup-conda.ps1` / `scripts/setup-venv.ps1` / `scripts/doctor.ps1` - `environment.yml`:需联网时的精确复现方案 - `pyproject.toml`:核心依赖只 4 个,其余走 extras - `docs/02-architecture.md` §4.7「类型约定」:注解要写全、numpy 标量的判断写法、 非法类型必须抛 `TypeError`。配套在 `docs/03-roadmap.md` 的开发纪律里加了指针。 - `docs/dev/personal-pitfalls.md`:个人易错点清单,**只追加、不通读** - `data.clean`:四个函数落地(**由用户本人实现**)——`clean_missing`(L0 第 4 项)、 `impute_missing` / `detect_outliers` / `winsorize`(原属 L1,顺手写了)。 - `tests/test_data.py`:新增 13 条 clean 行为测试,覆盖 `clean_missing` 的阈值语义与轴向、 `impute_missing` 的 mean/ffill/mode 与全 NaN 列、`winsorize` 的分位点与异常类型。 - `tests/test_data.py`:另加 7 条 `xfail`,把**已知但未修的契约**钉在测试报告里(见 Notes)。 - `docs/algorithms/detect_outliers.md`:**第一张 S 档函数卡片**(roadmap §3.1 要求 约 15 张,这是第 1 张)。除模板九节外,额外记了三条容易踩的判断:为什么不返回 `Result`、 非数值列恒 `False` 的两条备选路各错在哪、以及 `ddof` 口径与 `stats.zscore` 不一致。 - `examples/01_clean_and_describe.py`:`load_table` / `save_table` / `clean_missing` / `impute_missing` / `detect_outliers` / `winsorize` 六个函数的最小用法,实测跑通(exit 0); 末尾留 `TODO(L0)` 等 `stats.describe`。 - `tests/test_data.py`:`data` 的 transform 段 **18 条用例** —— `normalize` 的三种 method、 `feature_range` 区间与两条校验、常量列不出 nan、docstring 那个 Examples 的精确值、按列语义; `standardize` 的按列与 `ddof`(用解析关系钉住:`ddof=1` 时结果的 std = `sqrt((n-1)/n)`); `log_transform` 的定义域平移;`one_hot_encode` 的自动识别、`drop_first`、列不存在报错。 其中两条是把原先挂着的 `skip` 转为实跑(见坑 9 / 个人易错点 9)。 - `tests/test_stats.py`:新写 **20 条**用例 —— `describe` 的方向约定(行 = 指标、列 = 变量)、 `missing` / `missing_ratio` / `skewness` / `kurtosis` 四行**不能是 NaN**、非数值列自动跳过、 接受 ndarray / list / Series、加权均值与加权标准差对照手算公式、5 种非法 `weights`; `zscore` 的手算对照 / 按列 / 常量列 / 非法类型;`coefficient_of_variation` 的手算对照、 单列 DataFrame、均值为 0 抛 `ValueError`、空数组与二维输入。 - `examples/01_clean_and_describe.py`:填掉末尾的 `TODO(L0)`,加一段 `describe` 输出(实测 exit 0)。 ### Changed - `core.get_rng` / `core.set_seed`:实现(L0 第 2 项)。`get_rng` 收 `int` / `np.integer` / `Generator` / `None`,其余类型抛 `TypeError`;无参调用复用同一个会推进的共享生成器。 `set_seed` 同时覆盖 numpy 旧全局状态,让 sklearn / statsmodels 也可复现。 - `core.Result`:补上 `__repr__` 与 `summary()` 实现(L0 第 1 项落地)。 `repr` 紧凑单行且不打印 `data` 内容;`summary()` 输出可直接贴进论文的 Markdown 表格。 - `data.load_table`:实现(L0 第 3 项)。按扩展名分派 `.csv` / `.xlsx` / `.xls`; `.csv` 默认做编码探测(utf-8 → utf-8-sig → gbk → gb18030),显式传 `encoding=` 时不再探测; `na_values` 默认吃下空串 / `NA` / `N/A` / `#N/A` / `null` / `-`。 - `data.save_table`:实现(原属 L1,顺手落地)。按扩展名分派,父目录不存在时自动创建, 返回实际写入的 `Path`。 - `data.load_table` / `data.save_table`:docstring 补 `Parameters` / `Raises` (`docs/02-architecture.md` §4.7 对每个公开函数的硬要求)。 - `data.load_table` 的返回注解 `Any` → `pd.DataFrame`,与 docstring 的承诺对齐。 - `data.clean`:docstring 按 `docs/02-architecture.md` §4.7 补全 Parameters / Returns / Raises (Codex 代写,与 io.py 同一套 numpydoc 格式)。 - `data.clean`:ruff 从 6 条清到 1 条——导入分组(I001)、elif 链改 if 加末尾无条件 return (RET505 / RET503)、去掉多余 f 前缀(F541)、删掉只会被覆盖的 mask 初始化(死代码)、 去掉行尾空白(W293)。 - `pyright` 报"无法解析导入 numpy / pandas"(17 errors)的原因:分析器会去 PATH 上找 python, 本机 PATH 上只有 Codex 运行时的 3.14 shim(无科学栈),且 pyright 不认 `pyvenv.cfg` 的 `include-system-site-packages`。 改为 `[tool.pyright]` 指定 `venvPath = "."` / `venv = ".venv"` / `pythonVersion = "3.13"`,并让 `scripts/setup-venv.ps1` 往 `.venv` 里写一个 指向 base site-packages 的 `.pth`。pyright 17 errors → **0**。 详见 `docs/04-environment.md` 坑 7。 - `tests/test_data.py`:`df["b"].isna().all()` 的类型是 `Series | bool`, 直接当断言条件 pyright 会报"条件操作数无效",外面套 `bool()`; 两条故意传错类型的用例加 `# type: ignore[arg-type]`。 - `data.save_table` 的后缀校验排在 `path.parent.mkdir(...)` **之后**,传 `.txt` 会先留下一个 空目录再抛 `ValueError`。改为**先校验、后副作用**,并加回归测试。 - `data/io.py` 里 `__all__` 与下一个 `def` 之间只有 1 个空行(PEP 8 要 2 个)。 - `tests/test_data.py` 顶上的**模块级** `pytestmark = pytest.mark.skip(...)` 会连带跳过同文件里 已经写好的函数,导致 `load_table` 零覆盖。改为逐条 skip 未实现的算法,并补 8 条 io 测试 (编码探测、NA 归一、后缀校验、缺文件报错、往返读写、类型校验)。测试数 31 → **39 passed**。 - conda base 里 zstandard 的 `*.conda_trash` 残骸目录被 Python 当成 namespace package 导入, 使 `.venv`(= base)的 `pd.read_csv` 抛 `ImportError: Can't determine version for zstandard`。 已把残骸目录改名禁用(**可逆**,未删除),详见 `docs/04-environment.md` 坑 6。 - `data.clean` 第二轮审查(2026-10-02),Codex 直接修掉 4 处**轻度**问题: `winsorize(Series)` 因 `select_dtypes` 在 `Series` 上不存在而直接崩(Series 分支提到最前, 并加数值 dtype 守卫);`winsorize` 的 `data.quantile()` 改为只在数值列上算 (文本列不再崩,与已有的 `numeric_cols` 判断对齐);删掉 `detect_outliers` 里已成空操作的 `df = pd.DataFrame(data)`(数据在更早处就转过了,顺手去掉 `df` 这个别名); 清掉行尾空白(W293)、函数体内的连续空行,并把 `clean_missing` 的嵌套 `if` 合并 (SIM102,ruff 最后 1 条)。ruff `1 error` → **`All checks passed`**,pyright 仍 0。 - `tests/test_data.py`:5 条 `xfail` 经 XPASS 信号确认已修好,**摘掉 mark 转正式用例** (`detect_outliers` 的 4 条 + `winsorize` 混合类型 1 条)。测试 **53 passed / 7 xfailed → 58 passed / 2 xfailed**。 - `CHANGELOG.md`:[Unreleased] 里有一句被重复追加撑坏 ("…删掉它即全绿。:分析器会去 PATH 上找 python,"),已修。 - `data.detect_outliers` 收尾(2026-10-02,**由用户本人实现**):统计量(`quantile` / `mean` / `std`)与掩码写入**都**收窄到数值列,含文本列的赛题表不再崩; 非数值列在掩码里恒 `False`(用户本人定的语义,见决策 32)。docstring `Notes` 补上这条语义。 - `data.detect_outliers` 里 `data=data.copy()` 是死代码:下面的 `to_frame()` / `pd.DataFrame()` 本来就返回新对象,函数全程也没原地改过 `data`,白白复制一份数据;而且它排在类型分派 **之前**,会把错误入参的报错从 `ValueError` 变成 `AttributeError`。已删。 - `data.detect_outliers`:删掉那行死代码后 pyright 反而报了 1 个真错 —— `df[Index]` 被类型存根当成 `Series`,`.std()` 于是被判成标量、`std.replace(...)` 非法。 在取列子表处用 `cast(pd.DataFrame, ...)` 按运行时真值收窄。详见 `docs/04-environment.md` 坑 8。 - `tests/test_data.py`:`test_detect_outliers_survives_text_columns` 摘掉 xfail 并加强断言 (文本列必须恒 `False`);新增 `test_detect_outliers_all_text_frame_returns_all_false` 钉住"整表没有数值列"这条边界。测试 **58 passed / 2 xfailed → 60 passed / 1 xfailed**。 - `docs/dev/personal-pitfalls.md`:追加第 8 条(限制掩码 ≠ 限制统计量)。 - `data.impute_missing` 的 mode 分支改为**返回新表**(原先 `data[col] = ...` 会就地改调用方的 DataFrame,另外四支都返回新对象)。最后一条 `xfail` 随之转正,**测试里 `xfail` 清零**。 - `data.detect_outliers` 补**入参类型校验**(§4.7:非法类型抛 `TypeError`,不静默兜底): 接受 `DataFrame` / `Series` / `list` / `tuple` / `ndarray`。`tuple` 同时补进 `is_ld_array`, 与 `list` 同等对待;`dict` 不再被接受(以前靠 `pd.DataFrame()` 碰巧能过,见决策 33)。 - `tests/test_data.py`:摘掉最后一条 `xfail`(mode 就地改)并加强断言;新增 3 条 (非法类型抛 `TypeError`、元组按一维数组型处理、全 NaN 列与单元素列不抛异常)。 测试 **60 passed / 1 xfailed → 64 passed / 0 xfailed**。 - `data.transform` 四个函数实现(**由用户本人写**):`normalize`(minmax / vector / max,均按列)、 `standardize`(按列,`ddof` 真的传下去)、`log_transform`(`offset=None` 时自动取 `1 - min(x)`)、 `one_hot_encode`。`data/` 下的 L0 至此结清。 - `data.transform`:Codex 清掉 5 条 ruff —— RET503(`normalize` 末尾缺无条件 `return`)、 F541(无占位符的 f-string)、RET504(多余的 `result` 中转)、RET505(`return` 后的 `elif`)、 SIM108(`log_transform` 的 if/else 改三元)。 - `data.transform.normalize`:`supported_method` 与 `feature_range` 的校验提到 `np.asarray` **之前** (§4.7 的"守卫集中在开头");并修掉说反的报错文案 —— 条件是要求下界 < 上界, 原文却写"上界必须小于下界",且 `TypeError` 那条用了无占位符 f-string。 - `data.transform.one_hot_encode`:自动识别类别列时显式加上 `'str'` / `'string'`。 原来只写 `['object', 'category']`,在 pandas 3 上是**已弃用路径**(每次调用发 `Pandas4Warning`), 未来会变成一列都选不中的**静默错误**。详见坑 9。 - 测试计数:**64 passed → 82 passed**(新增 18 条,另两条 skip 转为实跑),skip 42 → 40。 - `data.transform`:修掉上一版引入的 **3 处真 bug**(**Codex 代修,用户授权"报错顺手修掉"**)。 症状是 7 条测试变红 + pyright 在 `transform.py:105` / `:133` 报 `reportArgumentType`。 **这不是编辑器误报** —— pyright CLI 与 `pytest` 都能复现,所以**一行 `# type: ignore` 都没加**: - 输入白名单漏了 `pd.DataFrame`:`standardize(df)` / `log_transform(df)` 抛 `TypeError`, 函数体里的 `is_df` 分支全是**死代码**。白名单统一成模块级 `_NUMERIC_TYPES` 并移到 `np.asarray` 之前。 - `normalize` 的三个 method 分支**互相覆盖**:`minmax` / `vector` 算完的结果被末尾无条件的 `x / max` 冲掉,三种 method 实际都只走 max(`test_normalize_*` 5 条因此变红)。改成 `if / elif / else`。 - `one_hot_encode` 把 `get_dummies` 的结果按 `data.columns` 重包一层 `DataFrame`: 哑变量列名(`city_A`)与原列名(`city`)对不上 → 编码列被**整列丢掉**。改为直接返回 `get_dummies` 结果。 - `log_transform` 的 `offset` 守卫条件**写反**(`offset > 0` 就抛错,恰好把唯一合法用例拒掉), 改为校验定义域前提 `min(x) + offset > 0`;`offset=0` 对全正数据合法(见决策 35)。 - `data.transform` 四个函数的 docstring 按 `docs/02-architecture.md` §4.7 补齐 `Parameters` / `Returns` / `Raises`(Codex 代写,与 `io.py` / `clean.py` 同一套 numpydoc 格式), 并把常量列行为(决策 34)写进 `Notes`。 - `data.transform`:口径定稿后 ruff 剩的 4 条 RET505(`return` 后的 `elif`)一并清掉 —— `All checks passed`;pyright **2 errors → 0 errors**;测试 **7 failed / 75 passed → 82 passed / 40 skipped**。 - `stats.describe`:实现落地(**由用户本人写**,Codex 审查 + 代修),并修掉一个**静默 bug** —— `df_num.describe().T` 把表转反了(行 = 变量、列 = 指标),而 docstring 与 `test_describe_reports_expected_keys` 定的是"行 = 指标、列 = 变量"。 转置后 `.loc[指标名] = <按变量索引的 Series>` 与列名**零交集**,pandas 不报错, 直接把这四行(`missing` / `missing_ratio` / `skewness` / `kurtosis`)**整行填成 NaN**; `weights` 分支则报 `cannot set a row with mismatched columns`。去掉 `.T` 一处修好三件事。 另补 `median` 行(pandas 只有 `50%`,测试契约定的是 `median`)、入参放宽到 `list` / `tuple` / `ndarray`(`00_quickstart.py` 传的就是 ndarray)、 `weights` 校验补齐(维度 / 长度 / NaN / 负值 / 和为 0,原先和为 0 时裸抛 `ZeroDivisionError`)、 docstring 按 §4.7 补 `Parameters` / `Returns` / `Raises`。 - `stats.zscore`:修 RET505;docstring 按 §4.7 补齐;DataFrame 取列处按坑 8 用 `cast` 收窄 (pyright 的 `DataFrame | Series` 重载)。 - `stats.coefficient_of_variation`:均值为 0 由 `TypeError` 改为 `ValueError` (那是**取值**问题不是类型问题,§4.7);空数组提前抛 `ValueError`(原先静默返回 nan + RuntimeWarning); 报错文案里"异变系数"的错别字与 "N 维数组" 改为 "N 列"。 - `stats.coefficient_of_variation`:分母由 `mean` 改为 `|mean|`(**用户本人改的**,决策 38)—— 均值为负时 `σ/μ` 会给出负值,容易被读成"波动更小";现在 CV 恒非负。docstring 的 `Notes` 同步, 并加一条测试钉住"均值为负时 CV 仍为正"。 - `tests/test_stats.py`:去掉**模块级** `pytestmark = pytest.mark.skip` —— 它把该文件 6 条测试 **全部**跳过(包括已经实现好的 `describe`),正是决策 26 要禁的写法:实现零覆盖而测试全绿。 改为逐条 skip 未实现的 5 条。 - 测试计数:**82 passed / 40 skipped → 103 passed / 39 skipped**(新增 21 条); ruff 7 error → **All checks passed**;pyright **1 error → 0**。 - `stats.correlation_matrix`:实现落地(**由用户本人写**,Codex 审查 + 代修),修掉一个**硬 bug** —— `if numeric_cols == 0:` 拿 `Index` 和标量比,得到的是布尔**数组**:0 列时静默不报错、 1 列时碰巧能跑、**≥2 列直接 `ValueError: The truth value of an array ... is ambiguous`** —— 而"≥2 个数值列"正是最常见的情况,等于整个函数不可用。改成 `len(numeric_cols) == 0`, 并加回归用例(见易错点 13)。 - `stats.correlation_matrix`:pyright 的两处报错**都不是运行时问题,也都没挂 `# type: ignore`** —— `data[numeric_cols]` 是坑 8 的 `DataFrame | Series` 重载(`cast` 收窄); `_, p = stats.pearsonr(...)` 解包后会退化成 `object`(scipy 的返回值是 `_make_tuple_bunch` 在运行时拼的类,存根里看不到 `pvalue`)。新增 `_pvalue(result: Any) -> float` 小助手统一取 p 值, 后面 `t_test` / `anova` / `chi_square_test` 可复用(见 `docs/04-environment.md` 坑 10、决策 40)。 - `stats.correlation_matrix`:`method` 校验提到最前(守卫集中开头);`add_stars` 的 `elif` 链改 `if` (RET505);清掉 3 条 F541 与 1 条 I001(import 未排序)—— ruff 归零。 - `tests/test_stats.py`:`test_correlation_matrix_diagonal_is_one` 由 `skip` 改成 `xfail(strict=False)` (决策 30)—— 契约要**数值**矩阵,实现返回带星标的字符串表,口径待拍板; 另新增 2 条(多列数值表能跑通、非法输入报错)。 - 测试计数:**103 passed / 39 skipped → 105 passed / 38 skipped / 1 xfail**。 ### Notes - `detect_outliers` 原有的三个 bug 加一处设计空白**已全部收口**:`threshold` 范围校验挪进 `clean_missing`、`is_series` / `is_ld_array` 提到类型转换**之前**、统计量与掩码都限制在数值列、 非数值列恒 `False`(决策 32)、入参类型校验补上(决策 33)。 S 档卡片与 `examples/` 用法也已落地 —— `detect_outliers` 这条线**结清**。 - `transform.py` 的 **4 个口径已全部收口**:① 三个函数共用 `_NUMERIC_TYPES` 白名单(含 `DataFrame`); ② 全部接受 `DataFrame` 且保持"DataFrame 进 DataFrame 出";③ 显式 `offset` 按定义域校验 (`min(x) + offset > 0`,见决策 35,**待用户确认**);④ 常量列行为写进 docstring 的 `Notes`; 另外 `Parameters` / `Returns` / `Raises` 已按 §4.7 补齐。 - 新增易错点第 10 条:**把真报错当成"编辑器误报"** —— 红线先跑 CLI 与 `pytest` 再判断(`docs/dev/personal-pitfalls.md`)。 - `stats` 这条线开工了:`describe` 落地,填掉 `examples/01_clean_and_describe.py` 的 `TODO(L0)`; `correlation_matrix` 功能上也能跑了(硬 bug 已修)。 - `correlation_matrix` 的**返回契约待拍板**:带星标的字符串表(好贴论文)vs 数值矩阵 (能做后续计算)。另有两处一并待定:**对角线不该带 `***`**;`df_num` 含 NaN 时 `corr` 成对删失而 `pearsonr` 不删,r 与 p 会对不上。A/B/C 三个选项见 `STATUS.md`「下一步」第 1 条。 - 三处口径已拍板(2026-10-03):`log_transform` 的 `offset` 维持现状(决策 35); `describe` / `zscore` **默认丢掉**非数值列、与 `detect_outliers` 的"保留列"刻意不统一(决策 37); 变异系数改用 `σ/|μ|`(决策 38)。另立决策 39 明确分工:语义口径用户拍板、工程事务 Codex 顺手补。 - 已实现函数的 `examples/` 最小用法仍不全:`00_quickstart.py` 要等 `bootstrap_ci` / `viz`, `Result` 的用法届时并进去。 - 已知缺口: - 除 `core.Result`、`core.get_rng`、`core.set_seed`、`data.load_table`、`data.save_table` 外,其余函数体**尚未实现**(统一 `raise NotImplementedError` + `TODO(L0/L1/v0.2)` 标记)。 这是刻意的:先定接口,再按 `docs/03-roadmap.md` 的 L0 → L1 顺序填实现。 - 已知缺口:`SALib`(Sobol/Morris)、`pymoo`、`pyright` 已装进 conda 环境 `mcm-toolkit`,`.venv` 里没有。 ## [0.0.1] - 2026-10-01 ### Added - 项目初始化,包结构落盘(实现为空)。