src/stats/descriptive.py 由用户本人实现,这里做审查 + 代修。
describe:
- 真 bug:df_num.describe().T 把表转反(行=变量、列=指标),而 docstring 与
test_describe_reports_expected_keys 定的是"行=指标、列=变量"。转置后
.loc[指标名] = <按变量索引的 Series> 与列名零交集,pandas 不报错,直接把
missing / missing_ratio / skewness / kurtosis 四行整行填成 NaN(静默丢数据);
weights 分支则报 cannot set a row with mismatched columns。去掉 .T 一处修好三件事
- 补 median 行(pandas 只有 50%,测试契约定的是 median)
- 入参放宽到 list / tuple / ndarray(00_quickstart.py 传的就是 ndarray)
- weights 校验补齐:维度 / 长度 / NaN / 负值 / 和为 0(原先和为 0 时裸抛 ZeroDivisionError)
- docstring 按 4.7 补 Parameters / Returns / Raises
zscore: 修 RET505;docstring 补齐;DataFrame 取列处按坑 8 用 cast 收窄
coefficient_of_variation: 均值为 0 改抛 ValueError(取值问题不是类型问题);
空数组提前报错(原先静默 nan + RuntimeWarning);错别字与文案修正
tests: 去掉 test_stats.py 的模块级 pytestmark —— 它把 6 条测试全部跳过(含已实现的
describe),正是决策 26 要禁的写法。改为逐条 skip 未实现的 5 条,并新增 20 条用例。
验证:pytest 102 passed / 39 skipped(原 82/40);ruff All checks passed;pyright 0 errors。
examples/01_clean_and_describe.py 填掉 TODO(L0),实测 exit 0。
docs: STATUS.md L0 8/12 + 决策 35/36 + 三处待拍板口径;CHANGELOG 记录;
personal-pitfalls 追加第 11 条(pandas 对不齐就静默填 NaN)
64 lines
2.4 KiB
Python
64 lines
2.4 KiB
Python
"""data + stats 最小用法:读 → 清 → 补 → 判离群 → 缩尾 → 描述。
|
||
|
||
每一步都对应 `tests/test_data.py` 里的一条用例;体量刻意压到最小。
|
||
`stats.describe` 收尾:行 = 指标、列 = 变量,可直接贴进论文。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import tempfile
|
||
from pathlib import Path
|
||
|
||
import numpy as np
|
||
import pandas as pd
|
||
|
||
import mcm_toolkit as mcm
|
||
|
||
|
||
def main() -> None:
|
||
mcm.set_seed(20260101)
|
||
|
||
# 1) 假装这是从 Excel 导出的赛题数据:带文本列、带缺失、带两个录入错误
|
||
df = pd.DataFrame(
|
||
{
|
||
"city": ["A", "B", "C", "D", "E", "F"],
|
||
"gdp": [1000.0, 1100.0, np.nan, 1300.0, 1400.0, 9999.0],
|
||
"pop": [10.0, 11.0, np.nan, np.nan, 20.0, 15.0],
|
||
"notes": ["ok", "ok", np.nan, "ok", np.nan, "异常"],
|
||
}
|
||
)
|
||
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
path = Path(tmp) / "demo.csv"
|
||
|
||
# 2) 写盘再读回:load_table 按扩展名分派,.csv 会自动探测编码
|
||
mcm.data.save_table(df, path)
|
||
loaded = mcm.data.load_table(path)
|
||
print(f"读回 {loaded.shape[0]} 行 × {loaded.shape[1]} 列")
|
||
|
||
# 3) 缺失太多的行先删:缺失比例 > 0.5 的走人(4 列,阈值 0.5 -> 至少要 2 个非空)
|
||
kept = mcm.data.clean_missing(loaded, axis=0, threshold=0.5)
|
||
print(f"按缺失比例删行:{loaded.shape[0]} -> {kept.shape[0]}")
|
||
|
||
# 4) 剩下的缺失做填补。mean 只作用于数值列,notes 列原样保留缺失
|
||
filled = mcm.data.impute_missing(kept, method="mean")
|
||
print("填补后每列缺失数:", filled.isna().sum().to_dict())
|
||
|
||
# 5) 找离群点:只标不删,删不删由人决定
|
||
mask = mcm.data.detect_outliers(filled, method="iqr", k=1.5)
|
||
print("检出离群点:")
|
||
print(mask)
|
||
print(f"共 {int(mask.sum().sum())} 个;notes 列恒 False(非数值列不参与统计)")
|
||
|
||
# 6) 不删也能用:缩尾把极端值压到分位点上,而不是丢掉整行
|
||
w = mcm.data.winsorize(filled, lower=0.01, upper=0.99)
|
||
print(f"gdp 最大值 {filled['gdp'].max():.1f} -> 缩尾后 {w['gdp'].max():.1f}")
|
||
|
||
# 7) 描述统计:行 = 指标、列 = 变量,可直接贴进论文
|
||
table = mcm.stats.describe(filled[["gdp", "pop"]])
|
||
print(table.round(2))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|