feat(stats): describe 落地,修掉一个静默填 NaN 的 bug(L0 8/12)

src/stats/descriptive.py 由用户本人实现,这里做审查 + 代修。

describe:
- 真 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

zscore: 修 RET505;docstring 补齐;DataFrame 取列处按坑 8 用 cast 收窄
coefficient_of_variation: 均值为 0 改抛 ValueError(取值问题不是类型问题);
  空数组提前报错(原先静默 nan + RuntimeWarning);错别字与文案修正

tests: 去掉 test_stats.py 的模块级 pytestmark —— 它把 6 条测试全部跳过(含已实现的
  describe),正是决策 26 要禁的写法。改为逐条 skip 未实现的 5 条,并新增 20 条用例。

验证:pytest 102 passed / 39 skipped(原 82/40);ruff All checks passed;pyright 0 errors。
examples/01_clean_and_describe.py 填掉 TODO(L0),实测 exit 0。

docs: STATUS.md L0 8/12 + 决策 35/36 + 三处待拍板口径;CHANGELOG 记录;
      personal-pitfalls 追加第 11 条(pandas 对不齐就静默填 NaN)
This commit is contained in:
guzhujushi committed 2026-10-03 13:17:58 +08:00
1 parent 4552aee41c
commit e9624cb3a7
6 files changed
+468 -34

No files matched your search

