Files
MCM-ICM_Toolkit/STATUS.md
T
guzhujushi 4552aee41c fix(data): 修掉 transform.py 的 3 处真 bug,并收口 4 个口径
不是编辑器误报:pyright CLI 与 pytest 都能复现(修前 7 failed / 75 passed),
所以一行 # type: ignore 都没加。

- 输入白名单漏了 pd.DataFrame -> standardize/log_transform 直接 TypeError,
  函数体里的 is_df 分支全是死代码;统一成模块级 _NUMERIC_TYPES 并前置到 np.asarray 之前
- normalize 三个 method 分支互相覆盖(minmax/vector 的结果被末尾无条件的 x/max 冲掉,
  实际三种 method 都只走 max),改成 if/elif/else
- one_hot_encode 按 data.columns 重包一层 DataFrame -> 哑变量列名对不上,编码列整列丢失,
  改为直接返回 get_dummies 的结果
- log_transform 的 offset 守卫写反(offset > 0 就抛错,恰好拒掉唯一合法用例),
  改为校验定义域前提 min(x) + offset > 0;offset=0 对全正数据合法
- 四个函数 docstring 按 docs/02-architecture.md 4.7 补齐 Parameters/Returns/Raises
- ruff 4 条 RET505 清掉;pyright 2 errors -> 0;测试 82 passed / 40 skipped

docs: STATUS.md L0 进度 7/12 + 决策 35;CHANGELOG 记录本轮回归与修复;
      personal-pitfalls 追加第 10 条(把真报错当成编辑器误报)
2026-10-02 21:48:27 +08:00

23 KiB
Raw Blame History

STATUS — 当前进度

更新日期:2026-10-02

本次任务

第一阶段(已完成):审查并精简 ChatGPT 的建库建议 → 生成骨架文件 → 配置 conda 与 venv 环境 → 初始化 git 仓库 → 接入 Gitea 远端。 两套环境均已建成并通过验证,仓库已推送到 code.guzhujushi.cn。

第二阶段(进行中):按 docs/03-roadmap.md 的 L0 → L1 顺序逐个实现函数, 用户本人写 src/、Codex 负责审查 + tests/(决策 25)。 当前 7/12:Result、get_rng/set_seed、load_table/save_table、clean_missing、detect_outliers、normalize/standardize。

任务清单

✅ 已完成

A. 建议审查与精简

  • 通读 chatGPT_Suggestion.md(33 节,约 2.4 万字),输出 docs/01-suggestion-review.md
    • 总裁决:方向对,量级错
    • 14 个包 → 7 个;约 250 个函数 → 约 40 个;5 个 Result 类 → 1 个
    • 单函数文档要求 10 项 → 2 档(只有约 15 个招牌算法写完整卡片)
    • 逐项给出"砍什么 / 为什么 / 替代什么",并补上原文缺失的六件事 (论文产出链路、指标方向与量纲、时间预算、可复现性、测试测什么、离线约束)

B. 骨架文件生成(76 个文件,约 6800 行)

  • docs/02-architecture.md 架构、docs/03-roadmap.md 路线图与永久不做清单
  • docs/04-environment.md 环境文档、docs/dev/function-card-template.md 卡片模板
  • src/mcm_toolkit/ 7 个包 23 个模块(只有签名 + docstring + NotImplementedError)
  • tests/ 9 个文件、examples/ 4 个、problems/ notebooks/ figures/ tables/
  • pyproject.toml(核心依赖 4 个 + 6 组 extras)、environment.yml
  • .gitignore .gitattributes .editorconfig LICENSE CHANGELOG.md README.md
  • 脚本 scripts/setup-venv.ps1 setup-conda.ps1 doctor.ps1

