Files
MCM-ICM_Toolkit/CHANGELOG.md
T
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

17 KiB
Raw Blame History

更新日志

格式参考 Keep a Changelog, 版本号参考 语义化版本。

[Unreleased]

Added

  • 仓库骨架:src/mcm_toolkit/ 7 个包(core / data / stats / models / solve / decision / viz)

  • 顶层 __init__.py 只导出 7 个子包 + 8 个快捷函数,__all__ 即公开 API 清单

  • core.Result:唯一的结果对象(替代原建议里的 5 个 Result 类)

  • docs/01-suggestion-review.md:对 ChatGPT 建库建议的审查与精简(14 包 → 7 包,~250 函数 → ~40)

  • docs/02-architecture.md:包边界、Result / RNG / fit-predict-score 协议、依赖分层

  • docs/03-roadmap.md:L0/L1/L2 分级 + 永久不做清单

  • docs/04-environment.md:conda / venv 配置与三个已踩的坑

  • docs/dev/function-card-template.md:S 档算法的函数卡片模板

  • .venv(基于 Anaconda base,--system-site-packages)+ 可编辑安装 —— 10.5 MB,离线可用

  • conda 环境 mcm-toolkit(D:\anaconda\envs\mcm-toolkit)—— Python 3.13.15,含 SALib / pymoo / pyright

  • scripts/setup-conda.ps1 / scripts/setup-venv.ps1 / scripts/doctor.ps1

  • environment.yml:需联网时的精确复现方案

  • pyproject.toml:核心依赖只 4 个,其余走 extras

  • docs/02-architecture.md §4.7「类型约定」:注解要写全、numpy 标量的判断写法、 非法类型必须抛 TypeError。配套在 docs/03-roadmap.md 的开发纪律里加了指针。

  • docs/dev/personal-pitfalls.md:个人易错点清单,只追加、不通读

  • data.clean:四个函数落地(由用户本人实现)——clean_missing(L0 第 4 项)、 impute_missing / detect_outliers / winsorize(原属 L1,顺手写了)。

  • tests/test_data.py:新增 13 条 clean 行为测试,覆盖 clean_missing 的阈值语义与轴向、 impute_missing 的 mean/ffill/mode 与全 NaN 列、winsorize 的分位点与异常类型。

  • tests/test_data.py:另加 7 条 xfail,把已知但未修的契约钉在测试报告里(见 Notes)。

  • docs/algorithms/detect_outliers.md:第一张 S 档函数卡片(roadmap §3.1 要求 约 15 张,这是第 1 张)。除模板九节外,额外记了三条容易踩的判断:为什么不返回 Result、 非数值列恒 False 的两条备选路各错在哪、以及 ddof 口径与 stats.zscore 不一致。

  • examples/01_clean_and_describe.py:load_table / save_table / clean_missing / impute_missing / detect_outliers / winsorize 六个函数的最小用法,实测跑通(exit 0); 末尾留 TODO(L0) 等 stats.describe。

  • tests/test_data.py:data 的 transform 段 18 条用例 —— normalize 的三种 method、 feature_range 区间与两条校验、常量列不出 nan、docstring 那个 Examples 的精确值、按列语义; standardize 的按列与 ddof(用解析关系钉住:ddof=1 时结果的 std = sqrt((n-1)/n)); log_transform 的定义域平移;one_hot_encode 的自动识别、drop_first、列不存在报错。 其中两条是把原先挂着的 skip 转为实跑(见坑 9 / 个人易错点 9)。

  • tests/test_stats.py:新写 20 条用例 —— describe 的方向约定(行 = 指标、列 = 变量)、 missing / missing_ratio / skewness / kurtosis 四行不能是 NaN、非数值列自动跳过、 接受 ndarray / list / Series、加权均值与加权标准差对照手算公式、5 种非法 weights; zscore 的手算对照 / 按列 / 常量列 / 非法类型;coefficient_of_variation 的手算对照、 单列 DataFrame、均值为 0 抛 ValueError、空数组与二维输入。

  • examples/01_clean_and_describe.py:填掉末尾的 TODO(L0),加一段 describe 输出(实测 exit 0)。

