Files
MCM-ICM_Toolkit/STATUS.md
T
guzhujushi 58f2236703 docs(status): 记入分工硬规矩(决策 47)+「明天从这里开始」;test_solve 改逐条 skip
- STATUS.md:新增决策 47 —— 算法一律由用户本人写(即便用户改口也不代写),
  工程事务(入口/校验、tests/、lint、docstring、卡片、STATUS/CHANGELOG)由 Codex 写;
  L0 计数 9/12 -> 10/12;skip 说明 37 -> 34 条。
- STATUS.md:新增「⭐ 明天从这里开始」区块 —— 第一个要写的文件是
  src/mcm_toolkit/solve/simulate.py 的 monte_carlo(L0 11/12,S 档),
  含参考契约、向量化要求、get_rng 口径、三条测试建议、卡片路径与一个待拍板口径。
- tests/test_solve.py:模块级 pytestmark 拆成 9 条逐条 skip(决策 26、易错点 9)。
- CHANGELOG.md:同步记录。

校验:pytest 177 收集 / 143 passed / 34 skipped / 0 xfail;ruff 0;pyright 0。
2026-10-03 20:12:46 +08:00

425 lines
45 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# STATUS — 当前进度
> 更新日期:2026-10-03
## 本次任务
**第一阶段(已完成)**:审查并精简 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 10/12**:`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`、`clean_missing`、
`detect_outliers`、`normalize`/`standardize`、`describe`、`correlation_matrix`、`bootstrap_ci`;
另**提前落地**了 L1 的 `confidence_interval` / `t_test`。剩余 2 项:`monte_carlo`(用户写)、
`save_figure`(Codex 写)。
## 任务清单
### ✅ 已完成
**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__` 恒为 `"<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 条**
- [x] **工程收尾(本轮)**:`tests/test_solve.py` 的模块级 `pytestmark = pytest.mark.skip(...)`
拆成 **9 条逐条** `@pytest.mark.skip(reason=_NOT_IMPLEMENTED)` —— 模块级 skip 会把整个文件吞掉,
正是易错点 9 的根源(决策 26)。收集数不变(177)、跳过数不变(34),只是粒度变细。
- [ ] 剩余 2 项:`monte_carlo`(**用户写**)、`save_figure`(Codex 写)
### 验证结果
| 检查 | venv | conda `mcm-toolkit` |
|---|---|---|
| `pytest` | 见下(单列) | **143 passed, 34 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(与两套环境一致)
- **177 条用例:143 passed / 34 skipped / 0 xfail**。收集数按文件:`test_data.py` 52、`test_api_surface.py` 20、
`test_core.py` 23、`test_solve.py` 9、`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`
- 34 条 skip 是等实现的算法(`train_test_split`、`monte_carlo`、`anova`、`fit_distribution` 等);
`bootstrap_ci` 已落地摘掉 3 条 skip(本轮新增 6 条用例,净 +3);`test_solve.py` 9 条按决策 26 改为逐条 skip;
`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 仓库设置。
- [ ] L0 完成 **10/12**(`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`、`clean_missing`、
`detect_outliers`、`normalize`、`standardize`、`describe`、`correlation_matrix`、`bootstrap_ci`),
另 L1 的 `confidence_interval` / `t_test` 已提前落地;
**只剩 `monte_carlo`(用户写)与 `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 的"语义由用户拍板"继续有效。
## 下一步
### ⭐ 明天从这里开始(第一个要写的文件)
**文件:`src/mcm_toolkit/solve/simulate.py` → 写 `monte_carlo`(L0 第 11/12 项,S 档)。**
同一文件里 `monte_carlo_probability` / `monte_carlo_integral` / `random_walk` 先别动 —— 一次一个函数。
**① 参考契约(docstring 里已定,签名别改)**
- `monte_carlo(sampler, simulator, n=10000, random_state=None, **kwargs) -> Result`
- `sampler(rng, **kwargs) -> 参数`:一次抽**一整批**参数(可以是标量/数组/元组,随便)
- `simulator(params) -> float`:单次仿真结果
- 返回 `Result`:`data` = `n` 个结果组成的 `np.ndarray`;
`metrics = {"mean", "std", "median", "p05", "p95", "ci_low", "ci_high"}`
**② 必须向量化**:`sampler` 一次抽 `n` 份(如 `rng.normal(0, 1, size=n)`),`simulator` 对整个参数数组算一次,
**绝不在 `simulator` 里写 Python 循环**。10^4 次循环还能忍,10^6 次直接放弃 —— 这是"能用"和"不能用"的分界线。
**③ 随机数无条件走 `core.get_rng`**(决策 46 的教训,别重演 bootstrap 那个坑):
`rng = get_rng(random_state)`,不要自己另开 `np.random.default_rng()`,否则 `set_seed` 管不到它、区间不可复现。
**④ 先写一条会失败的测试**(roadmap §3.2 的纪律)。`tests/test_solve.py` 现在**没有**直接测 `monte_carlo` 的用例
(`test_monte_carlo_is_reproducible` 名字像,实际调的是 `monte_carlo_probability`)。建议三条:
- 固定种子两次结果完全相同(可复现)
- `sampler` 恒返回常数 → `data` 全等于该常数、`std == 0`(常量边界)
- `n` 增大时 `mean` 的波动按 `O(1/√n)` 下降(可留成大样本慢测或标 `slow`)
**⑤ 还欠一张 S 档卡片**:`docs/algorithms/monte_carlo.md`(模板 `docs/dev/function-card-template.md`,
照抄 `docs/algorithms/bootstrap_ci.md` 的骨架),写完把 `docs/algorithms/README.md` 的进度表打勾。
**⑥ 等你拍板的一个口径(Codex 不代选,决策 39/47)**:`ci_low` / `ci_high` 用**分位数法**
(`np.percentile(data, [2.5, 97.5])`)还是**正态近似**(`mean ± 1.96·std/√n`)?两个都能用,选一个我记进卡片。
### 其余待办
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 的变体:同一段算法写了两遍,只实现了一遍)