feat(data): detect_outliers 收尾(文本列恒 False)+ 补测试与文档
实现(用户本人写 + Codex 轻度收尾): - 统计量(quantile / mean / std)与掩码写入都收窄到数值列,含文本列的赛题表不再崩 - 非数值列在掩码里恒 False(用户定的语义),docstring Notes 补上这条 - 删掉死代码 data=data.copy():下面的 to_frame()/pd.DataFrame() 本就返回新对象, 函数全程只读 data,白复制一份;且它排在类型分派之前,会把错误入参的报错 从 ValueError 变成 AttributeError - 删掉那行死代码后 pyright 反而暴露 1 个真错:df[Index] 被类型存根当成 Series, .std() 于是被判成标量、std.replace(...) 非法。在取列子表处 cast 成 DataFrame 按运行时真值收窄(详见 docs/04-environment.md 坑 8) 测试: - test_detect_outliers_survives_text_columns 摘掉 xfail 并加强断言(文本列必须恒 False) - 新增 test_detect_outliers_all_text_frame_returns_all_false,钉住"整表无数值列"边界 - 58 passed / 2 xfailed -> 60 passed / 1 xfailed;ruff 0;pyright 0 - 仅剩 1 条 xfail:impute_missing 的 mode 分支就地改调用方的表(要选语义,留给用户) 文档: - STATUS.md:L0 5/12、验证结果表、下一步重写、新增决策 32(非数值列恒 False) - docs/04-environment.md:新增坑 8(pandas 存根让 df[Index] 退化成 Series,及三种无效修法) - docs/dev/personal-pitfalls.md:只追加第 8 条(限制掩码 ≠ 限制统计量) - CHANGELOG.md:本轮变更
This commit is contained in:
1 parent
319b16b17c
commit
b6dc4bc5a6
6 files changed
+144
-43
No files matched your search
@@ -228,6 +228,51 @@ pythonVersion = "3.13" # 不写就跟着 PATH 上那个 3.14 走
|
||||
**一句诊断口诀**:编辑器说"解析不到包"时,先跑 `pyright --verbose` 看它的
|
||||
`Search paths`,而不是先去 `pip install`。
|
||||
|
||||
**坑 8 —— `df[Index]` 被 pyright 当成 `Series`,`.std()` 的结果于是被判成标量。**
|
||||
|
||||
现象:`data/clean.py` 里
|
||||
|
||||
```python
|
||||
numeric = data[numeric_cols] # numeric_cols = df.select_dtypes(...).columns
|
||||
std = numeric.std()
|
||||
std = std.replace(0, np.nan) # pyright: 无法访问类"float"的属性"replace"
|
||||
```
|
||||
|
||||
运行时 `numeric` 明明是 DataFrame、`std` 明明是按列的 Series,pyright 却报这个错。
|
||||
|
||||
原因:pandas 自带的类型注解里,`DataFrame.__getitem__` 只声明了「`Hashable` → `Series`」
|
||||
这一支。拿一个 `Index` 去索引时它挑中这个重载,`numeric` 就被推断成 `Series | DataFrame`;
|
||||
而 `Series.std()` 返回的是**标量**,两支一取并集就把 `float` 带了进来 —— `float` 当然没有
|
||||
`.replace`。用 `reveal_type` 一问就现原形:
|
||||
|
||||
```text
|
||||
“numeric”的类型为“Series | Unknown | Self@DataFrame”
|
||||
“numeric.std(axis=0)”的类型为“Series | float | Unknown”
|
||||
```
|
||||
|
||||
几个**没用**的修法(都实测过):
|
||||
|
||||
- `numeric.std(axis=0)` —— 接收方本身还是并集,重载照样解析成并集;
|
||||
- `data[list(numeric_cols)]` —— 不改变重载选择;
|
||||
- 把参数改由一个类型明确的 `frame` 局部变量承接 —— `frame` 确实干净了,
|
||||
但 `frame[Index]` 还是同一个重载问题。
|
||||
|
||||
有效的修法是**只在取列子表那一句上按运行时的真值收窄**:
|
||||
|
||||
```python
|
||||
from typing import cast
|
||||
|
||||
numeric = cast(pd.DataFrame, data[numeric_cols])
|
||||
```
|
||||
|
||||
`cast` 零运行时开销,而且能把**下面所有行**留在类型检查范围内 —— 比在 `.replace` 那一行
|
||||
挂 `# pyright: ignore` 好:那样等于连带放弃后面每一行的检查。
|
||||
|
||||
**一句诊断口诀**:pyright 说某行"属性不存在"、但运行时确实能跑,先 `reveal_type(...)`
|
||||
把真实推断打出来,看并集里混进了什么,别急着加 `# type: ignore`。
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 五、两套环境的实际版本(已对齐)
|
||||
|
||||
@@ -114,3 +114,17 @@
|
||||
逐条问"这句话对每种类型都成立吗";不成立的往后挪,
|
||||
挪进 `if isinstance(...)` 里面,让它只有对应的那一支能走到。
|
||||
- 首次:2026-10-02|复发:0
|
||||
|
||||
### 8. 改了一处,忘了同源的另一处
|
||||
|
||||
- 现象:给 `detect_outliers` 加"文本列不参与"的修复时,只把**掩码写入**收窄到数值列
|
||||
(`mask[numeric_cols] = ...`),却让 `data.quantile()` / `data.mean()` / `data.std()`
|
||||
继续在全表上算。测试照样红 —— 文本列在**算统计量**那一步就崩了,跟掩码怎么写没关系。
|
||||
- 为什么会错:眼睛盯着"结果往哪放"(掩码),没回头问"这些数是从哪儿来的"。
|
||||
同一份数据在函数里要过两道加工:**先出统计量(分位数 / 均值 / 标准差),再拿去比较**。
|
||||
只把后一道收窄、前一道还是全表,等于没修。
|
||||
修完测试报告里那条仍是 XFAIL,就是这个信号 —— 报告没变绿,说明根因没抓着。
|
||||
- 以后怎么防:修"某处不该参与计算"这类问题时,顺着数据从入到出**走一遍**,
|
||||
把该收窄的地方一次列全,别改完第一处就收手;
|
||||
并且以测试信号(XFAIL → XPASS)为准,别以"我改的那一行看起来对了"为准。
|
||||
- 首次:2026-10-02|复发:0
|
||||
Reference in new issue
Block a user