Changed

  • core.get_rng / core.set_seed:实现(L0 第 2 项)。get_rng 收 int / np.integer / Generator / None,其余类型抛 TypeError;无参调用复用同一个会推进的共享生成器。 set_seed 同时覆盖 numpy 旧全局状态,让 sklearn / statsmodels 也可复现。

  • core.Result:补上 __repr__ 与 summary() 实现(L0 第 1 项落地)。 repr 紧凑单行且不打印 data 内容;summary() 输出可直接贴进论文的 Markdown 表格。

  • data.load_table:实现(L0 第 3 项)。按扩展名分派 .csv / .xlsx / .xls; .csv 默认做编码探测(utf-8 → utf-8-sig → gbk → gb18030),显式传 encoding= 时不再探测; na_values 默认吃下空串 / NA / N/A / #N/A / null / -。

  • data.save_table:实现(原属 L1,顺手落地)。按扩展名分派,父目录不存在时自动创建, 返回实际写入的 Path。

  • data.load_table / data.save_table:docstring 补 Parameters / Raises (docs/02-architecture.md §4.7 对每个公开函数的硬要求)。

  • data.load_table 的返回注解 Any → pd.DataFrame,与 docstring 的承诺对齐。

  • data.clean:docstring 按 docs/02-architecture.md §4.7 补全 Parameters / Returns / Raises (Codex 代写,与 io.py 同一套 numpydoc 格式)。

  • data.clean:ruff 从 6 条清到 1 条——导入分组(I001)、elif 链改 if 加末尾无条件 return (RET505 / RET503)、去掉多余 f 前缀(F541)、删掉只会被覆盖的 mask 初始化(死代码)、 去掉行尾空白(W293)。

  • pyright 报"无法解析导入 numpy / pandas"(17 errors)的原因:分析器会去 PATH 上找 python, 本机 PATH 上只有 Codex 运行时的 3.14 shim(无科学栈),且 pyright 不认 pyvenv.cfg 的 include-system-site-packages。 改为 [tool.pyright] 指定 venvPath = "." / venv = ".venv" / pythonVersion = "3.13",并让 scripts/setup-venv.ps1 往 .venv 里写一个 指向 base site-packages 的 .pth。pyright 17 errors → 0。 详见 docs/04-environment.md 坑 7。

  • tests/test_data.py:df["b"].isna().all() 的类型是 Series | bool, 直接当断言条件 pyright 会报"条件操作数无效",外面套 bool(); 两条故意传错类型的用例加 # type: ignore[arg-type]。

  • data.save_table 的后缀校验排在 path.parent.mkdir(...) 之后,传 .txt 会先留下一个 空目录再抛 ValueError。改为先校验、后副作用,并加回归测试。

  • data/io.py 里 __all__ 与下一个 def 之间只有 1 个空行(PEP 8 要 2 个)。

  • tests/test_data.py 顶上的模块级 pytestmark = pytest.mark.skip(...) 会连带跳过同文件里 已经写好的函数,导致 load_table 零覆盖。改为逐条 skip 未实现的算法,并补 8 条 io 测试 (编码探测、NA 归一、后缀校验、缺文件报错、往返读写、类型校验)。测试数 31 → 39 passed。

  • conda base 里 zstandard 的 *.conda_trash 残骸目录被 Python 当成 namespace package 导入, 使 .venv(= base)的 pd.read_csv 抛 ImportError: Can't determine version for zstandard。 已把残骸目录改名禁用(可逆,未删除),详见 docs/04-environment.md 坑 6。

  • data.clean 第二轮审查(2026-10-02),Codex 直接修掉 4 处轻度问题: winsorize(Series) 因 select_dtypes 在 Series 上不存在而直接崩(Series 分支提到最前, 并加数值 dtype 守卫);winsorize 的 data.quantile() 改为只在数值列上算 (文本列不再崩,与已有的 numeric_cols 判断对齐);删掉 detect_outliers 里已成空操作的 df = pd.DataFrame(data)(数据在更早处就转过了,顺手去掉 df 这个别名); 清掉行尾空白(W293)、函数体内的连续空行,并把 clean_missing 的嵌套 if 合并 (SIM102,ruff 最后 1 条)。ruff 1 error → All checks passed,pyright 仍 0。

  • tests/test_data.py:5 条 xfail 经 XPASS 信号确认已修好,摘掉 mark 转正式用例 (detect_outliers 的 4 条 + winsorize 混合类型 1 条)。测试 53 passed / 7 xfailed → 58 passed / 2 xfailed。

  • CHANGELOG.md:[Unreleased] 里有一句被重复追加撑坏 ("…删掉它即全绿。:分析器会去 PATH 上找 python,"),已修。

  • data.detect_outliers 收尾(2026-10-02,由用户本人实现):统计量(quantile / mean / std)与掩码写入都收窄到数值列,含文本列的赛题表不再崩; 非数值列在掩码里恒 False(用户本人定的语义,见决策 32)。docstring Notes 补上这条语义。

  • data.detect_outliers 里 data=data.copy() 是死代码:下面的 to_frame() / pd.DataFrame() 本来就返回新对象,函数全程也没原地改过 data,白白复制一份数据;而且它排在类型分派 之前,会把错误入参的报错从 ValueError 变成 AttributeError。已删。

  • data.detect_outliers:删掉那行死代码后 pyright 反而报了 1 个真错 —— df[Index] 被类型存根当成 Series,.std() 于是被判成标量、std.replace(...) 非法。 在取列子表处用 cast(pd.DataFrame, ...) 按运行时真值收窄。详见 docs/04-environment.md 坑 8。

  • tests/test_data.py: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。

  • docs/dev/personal-pitfalls.md:追加第 8 条(限制掩码 ≠ 限制统计量)。

  • data.impute_missing 的 mode 分支改为返回新表(原先 data[col] = ... 会就地改调用方的 DataFrame,另外四支都返回新对象)。最后一条 xfail 随之转正,测试里 xfail 清零。

  • data.detect_outliers 补入参类型校验(§4.7:非法类型抛 TypeError,不静默兜底): 接受 DataFrame / Series / list / tuple / ndarray。tuple 同时补进 is_ld_array, 与 list 同等对待;dict 不再被接受(以前靠 pd.DataFrame() 碰巧能过,见决策 33)。

  • tests/test_data.py:摘掉最后一条 xfail(mode 就地改)并加强断言;新增 3 条 (非法类型抛 TypeError、元组按一维数组型处理、全 NaN 列与单元素列不抛异常)。 测试 60 passed / 1 xfailed → 64 passed / 0 xfailed。

  • data.transform 四个函数实现(由用户本人写):normalize(minmax / vector / max,均按列)、 standardize(按列,ddof 真的传下去)、log_transform(offset=None 时自动取 1 - min(x))、 one_hot_encode。data/ 下的 L0 至此结清。

  • data.transform:Codex 清掉 5 条 ruff —— RET503(normalize 末尾缺无条件 return)、 F541(无占位符的 f-string)、RET504(多余的 result 中转)、RET505(return 后的 elif)、 SIM108(log_transform 的 if/else 改三元)。

  • data.transform.normalize:supported_method 与 feature_range 的校验提到 np.asarray 之前 (§4.7 的"守卫集中在开头");并修掉说反的报错文案 —— 条件是要求下界 < 上界, 原文却写"上界必须小于下界",且 TypeError 那条用了无占位符 f-string。

  • data.transform.one_hot_encode:自动识别类别列时显式加上 'str' / 'string'。 原来只写 ['object', 'category'],在 pandas 3 上是已弃用路径(每次调用发 Pandas4Warning), 未来会变成一列都选不中的静默错误。详见坑 9。

  • 测试计数:64 passed → 82 passed(新增 18 条,另两条 skip 转为实跑),skip 42 → 40。

  • data.transform:修掉上一版引入的 3 处真 bug(Codex 代修,用户授权"报错顺手修掉")。 症状是 7 条测试变红 + pyright 在 transform.py:105 / :133 报 reportArgumentType。 这不是编辑器误报 —— pyright CLI 与 pytest 都能复现,所以一行 # type: ignore 都没加:

    • 输入白名单漏了 pd.DataFrame:standardize(df) / log_transform(df) 抛 TypeError, 函数体里的 is_df 分支全是死代码。白名单统一成模块级 _NUMERIC_TYPES 并移到 np.asarray 之前。
    • normalize 的三个 method 分支互相覆盖:minmax / vector 算完的结果被末尾无条件的 x / max 冲掉,三种 method 实际都只走 max(test_normalize_* 5 条因此变红)。改成 if / elif / else。
    • one_hot_encode 把 get_dummies 的结果按 data.columns 重包一层 DataFrame: 哑变量列名(city_A)与原列名(city)对不上 → 编码列被整列丢掉。改为直接返回 get_dummies 结果。
    • log_transform 的 offset 守卫条件写反(offset > 0 就抛错,恰好把唯一合法用例拒掉), 改为校验定义域前提 min(x) + offset > 0;offset=0 对全正数据合法(见决策 35)。
  • data.transform 四个函数的 docstring 按 docs/02-architecture.md §4.7 补齐 Parameters / Returns / Raises(Codex 代写,与 io.py / clean.py 同一套 numpydoc 格式), 并把常量列行为(决策 34)写进 Notes。

  • data.transform:口径定稿后 ruff 剩的 4 条 RET505(return 后的 elif)一并清掉 —— All checks passed;pyright 2 errors → 0 errors;测试 7 failed / 75 passed → 82 passed / 40 skipped。

  • stats.describe:实现落地(由用户本人写,Codex 审查 + 代修),并修掉一个静默 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。

  • stats.zscore:修 RET505;docstring 按 §4.7 补齐;DataFrame 取列处按坑 8 用 cast 收窄 (pyright 的 DataFrame | Series 重载)。

  • stats.coefficient_of_variation:均值为 0 由 TypeError 改为 ValueError (那是取值问题不是类型问题,§4.7);空数组提前抛 ValueError(原先静默返回 nan + RuntimeWarning); 报错文案里"异变系数"的错别字与 "N 维数组" 改为 "N 列"。

  • tests/test_stats.py:去掉模块级 pytestmark = pytest.mark.skip —— 它把该文件 6 条测试 全部跳过(包括已经实现好的 describe),正是决策 26 要禁的写法:实现零覆盖而测试全绿。 改为逐条 skip 未实现的 5 条。

  • 测试计数:82 passed / 40 skipped → 102 passed / 39 skipped(新增 20 条); ruff 7 error → All checks passed;pyright 1 error → 0。

