fix(stats): correlation_matrix 修硬 bug,两处 pyright 报错按坑 8/10 正经修(无 ignore)
用户报的是"63 行 p 值赋值报错"。查下来:63 行运行完全正常(p 是 np.float64),
pyright 那条是 scipy 存根的缺陷。但顺着看发现了真正跑不起来的第 41 行。
correlation_matrix:
- 硬 bug: `if numeric_cols == 0` 拿 Index 和标量比 -> 得到布尔数组。
0 列时静默不报错、1 列时碰巧能跑、>=2 列直接 ValueError 崩(而 >=2 列正是常见情况)。
改成 len(numeric_cols) == 0,并加回归用例
- pyright 两处报错都不是运行时问题,且一处 # type: ignore 都没挂:
- data[numeric_cols] 是坑 8 的 DataFrame | Series 重载 -> cast 收窄
- `_, p = stats.pearsonr(...)` 解包退化成 object(scipy 的返回值是 _make_tuple_bunch
运行时拼的类,存根里没有 pvalue)-> 新增 _pvalue(result: Any) -> float 小助手统一取,
后面 t_test / anova / chi_square_test 可复用
- method 校验提到最前;add_stars 的 elif 链改 if;清掉 3 条 F541 与 1 条 I001,ruff 归零
- 顺带记录两处待定:对角线被标了 ***;含 NaN 时 corr 成对删失而 pearsonr 不删,r 与 p 对不上
tests:
- test_correlation_matrix_diagonal_is_one 由 skip 改成 xfail(strict=False)(决策 30):
契约要数值矩阵、实现返回带星标字符串表,口径待拍板
- 新增 2 条:多列数值表能跑通、非法输入报错
验证: pytest 105 passed / 38 skipped / 1 xfail;ruff All checks passed;pyright 0 errors。
docs: STATUS.md(correlation_matrix 进行中 + 决策 40 + 下一步 A/B/C 选项);CHANGELOG;
personal-pitfalls 追加第 13 条(Index == 标量 得到布尔数组);
docs/04-environment.md 追加坑 10(scipy 检验结果的存根缺陷)
This commit is contained in:
1 parent
cfc1be31ca
commit
3432bf3e50
6 files changed
+215
-24
No files matched your search
+22
-1
@@ -173,6 +173,23 @@
|
|||||||
- 测试计数:**82 passed / 40 skipped → 103 passed / 39 skipped**(新增 21 条);
|
- 测试计数:**82 passed / 40 skipped → 103 passed / 39 skipped**(新增 21 条);
|
||||||
ruff 7 error → **All checks passed**;pyright **1 error → 0**。
|
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
|
### Notes
|
||||||
- `detect_outliers` 原有的三个 bug 加一处设计空白**已全部收口**:`threshold` 范围校验挪进
|
- `detect_outliers` 原有的三个 bug 加一处设计空白**已全部收口**:`threshold` 范围校验挪进
|
||||||
`clean_missing`、`is_series` / `is_ld_array` 提到类型转换**之前**、统计量与掩码都限制在数值列、
|
`clean_missing`、`is_series` / `is_ld_array` 提到类型转换**之前**、统计量与掩码都限制在数值列、
|
||||||
@@ -183,7 +200,11 @@
|
|||||||
(`min(x) + offset > 0`,见决策 35,**待用户确认**);④ 常量列行为写进 docstring 的 `Notes`;
|
(`min(x) + offset > 0`,见决策 35,**待用户确认**);④ 常量列行为写进 docstring 的 `Notes`;
|
||||||
另外 `Parameters` / `Returns` / `Raises` 已按 §4.7 补齐。
|
另外 `Parameters` / `Returns` / `Raises` 已按 §4.7 补齐。
|
||||||
- 新增易错点第 10 条:**把真报错当成"编辑器误报"** —— 红线先跑 CLI 与 `pytest` 再判断(`docs/dev/personal-pitfalls.md`)。
|
- 新增易错点第 10 条:**把真报错当成"编辑器误报"** —— 红线先跑 CLI 与 `pytest` 再判断(`docs/dev/personal-pitfalls.md`)。
|
||||||
- `stats` 这条线开工了:`describe` 落地,并填掉 `examples/01_clean_and_describe.py` 的 `TODO(L0)`。
|
- `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);
|
- 三处口径已拍板(2026-10-03):`log_transform` 的 `offset` 维持现状(决策 35);
|
||||||
`describe` / `zscore` **默认丢掉**非数值列、与 `detect_outliers` 的"保留列"刻意不统一(决策 37);
|
`describe` / `zscore` **默认丢掉**非数值列、与 `detect_outliers` 的"保留列"刻意不统一(决策 37);
|
||||||
变异系数改用 `σ/|μ|`(决策 38)。另立决策 39 明确分工:语义口径用户拍板、工程事务 Codex 顺手补。
|
变异系数改用 `σ/|μ|`(决策 38)。另立决策 39 明确分工:语义口径用户拍板、工程事务 Codex 顺手补。
|
||||||
|
|||||||
@@ -154,13 +154,27 @@
|
|||||||
决策 26 说的就是这个,实现写完了却零覆盖),改成逐条 skip 未实现的 5 条,
|
决策 26 说的就是这个,实现写完了却零覆盖),改成逐条 skip 未实现的 5 条,
|
||||||
并新写 20 条 describe / zscore / 变异系数用例
|
并新写 20 条 describe / zscore / 变异系数用例
|
||||||
- [x] `examples/01_clean_and_describe.py`:填掉末尾的 `TODO(L0)`,加一段 `describe` 输出
|
- [x] `examples/01_clean_and_describe.py`:填掉末尾的 `TODO(L0)`,加一段 `describe` 输出
|
||||||
- [ ] 剩余 4 项:`correlation_matrix` `bootstrap_ci` `monte_carlo` `save_figure`
|
- [ ] `stats/inference.py` 的 `correlation_matrix`(L0 第 9 项,**进行中**;**由用户本人实现**,Codex 审查 + 代修)
|
||||||
|
- **硬 bug**:`if numeric_cols == 0:` 拿 `Index` 和标量比,得到的是布尔**数组** ——
|
||||||
|
0 列时静默不报错、1 列时碰巧能跑、**≥2 列直接 `ValueError` 崩**;
|
||||||
|
而"≥2 个数值列"正是最常见的情况,等于整个函数不可用。改成 `len(numeric_cols) == 0`(易错点 13)
|
||||||
|
- pyright 的报错也都不是运行时问题:`data[numeric_cols]` 是坑 8 的 `DataFrame | Series` 重载
|
||||||
|
(`cast` 收窄);`_, p = stats.pearsonr(...)` 解包会退化成 `object` —— scipy 的返回值是
|
||||||
|
`_make_tuple_bunch` 运行时拼的类、存根里看不到 `pvalue`(见 `docs/04-environment.md` 坑 10)。
|
||||||
|
加了 `_pvalue(result: Any)` 小助手统一取 p 值,**一处都没挂 `# type: ignore`**
|
||||||
|
- `method` 校验提到最前、清掉 3 条 F541 / 1 条 RET505 / I001,ruff 归零
|
||||||
|
- **L0 契约待拍板**:返回值现在是**带星标的字符串表**(`1.000***`),
|
||||||
|
而 docstring 契约与 L0 用例都要求**数值矩阵**(对角 = 1.0);
|
||||||
|
该用例已按决策 30 从 `skip` 改成 `xfail(strict=False)`,见「下一步」第 1 条
|
||||||
|
- 另两处待确认:**对角线也被标了 `***`**(自己和自己相关不该有显著性星标);
|
||||||
|
`df_num` 含 NaN 时 `corr` 走成对删失、而 `pearsonr` 不删 → r 与 p 对不上
|
||||||
|
- [ ] 剩余 3 项:`bootstrap_ci` `monte_carlo` `save_figure`
|
||||||
|
|
||||||
### 验证结果
|
### 验证结果
|
||||||
|
|
||||||
| 检查 | venv | conda `mcm-toolkit` |
|
| 检查 | venv | conda `mcm-toolkit` |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `pytest` | 见下(单列) | **103 passed, 39 skipped(0 xfail)** |
|
| `pytest` | 见下(单列) | **105 passed, 38 skipped(1 xfail)** |
|
||||||
| `import mcm_toolkit` | 0.0.1 / 16 项 | 0.0.1 / 16 项 |
|
| `import mcm_toolkit` | 0.0.1 / 16 项 | 0.0.1 / 16 项 |
|
||||||
| `ruff check` | All checks passed | **All checks passed**(0 error) |
|
| `ruff check` | All checks passed | **All checks passed**(0 error) |
|
||||||
| `scripts/doctor.ps1` | 跑通 | — |
|
| `scripts/doctor.ps1` | 跑通 | — |
|
||||||
@@ -170,13 +184,13 @@
|
|||||||
(修配置之前是 17 errors,全是"无法解析导入 numpy / pandas",见决策 29 与
|
(修配置之前是 17 errors,全是"无法解析导入 numpy / pandas",见决策 29 与
|
||||||
`docs/04-environment.md` 坑 7)
|
`docs/04-environment.md` 坑 7)
|
||||||
- base 亦已复核:numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6(与两套环境一致)
|
- base 亦已复核:numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6(与两套环境一致)
|
||||||
- **142 条用例:103 passed / 39 skipped**。收集数按文件:`test_data.py` 52、`test_api_surface.py` 20、
|
- **144 条用例:105 passed / 38 skipped / 1 xfail**。收集数按文件:`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` 26、
|
`test_core.py` 15、`test_solve.py` 9、`test_viz.py` 8、`test_decision.py` 7、`test_stats.py` 28、
|
||||||
`test_models.py` 5
|
`test_models.py` 5
|
||||||
- **`xfail` 清零**:累计 7 条"契约已写进 docstring、实现没跟上"的用例全部转正,
|
- 目前 **1 条 xfail**:`test_correlation_matrix_diagonal_is_one` —— 契约(数值矩阵)与实现(带星标字符串表)冲突,口径待拍板
|
||||||
决策 30 那套信号(XPASS → 摘 mark)跑完了一个完整闭环
|
- 上一轮那 7 条 xfail 已全部转正,决策 30 那套信号(XPASS → 摘 mark)跑完过一个完整闭环
|
||||||
- `examples/01_clean_and_describe.py` 实测跑通(exit 0);`00_quickstart.py` 仍等 `stats` / `viz`
|
- `examples/01_clean_and_describe.py` 实测跑通(exit 0);`00_quickstart.py` 仍等 `stats` / `viz`
|
||||||
- 39 条 skip 是等实现的算法(`train_test_split`、`describe`、`bootstrap_ci`、`monte_carlo` 等)
|
- 38 条 skip 是等实现的算法(`train_test_split`、`describe`、`bootstrap_ci`、`monte_carlo` 等)
|
||||||
- **本轮回归(已修)**:`transform.py` 上一版有 3 处真 bug —— 修前 **7 failed / 75 passed**,
|
- **本轮回归(已修)**:`transform.py` 上一版有 3 处真 bug —— 修前 **7 failed / 75 passed**,
|
||||||
且 pyright 在同一处报 `reportArgumentType`。**不是静态误报**:pyright CLI 复现 + 测试真红。
|
且 pyright 在同一处报 `reportArgumentType`。**不是静态误报**:pyright CLI 复现 + 测试真红。
|
||||||
结论:见到红线先跑 CLI 与 `pytest`,**不许直接挂 `# type: ignore`**(见个人易错点 10)
|
结论:见到红线先跑 CLI 与 `pytest`,**不许直接挂 `# type: ignore`**(见个人易错点 10)
|
||||||
@@ -247,19 +261,26 @@
|
|||||||
| 38 | `coefficient_of_variation` 用 `σ / |mean|`(**用户本人改的**) |
|
| 38 | `coefficient_of_variation` 用 `σ / |mean|`(**用户本人改的**) |
|
||||||
均值为负时 `σ/μ` 会给出负值,容易被读成"波动更小",其实只是符号跟着均值走。取绝对值后 CV 恒非负。可比性仍只在均值远离 0 的比率尺度数据上成立(已写进 docstring 的 Notes) |
|
均值为负时 `σ/μ` 会给出负值,容易被读成"波动更小",其实只是符号跟着均值走。取绝对值后 CV 恒非负。可比性仍只在均值远离 0 的比率尺度数据上成立(已写进 docstring 的 Notes) |
|
||||||
| 39 | 工作约定:**语义 / 非法输入口径由用户拍板,Codex 只列选项与影响、不代选**;**ruff、`STATUS.md`、`CHANGELOG.md`、`tests/`、docstring 这类与算法无关的工程事务,Codex 顺手补掉** |
|
| 39 | 工作约定:**语义 / 非法输入口径由用户拍板,Codex 只列选项与影响、不代选**;**ruff、`STATUS.md`、`CHANGELOG.md`、`tests/`、docstring 这类与算法无关的工程事务,Codex 顺手补掉** |
|
||||||
|
| 40 | scipy 的检验结果统一走 `_pvalue(result: Any) -> float` 助手取 p 值,不逐处挂 `# type: ignore` |
|
||||||
|
`PearsonRResult` / `SignificanceResult` 是 `_make_tuple_bunch` 在**运行时**拼出来的类,存根里没有 `pvalue` 字段:解包会退化成 `object`、属性访问会报"未知"。运行时完全正常(实测三个函数返回的都是 `np.float64`)。收成 `Any` 是**一处**收窄,调用点仍然干净;`# type: ignore` 要挂在每一处,且会连带关掉该行后面的检查。见 `docs/04-environment.md` 坑 10 |
|
||||||
用户 2026-10-03 明确:没考虑到的非法情况"告诉我我自己改"——因为那是设计决策,得由写实现的人形成判断;而 lint / 进度板 / 测试这类机械活再由用户动手只是浪费。这条与决策 25 / 31 配套:25 定"谁写 src",31 定"Codex 可改的 bug 范围",39 定"非算法事务的默认归口" |
|
用户 2026-10-03 明确:没考虑到的非法情况"告诉我我自己改"——因为那是设计决策,得由写实现的人形成判断;而 lint / 进度板 / 测试这类机械活再由用户动手只是浪费。这条与决策 25 / 31 配套:25 定"谁写 src",31 定"Codex 可改的 bug 范围",39 定"非算法事务的默认归口" |
|
||||||
docstring 与 `test_describe_reports_expected_keys` 早就写死了这个方向,实现里多做的 `.T` 是净负担 —— 且不是"风格不同"那么轻:转置后 `.loc[指标名] = <按变量索引的 Series>` 与列名零交集,pandas **不报错**、把四行整行填成 NaN。方向约定以"能按列名对齐"为准,见易错点 11 |
|
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",改回即可 |
|
原码写的是 `if offset > 0: raise`,把唯一合法的用例(正 offset 正好抬定义域)全拒了,报错文案却是"offset 必须大于 0",自相矛盾。改成校验真正的前提:`log` 要求自变量取正。`offset=0` 对全正数据合法(`offset=None` 的自动分支本来就取 0)。**Codex 代选、待用户确认** —— 若原意是"只收正 offset",改回即可 |
|
||||||
## 下一步
|
## 下一步
|
||||||
|
|
||||||
1. **`correlation_matrix` 是 L0 里下一个该写的**(A 档,`stats/inference.py`)——
|
1. **`correlation_matrix` 的返回契约待你拍板**(这是 L0 第 9 项唯一还没定的地方):
|
||||||
写法和 `describe` 一样:先想"替代了哪段手写代码" → 先写一条会红的测试 → 再实现
|
现在返回的是**带星标的字符串表**(`1.000***`),方便直接贴论文;
|
||||||
→ 补 `examples/` 一行(`docs/03-roadmap.md` §3.2)。
|
但 docstring 的契约与 L0 用例要的是**数值矩阵**(对角 = 1.0),两者只能留一个。
|
||||||
2. 之后按 L0 剩余顺序推进:`bootstrap_ci`(**S 档,要写函数卡片**)→ `monte_carlo`(**S 档**)
|
三个选项:
|
||||||
→ `save_figure`(A)。
|
- **A(推荐)**:返回数值矩阵,另给一个 `correlation_table(corr, p)` 出星标字符串表
|
||||||
|
—— 数值矩阵能做的事(挑高相关对、喂给聚类/PCA、画热力图)字符串表都做不了,
|
||||||
|
而反过来随时能格式化。代价:多一个函数。
|
||||||
|
- **B**:就返回字符串表,把 docstring 契约改掉、L0 用例改成断言 `1.000`。代价:丢数值能力。
|
||||||
|
- **C**:返回数值矩阵,星标表挂到 `df.attrs["display"]` 之类的地方。代价:藏得深,不显式。
|
||||||
|
顺带两处一并定了:**对角线不该带 `***`**;`df_num` 含 NaN 时 `corr` 成对删失但 `pearsonr` 不删,
|
||||||
|
r 与 p 会对不上(要不要先 `dropna` 成对再算 p)。
|
||||||
|
2. 之后 L0 剩余:`bootstrap_ci`(**S 档,要写函数卡片**)→ `monte_carlo`(**S 档**)→ `save_figure`(A)。
|
||||||
3. L0 跑通后,找一道真题(建议 2025 C)走完整流程,反向沉淀函数。
|
3. L0 跑通后,找一道真题(建议 2025 C)走完整流程,反向沉淀函数。
|
||||||
4. **三处口径已拍板**(2026-10-03):
|
4. 已拍板的旧口径(留档):`log_transform` 的 `offset` 维持现状(决策 35);
|
||||||
- `log_transform` 的显式 `offset`:**按现状**(定义域前提校验,决策 35)✓
|
`describe` / `zscore` 默认丢掉非数值列(决策 37);变异系数用 `σ / |μ|`(决策 38);
|
||||||
- 非数值列:`describe` / `zscore` **默认丢掉**,与 `detect_outliers` 的"保留列、掩码恒 False"
|
分工见决策 39。
|
||||||
**不统一** —— 两者输出契约不同,理由见决策 37 ✓
|
|
||||||
- 变异系数:已改用 `σ / |μ|`(决策 38)✓
|
|
||||||
@@ -306,6 +306,28 @@ dtype is specified. This behavior is deprecated and will be removed in a future
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
**坑 10 —— scipy 的检验结果在类型检查器里没有 `pvalue` 字段。**
|
||||||
|
|
||||||
|
现象:`_, p = stats.pearsonr(x, y)` 运行没问题,但 pyright 报
|
||||||
|
`无法将 "_T_co@tuple" 类型的参数分配给 ... 的 "float"`;改成 `.pvalue` 又报
|
||||||
|
`无法访问类 "PearsonRResult" 的属性 "pvalue"`。两条都像"库的锅",其实是存根看不全。
|
||||||
|
|
||||||
|
原因:`PearsonRResult` / `SignificanceResult` 不是普通的 `namedtuple`,
|
||||||
|
而是在 `scipy/stats/_stats_py.py` 里用 `_make_tuple_bunch(...)` 在**运行时**拼出来的类
|
||||||
|
(`PearsonRResult` 还继承自 `PearsonRResultBase`)。类型检查器看不到运行时才生成的字段,
|
||||||
|
于是解包会退化成 `object`、属性访问会报"未知"。
|
||||||
|
**这不是运行时问题** —— `pearsonr` / `kendalltau` / `spearmanr` 返回的 p 实测都是 `np.float64`。
|
||||||
|
|
||||||
|
修法:不要逐处挂 `# type: ignore`,写一个小助手把结果显式收成 `Any`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _pvalue(result: Any) -> float:
|
||||||
|
return float(result.pvalue)
|
||||||
|
```
|
||||||
|
|
||||||
|
一处收窄、调用点全干净,后面 `t_test` / `anova` / `chi_square_test` 可以复用。
|
||||||
|
|
||||||
|
---
|
||||||
## 五、两套环境的实际版本(已对齐)
|
## 五、两套环境的实际版本(已对齐)
|
||||||
|
|
||||||
**2026-10-01 已把 base 从 Anaconda 2025.12 升到 2026.07,两套环境版本已对齐。**
|
**2026-10-01 已把 base 从 Anaconda 2025.12 升到 2026.07,两套环境版本已对齐。**
|
||||||
|
|||||||
@@ -198,3 +198,22 @@
|
|||||||
- 同一轮的另一个同类例子见第 11 条(`.loc` 对齐失败静默填 NaN):
|
- 同一轮的另一个同类例子见第 11 条(`.loc` 对齐失败静默填 NaN):
|
||||||
这两个 bug 的共同点是"**不抛异常**",靠的是断言而不是报错。
|
这两个 bug 的共同点是"**不抛异常**",靠的是断言而不是报错。
|
||||||
- 首次:2026-10-03|复发:0
|
- 首次:2026-10-03|复发:0
|
||||||
|
|
||||||
|
### 13. `Index == 标量` 得到布尔数组,`if` 要等到第 2 列才炸
|
||||||
|
|
||||||
|
- 现象:`correlation_matrix` 里的判空写成了 `if numeric_cols == 0:`,本意是"一个数值列都没有就报错"。
|
||||||
|
实际行为分成三段,**每一段都不一样**:
|
||||||
|
- **0 列**:`array([], dtype=bool)` → `bool(...)` 是 `False` → **静默不报错**,带着空表继续往下跑;
|
||||||
|
- **1 列**:`array([False])` → 单元素数组能转 `bool` → **碰巧正常**;
|
||||||
|
- **≥2 列**:`array([False, False, ...])` → `ValueError: The truth value of an array
|
||||||
|
with more than one element is ambiguous` → **直接崩**。
|
||||||
|
而"≥2 个数值列"恰恰是最常见的情况,所以这一行等于让整个函数不可用。
|
||||||
|
- 为什么会错:`Index` 重载了 `==`,做的是**逐元素比较**,返回的是**数组**不是 `bool`。
|
||||||
|
脑子里比的是"这个序列是不是空的",手上写的是"这个序列的每个元素是不是 0"。
|
||||||
|
更阴的是 1 列时它能跑通 —— 拿单列试一下就会以为这行没问题。
|
||||||
|
- 没被抓住的原因:对应的 L0 用例当时还挂着 `skip`(见第 9 条),所以实现零覆盖。
|
||||||
|
- 以后怎么防:① 判"空"一律 `len(x) == 0`(或 `.empty`),绝不用 `== 0`;
|
||||||
|
② 判断容器里有没有东西,用 `if not len(...)` 而**不是** `if x:` ——
|
||||||
|
`if x:` 对 `DataFrame` / `Index` / 多元素 `ndarray` 会抛同一个歧义错误;
|
||||||
|
③ 新写的分支,**至少拿 0 / 1 / 多三种形状各跑一遍**,单列能过不代表多列能过。
|
||||||
|
- 首次:2026-10-03|复发:0
|
||||||
@@ -7,7 +7,11 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any, Literal, cast
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import scipy.stats as stats
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"anova",
|
"anova",
|
||||||
@@ -20,20 +24,98 @@ __all__ = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def correlation_matrix(data: Any, method: str = "pearson", min_periods: int = 1) -> Any:
|
def _pvalue(result: Any) -> float:
|
||||||
|
"""把 scipy 检验结果取成 p 值(float)。
|
||||||
|
|
||||||
|
scipy 的 `SignificanceResult` / `PearsonRResult` 是 `_make_tuple_bunch` 在**运行时**
|
||||||
|
拼出来的类,类型检查器看不到 `pvalue` 字段 —— 这里显式收成 `Any` 再取,
|
||||||
|
比给每一处调用挂 `# type: ignore` 干净,后面 `t_test` / `anova` 也能复用。
|
||||||
|
"""
|
||||||
|
return float(result.pvalue)
|
||||||
|
|
||||||
|
|
||||||
|
def correlation_matrix(
|
||||||
|
data: Any,
|
||||||
|
method: Literal["pearson", "kendall", "spearman"] = "pearson",
|
||||||
|
min_periods: int = 1,
|
||||||
|
) -> Any:
|
||||||
"""相关矩阵 + 显著性星标。
|
"""相关矩阵 + 显著性星标。
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data:
|
||||||
|
必须是 `pandas.DataFrame`。**只有数值列参与**,非数值列自动跳过。
|
||||||
|
method:
|
||||||
|
`"pearson"` / `"kendall"` / `"spearman"`。
|
||||||
|
min_periods:
|
||||||
|
每对变量至少要有这么多个**共同非缺失**的观测才给相关系数,透传给 `DataFrame.corr`。
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
DataFrame,值 = 相关系数;`min_periods` 控制缺失值容差。
|
DataFrame,值 = 相关系数;`min_periods` 控制缺失值容差。
|
||||||
|
⚠️ 当前实现返回的是**带星标的字符串表**(`1.000***` 这种),与上面这句契约不一致 ——
|
||||||
|
口径待定,见 `STATUS.md`。
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
TypeError
|
||||||
|
`data` 不是 `pandas.DataFrame`。
|
||||||
|
ValueError
|
||||||
|
`data` 里没有数值列,或 `method` 不在支持列表内。
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
星标读法:`***` p < 0.001,`**` p < 0.01,`*` p < 0.05,其余不带星。
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
>>> correlation_matrix(df).round(3) # doctest: +SKIP
|
>>> correlation_matrix(df).round(3) # doctest: +SKIP
|
||||||
"""
|
"""
|
||||||
# TODO(L0)
|
if not isinstance(data, pd.DataFrame):
|
||||||
raise NotImplementedError
|
raise TypeError(f"data必须是 pd.DataFrame,而你传的是{type(data)}")
|
||||||
|
|
||||||
|
valid_method = ["pearson", "kendall", "spearman"]
|
||||||
|
if method not in valid_method:
|
||||||
|
raise ValueError(f"不支持的方法:{method},仅支持{valid_method}")
|
||||||
|
|
||||||
|
numeric_cols = data.select_dtypes(include=[np.number]).columns
|
||||||
|
if len(numeric_cols) == 0:
|
||||||
|
raise ValueError("没有找到数值列")
|
||||||
|
# data[Index] 在 pandas 存根里是 DataFrame | Series 重载,按运行时真值收窄(坑 8)
|
||||||
|
df_num = cast(pd.DataFrame, data[numeric_cols])
|
||||||
|
|
||||||
|
corr_matrix = df_num.corr(method=method, min_periods=min_periods)
|
||||||
|
|
||||||
|
p_values = pd.DataFrame(0.0, index=corr_matrix.index, columns=corr_matrix.columns, dtype=float)
|
||||||
|
for i in range(len(corr_matrix.columns)):
|
||||||
|
for j in range(len(corr_matrix.columns)):
|
||||||
|
if i == j:
|
||||||
|
continue
|
||||||
|
if method == "pearson":
|
||||||
|
p = _pvalue(stats.pearsonr(df_num.iloc[:, i], df_num.iloc[:, j]))
|
||||||
|
elif method == "kendall":
|
||||||
|
p = _pvalue(stats.kendalltau(df_num.iloc[:, i], df_num.iloc[:, j]))
|
||||||
|
else:
|
||||||
|
p = _pvalue(stats.spearmanr(df_num.iloc[:, i], df_num.iloc[:, j]))
|
||||||
|
p_values.iloc[i, j] = p
|
||||||
|
|
||||||
|
def add_stars(val: float, p: float) -> str:
|
||||||
|
# 保留三位小数
|
||||||
|
val_str = f"{val:.3f}"
|
||||||
|
if p < 0.001:
|
||||||
|
return val_str + "***"
|
||||||
|
if p < 0.01:
|
||||||
|
return val_str + "**"
|
||||||
|
if p < 0.05:
|
||||||
|
return val_str + "*"
|
||||||
|
return val_str
|
||||||
|
|
||||||
|
formatted_corr = corr_matrix.copy().astype(str)
|
||||||
|
for i in range(len(corr_matrix)):
|
||||||
|
for j in range(len(corr_matrix)):
|
||||||
|
formatted_corr.iloc[i, j] = add_stars(corr_matrix.iloc[i, j], p_values.iloc[i, j])
|
||||||
|
|
||||||
|
return formatted_corr
|
||||||
|
|
||||||
def confidence_interval(
|
def confidence_interval(
|
||||||
x: Any,
|
x: Any,
|
||||||
|
|||||||
+28
-2
@@ -59,15 +59,41 @@ def test_confidence_interval_matches_scipy(rng):
|
|||||||
assert np.allclose([lo, hi], ref, rtol=1e-6)
|
assert np.allclose([lo, hi], ref, rtol=1e-6)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
@pytest.mark.xfail(
|
||||||
|
strict=False,
|
||||||
|
reason="契约要求返回**数值**矩阵(对角 = 1.0),实现返回带星标的字符串表;口径待定,见 STATUS.md",
|
||||||
|
)
|
||||||
def test_correlation_matrix_diagonal_is_one(rng):
|
def test_correlation_matrix_diagonal_is_one(rng):
|
||||||
from mcm_toolkit.stats import correlation_matrix
|
from mcm_toolkit.stats import correlation_matrix
|
||||||
|
|
||||||
x = rng.normal(size=(200, 4))
|
x = pd.DataFrame(rng.normal(size=(200, 4)), columns=["a", "b", "c", "d"])
|
||||||
c = correlation_matrix(x)
|
c = correlation_matrix(x)
|
||||||
assert np.allclose(np.diag(np.asarray(c)), 1.0)
|
assert np.allclose(np.diag(np.asarray(c)), 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_correlation_matrix_runs_on_multicolumn_frame(rng):
|
||||||
|
"""回归:`if numeric_cols == 0` 拿 Index 跟 0 比,多列数值表当场 ValueError 崩掉。"""
|
||||||
|
from mcm_toolkit.stats import correlation_matrix
|
||||||
|
|
||||||
|
x = pd.DataFrame(rng.normal(size=(50, 3)), columns=["a", "b", "c"])
|
||||||
|
out = correlation_matrix(x)
|
||||||
|
|
||||||
|
assert out.shape == (3, 3)
|
||||||
|
assert list(out.columns) == ["a", "b", "c"]
|
||||||
|
assert list(out.index) == ["a", "b", "c"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_correlation_matrix_rejects_bad_input():
|
||||||
|
from mcm_toolkit.stats import correlation_matrix
|
||||||
|
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
correlation_matrix([[1.0, 2.0]])
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
correlation_matrix(pd.DataFrame({"city": ["X", "Y"]}))
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
correlation_matrix(pd.DataFrame({"a": [1.0, 2.0]}), method="bogus") # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------ stats: describe
|
# ------------------------------------------------------ stats: describe
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user