feat(models): 落地 compare_models(指标名别名映射)+ 同步你的三条入口口径

你的三条改动(决策 61,已复测入库):
- verify_yt_yp 拦 Inf:rmse([1.0, inf], ...) 从安静返回 inf 变成 ValueError。
- 至少 2 个样本:rmse(1.0, 1.0) 不再被当成 1 个样本算出 0.0。
- mape 从"只拦 0"改成拦 y_true <= 0:原来对负数返回 -1.0(无意义)。
Codex 侧把这三条写进 7 处 Raises(易错点 30 复发 1 次),
test_mape_rejects_zero_in_y_true 的 match 文案改为"非正数",另补 Inf / 样本数两条测试。

compare_models(决策 62,A 档,Codex 实现):
- 别名映射 _METRIC_FUNCS:r2 与 r2_score 指向同一个函数,传哪个列名就叫哪个,
  两个一起传报 ValueError;adjusted_r2 不进表(需要 n_features)。
- cv=0 全量拟合后打分;cv>=2 用手写 K 折(get_rng(random_state).permutation +
  np.array_split),不引入 sklearn 依赖;报折间均值;每折至少 2 个样本 => cv <= n // 2。
- 排序方向跟第一个指标走(误差类升序、R² 降序);models 必须传模型对象(fit/predict),
  X 必须二维,metrics 必须是 tuple/list。
- 原 test_compare_models_... 的用法是错的(传拟合结果当模型),改成测试桩后摘 skip。

tests/test_models.py:23 -> 31 条(排序两条、别名、坏指标名 / 坏 models / 坏 cv、
每折下限、random_state 可复现)。

三连:pytest 233 passed / 25 skipped(258 收集)、ruff 0、pyright 0。

文档:STATUS 决策 61 / 62 + 第十轮(第九轮那 4 条标办结);CHANGELOG 本轮条目;
personal-pitfalls 易错点 30 记复发 1 次 + 新增 33(多行文案少引号 / ValueError 多参数)。
This commit is contained in:
guzhujushi committed 2026-10-12 16:53:01 +08:00
1 parent 9907289ec8
commit 748affb7f0
5 files changed
+414 -50

No files matched your search

+31
View File
@@ -644,6 +644,37 @@
`rmse(1.0, 1.0)` 这类标量被当成 1 个样本、`compare_models` 的键名 `"r2"` 与函数名 `r2_score` 不一致。
详见 `STATUS.md`「第九轮新增:4 条口径待你拍板」。
### Added
- `models.compare_models`:落地(**Codex 按 A 档实现**,决策 62)。返回 `pd.DataFrame`(行 = 模型、列 = 指标),
按第一个指标排序(误差类升序、R² 降序)。`cv=0` 全量拟合后打分;`cv >= 2` 用**手写 K 折**
(`get_rng(random_state).permutation` + `np.array_split`,**不引入 sklearn 依赖**)报折间均值。
- `models/metrics.py` 的**指标名别名映射** `_METRIC_FUNCS`:`"r2"` 与 `"r2_score"` 指向同一个函数,
传哪个名字列名就叫哪个;两个一起传报 `ValueError`。`adjusted_r2` 不进表(需要 `n_features`)。
- `tests/test_models.py`:23 → **31 条**。首列排序(升序 / 降序)、别名映射、坏指标名 / 坏 `models` / 坏 `cv`、
每折样本数下限、`random_state` 可复现;另新增 `Inf` 与"至少 2 个样本"两条入口测试。
原 `test_compare_models_returns_table_sorted_by_first_metric` 的**用法是错的**(把拟合结果当模型传),
已改写成用测试桩 `_MeanModel` / `_LinearModel` 并摘掉 `skip`。
### Changed
- `models/metrics.py`:用户的第三轮入口改动入库(决策 61)—— `verify_yt_yp` 增加 `Inf` 拦截、
增加"至少 2 个样本"(标量不再被当成 1 个样本);`mape` 由"只拦 0"改成拦 `y_true <= 0`
(原式对负数返回**负数**,实测 `-1.0`)。
- `models/metrics.py`:三条新口径同步进 7 处 `Raises`,`mape` 的 `Returns` 与摘要改为"`y_true` 必须全为正数"
(**易错点 30 复发 1 次**:口径一改、docstring 立刻过期)。
- `models/metrics.py`:`compare_models` 的入口收口 —— `X` 必须二维(`as_array(ndim=2)` 不替你猜)、
`cv` 白名单(`bool` 拒收 / `cv=1` 无意义 / `cv <= n // 2`)、`models` 必须是"有 `fit` / `predict` 的对象"、
`metrics` 必须是 `tuple` / `list`(单个字符串给专门的报错文案)。
- `models/metrics.py`:模块 docstring 改为"指标函数只返回 `float`,唯一例外是 `compare_models`(返回 `DataFrame`)"。
- `STATUS.md`:新增**决策 61 / 62**,第九轮那 4 条口径标记办结,「下一步」推进到第十轮;
验证计数更新为 **233 passed / 25 skipped(258 收集)**。
- `docs/dev/personal-pitfalls.md`:易错点 30 记为**复发 1 次**;新增易错点 33
(多行报错文案少引号 / `raise ValueError("a", "b")` 多参数 —— ruff 一秒能抓,别等运行时)。
### Notes
- **1 条小口径待用户拍板**:`verify_yt_yp` 里 `yt.ndim == 0` 是死代码(`as_array(ndim=1)` 必然摊平),
可塌成 `if yt.shape[0] <= 1:`。详见 `STATUS.md`「第十轮新出现的 1 条小口径」。
- **等 `fit_linear` 落地时补一条集成测试**:`compare_models` + 真实模型对象。
## [0.0.1] - 2026-10-01
### Added