Notes

  • detect_outliers 原有的三个 bug 加一处设计空白已全部收口:threshold 范围校验挪进 clean_missing、is_series / is_ld_array 提到类型转换之前、统计量与掩码都限制在数值列、 非数值列恒 False(决策 32)、入参类型校验补上(决策 33)。 S 档卡片与 examples/ 用法也已落地 —— detect_outliers 这条线结清。
  • transform.py 的 4 个口径已全部收口:① 三个函数共用 _NUMERIC_TYPES 白名单(含 DataFrame); ② 全部接受 DataFrame 且保持"DataFrame 进 DataFrame 出";③ 显式 offset 按定义域校验 (min(x) + offset > 0,见决策 35,待用户确认);④ 常量列行为写进 docstring 的 Notes; 另外 Parameters / Returns / Raises 已按 §4.7 补齐。
  • 新增易错点第 10 条:把真报错当成"编辑器误报" —— 红线先跑 CLI 与 pytest 再判断(docs/dev/personal-pitfalls.md)。
  • stats 这条线开工了:describe 落地,并填掉 examples/01_clean_and_describe.py 的 TODO(L0)。
  • 还余三处口径待用户拍板:log_transform 的显式 offset、zscore 丢掉非数值列、 coefficient_of_variation 在均值为负时返回负值。见 STATUS.md「下一步」第 4 条与决策 35/36。
  • 已实现函数的 examples/ 最小用法仍不全:00_quickstart.py 要等 bootstrap_ci / viz, Result 的用法届时并进去。
  • 已知缺口:
  • 除 core.Result、core.get_rng、core.set_seed、data.load_table、data.save_table 外,其余函数体尚未实现(统一 raise NotImplementedError + TODO(L0/L1/v0.2) 标记)。 这是刻意的:先定接口,再按 docs/03-roadmap.md 的 L0 → L1 顺序填实现。
  • 已知缺口:SALib(Sobol/Morris)、pymoo、pyright 已装进 conda 环境 mcm-toolkit,.venv 里没有。

[0.0.1] - 2026-10-01

Added

  • 项目初始化,包结构落盘(实现为空)。