Files
MCM-ICM_Toolkit/STATUS.md
T
guzhujushi 0955890010 feat(solve): 审查 markov_steady_state:工程侧收口 + 8 条测试
用户实现的 markov_steady_state 经探针实测算法本体正确(3 状态与特征分解逐位一致、
200x200 一致到 1.9e-12 且只用 6 次迭代 / 0.10s),本次只动工程侧,未改任何算法行:

- ruff 5 条 W293 全清;mm1_metrics 前 4 个空行 -> 2;失败文案改成全角冒号
- docstring 补 max_iter / tol 两个参数与 Raises,写清 tol 是相邻差口径
  (实测慢混合链 tol=1e-10 -> 实际误差 4.6e-9,被 1/(1-|lambda2|) 放大约 50 倍)
- Notes 修正"周期链会不收敛"的说法:可约链的实情是平稳分布不唯一,
  迭代法给的是离初值最近的那个(均匀初值对双随机矩阵本身就是不动点)
- tests/test_solve.py:摘掉 1 条 skip + 新增 7 条(该函数共 8 条实跑)
- STATUS.md 新增决策 57(6 条待拍板口径)与第六轮「下一步」;CHANGELOG;易错点 28

校验:pytest 196 passed / 30 skipped(226 收集);ruff 0;pyright 0
2026-10-09 23:20:13 +08:00

76 KiB
Raw Blame History

STATUS — 当前进度

更新日期:2026-10-05

本次任务

第一阶段(已完成):审查并精简 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(均为用户写)。剩余 1 项:save_figure(Codex 写)。 monte_carlo 的口径已拍板(决策 48);monte_carlo_probability / random_walk 的待办均已办结; markov_simulate(决策 56,5 条)与 markov_steady_state(决策 57,6 条)的入口口径待你拍板。 另 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 真能跑 - ⏳ 留给你的 5 条见"下一步 → 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 真能跑 - ⏳ 留给你的 6 条口径见"下一步 → markov_steady_state 待你拍板"(决策 57)

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

验证结果

检查 venv conda mcm-toolkit
pytest 见下(单列) 196 passed, 30 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(与两套环境一致)
  • 226 条用例:196 passed / 30 skipped / 0 xfail。收集数按文件:test_data.py 52、test_api_surface.py 20、 test_core.py 23、test_solve.py 58(本轮 +8、摘掉 1 条 skip)、test_viz.py 8、test_decision.py 7、 test_stats.py 53、test_models.py 5
  • xfail 再次清零:correlation_matrix 的契约冲突已按方案 A 解决(决策 41),那条用例转正
  • 上一轮那 7 条 xfail 已全部转正,决策 30 那套信号(XPASS → 摘 mark)跑完过一个完整闭环
  • examples/01_clean_and_describe.py 实测跑通(exit 0);00_quickstart.py 仍等 stats / viz
  • 30 条 skip 是等实现的算法(train_test_split、mm1_metrics、anova、fit_distribution、 knapsack_dp、linear_program、simulated_annealing 等); monte_carlo_probability / monte_carlo_integral / markov_steady_state 已落地,分别摘掉 2 / 1 / 1 条 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。可选口径见下一步的 A / B / C。

| 57 | markov_steady_state 的 6 条口径待用户拍板(Codex 只报选项,未代改,决策 49) | 探针实测出的口子:① 含负元素的"行和为 1"矩阵([[1.5, -0.5], [0, 1]])会溢出成 nan,而 data=[nan, nan] 却 success=True —— 违反决策 48 的 NaN 口径(根因:没查 arr >= 0);附带的 RuntimeWarning: overflow encountered in matmul 因为 pyproject 设了 filterwarnings = ["error::RuntimeWarning"], 能让测试直接报错,所以不能只靠测试兜;② tol=True 因 isinstance(True, int) 通过校验 → tol=1 → 1 次就"收敛",给出 [0.55, 0.45] 的假答案(max_iter 查了 bool、tol 没查,易错点 1 复发); ③ tol=np.nan 因 nan <= 0 为假穿过校验 → 白跑 10000 次迭代;④ converged=False 仍 success=True 且 data 是未收敛的迭代值(max_iter=5 → [0.6387, 0.3613]);⑤ 类型错也抛 ValueError (max_iter=100.0),与决策 54 的"类型错 TypeError"分工不一致;⑥ params 只记 max_iter / tol, 没记转移矩阵(monte_carlo_integral 记了 bounds),事后复现不了。可选口径见"下一步"。

