Files
MCM-ICM_Toolkit/STATUS.md
T
guzhujushi 748affb7f0 feat(models): 落地 compare_models(指标名别名映射)+ 同步你的三条入口口径
你的三条改动(决策 61,已复测入库):
- verify_yt_yp 拦 Inf:rmse([1.0, inf], ...) 从安静返回 inf 变成 ValueError。
- 至少 2 个样本:rmse(1.0, 1.0) 不再被当成 1 个样本算出 0.0。
- mape 从"只拦 0"改成拦 y_true <= 0:原来对负数返回 -1.0(无意义)。
Codex 侧把这三条写进 7 处 Raises(易错点 30 复发 1 次),
test_mape_rejects_zero_in_y_true 的 match 文案改为"非正数",另补 Inf / 样本数两条测试。

compare_models(决策 62,A 档,Codex 实现):
- 别名映射 _METRIC_FUNCS:r2 与 r2_score 指向同一个函数,传哪个列名就叫哪个,
  两个一起传报 ValueError;adjusted_r2 不进表(需要 n_features)。
- cv=0 全量拟合后打分;cv>=2 用手写 K 折(get_rng(random_state).permutation +
  np.array_split),不引入 sklearn 依赖;报折间均值;每折至少 2 个样本 => cv <= n // 2。
- 排序方向跟第一个指标走(误差类升序、R² 降序);models 必须传模型对象(fit/predict),
  X 必须二维,metrics 必须是 tuple/list。
- 原 test_compare_models_... 的用法是错的(传拟合结果当模型),改成测试桩后摘 skip。

tests/test_models.py:23 -> 31 条(排序两条、别名、坏指标名 / 坏 models / 坏 cv、
每折下限、random_state 可复现)。

三连:pytest 233 passed / 25 skipped(258 收集)、ruff 0、pyright 0。

文档:STATUS 决策 61 / 62 + 第十轮(第九轮那 4 条标办结);CHANGELOG 本轮条目;
personal-pitfalls 易错点 30 记复发 1 次 + 新增 33(多行文案少引号 / ValueError 多参数)。
2026-10-12 16:53:01 +08:00

93 KiB
Raw Blame History

STATUS — 当前进度

更新日期:2026-10-12

本次任务

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

第二阶段(进行中):按 docs/03-roadmap.md 的 L0 → L1 顺序逐个实现函数。

分工硬规矩(决策 47,用户 2026-10-03 明确):

  • 算法一律由用户本人写(S 档尤其)—— 即便用户说"你写吧"也不代写, 要保住"一边学一边自己写"的路径。
  • 工程事务由 Codex 写、用户读:入口/校验重构、tests/、docstring、ruff、 STATUS.md / CHANGELOG.md、函数卡片、.gitignore 等。
  • A 档薄封装(rmse/mae/fit_linear/train_test_split…)算工程,Codex 可直接实现。
  • 分地盘按文件划,不按函数划(同一文件两人各写一半曾导致用户编辑器保存失败)。

当前 L0 11/12:Result、get_rng/set_seed、load_table/save_table、clean_missing、 detect_outliers、normalize/standardize、describe、correlation_matrix、bootstrap_ci、 monte_carlo(用户写,2026-10-04);另提前落地了 L1 的 confidence_interval / t_test / monte_carlo_probability、monte_carlo_integral、random_walk、markov_simulate、 markov_steady_state、mm1_metrics(均为用户写)—— solve 层的 L1 清单已全部落地。 剩余 1 项:save_figure(Codex 写)。 models/metrics.py 已完成(第九~十轮):6 个指标(rmse / mae / mape / smape / r2_score / adjusted_r2)+ 共享入口 verify_yt_yp 由用户写、Codex 审;compare_models 由 Codex 按 A 档落地 (含指标名别名映射)。models 下一步是 regression.py。 monte_carlo 的口径已拍板(决策 48);monte_carlo_probability / random_walk 的待办均已办结; markov_simulate(决策 56)/ markov_steady_state(决策 57)/ mm1_metrics(决策 58) 的入口口径已办掉大半,各剩 1~2 条细枝末节(详见"下一步"第八轮)。 另 3 个小口径仍悬着:ci_low/ci_high 的语义、monte_carlo_integral 的第一道 isinstance 分支可塌成两行、 random_state 注解与 std 的 ddof 是否全项目对齐。

任务清单

