fix(data): clean.py 轻度修复 + 摘掉 5 条已修好的 xfail

Codex 直接改的(格式 / 死代码 / 纯 bug,不动接口与语义):
- winsorize(Series) 直接崩:select_dtypes 在 Series 上不存在,Series 分支提到最前并加数值 dtype 守卫
- winsorize 的分位数改为只在数值列上算,含文本列的赛题表不再崩(与已有 numeric_cols 判断对齐)
- 删掉 detect_outliers 里已成空操作的 df = pd.DataFrame(data),去掉 df 别名
- clean_missing 的嵌套 if 合并(ruff SIM102),清掉 W293 行尾空白与函数内连续空行

测试:
- 5 条 xfail 经 XPASS 信号确认已修好,摘掉 mark 转正式用例
  (detect_outliers 4 条 + winsorize 混合类型 1 条)
- 53 passed / 7 xfailed  ->  58 passed / 2 xfailed
- 剩 2 条 xfail 保留,且不是漏改而是设计没定:
  detect_outliers 遇文本列返回什么、impute_missing 的 mode 分支要不要就地改调用方的表
- ruff: 1 error -> All checks passed;pyright: 0 errors 不变

文档:
- STATUS.md:L0 进度 4/12、验证结果表、下一步重写、新增决策 31
  (Codex 可直接改 src 的范围:纯 bug / 格式 / 死代码;要选语义的留给用户)
- CHANGELOG.md:本轮变更,并修掉 [Unreleased] 里被重复追加撑坏的一句
- docs/dev/personal-pitfalls.md:只追加第 7 条(守卫子句要排在类型分派之后)
This commit is contained in:
guzhujushi committed 2026-10-02 15:18:15 +08:00
1 parent c2595b6d07
commit 319b16b17c
5 files changed
+89 -66

No files matched your search