下一步

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

本轮:markov_steady_state(你写的)已审查通过 —— 算法本体正确,一行没动(3 状态与特征分解逐位一致、 200×200 只用 6 次迭代 / 0.10s);Codex 只清工程侧(ruff 5 条、docstring 补 max_iter/tol/Raises、 修正"周期链"表述、全角冒号),并 +8 条测试、摘掉 1 条 skip。 三连:pytest 196 passed / 30 skipped、ruff 0、pyright 0。

接下来三件事

  1. markov_steady_state 的 6 条口径待你拍板(下节,决策 57)。
  2. markov_simulate 的 5 条口径仍在挂着(决策 56 的 A / B / C),与 solve 的收尾一起定。
  3. L0 最后一项 save_figure(A 档 = 工程,viz/export.py)—— Codex 直接实现,你只读。

⏳ markov_simulate 待你拍板(5 条,Codex 只报不改)

① initial 的语义(最大的一条,三个选项) 现在标量分支按路径数铺满、数组分支按状态数校验长度,两条口径互相矛盾(详见决策 56)。

  • A:只收标量状态,另加 initial_dist 参数显式传"初始分布"(分布再抽一次,语义最干净,签名变长)
  • B:只收长度 n_paths 的数组(标量走 np.full 铺满),data 首列就是给定的初始状态序列(与 paths[:, 0] 赋值天然一致)
  • C:沿用现状,但把类型收成 int | np.integer + 先判 bool,把 paths[:, 0] = ... 挪进 try,docstring 写清"数组按路径数"

② 转移矩阵没查非负:[[1.5, -0.5], [0, 1]] 行和恰为 1.0 → 通过校验,但 cumsum 非单调 → 落格结果全是 0(静默错)。建议入口加一条 np.any(arr < 0) → ValueError。 ③ paths[:, 0] 赋值在 try 之外:长度不匹配时裸抛 ValueError,与"出错统一 success=False"(决策 48)不一致。 ④ 状态越界没在入口拦:initial=5(s=2)第一轮才 IndexError,被吞成 success=False; 建议入口加 0 <= initial < s。 ⑤ std_terminal 对分类状态的意义:状态编号是标称量,均值和标准差只对"有序状态"(如排队人数)成立 —— 要不要把 metrics 换成状态频率(terminal_freq)?你定。

⏳ markov_steady_state 待你拍板(6 条,Codex 只报不改)

① 转移矩阵没查非负(与 markov_simulate 的待办 ② 是同一块):[[1.5, -0.5], [0, 1]] 行和恰为 1.0 → 通过校验 → v 指数发散 → data=[nan, nan] 但 success=True(违反决策 48 的 NaN 口径)。 实测还会先抛 RuntimeWarning: overflow encountered in matmul,而 pyproject 设了 filterwarnings = ["error::RuntimeWarning"] —— 这条能让测试直接报错,所以得在校验层拦,不能只靠测试兜。

② tol 没查 bool:tol=True → isinstance(True, int) 为真 → tol=1 → 1 次就"收敛",给出 [0.55, 0.45] 的假答案(success=True, converged=True, iterations=1)。 max_iter 查了 bool,tol 漏了(易错点 1 复发)。

③ tol 没查 NaN:np.nan <= 0 为假 → 穿过校验 → 白跑 10000 次迭代后返回(结果本身是对的,纯浪费)。

④ converged=False 仍 success=True,且 data 是未收敛的迭代值(max_iter=5 → [0.6387, 0.3613])。 选项:A 保持现状(converged 自己看);B 不收敛判 success=False;C 不收敛时 data=None、只留 metrics。

⑤ 类型错也抛 ValueError(max_iter=100.0),与文件内其它函数(决策 54:类型错 TypeError、取值错 ValueError) 不一致 —— 要不要对齐?tol 同理。

⑥ params 没记转移矩阵:{"max_iter": 10000, "tol": 1e-10} 复现不了"算的是哪个矩阵" (monte_carlo_integral 记了 bounds)—— 要不要补?

你下一个要写的(S 档,仍在 solve/simulate.py):mm1_metrics(M/M/1 闭式指标,10 行)。 docstring 已写明口径:λ >= μ 时系统不稳定、队列长度无界 —— 这种情况返回 success=False 并说明原因, 而不是返回 inf。写完 solve 这一层就齐了,L1 只剩 decision / models 那几条。

✅ 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 的变体:同一段算法写了两遍,只实现了一遍)