# STATUS — 当前进度 > 更新日期:2026-10-18 ## 本次任务 **第一阶段(已完成)**:审查并精简 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 12/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 清单**已全部落地**。 `save_figure` 已于 2026-10-18 由 Codex 落地,L0 收官。 **`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. 建议审查与精简** - [x] 通读 `chatGPT_Suggestion.md`(33 节,约 2.4 万字),输出 `docs/01-suggestion-review.md` - 总裁决:**方向对,量级错** - 14 个包 → **7 个**;约 250 个函数 → **约 40 个**;5 个 Result 类 → **1 个** - 单函数文档要求 10 项 → **2 档**(只有约 15 个招牌算法写完整卡片) - 逐项给出"砍什么 / 为什么 / 替代什么",并补上原文缺失的六件事 (论文产出链路、指标方向与量纲、时间预算、可复现性、测试测什么、离线约束) **B. 骨架文件生成**(76 个文件,约 6800 行) - [x] `docs/02-architecture.md` 架构、`docs/03-roadmap.md` 路线图与**永久不做清单** - [x] `docs/04-environment.md` 环境文档、`docs/dev/function-card-template.md` 卡片模板 - [x] `src/mcm_toolkit/` 7 个包 23 个模块(**只有签名 + docstring + `NotImplementedError`**) - [x] `tests/` 9 个文件、`examples/` 4 个、`problems/` `notebooks/` `figures/` `tables/` - [x] `pyproject.toml`(核心依赖 4 个 + 6 组 extras)、`environment.yml` - [x] `.gitignore` `.gitattributes` `.editorconfig` `LICENSE` `CHANGELOG.md` `README.md` - [x] 脚本 `scripts/setup-venv.ps1` `setup-conda.ps1` `doctor.ps1` **C. 环境配置** - [x] **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 升级后自动同步) - [x] **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 - [x] **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` 留了回滚点 - [x] 环境脚本按实测结果重写:`setup-conda.ps1` 默认改为**精准包列表** (`--clone base` 实测会走完整下载安装,不再是"离线快捷方式") **D. Git 仓库** - [x] `git init -b main`,`core.autocrlf=false`,首次提交 `5319589`(76 文件 / 6812 行),工作区干净 - [x] `git ls-files --eol` 复核:**全部 LF**,`.gitattributes` 生效 - [x] 仓库由**本人账户 guzhu** 创建(见决策 17) - [x] **远端 Gitea 已接通并推送**:`origin` = `git@code.guzhujushi.cn:guzhujushi/MCM-ICM_Toolkit.git`, `main` 跟踪 `origin/main`,远端 HEAD = `96080b9`,76 个文件已在服务器上(见决策 19/20) - [x] 推送通道:公网 22 被防火墙过滤 → 改用 Tailscale(`100.115.86.48:22`), `~/.ssh/config` 加了一条域名别名,remote 里仍写域名 **E. L0 实现进度(10/12)** - [x] `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 结构、可重复调用) - [x] `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、非法类型、共享推进) - [x] `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` - [x] `data/clean.py` 的 `clean_missing`(L0 第 4 项) - 实现由**用户本人**写,Codex 只做审查、补测试与文档 - `threshold` 用 `math.ceil` 算 `min_non_na`:3 列、阈值 0.5 时必须是 2, 写 `int()` 截断会漏删 - 顺手落地兄弟函数 `impute_missing` / `winsorize`(原属 L1) - [x] `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 条** - [x] `data/clean.py` 的两处收尾(**Codex 代写,用户授权**) - `impute_missing` 的 mode 分支改为**返回新表**(原先就地改调用方的表,与另外四支不一致) —— 最后一条 `xfail` 随之转正,**`xfail` 清零** - `detect_outliers` 补**入参类型校验**(§4.7 要求非法类型抛 `TypeError`): 接受 `DataFrame` / `Series` / `list` / `tuple` / `ndarray`,`dict` 不再接受(决策 33) - [x] `examples/01_clean_and_describe.py`:`load_table` / `save_table` / `clean_missing` / `impute_missing` / `detect_outliers` / `winsorize` 六个函数的最小用法,实测可直接跑通 - [x] `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 段 - [x] **`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** - [x] `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 / 变异系数用例 - [x] `examples/01_clean_and_describe.py`:填掉末尾的 `TODO(L0)`,加一段 `describe` 输出 - [x] `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 拆成三个函数(见下) - [x] `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²))` 对照) - [x] `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 条) - [x] **`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`,一份实现两张脸 - [x] `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__` 恒为 `""`, 导致 `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 条** - [x] **工程收尾(本轮)**:`tests/test_solve.py` 的模块级 `pytestmark = pytest.mark.skip(...)` 拆成 **9 条逐条** `@pytest.mark.skip(reason=_NOT_IMPLEMENTED)` —— 模块级 skip 会把整个文件吞掉, 正是易错点 9 的根源(决策 26)。收集数不变(177)、跳过数不变(34),只是粒度变细。 - [x] `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 - [x] `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 条 - [x] `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` 三行重复)见"下一步" - [x] `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 代改) - [x] `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) - [x] `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) - [x] `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 语法解析 ### ⚠️ 需要注意 / 遗留 - [x] ~~两套环境版本不一致~~ → **已解决**:base 升级到 2026.07 后, numpy/scipy/pandas/matplotlib/sklearn/statsmodels/networkx/sympy 版本完全一致, 只差 Python 补丁号(3.13.9 vs 3.13.15)。详见 `docs/04-environment.md` §5。 - [x] ~~Gitea 远程尚未配置~~ → **已配置并推送完成**,见上面 D 节与决策 19/20。 note:仓库当前是 **Public**(匿名 `git ls-remote` 可读),要改私有去 Gitea 仓库设置。 - [x] L0 完成 **12/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` 于 2026-10-18 落地),详情见"下一步"第十一轮 - [ ] 本机 `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)` 再判 `""` | 原码 `random_state=None` 时另开 `np.random.default_rng()`,等于绕开决策 24(`set_seed` 对它无效,区间不可复现)。**Codex 代改**:`get_rng(None)` 本来就是设计好的"复用模块级生成器且推进",直接传下去即可。`statistic_name` 的 `else "custom"` 是死代码 —— lambda 也有 `__name__`(值恒为 `""`),实测拿不到 `"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-18 第十一轮) **本轮:L0 收官 —— `save_figure` 落地(Codex 写,A 档)。** - `viz/export.py`:实现 `save_figure`。入口先校验 `fig` / `dpi` / `formats` / `bbox_inches` / `transparent` / `path`,**全部校验通过后才建目录、写文件**;`formats` 支持大小写归一, 空 / 未知 / 重复格式一律报错;`path` 带图片后缀时自动去后缀(不会写出 `fig1.png.png`)。 - `tests/test_viz.py`:去掉违反决策 26 的模块级 `pytestmark`,改为逐条 skip; `test_save_figure_writes_png_and_pdf` 不再依赖未实现的 `plot()`,改用 `plt.subplots()` 构造图。 新增 8 条:后缀剥离、自动建目录、格式归一与顺序、单个字符串 / 空 / 未知 / 重复格式、 校验先于副作用、坏 `dpi` / `bbox_inches` / `transparent` / 非 `Figure`。 - 三连:`pytest` **242 passed / 24 skipped**、`ruff` **0**、`pyright` **0**。 **下一步** 1. `models/regression.py` 的 `fit_linear` / `fit_polynomial`(A 档)。**开工前先定返回契约**: `Result` 没有 `fit/predict/score`,与 `compare_models` 要求 `Model` 协议的入参对不上, 需要定成"`Result.data` 放已拟合模型 + `compare_models` 接受两种入参"之类的桥接方案。 2. `decision/entropy_weights`(S 档,用户写);Codex 先补函数卡片 / 测试骨架。 3. 集中拍板 `STATUS.md` 里挂着的小口径(`t_test` / `as_array` / `bootstrap_ci` / `markov_*` / `mm1` / 死代码)。 ### ⭐ 当前进度与待办(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 的变体:同一段算法写了两遍,只实现了一遍)