feat(models): 审查 metrics.py:工程侧收口 + 修 r2/smape 两个真 bug(+18 条测试)

models/metrics.py(用户写、Codex 审):6 个回归指标 + 共享入口 verify_yt_yp 落地,
compare_models 仍是 TODO(L1)。

真 bug(决策 59,由用户亲手改定):
- r2_score 的 SS_tot 基准用了 np.mean(y_pred),必须是 np.mean(y_true)。
  探针实测:修前 0.8803827751196172、修后 0.875 = sklearn.metrics.r2_score 一致;
  adjusted_r2 复用它,一并修正。完美拟合(r2_score(y, y) == 1)与常量 y 都掩盖了这个错,
  所以新测试特意用"非完美拟合"的数据。ss_tol 顺带改名 ss_tot。
- smape 的 np.where 掩码防不住零除(两条分支都会被求值),在
  filterwarnings = ["error::RuntimeWarning"] 下任何含 0 的用例直接红 → 改 np.divide(where=),
  数值结果不变(决策 60)。

工程侧(决策 47):报错文案统一为"参数名 + 实际值";补 7 处 docstring
(Parameters / Returns / Raises,把 NaN 报错、mape 遇 0、r2 常量 y、adjusted_r2 分母保护
写成契约;n_features 注明不含截距);ruff 9 条清零(6 F541 + 3 W293)。

tests/test_models.py:5 条 → 23 条(2 条摘 skip 转正 + 18 条新增)。

三连:pytest 224 passed / 26 skipped(250 收集)、ruff 0、pyright 0。

文档:STATUS 决策 59 / 60 + 第九轮(含 4 条待拍板口径);CHANGELOG 本轮条目;
docs/dev/personal-pitfalls.md 追加易错点 31(基准量抄错对象)与 32(np.where 掩码防不住零除)。
This commit is contained in:
guzhujushi committed 2026-10-10 17:07:46 +08:00
1 parent 3bb31f95de
commit 9907289ec8
5 files changed
+463 -26

No files matched your search

+31
View File
@@ -613,6 +613,37 @@
`markov_steady_state` 未收敛时是否带 `data`、非方阵的 `TypeError` / `ValueError` 不一致、
`mm1_metrics` 的 `data` 该不该是 dict。
### Added
- `models/metrics.py`:6 个回归指标落地(**由用户本人实现**)—— `rmse` / `mae` / `mape` / `smape` /
`r2_score` / `adjusted_r2`,共用一个入口守卫 `verify_yt_yp`(`as_array` → 形状 / 空 / NaN 三道校验)。
`compare_models` 仍是 `TODO(L1)`。
- `tests/test_models.py`:5 条 → **23 条**。`test_metrics_known_values` / `test_r2_is_one_for_perfect_fit`
两条摘掉 `skip` 转正,另新增 18 条 —— 形状不一致 / 空 / NaN / `str` 被 `TypeError` 挡、四种入参
(`DataFrame` / `Series` / `ndarray` / `list`)结果一致 ×5、`mape` 遇 0、`smape` 全 0 且**不许漏告警**、
`adjusted_r2` 的 `bool` / `float` / `0` / 分母保护 / `np.int64` / 惩罚单调性,以及 `r2_score` 的
**非完美拟合**解析值与 `sklearn.metrics.r2_score` 对照。
### Changed
- `models/metrics.py`:`r2_score` 的 `SS_tot` 基准由 `np.mean(y_pred)` 改为 `np.mean(y_true)`(**真 bug**,
由用户亲手改定,见 STATUS 决策 59 / 个人易错点 31),`ss_tol` 随之改名 `ss_tot`;修前 0.8803827751196172、
修后 0.875(与 sklearn 一致),`adjusted_r2` 同步修正。
- `models/metrics.py`:`smape` 的零除改用 `np.divide(..., out=..., where=denom != 0.0)` ——
`np.where` 的两条分支都会被求值,原写法在 `filterwarnings = ["error::RuntimeWarning"]` 下会让
"含 0 的 smape"测试直接失败(决策 60)。数值结果与口径不变。
- `models/metrics.py`:补 7 处 docstring(`Parameters` / `Returns` / `Raises`),把"NaN 报错 / `mape` 遇 0 报错 /
`r2_score` 常量 y 报错 / `adjusted_r2` 的分母保护"写成契约;`n_features` 的口径写明**不含截距**。
- `models/metrics.py`:报错文案统一为"参数名 + 实际值"(形状不一致会带上两个 `shape`);
ruff 9 条清零(6 条 `F541` 无占位符 f-string + 3 条 `W293`)+ 补函数间空行。
- `STATUS.md`:新增**决策 59 / 60**,「下一步」推进到第九轮(含 4 条待拍板口径);
验证计数更新为 **224 passed / 26 skipped(250 收集)**。
- `docs/dev/personal-pitfalls.md`:新增易错点 31(公式的"基准量"抄错对象,且完美拟合会掩盖它)、
32(`np.where` 掩码防不住零除)。
### Notes
- **4 条口径待用户拍板**(Codex 只报不改):`mape` 遇负 `y_true` 现在返回负数、只拦 `NaN` 不拦 `inf`、
`rmse(1.0, 1.0)` 这类标量被当成 1 个样本、`compare_models` 的键名 `"r2"` 与函数名 `r2_score` 不一致。
详见 `STATUS.md`「第九轮新增:4 条口径待你拍板」。
## [0.0.1] - 2026-10-01
### Added