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
@@ -8,7 +8,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import Any, Literal
|
||||
from typing import Any, Literal, cast
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
@@ -147,6 +147,9 @@ def detect_outliers(data: Any, method: Literal["iqr", "zscore"] = "iqr", **kwarg
|
||||
**只标不删**是刻意的:删点是有代价的判断,必须由人来做,
|
||||
并且要在论文里写清楚删了几个、依据是什么。
|
||||
|
||||
非数值列**不参与统计**,掩码里恒为 `False`:均值和分位数对文本没有意义,
|
||||
但"这一列没有离群点"比"整张表算不出来"有用得多。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> mask = detect_outliers([1, 2, 3, 4, 100], method="iqr")
|
||||
@@ -168,22 +171,29 @@ def detect_outliers(data: Any, method: Literal["iqr", "zscore"] = "iqr", **kwarg
|
||||
else:
|
||||
raise ValueError(f"不支持的方法:{method},仅支持'iqr'或'zscore'")
|
||||
|
||||
numeric_cols = data.select_dtypes(include=['number']).columns
|
||||
mask = pd.DataFrame(False,index=data.index,columns=data.columns)
|
||||
# 取列子表得到的一定是 DataFrame,但 pandas 的类型存根只声明了「Hashable -> Series」,
|
||||
# pyright 于是把 numeric 当成 Series,再把 `.std()` 的返回当成标量、判 `.replace` 非法。
|
||||
# 这里按运行时的真值显式收窄,让下面几行继续参与类型检查。
|
||||
numeric = cast(pd.DataFrame, data[numeric_cols])
|
||||
|
||||
if method == 'iqr':
|
||||
Q1 = data.quantile(0.25)
|
||||
Q3 = data.quantile(0.75)
|
||||
Q1 = numeric.quantile(0.25)
|
||||
Q3 = numeric.quantile(0.75)
|
||||
IQR = Q3 - Q1
|
||||
lower_bound = Q1 - k * IQR
|
||||
upper_bound = Q3 + k * IQR
|
||||
|
||||
mask = (data < lower_bound) | (data > upper_bound)
|
||||
mask[numeric_cols] = (numeric < lower_bound) | (numeric > upper_bound)
|
||||
|
||||
elif method == 'zscore':
|
||||
mean = data.mean()
|
||||
std = data.std()
|
||||
mean = numeric.mean()
|
||||
std = numeric.std()
|
||||
|
||||
std = std.replace(0, np.nan)
|
||||
z_scores = (data - mean) / std
|
||||
mask = z_scores.abs() > threshold
|
||||
z_scores = (numeric - mean) / std
|
||||
mask[numeric_cols] = z_scores.abs() > threshold
|
||||
|
||||
if is_series:
|
||||
return mask.iloc[:,0]
|
||||
|
||||
Reference in new issue
Block a user