fix(data): clean.py 轻度修复 + 摘掉 5 条已修好的 xfail

Codex 直接改的(格式 / 死代码 / 纯 bug,不动接口与语义):
- winsorize(Series) 直接崩:select_dtypes 在 Series 上不存在,Series 分支提到最前并加数值 dtype 守卫
- winsorize 的分位数改为只在数值列上算,含文本列的赛题表不再崩(与已有 numeric_cols 判断对齐)
- 删掉 detect_outliers 里已成空操作的 df = pd.DataFrame(data),去掉 df 别名
- clean_missing 的嵌套 if 合并(ruff SIM102),清掉 W293 行尾空白与函数内连续空行

测试:
- 5 条 xfail 经 XPASS 信号确认已修好,摘掉 mark 转正式用例
  (detect_outliers 4 条 + winsorize 混合类型 1 条)
- 53 passed / 7 xfailed  ->  58 passed / 2 xfailed
- 剩 2 条 xfail 保留,且不是漏改而是设计没定:
  detect_outliers 遇文本列返回什么、impute_missing 的 mode 分支要不要就地改调用方的表
- ruff: 1 error -> All checks passed;pyright: 0 errors 不变

文档:
- STATUS.md:L0 进度 4/12、验证结果表、下一步重写、新增决策 31
  (Codex 可直接改 src 的范围:纯 bug / 格式 / 死代码;要选语义的留给用户)
- CHANGELOG.md:本轮变更,并修掉 [Unreleased] 里被重复追加撑坏的一句
- docs/dev/personal-pitfalls.md:只追加第 7 条(守卫子句要排在类型分派之后)
This commit is contained in:
guzhujushi committed 2026-10-02 15:18:15 +08:00
1 parent c2595b6d07
commit 319b16b17c
5 files changed
+89 -66

No files matched your search

+10 -9
View File
@@ -231,20 +231,24 @@ def test_winsorize_rejects_non_frame_with_typeerror():
def test_detect_outliers_rejects_unknown_method():
"""方法名校验在取随机取值之前,所以这条现在就是绿的。"""
"""方法名校验在取默认取值之前,所以这条现在就是绿的。"""
from mcm_toolkit.data import detect_outliers
with pytest.raises(ValueError, match="bogus"):
detect_outliers([1.0, 2.0], method="bogus") # type: ignore[arg-type]
# ------------------------------- 待修:契约已写进 docstring,实现还没跟上(xfail)
# ------------------- 契约先写进 docstring,实现逐条跟上(剩余 2 条 xfail)
#
# 这些用例写的是**文档里已经承诺的行为**。现在跑是红的,用 xfail 让它们照样出现在
# 报告里(显示成 x);实现修好之后会自动变成 XPASS —— 看到 XPASS 就把 mark 摘掉。
# 这些用例写的是**文档里已经承诺的行为**。实现没跟上时用 xfail,让它们照样出现在
# 报告里(显示成 x);修好之后会自动变成 XPASS —— 看到 XPASS 就把 mark 摘掉。
#
# 已摘掉:threshold 范围校验、IQR 的 UnboundLocalError、(n,1) 形状、zscore 默认阈值、
# winsorize 的文本列。剩下 2 条不是漏改,是**设计还没定**:
# - detect_outliers 遇到文本列返回什么(全 False?还是只对数值列算掩码?)
# - impute_missing 的 mode 分支该不该就地改调用方的表(其余分支都返回新表)
@pytest.mark.xfail(reason="threshold 只在 zscore 分支赋值,iqr 走进去 UnboundLocalError", strict=False)
def test_detect_outliers_finds_planted_points():
"""人工埋 3 个离群点,IQR 法应正好命中 3 个。"""
from mcm_toolkit.data import detect_outliers
@@ -255,7 +259,6 @@ def test_detect_outliers_finds_planted_points():
assert bool(mask[-3:].all())
@pytest.mark.xfail(reason="返回 (n,1),契约是「与原数据同形」", strict=False)
def test_detect_outliers_returns_mask_not_filtered_data():
"""约定:只标不删——返回与原数据同形的布尔掩码。"""
from mcm_toolkit.data import detect_outliers
@@ -266,7 +269,6 @@ def test_detect_outliers_returns_mask_not_filtered_data():
assert mask.dtype == bool
@pytest.mark.xfail(reason="zscore 的 threshold 被按 [0,1] 校验,默认 3.0 直接被拒", strict=False)
def test_detect_outliers_zscore_flags_planted_point():
from mcm_toolkit.data import detect_outliers
@@ -285,8 +287,8 @@ def test_detect_outliers_survives_text_columns():
assert int(np.asarray(detect_outliers(df)).sum()) == 1
@pytest.mark.xfail(reason="quantile 在全表上算,遇到文本列直接崩", strict=False)
def test_winsorize_survives_text_columns():
"""文本列必须原样留着:分位数只在数值列上算。"""
from mcm_toolkit.data import winsorize
df = pd.DataFrame({"city": ["a", "b", "c", "d"], "v": [1.0, 2.0, 3.0, 1000.0]})
@@ -295,7 +297,6 @@ def test_winsorize_survives_text_columns():
assert float(out["v"].iloc[-1]) < 1000.0
@pytest.mark.xfail(reason="threshold 没做范围校验,传 1.5 会变成「什么都不删」", strict=False)
def test_clean_missing_rejects_out_of_range_threshold():
from mcm_toolkit.data import clean_missing