C. 环境配置

  • venv 已建好:.venv(10.5 MB,Anaconda base + --system-site-packages) + pip install -e . --no-deps --no-build-isolation; 可直接导入 numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6 / matplotlib 3.11.2 / sklearn 1.9.1 / statsmodels 0.15.0 / networkx 3.6.1 / sympy 1.14.0 (版本随 base 走,base 升级后自动同步)
  • conda 环境已建好:D:\anaconda\envs\mcm-toolkit(1.55 GB,Python 3.13.15) - 科学栈:numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6 / matplotlib 3.11.2 / sklearn 1.9.1 / statsmodels 0.15.0 / networkx 3.6.1 / sympy 1.14.0(MKL 后端) - 已补装 pip-only 依赖:SALib、pymoo 0.6.2、pyright 1.1.414 - 已在本项目中可编辑安装 - 实测耗时 868 秒,下载约 3 GB
  • Anaconda base 已升级 2025.12 → 2026.07(365 个包,下载 638 MB,耗时 1033 秒) - conda 自身 25.11.1 → 26.7.3(实测复核) - 并把项目环境 numpy 对齐到 base 的 2.4.6,两套环境数值栈现已完全一致(MKL 后端) - 升级前已用 conda list -n base --revisions 留了回滚点
  • 环境脚本按实测结果重写:setup-conda.ps1 默认改为精准包列表 (--clone base 实测会走完整下载安装,不再是"离线快捷方式")

D. Git 仓库

  • git init -b main,core.autocrlf=false,首次提交 5319589(76 文件 / 6812 行),工作区干净
  • git ls-files --eol 复核:全部 LF,.gitattributes 生效
  • 仓库由本人账户 guzhu 创建(见决策 17)
  • 远端 Gitea 已接通并推送:origin = git@code.guzhujushi.cn:guzhujushi/MCM-ICM_Toolkit.git, main 跟踪 origin/main,远端 HEAD = 96080b9,76 个文件已在服务器上(见决策 19/20)
  • 推送通道:公网 22 被防火墙过滤 → 改用 Tailscale(100.115.86.48:22), ~/.ssh/config 加了一条域名别名,remote 里仍写域名

