Files
MCM-ICM_Toolkit/examples/01_clean_and_describe.py
guzhujushi e9624cb3a7 feat(stats): describe 落地,修掉一个静默填 NaN 的 bug(L0 8/12)
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)
2026-10-03 13:17:58 +08:00

64 lines
2.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()