+23 -28
View File
@@ -1,6 +1,6 @@
# STATUS — 当前进度
> 更新日期:2026-10-01
> 更新日期:2026-10-02
## 本次任务
@@ -10,7 +10,7 @@
**第二阶段(进行中)**:按 `docs/03-roadmap.md` 的 L0 → L1 顺序**逐个实现函数**,
用户本人写 `src/`、Codex 负责审查 + `tests/`(决策 25)。
当前 3/12:`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`。
当前 4/12:`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`、`clean_missing`。
## 任务清单
@@ -95,18 +95,17 @@
- `threshold` 用 `math.ceil` 算 `min_non_na`:3 列、阈值 0.5 时必须是 2,
写 `int()` 截断会漏删
- 顺手落地兄弟函数 `impute_missing` / `winsorize`(原属 L1)
- [ ] **待修:** `detect_outliers` 已写但有 3 个 bug(见下)
- [ ] 剩余 7 项:`normalize`
`detect_outliers` `normalize` `standardize` `describe` `correlation_matrix`
`bootstrap_ci` `monte_carlo` `save_figure`
- [ ] **待收尾:** `detect_outliers` 只剩"文本列"1 处,且要先定设计(见「下一步」第 1 条)
- [ ] 剩余 8 项:`detect_outliers`(收尾)+ `normalize` `standardize` `describe`
`correlation_matrix` `bootstrap_ci` `monte_carlo` `save_figure`
### 验证结果
| 检查 | venv | conda `mcm-toolkit` |
|---|---|---|
| `pytest` | 见下(单列) | **53 passed, 42 skipped, 7 xfailed** |
| `pytest` | 见下(单列) | **58 passed, 42 skipped, 2 xfailed** |
| `import mcm_toolkit` | 0.0.1 / 16 项 | 0.0.1 / 16 项 |
| `ruff check` | All checks passed | **1 error**(就在 `detect_outliers` 那段待修的代码里) |
| `ruff check` | All checks passed | **All checks passed**(0 error) |
| `scripts/doctor.ps1` | 跑通 | — |
| SALib / pymoo / pyright | 缺失 | 已装 |
@@ -114,10 +113,10 @@
(修配置之前是 17 errors,全是"无法解析导入 numpy / pandas",见决策 29 与
`docs/04-environment.md` 坑 7)
- base 亦已复核:numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6(与两套环境一致)
- 53 条通过 = 20 条公开 API 契约测试 + 11 条 `core` 行为测试 + 22 条 `data` 行为测试
- 7 条 `xfail` 是**已知待修**的契约(`detect_outliers` 的 4 条、`winsorize` 混合类型、
`clean_missing` 阈值范围、`impute_missing` 的 mode 就地修改);
修好后会自动变 XPASS,那时把 `xfail` 摘掉即可(决策 30)
- 58 条通过 = 20 条公开 API 契约测试 + 11 条 `core` 行为测试 + 27 条 `data` 行为测试
- 2 条 `xfail` 是**已知待修**的契约:`detect_outliers` 遇文本列、`impute_missing` 的 mode 就地改。
两者都**不是漏改,是设计没定**;修好后会自动变 XPASS,那时把 `xfail` 摘掉即可(决策 30)
- 上一轮 5 条 `xfail` 正是靠这个 XPASS 信号确认修好并摘掉的
- 42 条 skip 是等实现的算法(`normalize` / `standardize` / `train_test_split` 等)
- 三个 `.ps1` 通过 PowerShell AST 语法解析
@@ -173,26 +172,22 @@
| 30 | 已知待修的契约用 `@pytest.mark.xfail(strict=False)`,不用 skip | skip 会让"契约还没写"和"写了但实现有问题"混在一起,5 条 skip 曾把 `clean.py` 的 bug 全藏住。xfail 照样进报告,修好后自动变 XPASS,是"修好了"的信号,而且 XPASS 不失败,不会卡住提交 |
| 29 | 静态分析器钉死到工作区 `.venv`,并把 base 的 site-packages 用 `.pth` 链进去 | pyright / Pylance **不认** `pyvenv.cfg` 的 `include-system-site-packages`,`.venv` 自己只有 `pip`;不钉的话它们会去 PATH 上找 python,本机 PATH 上那个是 Codex 运行时的 3.14 shim,没有科学栈,于是报"无法解析导入"。`.pth` 是唯一被 pyright 认的转发手段 |
| 28 | 不启用 ruff 的 preview 规则集 | `E302`(函数间要 2 个空行)在 ruff 0.16 里是 preview 规则,`select = ["E"]` 默认**不包含**它——所以少一个空行 ruff 照样报 "All checks passed"。行数空行得自己盯,别指望 lint |
| 31 | Codex 可**直接改** `src/` 的范围:纯 bug(不改接口与语义)、格式、死代码 | 决策 25 说实现归用户,但"能跑"是底线:`winsorize(pd.Series)` 因 `select_dtypes` 在 `Series` 上不存在而直接崩、测试变红,这种必须当场修。凡是要**选一个语义**的(文本列返回什么、mode 要不要就地改)一律留给用户本人,Codex 只把选项和影响列清楚 |
| 27 | base 里的 zstandard 残骸目录**改名禁用**,不删除、不重装 | 删掉不可逆;`pip install zstandard` 会给 base 塞一个 conda 记录里根本没有的包,下次 `conda update --all` 又要重新判断。改名后 `import` 变回正常 `ModuleNotFoundError`,pandas 自动跳过 |
## 下一步
1. **修 `detect_outliers`**(唯一挡在 L0 前进路上的 🔴,三处都在 `src/mcm_toolkit/data/clean.py`):
- `clean.py:165-167` 的 `threshold` 范围校验放错了函数:`threshold` 只在 zscore 分支
赋值,iqr 走进去就是 `UnboundLocalError`;而且 z 的阈值本来就是 3.0,
用 `0 <= threshold <= 1` 卡它必然把默认值拒掉。
你本意多半是想校验 `clean_missing` 的 `threshold`(那个才该是 [0,1])——把它挪过去,
`detect_outliers` 里删掉。ruff 剩下的那 1 条(SIM102)也在这 3 行里,删掉就全绿。
- `clean.py:145` 的 `df.quantile()` 要在**数值列**上算:`df.select_dtypes(include='number')`,
否则赛题表一带文本列就崩。`winsorize` 的 `clean.py:186` 同病,
而且它的空数值列判断排在了分位数之后,等于没兜住。
- `clean.py:130-131` 的 `is_series` / `is_ld_array` **永远为 False**:
第 114-117 行已经把 `data` 转成 DataFrame 了,再判断 `isinstance(data, pd.Series)` 自然不成立。
要在转换**之前**记下来,或者干脆用 `inspect` 拿原始入参。修好后
`test_detect_outliers_returns_mask_not_filtered_data` 会从 xfail 变 XPASS。
1. **收尾 `detect_outliers`**(`src/mcm_toolkit/data/clean.py`,只剩 1 处,且**要先定设计**):
非数值列怎么办 —— 一律返回 `False`,还是只对数值列算掩码?
现在 iqr / zscore 两条分支都直接在**全表**上 `quantile()` / `mean()`,赛题表一带文本列就崩。
定完把它换成 `data[numeric_cols].quantile(...)`(`winsorize` 已按"只对数值列算"改好,可直接照抄),
并把 `tests/test_data.py::test_detect_outliers_survives_text_columns` 的 xfail 摘掉。
2. **写 `detect_outliers` 的 S 档函数卡片**(`docs/algorithms/` 下,照 `docs/dev/function-card-template.md`)。
roadmap 要求 S 档先写卡片再写码 —— 这次是反着来的,所以三个 bug 都在"该想清楚"的地方翻车:
上界符号、布尔优先级、维度。卡片本身就是防这个的。
3. 顺手把 `impute_missing` 的 mode 分支改成**返回新表**(现在就地改调用方的 DataFrame,和另外四个分支不一致)。
4. 每写完一个函数,在 `examples/` 里加一行最小用法。
5. L0 跑通后,找一道真题(建议 2025 C)走完整流程,反向沉淀函数。
3. 顺手把 `impute_missing` 的 mode 分支改成**返回新表**
(现在就地改调用方的 DataFrame,和另外四个分支不一致;`data = data.copy()` 一行的事)。
4. **下一个文件**:`data/transform.py` 的 `normalize` / `standardize`
(`tests/test_data.py` 里两条 skip 已就位;写之前先定 `method` 的取值集合与 `axis` 语义)。
5. 每写完一个函数,在 `examples/` 里加一行最小用法。
6. L0 跑通后,找一道真题(建议 2025 C)走完整流程,反向沉淀函数。