✅ 已完成

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 实现进度(10/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

  • stats/descriptive.py 的 describe(L0 第 8 项,由用户本人实现,Codex 审查 + 代修) - 一个静默 bug(最危险的那种):df_num.describe().T 把表转反了(行 = 变量、列 = 指标), 而 docstring 与 test_describe_reports_expected_keys 都要求"行 = 指标、列 = 变量"。 转置后 .loc[指标名] = <按变量索引的 Series> 与列名零交集,pandas 不报错, 直接把这四行(missing / missing_ratio / skewness / kurtosis)整行填成 NaN。 去掉 .T 一处即修好三件事:方向对了、NaN 没了、weights 分支不再报 cannot set a row with mismatched columns(见易错点 11、决策 36) - median 行补上(pandas 的 describe() 只有 50%,而测试契约定的是 median) - 入参放宽到 list / tuple / ndarray(兄弟函数都收,00_quickstart.py 传的就是 ndarray) - weights 校验补齐:长度、非一维、NaN/inf、负值、和为 0 都抛带原因的错误 (原先和为 0 时裸抛 ZeroDivisionError) - zscore:修 RET505,docstring 补 Parameters / Returns / Raises; coefficient_of_variation:均值为 0 由 TypeError 改为 ValueError(那是取值问题不是类型问题)、 空数组提前报错(原先静默返回 nan + RuntimeWarning)、改掉"异变系数"错别字;分母改用 |mean|(用户本人定,见决策 38) - pyright 的 DataFrame | Series 重载问题按坑 8 用 cast 收窄;ruff 7 条清到 0 - 测试:test_stats.py 去掉模块级 pytestmark(它把 6 条测试全部跳过了 —— 决策 26 说的就是这个,实现写完了却零覆盖),改成逐条 skip 未实现的 5 条, 并新写 20 条 describe / zscore / 变异系数用例

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

  • stats/inference.py 的 correlation_matrix(L0 第 9 项,由用户本人实现,Codex 审查 + 代修) - 硬 bug:if numeric_cols == 0: 拿 Index 和标量比,得到的是布尔数组 —— 0 列时静默不报错、1 列时碰巧能跑、≥2 列直接 ValueError 崩; 而"≥2 个数值列"正是最常见的情况,等于整个函数不可用。改成 len(numeric_cols) == 0(易错点 13) - pyright 的报错也都不是运行时问题:data[numeric_cols] 是坑 8 的 DataFrame | Series 重载 (cast 收窄);_, p = stats.pearsonr(...) 解包会退化成 object —— scipy 的返回值是 _make_tuple_bunch 运行时拼的类、存根里看不到 pvalue(见 docs/04-environment.md 坑 10)。 加了 _pvalue(result: Any) 小助手统一取 p 值,一处都没挂 # type: ignore - method 校验提到最前、清掉 3 条 F541 / 1 条 RET505 / I001,ruff 归零 - 现在返回:correlation_matrix → 数值系数矩阵(对角线 1.0); correlation_pvalues → 数值 p 值矩阵(对角线 NaN); correlation_table → 星标字符串表(对角线 1.000 不带星)。 - 返回契约冲突(已解决):上一版返回的是带星标的字符串表(1.000***), 而 docstring 契约与 L0 用例都要求数值矩阵(对角 = 1.0);按方案 A 拆成三个函数(见下)

  • correlation_matrix 的返回契约按方案 A 落地(2026-10-03,用户拍板): correlation_matrix 只返回数值矩阵;星标表拆成 correlation_table、p 值拆成 correlation_pvalues(三者共用 _check_corr_args 守卫与成对删缺失口径,见决策 41)。 顺带修掉两处:对角线不再带 ***(p 值矩阵的对角线是 NaN、"自己和自己"没有显著性); p 值与 r 用同一批观测(都成对删缺失、都尊重 min_periods),不再各算各的样本量。 p 值只算上三角再镜像,省一半重复计算。测试 +8 条(含用解析式 t = r·sqrt((n-2)/(1-r²)) 对照)

  • stats/inference.py 的 confidence_interval / t_test(L1 提前落地,实现由用户本人写,Codex 审查 + 代修) - 用户问的「307/308 两行报错」不是真错:那两行是 float(res.statistic) / float(res.pvalue), 报错来自 scipy 的 TtestResult —— 它和 PearsonRResult 一样是 _make_tuple_bunch 运行时拼的类, 存根里没有这两个字段。探针实测 TtestResult(statistic=3.873, pvalue=0.0305),运行时取值完全正常。 按决策 40 的路子补了一个 _statistic(result: Any) -> float 助手(与 _pvalue 成对), 一行 # type: ignore 都没挂。详见 docs/04-environment.md 坑 10 - confidence_interval 有真 bug(已修):原来返回 (float(min(0.0, lower)), float(max(upper, 1.0))) —— min / max 的参数写反(下界被钉死成 0.0),而且无条件把所有 method 的区间夹到 [0, 1]。 实测 confidence_interval(rng.normal(170, 8, 50), method="t") 的下界是 0.0(正确是 171.0 ± 2.1); confidence=1.5 更是被夹成 (0.0, nan) 而不报错。现在只有 proportion 夹(决策 42), normal / t 直接返回 mean ± z|t·se;method 非法改抛 ValueError(§4.7)、 confidence 按 (0, 1) 开区间校验、样本量 < 2 抛 ValueError - confidence_interval 的入参放宽到 list / tuple / ndarray / Series / DataFrame: docstring 的 Examples 传 list、L0 用例传 ndarray,原来两条都直接 TypeError - t_test:_clean_data 改用 isinstance(data, pd.DataFrame) 收窄(坑 8 的 cast)、alpha 开区间校验、 df 按 Welch–Satterthwaite / pooled / paired 分别给,docstring 补齐 Parameters / Returns / Raises / Notes(§4.7) - 测试 tests/test_stats.py:+10 条(36 → 46),含把原先挂着 skip 的 test_confidence_interval_matches_scipy 转正; 对照一律用解析式(z 公式、Welch df 公式、t = (mean − value)/(s/√n)),不拿实现对自己 - ⚠️ t_test 还有 3 个口径待用户拍板(Codex 未改,见「下一步」第 6 条)

  • core/validate.py 的 as_array(统一入口,2026-10-03 落地) - 用户提出"每个函数都抄一遍入口/校验/交换太麻烦"。结论:入口能统一,出口不能一刀切 —— 还原要求"输出与输入同形",而 describe / correlation_* / confidence_interval / t_test 的输出形状天生和输入不同,硬套只会重演易错点 14。 - 契约:as_array(x, *, numeric_only=True, dtype=float, ndim=None) -> (arr, kind), kind 是 "frame" / "series" / "array"(只报告原类型,还原由调用方手写,用户拍的板)。 - 覆盖掉重复:原先 select_dtypes / isinstance(..., pd.DataFrame|Series) 散在 5 个文件 31 处; 现在 normalize / standardize / log_transform / zscore / confidence_interval / t_test 各只剩 1 行入口。data/transform.py 的 _NUMERIC_TYPES 白名单连同 3 份副本一起删掉。 - 两条硬规矩:默认绝不摊平(要摊平显式 ndim=1);不删缺失值(删缺失是各函数的语义, 见决策 45 与易错点 15)。numeric_only 按"输出是否与输入同形"选,不按喜好选(决策 44)。 - check_array(L1,未实现)实现时直接复用 as_array,一份实现两张脸

  • stats/bootstrap.py 的 bootstrap_ci(L0 第 10 项,S 档,实现由用户本人写,Codex 审查 + 代修) - 移植正确性已用解析解钉住:大样本正态下百分位区间半宽 ≈ 2 × 1.96 × se(容差 10%) - Codex 代修的三处: ① random_state=None 时原来另开了 np.random.default_rng(),绕过了 get_rng —— 与决策 24 冲突(set_seed 后仍然不可复现)。改成无条件的 get_rng(random_state); ② statistic_name 写成 hasattr(statistic, "__name__"),而 lambda 的 __name__ 恒为 "<lambda>", 导致 else 分支是死代码、自定义统计量拿不到 "custom"(见易错点 17); ③ ruff 两条(I001 导入块、F541 空 f-string),并把 docstring 的 Parameters / Returns / Raises 补齐(§4.7) - 测试是重点:用户实现写完后,tests/test_stats.py 里等它的 3 条仍然挂着 skip, 全量照样显示"全绿" —— 正是易错点 9 的复发(已把该条 复发 记到 1)。 已摘掉 3 条 skip,并新增 6 条:estimate 等于原样本统计量、confidence 单调性、 五种容器一致、自定义 lambda 走回退分支、不给种子时跟随 set_seed、非法 method 抛错。 - S 档函数卡片已写:docs/algorithms/bootstrap_ci.md(第 2 张), 记了"极差统计量的区间上界必然塌到 estimate"这个非平凡性质,以及 5 个待拍板口径 - 测试:tests/test_stats.py 的 bootstrap 段共 9 条

  • 工程收尾(本轮):tests/test_solve.py 的模块级 pytestmark = pytest.mark.skip(...) 拆成 9 条逐条 @pytest.mark.skip(reason=_NOT_IMPLEMENTED) —— 模块级 skip 会把整个文件吞掉, 正是易错点 9 的根源(决策 26)。收集数不变(177)、跳过数不变(34),只是粒度变细。

  • solve/simulate.py 的 monte_carlo(L0 第 11 项,S 档,算法由用户本人写,2026-10-04) - 核心逻辑:向量化采样 → 一次仿真 → 8 个指标;get_rng 无条件调用(决策 46 已内化) - 用户第二轮改了三处(见决策 48):批量大小改由实现传 sampler(rng, size=n); 结果含 NaN 判失败;np.std(data, ddof=0) 写明 - Codex 只碰工程侧,算法一行未动: ① ruff 三条(I001 合并同源 import、F541 空 f-string ×2) ② docstring:补 Parameters(原文漏 random_state / **kwargs)与 Raises;按新契约改写 sampler 说明(必须能接住 size,且 **kwargs 里别再传 size);Notes 记 NaN 口径; 修掉跑不通的 Examples(原版 lambda rng, **k: rng.normal(0, 1) 返回标量,实测 success=False) ③ 按 §4.7 收严 n 校验:np.int64 不再被拒、n=True 改抛 TypeError、浮点 n 归 TypeError、 n<=0 归 ValueError ④ 格式与文案:success= True → success=True;失败文案"重采样" → "模拟"(从 bootstrap_ci 抄来没改) ⑤ 入口统一 dtype=float —— 修掉一个真 bug:布尔结果("事件是否发生",MC 最常见的用法) 会让 np.std 在 numpy 2 里抛 boolean subtract ... is not supported,整个函数判失败; 字符串结果也从"ufunc isnan 不支持"变成可读的 could not convert string to float - 测试:10 条(tests/test_solve.py)—— 返回长度与 8 个 key、simulator 只被调用一次(向量化的可执行版本)、 同种子可复现、省略种子时跟随 set_seed、E[X²]=1(n=2e5)、docstring Example 真能跑、np.int64 的 n、 布尔结果按 0/1 统计、NaN 结果判失败、5 个非法入参 - 行为探针实测:simulator 调用次数 = 1(真向量化);20/20 次区间覆盖 0

  • solve/simulate.py 的 monte_carlo_probability(L1,S 档,算法由用户本人写,2026-10-05) - 核心:向量化 event(batch) -> bool 数组 → p = mean(data) → Wilson score 区间并夹到 [0, 1] (比例量纲,沿用决策 42 的口径——同类问题没重犯) - 用户第二轮(2026-10-05)按 6 条待办改掉 5 条:Wilson 替 Wald、callable 入口校验、 confidence 记进 params、float() 统一 ci 类型、小概率不再判失败(改成 message 里的警告)——见决策 50 - Codex 第三轮只碰工程侧:① z_score^2 → z_score**2 —— 真 bug:^ 是异或, numpy 浮点直接抛 TypeError,被 except 吞成 success=False,6 条测试全红才暴露(易错点 21); ② message 无警告时的尾逗号;③ {"confidence":confidence} 的空格;④ docstring 的 "Wald" → "Wilson"; ⑤ 新增 1 条对照测试:区间必须等于 statsmodels.proportion_confint(method="wilson") - 行为探针实测:p≈0.5 正确(0.49991 @ n=2e5)、confidence 变大区间变宽、 同种子可复现、省略种子跟随 set_seed、np.int64 的 n 可收、confidence=1.5 抛 ValueError - Codex 只碰工程侧,算法一行未动: ① ruff 六条(SIM300 Yoda、W291 ×2、W293 ×2、F541) ② docstring:补 Parameters(原文只有 Returns/Notes/Examples)与 Raises, 修掉跑不通的 Examples(原 lambda rng, **k: rng.normal(0, 1) 返回标量,实测 success=False), 并把"Wald 区间 + 夹到 [0,1]"与"p <= 1e-3 判失败"两条现状如实写进 Returns / Notes ③ 格式:metrics 字典缩进、{"n":n, ...} 空格、行尾空白 ④ 文案:NaN 提示里的 "simulator" 改成 "event"(从 monte_carlo 抄来没改) - 测试:摘掉 2 条 skip(test_monte_carlo_probability_of_standard_normal、test_monte_carlo_is_reproducible —— 实现写完还挂着 skip,正是易错点 9)并新增 5 条,solve 段共 24 条

  • solve/simulate.py 的 monte_carlo_integral(L1,S 档,算法由用户本人写,2026-10-05) - 核心:bounds → 自己造 (n, d) 均匀样本 → 逐点求值 → mean(func(X) × 体积); 自造 sampler / simulator 再复用 monte_carlo —— "造批 → 一次算 → 汇总"这套骨架的第二次复用 - Codex 只改了一处真 bug(决策 31,纯 bug):体积写成 wights = lows - highs(名字打错 + 减反)。 奇数维体积为负:1D x² 于 [0,1] 实测 -0.3324(应 +1/3)、3D x 得 -0.5005(应 +1/2); 而 2D x·y 得 +0.2499 "看着对" —— 两个负号相乘抵消,把 bug 藏住了。 改成 widths = highs - lows 后复测 0.3324 / 0.5005 / 1.9971([0,2]) 全部正确 - Codex 只碰工程侧:① docstring 补 func / n / random_state 三节 Parameters 与 Raises; ② Returns 写清"metrics["mean"] 才是积分估计值,data 是 n 个样本各自的贡献"; ③ 修掉跑不通的 Examples(原版断言 abs(r.data - 1 / 3) < 0.01 —— data 是数组, bool(数组) 会抛 ValueError: truth value ... is ambiguous),改成断言 metrics["mean"]; ④ 格式:bounds_arr[:,0] → [:, 0]、函数间空行(def random_walk 前只留了 1 个空行) - 测试:新增 4 条 + 摘掉 1 条 skip(solve 段共 30 条)—— 1D 收敛到 1/3(n=2e5)、 3D 抓体积符号(奇数维)、[0,2] 抓宽度(偶数维藏不住)、2D + name/dimension/bounds 三个 params、 (1, 0) 抛 ValueError。三条"抓符号"用例就是照着上面那个"2D 掩盖效应"设计的 - ✅ 3 条待办已由用户第二轮办结(同日):message 改成 if result.success: / else: 两支(失败语义不再被覆盖); 入口补 isinstance(bounds, (list, tuple)) + ndim == 2 and shape[1] == 2 两道([0, 1] 从裸 IndexError 变成可读 ValueError); n 注解放宽为 int | np.integer。⏳ 尚有 2 条新口径(bounds 收不收 ndarray、if/else 三行重复)见"下一步"

  • solve/simulate.py 的 random_walk(L1,S 档,算法由用户本人写,2026-10-05) - 核心:step_sampler(rng, size=(n_paths, n_steps)) 一次抽完整批增量 → np.cumsum(..., axis=1) → 首列填 start、其余列 start + cumsum。data 形状 (n_paths, n_steps + 1) - Codex 改了一处真 bug(决策 31,纯 bug):默认采样器写 rng.integers(0, 2, size=size, dtype=float), 而 numpy 的 integers 不收 dtype=float(TypeError: Unsupported dtype dtype('float64') for integers) —— 默认路径 100% 失败(success=False、data=None),连 docstring 的 Examples 都跑不通。 改成 rng.integers(0, 2, size=size) * 2 - 1(后面的 np.asarray(..., dtype=float) 负责转 float)。 实测:(5, 101) 形状、diff 恒为 ±1、同种子可复现 - Codex 只碰工程侧:① ruff 两处 W293(只有空格的行);② docstring 补全(原只有 step_sampler 一节, 现补齐 n_steps / n_paths / start / random_state + Returns / Raises / Notes, 并写清 step_sampler 收的是二维 size=(n_paths, n_steps));③ Examples 增加一条"步长恒为 ±1"的断言 - 测试:新增 6 条(此前 random_walk 一条测试都没有)—— 形状与 start 列、默认步长恒 ±1、 同种子可复现、自定义高斯采样器(带漂移)、3 类非法入参、docstring Example 真能跑 - 用户第二轮(2026-10-05)按 4 条待办改定:n_steps / n_paths 拆成 TypeError(类型)/ ValueError(取值); 加 steps.shape != (n_paths, n_steps) 与 NaN 两条校验;默认采样器换成 rng.choice([-1.0, 1.0], size=size); params 补 random_state;metrics 由"抄 params"改成 mean_terminal / std_terminal(终点位置,ddof=0) - Codex 第二轮又抓到一处真 bug(决策 31):用户把 call_params = {...} 写进了 try 里,而 except 分支要引用它 —— 5 种失败输入 5 个全炸 UnboundLocalError(本该返回 success=False 的 Result,变成把异常抛给调用方)。 按原意把 call_params 移到 try 之前(不动任何口径),实测失败路径全部恢复正常;新增一条"必然失败"的回归测试(易错点 26) - 测试:本轮再 +3 → 共 9 条(新增"失败也必须返回 Result 且带 params"、NaN 判失败、√t 扩散); start="a" 那条恢复成 pytest.raises(TypeError)(用户恢复了入口校验,见下) - 第三轮(2026-10-05):start 校验恢复、注解放宽为 int | np.integer;最后一条"方阵歧义"用户选方案 B, step_sampler 契约改为"整数 size=n_paths * n_steps + 必须返回一维 + 内部行主序 reshape"(决策 55,Codex 代改)

  • solve/simulate.py 的 markov_simulate(L1,S 档,算法由用户本人写,2026-10-05) - 核心:arr[current_states] 取当前状态那一行 → cumsum 得累积分布 → 用 [0, 1) 均匀数"落格" ((cum_probs < r[:, None]).sum(axis=1))→ 写进下一列。data 形状 (n_paths, steps + 1),值是整数状态编号 - Codex 审查结论:算法本体正确(探针实测 (2000, 201)、状态 ∈ {{0,1}}、200 步后终点频率 [0.657, 0.343] 对照平稳分布 [2/3, 1/3]、同种子可复现) - Codex 只碰工程侧:① ruff 8 条全清(I001 导入排序 + 5 处 W293 / 2 处 W291 空白); ② arr, kind = as_array(...) 的 kind 全程未用 → 改 arr, _; ③ docstring 从一行补到完整的 Parameters / Returns / Raises / Notes / Examples; ④ 文案:马尔科夫 → 马尔可夫(3 处)、initail → initial、半角逗号 → 全角;paths[:,0] → paths[:, 0] - 测试:新增 9 条(此前 markov_simulate 一条都没有)—— 形状 / 首列 / 状态取值 / 整数 dtype、 单位矩阵吸收态自锁、可复现(同种子相同、异种子不同)、终点频率收敛到 [2/3, 1/3]、 metrics = 终点状态、非法转移矩阵(非方阵 / 行和不为 1)、非法 steps / n_paths(0 / float / bool)、 np.int64 可收、docstring Example 真能跑 - 第二轮(2026-10-09):用户补了 np.any(arr < 0) 非负校验 → 待办 ② 办结; Codex 补一条"行和是 1 但含负元素"的回归测试 - 第三轮(2026-10-10,用户改定):非负校验改带容差 -1e-12(⑦ 办结); initial 注解放宽到 (int, np.integer)(np.int64 已收);paths 的构造与首列赋值挪进 try(③ 办结, 长度不匹配/越界不再裸抛);params 的键 n_steps → steps(与参数名对齐)、initial 存成可 JSON 化的标量/列表; metrics 去掉 std_terminal、换成 terminal_distribution({状态: 频率} dict,⑤ 办结) —— Codex 同步 docstring 与 3 条测试(含"只列出现过的状态"这条口径) - ⏳ 余下 4 条见"下一步 → markov_simulate 待你拍板"(全是入口口径与少考虑情况,决策 49)

  • solve/simulate.py 的 markov_steady_state(L1,S 档,算法由用户本人写,2026-10-09) - 核心:v 从均匀分布出发,v_next = v @ arr 反复推进(行随机矩阵按行向量右乘), 直到 max|v_next - v| < tol 或耗尽 max_iter;data 是平稳分布向量,metrics={"converged", "iterations"} - Codex 审查结论:算法本体正确(探针实测)—— 2 状态 [2/3, 1/3](58 次迭代)、3 状态与 np.linalg.eig(P.T) 特征值 1 对应的特征向量逐位一致(39 次)、随机 6 状态一致(13 次)、200×200 一致到 1.9e-12(6 次、0.10s)、 与 markov_simulate 的终点频率互验 [0.6702, 0.3298] vs [0.6667, 0.3333]、sum(data) = 1.0 - Codex 只碰工程侧:① ruff 5 条 W293 全清,mm1_metrics 前 4 个空行 → 2; ② docstring 补 max_iter / tol 两个参数(原来 Parameters 里只有 transition)、补 Raises, 写清 tol 是相邻差口径(实测慢混合链 tol=1e-10 → 实际误差 4.6e-9,被 1/(1-|λ2|) 放大); ③ 修正 Notes 里"周期链会不收敛"的说法(可约链的实情是"平稳分布不唯一、迭代法给离初值最近的那个"); ④ 失败文案 失败: → 失败:(与文件内其它函数统一) - 测试:+8 条、摘掉 1 条 skip(此前该函数只有 1 条挂着的 skip)—— 2 状态 [2/3, 1/3] + converged / iterations、 3 状态与特征分解对拍、data 是分布(和为 1 / 非负 / float64)、慢混合链迭代次数 > 100、 np.int64 的 max_iter、非法转移矩阵、max_iter=0 / tol=0、docstring Example 真能跑 - 第二轮(2026-10-09,用户改定):补非负校验(① 办结)、params 记 transition(⑥ 办结)、 success=converged(选口径 B,④ 办结)、tol 拆成 TypeError / ValueError 且先判 bool(② 办结); Codex 顺手修文案(max_iter <= 0 那条写成 "tol 必须是正数")与 异常: → 异常:, 并补 2 条测试(负元素矩阵、max_iter=3 未收敛走 success=False) - 第三轮(2026-10-10,用户改定):非负校验带容差 -1e-12(⑦ 办结);max_iter 类型错改抛 TypeError(⑤ 办结); tol 加 np.isnan 拦截(③ 办结);非方阵改抛 TypeError - ⏳ 只剩 2 条(⑧ 未收敛带 data、非方阵的 TypeError/ValueError 与 markov_simulate 不一致)见"下一步"(决策 57)

  • solve/simulate.py 的 mm1_metrics(L1,S 档,算法由用户本人写,2026-10-09) - 核心:闭式解 ρ = λ/μ、L = ρ/(1-ρ)、Lq = ρ²/(1-ρ)、W = 1/(μ-λ)、Wq = W - 1/μ; λ >= μ 走"系统不稳定"分支返回 success=False - Codex 审查结论:公式全对(探针实测 3 组参数 (2,3) / (1,4) / (0.5,1.0) 与闭式公式逐位一致, W = Wq + 1/μ 恒等式也成立) - Codex 修了一处边界 bug(决策 31,纯 bug):稳定性判断写成 service_rate < arrival_rate, 而 docstring 与既有测试的口径都是 λ >= μ —— 于是 λ == μ 掉进 try、rho == 1.0 → ZeroDivisionError 被 except 吞成 success=False(结果碰巧对,message 却是"计算失败:ZeroDivisionError")。 两支的可观测输出完全相同(success=False / metrics={} / data=None),改 <= 只换 message,故直接修并加回归测试 - Codex 只碰工程侧:① ruff 6 条(5 处 W293 + F541 f-string 无占位符 + W292 文件末尾无换行); ② 文案:service_rate 的报错写成 "tol"、max_iter 的也写成 "tol"(同一轮里 3 处参数名抄错)、半角逗号 → 全角; ③ call_params={...} → call_params = {...};④ docstring 补 Raises + 说明 data 与 metrics 同值 - 测试:+6 条、摘掉 2 条 skip(此前只有 2 条挂着的 skip)—— 闭式公式逐项对拍(含 W = Wq + 1/μ)、 λ == μ 与 λ > μ 都判失败且带"不稳定"文案、入口(0 / 负数 / bool / 非数值)、 λ == μ 不能靠 ZeroDivisionError 落到失败分支、docstring Example - 第三轮(2026-10-10,用户改定):arrival_rate 放开到"非负"(λ = 0 空系统合法,① 办结)、 两个速率都收 numpy 标量(np.number,② 办结)、不稳定分支保留 rho 并写进 message(④ 办结)、 data=metrics.copy() 去掉同一个 dict 对象的别名(③ 部分办结:仍是 dict,只是不再别名) —— Codex 同步 docstring 与 4 条测试 - ⏳ 只剩 1 条(data 该不该是 dict)见"下一步 → mm1_metrics 待你拍板"(决策 58)

  • 剩余 1 项:save_figure(A 档 = 工程,Codex 直接写)

验证结果

检查 venv conda mcm-toolkit
pytest 见下(单列) 233 passed, 25 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(与两套环境一致)
  • 258 条用例:233 passed / 25 skipped / 0 xfail。收集数按文件:test_data.py 52、test_api_surface.py 20、 test_core.py 23、test_solve.py 64、test_viz.py 8、test_decision.py 7、 test_stats.py 53、test_models.py 31(第九轮 5 → 23、第十轮 23 → 31)
  • xfail 再次清零:correlation_matrix 的契约冲突已按方案 A 解决(决策 41),那条用例转正
  • 上一轮那 7 条 xfail 已全部转正,决策 30 那套信号(XPASS → 摘 mark)跑完过一个完整闭环
  • examples/01_clean_and_describe.py 实测跑通(exit 0);00_quickstart.py 仍等 stats / viz
  • 28 条 skip 是等实现的算法(train_test_split、anova、fit_distribution、knapsack_dp、 linear_program、simulated_annealing 等); monte_carlo_probability / monte_carlo_integral / markov_steady_state / mm1_metrics 已落地,分别摘掉 2 / 1 / 1 / 2 条 skip;markov_simulate 新增的 9 条全部实跑; TestValidate 的 4 条仍挂着 —— check_* 还是 NotImplementedError,但它们实现时应当直接复用 as_array
  • 本轮回归(已修):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 完成 11/12(Result、get_rng/set_seed、load_table/save_table、clean_missing、 detect_outliers、normalize、standardize、describe、correlation_matrix、bootstrap_ci、 monte_carlo(用户写,2026-10-04));另 L1 的 confidence_interval / t_test / monte_carlo_probability / monte_carlo_integral 已提前落地; L0 只剩 save_figure(Codex 写),详情见 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 才放行
36 describe 的返回表不转置:行 = 指标、列 = 变量
37 非数值列的处置按输出契约分,不按包分:describe / zscore 直接丢掉;detect_outliers 保留列、掩码恒 False
两族函数的产出物不一样:describe / zscore 吐的是数值结果表,文本列塞不进去,丢掉才能得到一张干净、可直接贴论文的表;detect_outliers 吐的是与原表同形的掩码,必须能按位置对回原表,丢列就废了。"不统一"不是前后矛盾,是契约不同。用户 2026-10-03 拍板:丢掉更好
38 coefficient_of_variation 用 `σ / mean
均值为负时 σ/μ 会给出负值,容易被读成"波动更小",其实只是符号跟着均值走。取绝对值后 CV 恒非负。可比性仍只在均值远离 0 的比率尺度数据上成立(已写进 docstring 的 Notes)
39 工作约定:语义 / 非法输入口径由用户拍板,Codex 只列选项与影响、不代选;ruff、STATUS.md、CHANGELOG.md、tests/、docstring 这类与算法无关的工程事务,Codex 顺手补掉
40 scipy 的检验结果统一走 _pvalue(result: Any) -> float 助手取 p 值,不逐处挂 # type: ignore
41 correlation_matrix 只返回数值矩阵;星标表拆 correlation_table、p 值拆 correlation_pvalues
用户 2026-10-03 选方案 A。数值矩阵能做的事(挑高相关对、喂聚类 / PCA、画热力图)字符串表都做不了,而反过来随时能格式化 —— 展示需求属于输出端,不该侵占返回值;一旦返回字符串,数值信息不可逆地丢失。三个函数共用 _check_corr_args 守卫与成对删缺失口径,correlation_table 在最外层调另外两个(计算型当主、展示型当辅)。见易错点 14
42 confidence_interval 只有 proportion 夹到 [0, 1];normal / t 是均值区间,一律不夹
原码把三种 method 的区间无条件夹进 [0, 1],且 min / max 参数写反(见易错点 15),实测把身高均值区间下界压成 0.0。均值区间的量纲本来就不在 [0, 1] 里;只有比例 p ± z·√(p(1−p)/n) 的边界才有意义。Codex 代改、待用户确认 —— 若原意是"什么区间都夹",改回 _clip 一行即可
43 scipy 检验结果的统计量也走 _statistic(result: Any) -> float 助手,与决策 40 的 _pvalue 成对
TtestResult 与 PearsonRResult 同源(_make_tuple_bunch 运行时拼类),存根里 .statistic / .pvalue 都没有。用户报的「307 / 308 行报错」就是这两处,不是真 bug(探针实测取值正常)。见 docs/04-environment.md 坑 10
44 所有收数值的函数统一从 core.validate.as_array 进;还原不代做,调用方按返回的 kind 自己手写
用户 2026-10-03 提"每个函数都抄一遍入口/校验/交换太麻烦"。能统一的只有入口:出口要求"输出与输入同形",而 describe / correlation_* / confidence_interval / t_test 天生不同形,硬套会重演易错点 14。用户明确"不要三个函数""转换回去自己手打",所以 as_array 只返回 (arr, kind),ArraySpec / restore 都不做。numeric_only 默认 True(用户拍的板),但按输出契约选:同形函数必须显式 False
45 as_array 不删缺失值;容器类型/内容转换失败统一抛 TypeError
删缺失是语义(df.corr 成对删、describe 整列统计、paired=True 的 t 检验必须成对删),在入口层删掉就把行对应关系永久丢了 —— 这正是 t_test 待拍板的口径之一。错误口径统一为 TypeError 是Codex 代选、待用户确认:normalize(含文本列的表) 原先由 numpy 抛 ValueError,现在是 TypeError(内容转不成 float);describe 的 weights 同类情况目前仍抛 ValueError(决策 39 说不统一就列出来)
46 bootstrap_ci 的随机数无条件走 core.get_rng;statistic_name 用 getattr(f, "__name__", None) 再判 "<lambda>"
原码 random_state=None 时另开 np.random.default_rng(),等于绕开决策 24(set_seed 对它无效,区间不可复现)。Codex 代改:get_rng(None) 本来就是设计好的"复用模块级生成器且推进",直接传下去即可。statistic_name 的 else "custom" 是死代码 —— lambda 也有 __name__(值恒为 "<lambda>"),实测拿不到 "custom",见易错点 17
PearsonRResult / SignificanceResult 是 _make_tuple_bunch 在运行时拼出来的类,存根里没有 pvalue 字段:解包会退化成 object、属性访问会报"未知"。运行时完全正常(实测三个函数返回的都是 np.float64)。收成 Any 是一处收窄,调用点仍然干净;# type: ignore 要挂在每一处,且会连带关掉该行后面的检查。见 docs/04-environment.md 坑 10
用户 2026-10-03 明确:没考虑到的非法情况"告诉我我自己改"——因为那是设计决策,得由写实现的人形成判断;而 lint / 进度板 / 测试这类机械活再由用户动手只是浪费。这条与决策 25 / 31 配套:25 定"谁写 src",31 定"Codex 可改的 bug 范围",39 定"非算法事务的默认归口"
docstring 与 test_describe_reports_expected_keys 早就写死了这个方向,实现里多做的 .T 是净负担 —— 且不是"风格不同"那么轻:转置后 .loc[指标名] = <按变量索引的 Series> 与列名零交集,pandas 不报错、把四行整行填成 NaN。方向约定以"能按列名对齐"为准,见易错点 11
原码写的是 if offset > 0: raise,把唯一合法的用例(正 offset 正好抬定义域)全拒了,报错文案却是"offset 必须大于 0",自相矛盾。改成校验真正的前提:log 要求自变量取正。offset=0 对全正数据合法(offset=None 的自动分支本来就取 0)。Codex 代选、待用户确认 —— 若原意是"只收正 offset",改回即可
47 算法一律由用户本人写;工程由 Codex 写(用户 2026-10-03 明确)
用户原话:"记住算法的东西一定要我来写,以后就算我说让你来写你也不要帮我写,工程我觉得我还可以,你写我看吧"。
这条比决策 25 更硬:25 说"默认归用户",47 说"即便用户改口也不代写"——目的是保住"一边学一边自己写"的学习路径。
工程侧(入口/校验、tests/、lint、文档、卡片、.gitignore)反过来由 Codex 直接落盘,用户只读;
分地盘按文件划,不按函数划。决策 39 的"语义由用户拍板"继续有效。

| 48 | monte_carlo 的四个口径由用户拍板:实现传 size=n;结果含 NaN 判失败;std 用 ddof=0;出错仍统一返回 success=False | 用户 2026-10-04 直接改在代码里。① 批量大小由实现传(sampler(rng, size=n, **kwargs)):调用方少写一次, 代价是每个 sampler 都必须能接住 size,且 **kwargs 里不能再传 size(会重复传参)—— 已写进 docstring。 ② 结果里只要出现 NaN 就 success=False:MC 的 NaN 多来自除零 / 模型发散,静默统计只会得到一列 nan 指标。 ③ np.std 默认 ddof=0,现在显式写明(与 describe 那条线的 ddof=1 不一致,属已知差异)。 ④ 计算期异常仍全部包成 success=False(保留 try/except Exception);代价是自己写错代码时没有 traceback。 配套 Codex 代改:入口 np.asarray(..., dtype=float) —— 布尔结果此前会让 np.std 抛错(真 bug,已修)。 唯一未定:ci_low/ci_high 保持"结果的分位数"(现状)还是改成"均值的置信区间"。

| 49 | 这类改动归用户本人:入口没收紧、少考虑情况、不符合简单原则 —— 用户 2026-10-04 明确"交给我改,不要帮我改" | 用户原话:"以后像这种入口没收紧、少考虑情况、不符合简单原则的情况交给我改,不要帮我改,把这几条经验写入经验文档"。 这是决策 47 的收窄:47 把"入口 / 校验重构"划给了 Codex,49 把它收回 —— 凡是带判断、取舍、设计成分的, 一律由用户写,Codex 只报告 + 列选项(与决策 39 同一逻辑)。判据一句话:这处改动要不要"想一下才对" —— 要,就留给用户;不要(ruff、格式化、docstring 缺节、tests/、STATUS / CHANGELOG、函数卡片)才由 Codex 顺手补。 本轮实例(本该留给用户,Codex 已代改、用户可回退重写):monte_carlo 的 n 按 §4.7 收严;入口 np.asarray(..., dtype=float)。 经验已写入 docs/dev/personal-pitfalls.md 第 18 / 19 / 20 条,并把第 1 条(类型判断只写 int)复发 +1。

| 50 | monte_carlo_probability 的区间由 Wald 改 Wilson score;小概率不再判失败(改成 message 警告);confidence 记进 params | 用户 2026-10-05 按"6 条待办"自行改定。Wilson 在 p 贴近 0 / 1 时不会退化成零宽:实测 p=1、n=1000 从 (1.0, 1.0) 变成 (0.99617, 1.0);p=0 从"判失败"变成 (0.0, 0.00383)(原 Wald 是 0)。区间口径与 statsmodels.proportion_confint(method="wilson") 对齐,已加成对照测试。 遗留(2026-10-05 第二轮复核,同日已清):event: Callable[..., bool] 注解与"吃整批、返回数组"不符 → 已改 Callable[..., Any]; 警告文案把 p≈1 也说成"小概率事件" —— 用户已于同日改掉(现在两半都覆盖,见 CHANGELOG 本轮)。

| 51 | monte_carlo_integral 的 data = n 个样本各自的贡献(func(X_i) × 体积);积分估计值看 metrics["mean"] | 用户 2026-10-05 写完实现。Codex 审查只改了一处纯 bug(决策 31):体积 lows - highs → highs - lows —— 奇数维体积为负(1D x² 于 [0,1] 实测 -0.3324、3D x 得 -0.5005),而 2D 因两个负号相乘"看着对" (+0.2499),把 bug 藏住了;改后实测 0.3324 / 0.5005 / 1.9971 全部正确。docstring 与测试都按上述口径写死。 留给用户 3 条:① result.message 无条件覆盖(失败被写成"成功");② bounds 形状没校验([0, 1] 抛裸 IndexError); ③ n: int 与全模块的 int | np.integer 不一致。

| 52 | monte_carlo_integral 的入口口径由用户第二轮改定:message 只在成功分支改写;bounds 必须是非空 list/tuple 且形状 (d, 2);n 收 int \| np.integer | 用户 2026-10-05 按 Codex 报的 3 条自行改定。message 走 if result.success: / else:(失败分支保留失败语义并追加追溯信息: 维度 / 体积 / 模拟次数)—— 修掉"success=False + 一句成功文案"的自相矛盾。bounds 入口两道校验把 [0, 1] 从 IndexError: too many indices 变成带当前形状的可读 ValueError。遗留 2 条新口径: ① isinstance(bounds, (list, tuple)) 把 np.array([[0, 1]]) 也拒了,与"尽量传 ndarray"的偏好冲突; ② if / else 两支里 name / params["bounds"] / params["dimension"] 各写了一遍(易错点 20)。

| 53 | random_walk 的默认步长采样器不能用 rng.integers(..., dtype=float)(numpy 直接抛 TypeError);step_sampler 收到的 size 是二维 (n_paths, n_steps) | 用户 2026-10-05 写完第一版。默认采样器原写 rng.integers(0, 2, size=size, dtype=float) * 2.0 - 1.0 —— Generator.integers 不收浮点 dtype(TypeError: Unsupported dtype dtype('float64') for integers), 于是整个默认路径 100% 失败,连 docstring 的 Examples 都跑不通;Codex 按决策 31 改成 rng.integers(0, 2, size=size) * 2 - 1 (交给后面的 np.asarray(..., dtype=float) 转类型),实测形状 (5, 101)、步长恒 ±1、同种子可复现。 step_sampler 的 size 是 (n_paths, n_steps) 而不是 monte_carlo 那样的一个整数 —— 契约已写进 docstring, 遗留 4 条待办见"下一步"(TypeError/ValueError 分工、steps 形状校验、NaN 口径、params 缺 random_state)。

| 54 | random_walk 第二轮口径由用户改定:n_steps/n_paths 类型错抛 TypeError、取值错抛 ValueError;加形状与 NaN 两条校验;默认步长改 rng.choice([-1.0, 1.0]);params 补 random_state;metrics 改 mean_terminal/std_terminal | 用户 2026-10-05 自行改定,Codex 只修了一处纯 bug:call_params 定义在 try 内、却被 except 引用, 导致所有失败路径抛 UnboundLocalError(实测 5/5 失败输入全中)。按原意前移到 try 之前,未改任何口径。 遗留 3 条:① start 的入口类型校验被删(start="a" 从 TypeError 变成 success=False),是有意还是顺手删的? ② 签名 n_steps: int / n_paths: int 与运行期收 np.int64 不一致(§4.7;monte_carlo_integral 的 n 已放宽); ③ n_paths == n_steps 时,返回转置形状的采样器仍会静默通过(形状校验天生发现不了,见易错点 25)。

| 55 | step_sampler 的"方阵歧义"按方案 B 解决:size 改成整数 n_paths * n_steps、必须返回一维数组,内部 reshape(n_paths, n_steps) | 形状校验(决策 54)对"方阵"天生失效 —— 转置 (n_steps, n_paths) 正好等于目标 (n_paths, n_steps)。 用户 2026-10-05 拍板选 B(Codex 代改,用户明确要求)。转置 (n_steps, n_paths) 与目标 (n_paths, n_steps) 在方阵时形状相同, 校验从形状上无法区分 —— 根源是契约本身有歧义(一个 tuple 尺寸给了两种朝向)。改成整数尺寸 + 要求一维后, 二维返回一律判失败,转置问题从根上消失。顺带与 monte_carlo 的 sampler 契约(决策 48 的 size=n)统一: 实测给 monte_carlo 写的采样器可以直接丢给 random_walk。关键点:不要用 .ravel() 救场 —— 那会把转置的二维摊平, 歧义原样回来。代价(已写进 docstring):step_sampler 不能再返回二维,步与路径的对应关系是行主序。 探针实测:默认采样器 (5, 101) 正常;后四种(转置二维 / 方阵二维 / 长度 +1 / (N, 1))一律判失败。

| 56 | markov_simulate 的 initial 语义待用户拍板(Codex 只报选项,未代改,决策 49) | 现状用 isinstance(initial, int) 区分"标量状态 / 状态数组",两条分支的口径互相打架:标量分支按路径数铺满 (np.full(n_paths, initial)),数组分支却按状态数校验长度(len(initial) != s 直接报错)—— 于是数组分支只有 n_paths == s 时才可能跑通;而 paths[:, 0] = current_states 又写在 try 外面, 长度不匹配时裸抛 ValueError(本该 success=False,同易错点 26 一族)。另有三个口子: np.int64(0) 掉进数组分支、被 as_array 拒(与 steps/n_paths 收 np.integer 不一致,易错点 1 复发); True 因 isinstance(True, int) 为真被当状态 1 静默放行;越界状态(如 initial=5、s=2)只在 第一轮索引时才炸、被 except 吞成 success=False。 2026-10-09 更新:② 已办结(用户补了 np.any(arr < 0),含负元素的行随机矩阵现在入口直接报错)。 2026-10-10 更新:③ 办结(paths 的构造与首列赋值挪进 try,长度不匹配/越界不再裸抛); ④ 办结(越界现在被吞成 success=False,但仍没在入口拦);⑤ 办结(std_terminal → terminal_distribution); ② 升级为带容差 -1e-12;params 的 n_steps → steps、initial 存成可 JSON 化的值。 只剩 ①:bool 仍被当状态 1(np.integer 已收),以及"数组按状态数 vs 按路径数"两条口径的矛盾(A / B / C)。

| 57 | markov_steady_state 的口径:4 条已办、3 条 + 2 个新口子仍待拍板(Codex 只报选项,未代改,决策 49) | 2026-10-09 用户第二轮办结:① 补 np.any(arr < 0)(含负元素的行随机矩阵现在入口即报错,原先会溢出成 nan 却报 success=True);② tol 拆成 TypeError(类型 + bool)与 ValueError(取值); ④ 选口径 B:converged=False → success=False;⑥ params 补 transition(arr.tolist(),可 JSON 化)。 仍待拍板:③ tol=np.nan(nan <= 0 为假,穿过校验白跑 §max_iter 次迭代); ⑤ max_iter 的类型错仍抛 ValueError(与决策 54 的"类型错 TypeError"不一致); ⑦ 两把尺子宽严不一——非负校验用精确 arr < 0,同一个函数里的行和校验却用宽容的 np.allclose, 实测 [[1.0, -1e-16], [0.5, 0.5]](行和都是 1)会被判非法,建议改成 < -1e-12 这类容差; ⑧ 未收敛时 success=False 但 data 仍是未收敛向量(文件内唯一"失败却带 data"的分支)—— 保持还是 data=None? 2026-10-10 更新:③(tol 拦 NaN)、⑤(max_iter 类型错 → TypeError)、⑦(非负校验带容差)办结。 只剩 2 条:⑧(上面那条),以及新出现的"非方阵抛 TypeError,而 markov_simulate 的同一条校验抛 ValueError"不一致。

| 58 | mm1_metrics 的稳定性分界由 Codex 按决策 31 直接修(< → <=),另有 4 条口径待用户拍板 | docstring 与既有测试写的都是 λ >= μ 不稳定,代码却写 service_rate < arrival_rate —— 于是 λ == μ 掉进 try、rho == 1.0 → ZeroDivisionError,被 except 吞成 success=False (碰巧对,但 message 成了"计算失败:ZeroDivisionError - float division by zero")。两支的可观测输出完全相同 (success=False / metrics={} / data=None),改 <= 只换 message,故直接修 + 加回归测试。 待拍板:① λ = 0 现在被 ValueError 拒(数学上是合法退化解:空系统 L = W = 0); ② np.int64 / np.float32 全被 TypeError 拒(文件内 random_walk 的 start 收 np.number、 整数参数收 np.integer,§4.7 有共识);③ data=metrics —— 同一个 dict 对象(r.data is r.metrics), 且成功分支给 dict、失败分支给 None,与文件内"data 是数组"的习惯不同; ④ 失败分支的 metrics={} 会丢掉 rho 等已算出的量(可选保留)。 2026-10-10 更新:① ② ④ 办结(λ = 0 放开为合法空系统、两个速率收 np.number、失败分支报 rho); ③ 只办了一半(data=metrics.copy() 去掉了同一个 dict 对象的别名,但 data 仍是 dict、失败时仍是 None)—— 只剩这一条。

| 59 | models/metrics.py 开工:6 个指标 + 共享入口 verify_yt_yp 由用户写,Codex 只做工程侧审查(决策 47 的延续) | 本轮把 metrics.py 的 L1 TODO 转成实现(compare_models 仍 NotImplementedError)。六个指标共用一个入口 verify_yt_yp:as_array(ndim=1) → 形状不一致 / 长度 0 / 含 NaN 一律 ValueError(与决策 48 的"NaN 判失败"同口径)。 Codex 侧(决策 47 授权范围):补 7 处 docstring(Parameters / Returns / Raises)、统一报错文案为"参数名 + 实际值"、 清 6 条 F541 + 3 条 W293、补空行;tests/test_models.py 两条指标测试转正 + 新增 18 条。 用户侧拍板并亲手改:r2_score 的 ss_tol 基准从 np.mean(yp) 改成 np.mean(yt)(真 bug,见决策 60 与易错点 31), 顺带把无意义的 ss_tol 改名 ss_tot。Codex 探针实测:修前 0.8803827751196172、修后 0.875 = sklearn.metrics.r2_score 一致。 另有 4 条口径仍待拍板:mape 遇负 y_true(现在返回无意义的负数)、只拦 NaN 不拦 inf、 rmse(1.0, 1.0) 标量被当 1 个样本、compare_models 的键名 "r2" 与函数名 r2_score 对不上。

| 60 | smape 的零除告警按决策 31 由 Codex 直接修(np.where → np.divide(..., where=)),数值结果不变 | 原式 np.where(denom == 0, 0.0, 2 * |yt - yp| / denom) 的结果是对的(分母 0 记 0),但 np.where 两条分支都会被求值, 分母为 0 时仍触发 RuntimeWarning: invalid value encountered in divide;而 pyproject.toml 开了 filterwarnings = ["error::RuntimeWarning"] —— 于是任何"含 0 的 smape"测试都会直接红(探针复现)。 改成 np.divide(..., out=np.zeros_like(denom), where=denom != 0.0) 后告警消失、数值与口径完全不变, 并补了一条测试把"不许告警"钉住(test_smape_is_silent_when_denominator_is_zero)。 判据:只是防告警、不改语义 → 工程侧;如果是"改公式"(如决策 59 的 r2),一律留给用户(决策 49)。 | 61 | models/metrics.py 的三条入口口径由用户第三轮改定(Inf / 至少 2 个样本 / mape 拦非正数),Codex 只同步 docstring + 测试 | 前一轮报的 4 条待拍板里,用户改了前三条:① verify_yt_yp 增加 np.isinf(yt).any() or np.isinf(yp).any() (只拦 NaN 时 rmse([1.0, inf], ...) 会安静地返回 inf);② 增加"至少 2 个样本"(1 个点的误差指标没有统计意义, r2_score 更是直接无定义),标量也因此被拦住(不再被当成 1 个样本悄悄算);③ mape 从"只拦 0"改成 np.any(yt <= 0)(原来的 mean(|yt - yp| / yt) 在 yt < 0 时返回负数,实测 -1.0)。 Codex 侧:把这三条口径写进 7 处 Raises(又是易错点 30:口径一改、六处 docstring 立刻过期,本轮复发 1); test_mape_rejects_zero_in_y_true 的 match 文案随新消息改为"非正数",并新增 Inf / 至少 2 个样本两条测试。 遗留小问题(Codex 没动):verify_yt_yp 里 yt.ndim == 0 那一支是死代码 —— as_array(..., ndim=1) 必然 reshape(-1),ndim 恒为 1,可整段塌成 if yt.shape[0] <= 1:(真正的判定条件)。

| 62 | compare_models 由 Codex 按 A 档落地,指标名口径选别名映射(用户 2026-10-12 拍板) | 前一轮问的是"metrics=("rmse","mae","r2") 里的 "r2" 与函数名 r2_score 对不上,用哪个"。用户选两个都收, 所以别名表落在模块级的 _METRIC_FUNCS("r2" 与 "r2_score" 指同一个函数):传哪个名字,输出表的列就叫哪个名字, 两个一起传报 ValueError(否则会出两列一模一样的数字)。别名只解决"名字认不认",不改变数值。 实现要点:models 必须传模型对象(fit / predict),不是拟合结果;X 必须二维(as_array(ndim=2) 不替你猜); 分折是手写 K 折(get_rng(random_state).permutation + np.array_split),不依赖 sklearn —— 只装了 4 个核心依赖也能跑; cv=0 全量拟合后打分(cv=1 报错:要么 0 要么 ≥2);cv >= 2 报折间均值;每折至少 2 个样本 ⇒ cv <= n // 2; 排序方向跟着第一个指标走(误差类升序、R² 降序)。adjusted_r2 不进表(还要 n_features,compare_models 不知道每个模型用了几列)。 测试 23 → 31 条:首列排序(升序 / 降序两条)、别名、坏指标名、坏 models / cv、每折样本数下限、random_state 可复现。

下一步

⭐ 当前进度与待办(2026-10-12 第十轮)

本轮:你办掉第九轮那 4 条口径里的 3 条,Codex 落地 compare_models(别名映射)。

你改的三条(决策 61):verify_yt_yp 拦 Inf、拦"只有 1 个样本"、mape 从"只拦 0"改成 yt <= 0。 三条我都复测过:rmse([1.0, inf], [1.0, 2.0]) 现在报 ValueError;rmse(1.0, 1.0) 也报错(标量不再被当成 1 个样本); mape([-1.0, 2.0], [1.0, 2.0]) 由返回 -1.0 变成报错。

Codex 这轮:① 把三条新口径写进 7 处 Raises(口径一改、docstring 就过期 —— 易错点 30 复发 1 次); ② compare_models 按 A 档实现(决策 62),指标名走别名映射:_METRIC_FUNCS 里 "r2" 与 "r2_score" 指向同一个函数, 传哪个列名就叫哪个、两个一起传报错;③ 顺手补了 X 必须二维、cv 白名单(bool 不收、cv=1 无意义、cv <= n // 2)、 models 必须是"有 fit/predict 的对象"等入口;④ 测试 23 → 31 条。

三连:pytest 233 passed / 25 skipped(258 收集)、ruff 0、pyright 0。

接下来(models 的完成标志 compare_models 已达成)

  1. regression.py 的 fit_linear / fit_polynomial(A 档薄封装;把 base.Model 三件套跑通, 顺带把 tests/test_models.py 里那两条还挂着的 skip 转正)。注意:fit_linear 落地后要补一条 "compare_models + 真实模型"的集成测试 —— 原来那条跳过用例写的是 compare_models({"linear": fit_linear(X, y)}), 把拟合结果当模型传是错的,本轮已把它改写成用测试桩(_MeanModel / _LinearModel)。
  2. models 剩下的 fit_nonlinear / timeseries / ml 按 roadmap 走。
  3. 之后转 decision/entropy_weights(S 档,你写,三个坑:方向 / 量纲 / 零值)。

⏳ 第十轮新出现的 1 条小口径(Codex 只报不改,决策 49)

verify_yt_yp 里 yt.ndim == 0 那一支是死代码:as_array(..., ndim=1) 内部一定 ravel(),ndim 恒为 1, 所以 if yt.ndim == 0 or yt.shape[0] <= 1: 里前半段永远为假,报错文案里的 else 1 也永远走不到。 整段可以塌成 if yt.shape[0] <= 1:(行为完全不变,只是少一段"看着像在防什么、其实没防"的代码)。 你决定改不改 —— 不改也能跑,只是以后回头看会多花几秒钟。

⭐ 当前进度与待办(2026-10-10 第九轮)

本轮:models 开工,你亲手写了 models/metrics.py —— 6 个回归指标(rmse / mae / mape / smape / r2_score / adjusted_r2)加一个共享入口 verify_yt_yp;compare_models 仍是 NotImplementedError。 Codex 只碰工程侧:7 处 docstring(补 Parameters / Returns / Raises,把口径写成契约)、报错文案统一成 "参数名 + 实际值"、清 6 条 F541 + 3 条 W293 + 空行、tests/test_models.py 5 条 → 23 条(两条转正 + 18 条新增)。

Codex 找到 1 个真 bug(已由你亲手改定):r2_score 的 ss_tot 用了 np.mean(yp),基准必须是 np.mean(yt)。 探针实测(y_true=[1,2,3,4,5]、y_pred=[1.5,2.5,2.5,4.5,5.5]):修前 0.8803827751196172、修后 0.875, 与 sklearn.metrics.r2_score 完全一致。它会连带污染 adjusted_r2。注意 r2_score(y, y) == 1 仍成立 —— 完美拟合与常量 y 都会掩盖这类错,所以新增的测试特意用"非完美拟合"的数据(见个人易错点 31)。

另修一处会真炸的工程问题(决策 60):smape 的 np.where 掩码防不住零除,filterwarnings=error::RuntimeWarning 下任何含 0 的测试都会红 → 改 np.divide(..., where=),数值结果不变,并补测试钉住"不许告警"。

三连:pytest 224 passed / 26 skipped(250 收集)、ruff 0、pyright 0。

接下来(按 roadmap §四,models 的完成标志就是 compare_models)

  1. compare_models(同文件最后一块;cv=0 走"全量拟合后打分"分支,返回按第一列指标排序的 DataFrame, 依赖 pd.DataFrame,顺带定 metrics 的键名口径)。
  2. regression.py 的 fit_linear / fit_polynomial(把 models/base.py 的 Model 协议三件套跑通)。
  3. 然后转 decision/entropy_weights(S 档,你写,三个坑:方向 / 量纲 / 零值)。

✅ 第九轮那 4 条口径(2026-10-12 全部办结)

  1. mape 遇负 y_true 返回负数 → 用户选"拦":改成 np.any(yt <= 0) 直接报错并点名改用 smape (另一条路是用 |y_true| 做分母,没选)。测试文案随之改成 match="非正数"。
  2. inf 不拦 → 用户选"跟 NaN 一样拦":verify_yt_yp 增加 np.isinf(...).any()(rmse 不再安静地返回 inf)。
  3. 标量被当成 1 个样本 → 用户选"要求至少 2 个点",于是 rmse(1.0, 1.0) 现在报 ValueError。 注意:这与 as_array 白名单里"标量要不要摘掉"是两个问题(那条仍未拍板,见下方"其余待办"第 6 条)。
  4. compare_models 的键名 "r2" vs "r2_score" → 用户选别名映射(两个都收),由 Codex 实现,见决策 62。

⭐ 当前进度与待办(2026-10-10 第八轮)

本轮:你按第七轮的清单改了 5 处口径 —— markov_simulate(非负校验带容差、收 np.integer、赋值挪进 try、 metrics 换成 terminal_distribution)、markov_steady_state(容差、max_iter 的 TypeError、 tol 拦 NaN)、mm1_metrics(λ = 0 放开、收 numpy 标量、失败分支报 rho、data 去别名)。 三处算法本体仍然正确(探针复测:终点分布 {0: 0.6644, 1: 0.3356} 对照 [2/3, 1/3]; λ = 0 给 L = Lq = Wq = 0、W = 1/μ;r.data is r.metrics 已为 False)。 Codex 只同步 docstring + 6 条测试、清 ruff(B905 的 zip 缺 strict + 2 条空白)。 三连:pytest 204 passed / 28 skipped、ruff 0、pyright 0。

接下来三件事

  1. 下面只剩 5 条小口径,基本都是"一个值 / 一个类型"级别的决定。
  2. L0 最后一项 save_figure(A 档 = 工程,viz/export.py)—— Codex 直接实现,你只读。
  3. 之后往 models / decision 走(见下方第八轮备忘)。

⏳ 只剩这 5 条口径待你拍板(Codex 只报不改)

① markov_simulate 的 bool 漏网:isinstance(True, (int, np.integer)) 为真 → initial=True 被当成状态 1。 实测现在不再静默给错答案(np.full(n_paths, True) 让后面 arr[current_states] 变成布尔掩码、崩掉后吞成 success=False), 但 message 是个看不懂的广播错误 —— 建议入口先判 bool → TypeError,把易错点 1 的最后一格补上。

② markov_simulate 数组分支的长度口径(决策 56 的 ①,仍是最大的那条):标量按路径数铺满、数组按状态数校验, 两条对不上(只有 n_paths == s 才通)。A / B / C 三个选项见决策 56。

③ markov_steady_state 的 data:converged=False → success=False,但 data 仍是未收敛向量 (文件内唯一"失败却带 data"的分支)。保持(B)还是 data=None(C)?

④ 非方阵的异常类型两处不一致:markov_steady_state 抛 TypeError、markov_simulate 抛 ValueError (同一条"不是二维方阵"的校验)。numpy 自己对形状问题抛的是 ValueError;要统一成哪个?

⑤ mm1_metrics 的 data:现在是 metrics.copy()(不再是同一个对象),但仍是 dict,失败分支是 None。 选项:A 保持;B data=None;C data 给一个 5 元素数组。

📦 第八轮备忘:models 与 decision 是干什么的

这两个包目前一行都没实现(全部 TODO + NotImplementedError),是 solve 之后的两大块。分工完全不同:

models —— 建模 decision —— 评价决策
回答的问题 "这条规律是什么?能不能预测?" "这几个方案哪个好?"
输入 特征 X + 目标 y(有监督)或只有 X(聚类/降维) 决策矩阵(行 = 方案、列 = 指标),通常无标签
输出 模型对象 + 预测值 + 指标;compare_models 出对比表 权重向量 + 排序表(rank)
主要依赖 sklearn / statsmodels(薄封装) numpy 手写为主(S 档:熵权、AHP、GM(1,1)、TOPSIS)
美赛场景 预测题(人口、销量、气温)、"要不要上复杂模型" 中文赛 / ICM 的评价题(AHP、TOPSIS、灰色关联、熵权)
roadmap 时间盒 第 2–3 周 第 4 周

models(19 个函数,5 个文件):base.py 已写好 Model 协议(fit / predict / score 三件套,已实现); metrics.py 是 8 个指标 + compare_models(返回 DataFrame,可直接 save_table(fmt="latex")); regression.py / timeseries.py / ml.py 全是"一行转发 + 参数校验 + Result 包装"。 注意两处:① rmse / mae / r2_score 的注释写的是 TODO(L0),但 roadmap 的 L0 12 项里没有它们, 它们属于 L1 的 models 清单 —— 注释待统一;② tests/test_models.py 用的是模块级 pytestmark, 违反决策 26(逐条 skip),开写时要拆开。

decision(8 个函数,4 个文件):weighting.py(等权 / 熵权 / CRITIC)、ahp.py(判断矩阵 + 一致性比率 CR)、 topsis.py、grey.py(灰色关联 + GM(1,1) 预测)。 使用顺序是硬规矩(decision/__init__.py 里写着):先 normalize 无量纲化 → 再声明方向 benefit_columns → 再 entropy_weights / ahp_weights → 最后 topsis。跳过第一步直接算权重是评价类题最常见的翻车方式。 S 档(要配函数卡片)三个:entropy_weights / ahp_weights / gm11_fit_predict,另有 topsis 也标了卡片。 永久不做:dea_efficiency(要解一串 LP、低频)、fuzzy_comprehensive_evaluation(隶属度函数没有通用形式)。

建议的开工顺序:先 models/metrics.py(纯函数、最容易、compare_models 是"要不要上复杂模型"的裁判, 也是 roadmap §四里 models 的完成标志)→ 再 fit_linear / fit_polynomial(把 Model 协议跑通) → 然后 decision 的 entropy_weights(S 档、20 行、三个坑:方向 / 量纲 / 零值)→ topsis → ahp → gm11。

✅ random_walk 的 4 条待办(2026-10-05 已办结)

  • ① 非整数 n_steps 抛 ValueError → 已拆成两条:类型错 TypeError、取值错(<= 0)ValueError,n_paths 同样处理。
  • ② steps 没有形状校验 → 已加 steps.shape != (n_paths, n_steps) 判失败(实测转置采样器现在能拦住)。
  • ③ NaN 不判失败 → 已加 np.isnan(steps).any(),与 monte_carlo 的 NaN 口径(决策 48)拉齐。
  • ④ params 少 random_state / metrics 抄了一遍 → 已补 random_state;metrics 换成 mean_terminal / std_terminal (终点位置的均值与标准差,ddof=0)—— 这才是"游走"该看的指标,测试里还顺手验了 std ≈ √t。

✅ random_walk 的口径(2026-10-05 全部办结)

  • ① start 的入口类型校验被删 → 已恢复;文案的"必须是整数"改成"必须是数值"(这条校验本来就收 float), 类型名统一 {type(start).__name__}。
  • ② 签名注解比行为窄 → 已改成 int | np.integer,np.int64 的测试补回来。
  • ③ n_paths == n_steps 的方阵歧义 → 用户选方案 B(决策 55):size=n_paths * n_steps(整数)、 要求返回一维、内部 reshape(n_paths, n_steps);二维返回一律判失败。实测转置二维 / 方阵二维 / 长度不符全部拦下, 且 monte_carlo 风格的采样器可直接复用。

✅ monte_carlo_probability 的 6 条待办(2026-10-05 全部办结)

  • ① p <= 1e-3 判 success=False → 改成 warning_message,仍返回区间(实测 p=0 → (0.0, 0.00383))。
  • ② Wald 在 p 靠近 1 时退化 → 换 Wilson(p=1、n=1000:(1.0, 1.0) → (0.99617, 1.0))。
  • ③ call_params 没记 confidence → 已加。
  • ④ event / sampler 不可调用被吞成 success=False → 已加 callable 校验,现在抛 TypeError。
  • ⑤ metrics 混 np.float64 → float() 统一(实测 4 个 key 全是 float)。
  • ⑥ n: int → 已改 int | np.integer。

还剩 1 条 → 已办(本轮)

  1. event: Callable[..., bool] 的注解与行为不符 → 已改 Callable[..., Any],与 sampler / simulator 对齐。 pyright 复测:向量化事件函数不再被拒(原注解会报 reportArgumentType)。决策 50 的遗留清零。

✅ monte_carlo_integral 的 3 条待办(2026-10-05 已办结)

  • ① result.message 被无条件覆盖 → 已改成 if result.success: / else: 两个分支; 失败时是"蒙特卡洛多重积分失败:维度 1,体积 1.0000,模拟 100 次。蒙特卡洛模拟失败:TypeError - ...", success 与 message 不再打架。
  • ② bounds 形状没校验 → 入口加了 isinstance(bounds, (list, tuple)) 与 ndim == 2 and shape[1] == 2 两道; bounds=[0, 1] 现在报"bounds 必须形如 [(lo, hi), ...],当前形状为 (2,),请检查是否漏了括号"。
  • ③ n: int → 已改 int | np.integer。

✅ monte_carlo_integral 的 2 条新口径(2026-10-05 已办 1 条、剩 1 条)

  • ① bounds 把 ndarray 拒了 → 已加 isinstance(bounds, np.ndarray) 分支,np.array([[0.0, 1.0]]) 现在能收。
  • ② if / else 里那三行重复 → 已把 name / params["bounds"] / params["dimension"] 提到 if 之前,只剩 message 分两支。

还剩 1 条(你的活)

  1. 第一道 isinstance 分支现在是冗余的:np.asarray 本来就同时收 list / tuple / ndarray, 而 []、"abc"、标量这些都会被下面那句形状校验抓住(报"当前形状为 (0,)"等)。 整段可以塌成两行:bounds_arr = np.asarray(bounds) + 形状校验 —— 顺带 bounds.copy() 也是多余的一次拷贝。

⏳ monte_carlo 只剩 1 个口径要你确认

ci_low / ci_high 是不是就按"结果的分位数"定?

现状 np.percentile(data, [2.5, 97.5])。实测 n=2e5、E[X²]=1 给出 [0.001, 5.0455] —— 这是模拟结果的分散度(预测区间),与 p05 / p95 同类,只是分位点不同。 而"均值估计的置信区间"(MC 的统计误差)是 mean ± 1.96·std/√n = [0.9962, 1.0086], 两者差两个数量级,论文话术也完全不同:

  • A(现状):写"95% 的模拟结果落在 [a, b]"(描述风险分布)
  • B:改成均值 CI → 写"估计值 1.0024 ± 0.0043(95% CI)"(证明 n 够大)
  • C:两者都要 → 指标要改名(如 p025/p975 + mean_ci_low/mean_ci_high)

注意:monte_carlo_probability 里的 ci_* 已经是估计量的 CI(Wilson 区间,决策 50),所以这两个函数的 ci_* 目前含义不同——这正是要给它们定口径的原因之一。

✅ 已拍板(决策 48)

  • 批量大小由实现传 size=n;sampler 必须能接住 size,**kwargs 里不要再传 size。
  • 结果含 NaN → success=False(不落 metrics)。
  • std 用 ddof=0。
  • 出错仍统一返回 success=False(try/except Exception 保留)。

⏳ 另外两个小口径(你说了算)

  • random_state 的注解:经验文档第 3 条写的标准是 int | np.integer | np.random.Generator | None, 但 stats/bootstrap.py、solve/simulate.py、stats/inference.py 现在都写 int | None(只有 core/random.py 是全的)—— 要不要全项目对齐?(运行期 get_rng 收 np.int64,所以是注解与行为不一致,不是功能 bug)
  • std 的 ddof:monte_carlo 用 ddof=0,describe 那条线用 ddof=1。要不要统一?

其余待办

  1. L0 的最后一项是 save_figure(A 档 = 工程,viz/export.py)—— 由 Codex 直接实现,用户只读。

  2. L0 跑通后,找一道真题(建议 2025 C)走完整流程,反向沉淀函数。

  3. 已拍板的旧口径(留档):log_transform 的 offset 维持现状(决策 35); describe / zscore 默认丢掉非数值列(决策 37);变异系数用 σ / |μ|(决策 38); 分工见决策 39;correlation_matrix 走方案 A(决策 41)。

  4. 仍未答的一个小问题:correlation_matrix 现在只收 DataFrame, 而 describe / zscore / transform 都收 ndarray —— 要不要也放开?(你说了算)

  5. t_test 有 4 个口径等你拍板(Codex 一律没动,按决策 39):

    • ① 单样本的结论文案说错了。 b=None 时 test_name 已经是"单样本 t 检验", 但 conclusion 仍写"两组数据存在统计学上的显著差异" —— 单样本根本没有第二组。 建议按分支拼文案(单样本说"该样本均值与 value 有显著差异")。
    • ② paired=True 时 NaN 是按各自的位置丢的,会把不同行配成一对(静默错位,不报错)。 两个方向:配对前先成对删缺失,或直接拒绝含 NaN 的配对输入。你定。
    • ③ value 只在 b is None 时生效,同时传 b 和 value 会被静默忽略 —— 要不要改成报错?
    • ④ t_test(DataFrame) 会把所有数值列摊平成一个样本(已写进 Notes)。 多列不同量纲(gdp + pop)会被混在一起,而且没法拿"一张表里两列"当两组比。 要不要改成:多数值列的 DataFrame 直接 ValueError、强制先自己取列?
  6. as_array 落地后留下的 3 个小口径(Codex 没代选,按决策 39):

    • int / float 标量还在白名单里(为兼容 normalize / standardize / log_transform 三个 docstring 写的"数值型标量")。但标量进来是 0 维数组,axis=0 的算法照样炸 —— 要不要把标量摘掉、同时改三处 docstring?
    • 内容转不成 float 时统一抛 TypeError(决策 45)—— normalize(含文本列的表) 原来抛 ValueError。 要不要改回 ValueError("取值问题")?
    • 现在 kind == "frame" 时你要自己再写一遍 x.select_dtypes(include=[np.number]).columns 取列名。 嫌烦的话可以让 as_array 顺带把选中的列返回(改成三元组),一句话的事。
  7. bootstrap_ci 的 5 个待拍板口径(Codex 一律没动,按决策 39;详见 docs/algorithms/bootstrap_ci.md §10):

    • x 里的 NaN 静默传进统计量 → nan 区间(confidence_interval / t_test 都是先丢掉的,口径不齐)
    • n_resamples <= 0 → 现在抛 IndexError: index -1 is out of bounds(应在入口给可读 ValueError)
    • confidence 越界 → 现在由 numpy 抛 Percentiles must be in the range [0, 100]
    • method="bca" → docstring 承诺过、实测 NotImplementedError(补实现 ≈25 行,还是从 docstring 删掉?)
    • bootstrap_statistic 仍是 NotImplementedError,而 bootstrap_ci 把那段重采样循环内联了 —— 建议抽出去让 bootstrap_ci 调它(易错点 16 的变体:同一段算法写了两遍,只实现了一遍)