+42 -17
View File
@@ -10,7 +10,7 @@
**第二阶段(进行中)**:按 `docs/03-roadmap.md` 的 L0 → L1 顺序**逐个实现函数**,
用户本人写 `src/`、Codex 负责审查 + `tests/`(决策 25)。
当前 7/12:`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`、`clean_missing`、`detect_outliers`、`normalize`/`standardize`。
当前 8/12:`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`、`clean_missing`、`detect_outliers`、`normalize`/`standardize`、`describe`。
## 任务清单
@@ -62,7 +62,7 @@
- [x] 推送通道:公网 22 被防火墙过滤 → 改用 Tailscale(`100.115.86.48:22`),
`~/.ssh/config` 加了一条域名别名,remote 里仍写域名
**E. L0 实现进度(7/12)**
**E. L0 实现进度(8/12)**
- [x] `core/result.py` 的 `Result`
- `__repr__`:单行紧凑;`data` 只给类型与规模,**绝不**整数组打印
(十万级数组的 repr 实测仍 < 200 字符);`success` 恒在
@@ -135,13 +135,32 @@
改成校验真正的定义域前提 `min(x) + offset > 0`(见决策 35)
- docstring 按 §4.7 补齐 `Parameters` / `Returns` / `Raises`(Codex 代写,与 `io.py` 同一套 numpydoc 格式)
- 口径定稿后 ruff 4 条 RET505 一并清掉 → `All checks passed`;pyright **2 errors → 0**
- [ ] 剩余 5 项:`describe` `correlation_matrix` `bootstrap_ci` `monte_carlo` `save_figure`
- [x] `stats/descriptive.py` 的 `describe`(L0 第 8 项,**由用户本人实现**,Codex 审查 + 代修)
- **一个静默 bug(最危险的那种)**:`df_num.describe().T` 把表转反了(行 = 变量、列 = 指标),
而 docstring 与 `test_describe_reports_expected_keys` 都要求"行 = 指标、列 = 变量"。
转置后 `.loc[指标名] = <按变量索引的 Series>` 与列名**零交集**,pandas 不报错,
直接把这四行(`missing` / `missing_ratio` / `skewness` / `kurtosis`)**整行填成 NaN**。
去掉 `.T` 一处即修好三件事:方向对了、NaN 没了、`weights` 分支不再报
`cannot set a row with mismatched columns`(见易错点 11、决策 36)
- `median` 行补上(pandas 的 `describe()` 只有 `50%`,而测试契约定的是 `median`)
- 入参放宽到 `list` / `tuple` / `ndarray`(兄弟函数都收,`00_quickstart.py` 传的就是 ndarray)
- `weights` 校验补齐:长度、非一维、NaN/inf、负值、和为 0 都抛带原因的错误
(原先和为 0 时裸抛 `ZeroDivisionError`)
- `zscore`:修 RET505,docstring 补 `Parameters` / `Returns` / `Raises`;
`coefficient_of_variation`:均值为 0 由 `TypeError` 改为 `ValueError`(那是取值问题不是类型问题)、
空数组提前报错(原先静默返回 nan + RuntimeWarning)、改掉"异变系数"错别字
- pyright 的 `DataFrame | Series` 重载问题按坑 8 用 `cast` 收窄;ruff 7 条清到 0
- 测试:`test_stats.py` 去掉**模块级** `pytestmark`(它把 6 条测试**全部**跳过了 ——
决策 26 说的就是这个,实现写完了却零覆盖),改成逐条 skip 未实现的 5 条,
并新写 20 条 describe / zscore / 变异系数用例
- [x] `examples/01_clean_and_describe.py`:填掉末尾的 `TODO(L0)`,加一段 `describe` 输出
- [ ] 剩余 4 项:`correlation_matrix` `bootstrap_ci` `monte_carlo` `save_figure`
### 验证结果
| 检查 | venv | conda `mcm-toolkit` |
|---|---|---|
| `pytest` | 见下(单列) | **82 passed, 40 skipped(0 xfail)** |
| `pytest` | 见下(单列) | **102 passed, 39 skipped(0 xfail)** |
| `import mcm_toolkit` | 0.0.1 / 16 项 | 0.0.1 / 16 项 |
| `ruff check` | All checks passed | **All checks passed**(0 error) |
| `scripts/doctor.ps1` | 跑通 | — |
@@ -151,13 +170,13 @@
(修配置之前是 17 errors,全是"无法解析导入 numpy / pandas",见决策 29 与
`docs/04-environment.md` 坑 7)
- base 亦已复核:numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6(与两套环境一致)
- **122 条用例:82 passed / 40 skipped**。收集数按文件:`test_data.py` 52、`test_api_surface.py` 20、
`test_core.py` 15、`test_solve.py` 9、`test_viz.py` 8、`test_decision.py` 7、`test_stats.py` 6、
- **141 条用例:102 passed / 39 skipped**。收集数按文件:`test_data.py` 52、`test_api_surface.py` 20、
`test_core.py` 15、`test_solve.py` 9、`test_viz.py` 8、`test_decision.py` 7、`test_stats.py` 25、
`test_models.py` 5
- **`xfail` 清零**:累计 7 条"契约已写进 docstring、实现没跟上"的用例全部转正,
决策 30 那套信号(XPASS → 摘 mark)跑完了一个完整闭环
- `examples/01_clean_and_describe.py` 实测跑通(exit 0);`00_quickstart.py` 仍等 `stats` / `viz`
- 40 条 skip 是等实现的算法(`train_test_split`、`describe`、`bootstrap_ci`、`monte_carlo` 等)
- 39 条 skip 是等实现的算法(`train_test_split`、`describe`、`bootstrap_ci`、`monte_carlo` 等)
- **本轮回归(已修)**:`transform.py` 上一版有 3 处真 bug —— 修前 **7 failed / 75 passed**,
且 pyright 在同一处报 `reportArgumentType`。**不是静态误报**:pyright CLI 复现 + 测试真红。
结论:见到红线先跑 CLI 与 `pytest`,**不许直接挂 `# type: ignore`**(见个人易错点 10)
@@ -170,8 +189,8 @@
只差 Python 补丁号(3.13.9 vs 3.13.15)。详见 `docs/04-environment.md` §5。
- [x] ~~Gitea 远程尚未配置~~ → **已配置并推送完成**,见上面 D 节与决策 19/20。
note:仓库当前是 **Public**(匿名 `git ls-remote` 可读),要改私有去 Gitea 仓库设置。
- [ ] L0 完成 **7/12**(`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`、`clean_missing`、
`detect_outliers`、`normalize`、`standardize`),其余按 `docs/03-roadmap.md` 的 L0 → L1 顺序推进;
- [ ] L0 完成 **8/12**(`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`、`clean_missing`、
`detect_outliers`、`normalize`、`standardize`、`describe`),其余按 `docs/03-roadmap.md` 的 L0 → L1 顺序推进;
详情见上面 E 节
- [ ] 本机 `exec_command` 在沙箱模式下**完全不可用**
(`helper_unknown_error: setup refresh had errors`),只有提权
@@ -222,15 +241,21 @@
| 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 自动跳过 |
| 35 | `log_transform` 的显式 `offset` 按**定义域前提**校验:`min(x) + offset > 0` 才放行 |
| 36 | `describe` 的返回表**不转置**:行 = 指标、列 = 变量 |
docstring 与 `test_describe_reports_expected_keys` 早就写死了这个方向,实现里多做的 `.T` 是净负担 —— 且不是"风格不同"那么轻:转置后 `.loc[指标名] = <按变量索引的 Series>` 与列名零交集,pandas **不报错**、把四行整行填成 NaN。方向约定以"能按列名对齐"为准,见易错点 11 |
原码写的是 `if offset > 0: raise`,把唯一合法的用例(正 offset 正好抬定义域)全拒了,报错文案却是"offset 必须大于 0",自相矛盾。改成校验真正的前提:`log` 要求自变量取正。`offset=0` 对全正数据合法(`offset=None` 的自动分支本来就取 0)。**Codex 代选、待用户确认** —— 若原意是"只收正 offset",改回即可 |
## 下一步
1. **`stats.describe` 是 L0 里下一个该写的**(A 档,`stats/descriptive.py`)——
`examples/00_quickstart.py` 与 `examples/01_clean_and_describe.py` 末尾的 `TODO(L0)` 都在等它。
写法照 `data/clean.py` 的 `clean_missing`:先想"替代了哪段手写代码" → 先写一条会红的测试
→ 再实现 → 补 `examples/` 一行(`docs/03-roadmap.md` §3.2)。
2. 之后按 L0 剩余顺序推进:`correlation_matrix`(A)→ `bootstrap_ci`(**S 档,要写函数卡片**)
→ `monte_carlo`(**S 档**)→ `save_figure`(A)。
1. **`correlation_matrix` 是 L0 里下一个该写的**(A 档,`stats/inference.py`)——
写法和 `describe` 一样:先想"替代了哪段手写代码" → 先写一条会红的测试 → 再实现
→ 补 `examples/` 一行(`docs/03-roadmap.md` §3.2)。
2. 之后按 L0 剩余顺序推进:`bootstrap_ci`(**S 档,要写函数卡片**)→ `monte_carlo`(**S 档**)
→ `save_figure`(A)。
3. L0 跑通后,找一道真题(建议 2025 C)走完整流程,反向沉淀函数。
4. **待你确认(决策 35)**:`log_transform` 的显式 `offset` 现在按"定义域前提"校验
(`min(x) + offset > 0`,否则 `ValueError`)。若你原意是"只收正 offset",说一声我改回去。
4. **三处口径待你拍板**(Codex 只列选项,不替你选):
- **决策 35**:`log_transform` 的显式 `offset` 按"定义域前提"校验(`min(x) + offset > 0`)。
若原意是"只收正 offset",说一声我改回去。
- **`zscore(df)` 遇到非数值列会静默丢掉那一列**(`describe` 也一样只描述数值列)。
`detect_outliers` 走的是"保留列、掩码恒 False"(决策 32)—— 要不要统一?
- **`coefficient_of_variation` 在均值为负时返回负值**(`σ/μ` 原样输出)。
备选:① 取绝对值 `σ/|μ|`;② 均值为负直接抛 `ValueError`;③ 维持现状 + Notes 提醒(我暂时按 ③ 写的)。