E. L0 实现进度(7/12)

  • core/result.py 的 Result - __repr__:单行紧凑;data 只给类型与规模,绝不整数组打印 (十万级数组的 repr 实测仍 < 200 字符);success 恒在 - summary():Markdown 表格,可直接贴进论文; 行序 success → params.* → metrics.* → extras.*; float 走 .6g,| 转义,message 以引用行附在表后;幂等无副作用 - 测试:tests/test_core.py 去掉 skip + 新增 3 条 (默认值/失败路径/大数组、Markdown 结构、可重复调用)
  • core/random.py 的 get_rng / set_seed(设计由用户本人写,Codex 补齐类型校验) - 无参 get_rng() 复用同一个模块级生成器且会推进:既保证重跑一致, 又避免两个都省略 random_state 的函数抽到同一串数(原决策 24 的取舍已消掉) - get_rng 收 int / np.integer / Generator / None,其余类型抛 TypeError: np.int64 不是 int 的子类,曾因此静默退回全局流 - set_seed 同时调 np.random.seed,覆盖 sklearn / statsmodels 走的 numpy 旧全局 (全项目唯一碰旧接口的地方,带 # noqa: NPY002 与理由) - 测试:TestRandom 共 7 条(可复现、透传、无参跟随、旧全局、np.integer、非法类型、共享推进)
  • data/io.py 的 load_table(+ save_table,原属 L1,顺手落地) - 实现由用户本人写(决策 25),Codex 只做审查 + 补测试 - .csv / .xlsx / .xls 按扩展名分派;.csv 默认编码探测 utf-8 → utf-8-sig → gbk → gb18030,显式传 encoding= 时不再探测 - na_values 默认吃下空串 / NA / N/A / #N/A / null / - - 不支持的扩展名抛 ValueError;文件不存在抛带路径的 FileNotFoundError - save_table 的校验顺序已修正:后缀判断提到 mkdir 之前 (此前传 .txt 会先留下一个空目录再抛错) - 测试:tests/test_data.py 由 1 条(且被模块级 skip 掉)扩到 9 条 - docstring 的 Parameters / Raises 已按 §4.7 补齐(Codex 代写,用户授权)—— numpydoc 格式,对齐参考实现 core/random.py 的 get_rng
  • data/clean.py 的 clean_missing(L0 第 4 项) - 实现由用户本人写,Codex 只做审查、补测试与文档 - threshold 用 math.ceil 算 min_non_na:3 列、阈值 0.5 时必须是 2, 写 int() 截断会漏删 - 顺手落地兄弟函数 impute_missing / winsorize(原属 L1)
  • data/clean.py 的 detect_outliers(L0 第 5 项,S 档,实现由用户本人写) - 语义由用户本人定:非数值列不参与统计,掩码里恒 False(见决策 32) - 三轮修掉的 4 个 bug:threshold 范围校验放错函数、IQR 上界符号、 is_series / is_ld_array 判在类型转换之后(永远为 False)、文本列上算分位数直接崩 - 关键教训:限制掩码 ≠ 限制统计量 —— 只把 mask[numeric_cols] = ... 收窄是不够的,quantile() / mean() / std() 也要一起收窄(见坑 8 / 个人易错点 8) - 返回形状与输入同形;Series 进 Series 出(列名保留)、一维数组进一维布尔数组出 - S 档函数卡片已写:docs/algorithms/detect_outliers.md (含"为什么不返回 Result"、非数值列恒 False 的两条备选路、ddof 口径不一致等) - 测试:tests/test_data.py 该函数共 9 条
  • data/clean.py 的两处收尾(Codex 代写,用户授权) - impute_missing 的 mode 分支改为返回新表(原先就地改调用方的表,与另外四支不一致) —— 最后一条 xfail 随之转正,xfail 清零 - detect_outliers 补入参类型校验(§4.7 要求非法类型抛 TypeError): 接受 DataFrame / Series / list / tuple / ndarray,dict 不再接受(决策 33)
  • examples/01_clean_and_describe.py:load_table / save_table / clean_missing / impute_missing / detect_outliers / winsorize 六个函数的最小用法,实测可直接跑通
  • data/transform.py 的四个函数(由用户本人实现):normalize / standardize(L0 第 6、7 项)、 log_transform / one_hot_encode(原属 L1,顺手写了) - normalize:三种 method(minmax / vector / max)都按列;feature_range 校验下界 < 上界; 常量列压到下界而不出 nan(决策 34) - standardize:按列,ddof 真的传下去 —— 测试用解析关系钉住 (ddof=0 → std 恰为 1;ddof=1 → sqrt((n-1)/n) = 0.866) - log_transform:offset=None 时自动取 1 - min(x),保证 x + offset >= 1 - one_hot_encode:自动识别的 dtype 集合已显式补上 'str' / 'string'(见坑 9) - Codex 清掉 5 条 ruff(RET503 / F541 / RET504 / RET505 / SIM108),并把 feature_range 的两条校验提到 np.asarray 之前、修掉说反的报错文案(条件是"下界必须小于上界") - 测试:新写 18 条,其中两条是把原先挂着的 skip 转为实跑 —— 见 tests/test_data.py 的 transform 段
  • transform.py 的 4 个口径已收口(2026-10-02,Codex 代修,用户授权"报错顺手修掉"): - 输入白名单统一成模块级 _NUMERIC_TYPES,DataFrame 补进去并移到 np.asarray 之前 (此前 DataFrame 不在白名单 → standardize(df) / log_transform(df) 直接 TypeError, 函数体里所有 is_df 分支都是死代码) - 三个函数都保持"DataFrame 进 DataFrame 出"(列名 + 索引原样带回),Series 进 Series 出 - normalize 的三个 method 分支原来互相覆盖(minmax / vector 算完的结果被末尾 无条件的 x / max 冲掉,三种 method 实际都只走 max),改成 if / elif / else - one_hot_encode 不再拿 data.columns 重包一层(哑变量列名对不上 → 编码列整列丢失) - log_transform 的 offset 守卫写反了(offset > 0 就抛错,恰好拒掉唯一合法用例), 改成校验真正的定义域前提 min(x) + offset > 0(见决策 35) - docstring 按 §4.7 补齐 Parameters / Returns / Raises(Codex 代写,与 io.py 同一套 numpydoc 格式) - 口径定稿后 ruff 4 条 RET505 一并清掉 → All checks passed;pyright 2 errors → 0
  • 剩余 5 项:describe correlation_matrix bootstrap_ci monte_carlo save_figure

验证结果

检查 venv conda mcm-toolkit
pytest 见下(单列) 82 passed, 40 skipped(0 xfail)
import mcm_toolkit 0.0.1 / 16 项 0.0.1 / 16 项
ruff check All checks passed All checks passed(0 error)
scripts/doctor.ps1 跑通 —
SALib / pymoo / pyright 缺失 已装
  • pyright(分析目标 = 工作区 .venv):0 errors, 0 warnings (修配置之前是 17 errors,全是"无法解析导入 numpy / pandas",见决策 29 与 docs/04-environment.md 坑 7)
  • base 亦已复核:numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6(与两套环境一致)
  • 122 条用例:82 passed / 40 skipped。收集数按文件:test_data.py 52、test_api_surface.py 20、 test_core.py 15、test_solve.py 9、test_viz.py 8、test_decision.py 7、test_stats.py 6、 test_models.py 5
  • xfail 清零:累计 7 条"契约已写进 docstring、实现没跟上"的用例全部转正, 决策 30 那套信号(XPASS → 摘 mark)跑完了一个完整闭环
  • examples/01_clean_and_describe.py 实测跑通(exit 0);00_quickstart.py 仍等 stats / viz
  • 40 条 skip 是等实现的算法(train_test_split、describe、bootstrap_ci、monte_carlo 等)
  • 本轮回归(已修):transform.py 上一版有 3 处真 bug —— 修前 7 failed / 75 passed, 且 pyright 在同一处报 reportArgumentType。不是静态误报:pyright CLI 复现 + 测试真红。 结论:见到红线先跑 CLI 与 pytest,不许直接挂 # type: ignore(见个人易错点 10)
  • 三个 .ps1 通过 PowerShell AST 语法解析

⚠️ 需要注意 / 遗留

  • 两套环境版本不一致 → 已解决:base 升级到 2026.07 后, numpy/scipy/pandas/matplotlib/sklearn/statsmodels/networkx/sympy 版本完全一致, 只差 Python 补丁号(3.13.9 vs 3.13.15)。详见 docs/04-environment.md §5。
  • Gitea 远程尚未配置 → 已配置并推送完成,见上面 D 节与决策 19/20。 note:仓库当前是 Public(匿名 git ls-remote 可读),要改私有去 Gitea 仓库设置。
  • L0 完成 7/12(Result、get_rng/set_seed、load_table/save_table、clean_missing、 detect_outliers、normalize、standardize),其余按 docs/03-roadmap.md 的 L0 → L1 顺序推进; 详情见上面 E 节
  • 本机 exec_command 在沙箱模式下完全不可用 (helper_unknown_error: setup refresh had errors),只有提权 (require_escalated)模式可用。沙箱模式只允许写 D:\MCM&ICM_Toolkit。
  • 副作用:沙箱通道(Node 子进程)以 CodexSandboxOffline 账户运行, 它写出的文件 Owner 是沙箱账户而非 guzhu。 实测 guzhu 对这些文件仍有 Modify 权限(继承自 Authenticated Users), 读写不受影响;但 git 的 dubious ownership 检查会因此误报, 所以 .git 必须由本人账户创建(见决策 17)。 若以后再用沙箱通道写文件,注意同一问题。

决策记录

# 决策 理由
1 包数从 14 砍到 7 regression/ml 重复;optimization/simulation/sensitivity 同族;dynamics/network 低频 → 降级为模块
2 只保留 1 个 Result 第一版没有任何函数需要区分 5 种结果类型,属过早分类
3 函数清单从 ~250 砍到 ~40 4 天赛程的工具,不是 6 个月的框架;没在真题里用过三次的不写
4 删掉 maximize / 重复的 normalize / 13 个 plot_* 等 同义或一行转发;plot(kind=...) 替代 13 个函数
5 文档从"每函数 10 项"降到 2 档 10 项文档的写作成本是实现的 20 倍,是最可能导致项目停滞的一条
6 补上原文缺失的论文产出链路 save_table / mcm_style 美赛是论文比赛;原文只给了一个 save_figure
7 补上永久不做清单 原文用"建议不要",防不住膨胀;写成硬规则
8 venv 用 --system-site-packages 10.5 MB、离线可用;作为轻量开发与断网兜底
9 conda 环境用精准包列表而非 --clone base 实测 --clone 会重放 base 全套包并逐个下载(连 pandoc 都下),远慢于精准列表
10 生成的是签名 + docstring + NotImplementedError 用户要自己读代码学算法;骨架定接口,实现留给学习过程
11 .ps1 写成纯 ASCII(英文注释/输出),而非中文 Windows PowerShell 5.1 无 BOM 时按 GBK 解析 .ps1,中文直接导致语法错误;保留中文就必须加 BOM,违反 AGENTS.md
12 .ps1 用 LF 而非 CRLF 遵守 AGENTS.md「shell 脚本 UTF-8 无 BOM + LF」;PS 5.1 对 LF 无问题
13 chatGPT_Suggestion.md 保持原样 用户手写的输入文件,不属于本次产物,不做无谓改动
14 base 升级用 conda update -n base --all,而不是装新 conda 计划干净(365 更新 / 0 删除 / 0 冲突),且能保留既有的 lingbot-map 环境
15 项目环境 numpy 降到 base 的 2.4.6 升 base 的 numpy 会连带把 BLAS 从 MKL 换成 OpenBLAS;宁可降一个 minor,也不动整套线性代数后端
16 新增 .gitattributes 强制 LF 仓库要给队友在 Windows/WSL/Linux 上 clone,不固定行尾会产生"整文件都变了"的假 diff
17 .git 由本人账户 guzhu 重建,不做 safe.directory 豁免 沙箱通道建的 .git Owner 是 CodexSandboxOffline,git 会报 dubious ownership。加全局豁免是把问题藏起来;直接以本人账户重建,仓库和远程(Gitea)身份一致
18 core.autocrlf=false(仓库级) 配合 .gitattributes 的 LF 策略,避免 Windows 上 checkout 又转成 CRLF
19 Gitea 的 SSH 走 Tailscale 内网,不在公网开 22 实测公网 8.145.36.108:22 超时(服务器侧过滤,本机出站 22 没被封:github.com:22 可通),而 Tailscale 100.115.86.48:22 可达且 git@ 认证直接过 Gitea。remote 里仍写域名,域名→内网 IP 的映射放在 ~/.ssh/config,换机只改一处
20 首次推送用 --force-with-lease 覆盖 Gitea 的 Initial commit 远端那个提交是建仓时自动初始化产生的,与本地无共同祖先,普通 push 必被拒。推送前已把其中的 LICENSE 与手写简介并入本地 README.md,原提交仍留在本机对象库(1c683cf)
21 Result.__repr__ 不打印 data 内容,只给类型与规模 模拟轨迹动辄十万个数,repr 一旦刷屏,traceback / 调试器 / notebook 输出就全废了。要看数据请直接取 result.data
22 Result.summary() 的行序 = 字典插入顺序,不排序 比赛时"想先看哪个指标"自己决定键的插入顺序即可,工具不该替人排序
23 set_seed 内部允许调用 np.random.seed(带 noqa) sklearn / statsmodels 默认走 numpy 旧全局状态,只记自己的 _GLOBAL_SEED 覆盖不到它们,"提交能重跑"就是假的。旧接口在这一个点上不是"取随机数"而是"设种子",性质不同
24 get_rng(None) 复用同一个模块级生成器(会推进),不每次新建 用户本人提出,取代 Codex 原来"每次新建 default_rng(全局种子)"的写法——那样两个都省略 random_state 的函数会抽到完全相同的数。共享单例既保证重跑一致,又让流自然推进
25 src/ 下的实现默认由用户本人写,Codex 只改 tests/ 与 docs/ 目标是"一边学一边自己写"。曾出现 Codex 直接落盘 random.py、与用户编辑器缓冲区冲突导致保存失败。需要代写时用户会明说
26 未实现的算法用逐条 @pytest.mark.skip,禁用模块级 pytestmark 模块级 skip 会把同文件里已经写好的函数一起跳过。本次 test_data.py 就这样让 load_table 零覆盖,直到有人手动数才被发现
30 已知待修的契约用 @pytest.mark.xfail(strict=False),不用 skip skip 会让"契约还没写"和"写了但实现有问题"混在一起,5 条 skip 曾把 clean.py 的 bug 全藏住。xfail 照样进报告,修好后自动变 XPASS,是"修好了"的信号,而且 XPASS 不失败,不会卡住提交
29 静态分析器钉死到工作区 .venv,并把 base 的 site-packages 用 .pth 链进去 pyright / Pylance 不认 pyvenv.cfg 的 include-system-site-packages,.venv 自己只有 pip;不钉的话它们会去 PATH 上找 python,本机 PATH 上那个是 Codex 运行时的 3.14 shim,没有科学栈,于是报"无法解析导入"。.pth 是唯一被 pyright 认的转发手段
28 不启用 ruff 的 preview 规则集 E302(函数间要 2 个空行)在 ruff 0.16 里是 preview 规则,select = ["E"] 默认不包含它——所以少一个空行 ruff 照样报 "All checks passed"。行数空行得自己盯,别指望 lint
34 常量列(max == min)不定区间也不崩:normalize 压到 feature_range 下界,standardize 给 0 除零必须有个确定的答案,不能出 nan / inf。给 0 与 sklearn 的 MinMaxScaler / StandardScaler 一致(sklearn 把常量列的 scale 设为 1,结果就是 0),换库不换结论。行为已由 test_normalize_constant_column_stays_finite 与 test_standardize_constant_column_stays_finite 钉住
33 detect_outliers 只接受 DataFrame / Series / list / tuple / ndarray,dict 一律 TypeError §4.7 要求非法类型抛 TypeError 而非静默兜底。dict 以前能"碰巧"被 pd.DataFrame() 转成表,但那不是文档承诺的形状,静默接受会让调用方以为传什么都行;tuple 则补进文档与 is_ld_array,与 list 同等对待
32 detect_outliers 对非数值列恒返回 False(既不跳过也不报错) 用户本人在两条路里选的。跳过会让 docstring 里"与原数据同形"变成谎话(形状对不上,没法按位置对回原表);报错则让一张带 city 列的真题表直接不可用。False 的读法是"这一列没有离群点",对文本列是最不坏的解释,也让整表能一次跑完
31 Codex 可直接改 src/ 的范围:纯 bug(不改接口与语义)、格式、死代码 决策 25 说实现归用户,但"能跑"是底线:winsorize(pd.Series) 因 select_dtypes 在 Series 上不存在而直接崩、测试变红,这种必须当场修。凡是要选一个语义的(文本列返回什么、mode 要不要就地改)一律留给用户本人,Codex 只把选项和影响列清楚
27 base 里的 zstandard 残骸目录改名禁用,不删除、不重装 删掉不可逆;pip install zstandard 会给 base 塞一个 conda 记录里根本没有的包,下次 conda update --all 又要重新判断。改名后 import 变回正常 ModuleNotFoundError,pandas 自动跳过
35 log_transform 的显式 offset 按定义域前提校验:min(x) + offset > 0 才放行
原码写的是 if offset > 0: raise,把唯一合法的用例(正 offset 正好抬定义域)全拒了,报错文案却是"offset 必须大于 0",自相矛盾。改成校验真正的前提:log 要求自变量取正。offset=0 对全正数据合法(offset=None 的自动分支本来就取 0)。Codex 代选、待用户确认 —— 若原意是"只收正 offset",改回即可

下一步

  1. stats.describe 是 L0 里下一个该写的(A 档,stats/descriptive.py)—— examples/00_quickstart.py 与 examples/01_clean_and_describe.py 末尾的 TODO(L0) 都在等它。 写法照 data/clean.py 的 clean_missing:先想"替代了哪段手写代码" → 先写一条会红的测试 → 再实现 → 补 examples/ 一行(docs/03-roadmap.md §3.2)。
  2. 之后按 L0 剩余顺序推进:correlation_matrix(A)→ bootstrap_ci(S 档,要写函数卡片) → monte_carlo(S 档)→ save_figure(A)。
  3. L0 跑通后,找一道真题(建议 2025 C)走完整流程,反向沉淀函数。
  4. 待你确认(决策 35):log_transform 的显式 offset 现在按"定义域前提"校验 (min(x) + offset > 0,否则 ValueError)。若你原意是"只收正 offset",说一声我改回去。