## 审查结论 算法移植是对的:大样本正态下百分位区间半宽与解析 `2 × 1.96 × se` 差 < 2%, 按块重采样(batch_size=1000)也照 docstring 的 Notes 做了。以下是要改的部分。 ## Fixed - `random_state=None` 时另开 `np.random.default_rng()`,**绕过了 `core.get_rng`** —— 与决策 24 冲突:`set_seed` 对它无效,同一段脚本重跑区间就变。改成无条件 `get_rng(random_state)`(决策 46)。 - `metrics["statistic_name"]` 用 `hasattr(statistic, "__name__")` 判不出 lambda: lambda 的 `__name__` 恒为 `"<lambda>"`,于是 `else "custom"` 是**死代码**, 实测自定义统计量拿到的是 `"<lambda>"`。改成 `getattr(f, "__name__", None)` + 判 `"<lambda>"`(易错点 17)。 - docstring 补齐 `Parameters` / `Returns` / `Raises`(§4.7),并如实写明 `method="bca"` **尚未实现** (此前 docstring 承诺支持、实现却抛 `NotImplementedError`)。 - ruff 两条:I001(导入块拆成三行且用了绝对导入)、F541(`f"暂不支持bca"` 无占位符)。 ## Tests - **摘掉 3 条 `skip`**:等 `bootstrap_ci` 的那 3 条一直挂着,实现写完照样全绿 —— 易错点 9 复发, 已在 `personal-pitfalls.md` 把该条 `复发` 记到 1 并注明本次经过。 - 新增 6 条:`estimate` 等于原样本统计量、`confidence` 单调性、五种容器一致、 自定义 lambda 走回退分支、**不给种子时跟随 `set_seed`**(钉住上面那个 rng 修复)、非法/未实现 method。 - 写测试时踩到一个真性质:极差的重采样值**永远 ≤ 原样本极差**,`high` 会正好等于 `estimate`, 断言不能写 `estimate < high`。这条已写进卡片 §5。 - 全量 **177 条:143 passed / 34 skipped / 0 xfail**;ruff 0,pyright 0;`examples/01` 跑通。 ## Docs - `docs/algorithms/bootstrap_ci.md`:**第 2 张 S 档卡片**(模板九节 + §10 待拍板)。 `docs/algorithms/README.md` 进度表打勾。 - `docs/dev/personal-pitfalls.md`:新增第 17 条(`hasattr(f, "__name__")` 挡不住 lambda,`else` 分支即死代码), 索引补到 17;第 9 条记一次复发。 - `STATUS.md`:E 节记 `bootstrap_ci`(L0 10/12 → 剩余 `monte_carlo` `save_figure`),决策 46, 「下一步」第 1 条改写、新增第 8 条列 5 个待拍板口径。 ## 待用户拍板(Codex 一律未动,按决策 39) - `x` 里的 NaN 静默传进统计量 → `nan` 区间(`confidence_interval` / `t_test` 都是先丢 NaN 的,口径不齐) - `n_resamples <= 0` 现在抛 `IndexError: index -1 is out of bounds`(应在入口给可读 ValueError) - `confidence` 越界交给 numpy 抛 `Percentiles must be in the range [0, 100]` - `method="bca"`:补实现(≈25 行,S 档)还是从 docstring 删掉 - `bootstrap_statistic` 仍是 `NotImplementedError`,而 `bootstrap_ci` 把那段重采样循环内联了 —— 建议抽出去让 `bootstrap_ci` 调它(易错点 16 的变体)
7.2 KiB
7.2 KiB
bootstrap_ci
| 项 | 内容 |
|---|---|
| 模块 | mcm_toolkit.stats.bootstrap |
| 档位 | S |
| 参考来源 | Efron (1979);Efron & Tibshirani (1993) An Introduction to the Bootstrap; |
scipy.stats.bootstrap(只做对照,没有调用) |
|
| 状态 | 实现已落地(2026-10-03),§10 有 4 个口径待拍板 |
1. 数学定义
给定样本 x_1..x_n 与统计量 T(·):从 x 中有放回抽 n 个得到 x*,重复 B 次,
得 T*_1..T*_B。百分位法区间
estimate = T(x) # 用**原样本**,不是重采样分布的均值
alpha = 1 - confidence
low = T*_(alpha/2) # 经验分布的 alpha/2 分位
high = T*_(1 - alpha/2)
输入是一个一维样本,输出是「点估计 + 区间」两个标量 —— 这是非参数区间,
不假设正态、也不需要 T 的标准误解析式。
2. 适用场景
用:T 没有解析分布(中位数、极差、自定义指标);样本有偏或分布未知;
想给区间但不想假设正态(mean ± z·s/√n 的替代品)。
不用:n < 10 —— 经验分布太粗,x 能重采样出的不同样本极少;
数据有强时间相关性(股票、气温),此时必须用 block bootstrap;
要估的是极值(最大值本身就是上界,见 §5)。
3. 参数与返回值
| 参数 | 类型 | 默认 | 含义 | 取值影响 |
|---|---|---|---|---|
x |
Any | — | 样本;走 core.as_array(x, numeric_only=True, ndim=1) |
DataFrame 只取数值列,再摊平 |
statistic |
Callable | np.mean |
任意 f(sample) -> float |
换 np.median 就得到中位数的区间 |
confidence |
float | 0.95 | 置信水平 | 越大区间越宽 |
n_resamples |
int | 10000 | 重采样次数 B |
越大端点越稳;10000 通常够 |
method |
str | "percentile" |
区间算法 | "bca" 尚未实现,传了抛 NotImplementedError |
random_state |
int | None | None |
随机种子 | None 时复用 core.get_rng 的模块级生成器且会推进 |
返回 Result:
| 字段 | 内容 |
|---|---|
params |
estimate / low / high / confidence / n_resamples |
metrics |
statistic_name(np.mean → "mean";lambda → "custom") |
data |
不使用 |
4. 复杂度
- 时间:
O(B · n)(B次重采样,每次都要在n个值上算一次T) - 空间:
O(B)—— 按块处理,每批batch_size = 1000行,绝不落地B × n的整矩阵 - 什么时候会跑不完:
n = 1e4, B = 1e4→1e8次操作,秒级,正常;n = 1e6, B = 1e4→1e10次操作,跑不完;而且 1000×1e6 的单批索引矩阵就要 8 GB, 此时应改用解析近似,或把B降到 1e3 以下、换更便宜的T。
5. 数值稳定性
- 统计量有上界时,百分位区间会「塌」到上界。 极差
max - min的重采样值永远不可能超过 原样本极差,B个值里有相当比例正好等于它(抽到原样本极值点的那些), 于是high == estimate精确成立。这不是 bug,是 bootstrap 的固有性质 —— 区间端点是样本的经验分位数,不能给出样本之外的结论。要估极值请用极值理论。 - 分块只是省内存,不改结果:每批都从同一个
rng按顺序抽索引,等价于一次抽B × n。 - 随机数只来自
core.get_rng(决策 24):给了种子可复现,不给则跟随set_seed。 - NaN 目前不处理:
x里的 NaN 会直接进T,np.mean于是给出nan区间。见 §10。
6. 常见错误
- 不写 seed → 每次跑出来的区间都不一样,论文里的数字对不上 → 传
random_state或先set_seed。 - 以为 bootstrap 能得出比样本更极端的结论 → 区间端点就是样本的经验分位数, "95% 置信上限 173.2cm" 不可能超过样本最大值。
- 拿它估最大值 / 最小值 → 区间会塌成一个点(原因同 §5),该换极值理论。
n < 10硬跑 → 能抽出的不同重采样样本太少,区间没有统计意义。- 把
n_resamples当成"样本量"写进论文 → 论文里要同时写清n、B、seed、method。
7. 最小示例
import numpy as np
from mcm_toolkit.stats import bootstrap_ci
r = bootstrap_ci(heights, statistic=np.median, n_resamples=10000, random_state=42)
print(r.params["low"], r.params["estimate"], r.params["high"])
print(r.summary()) # 可直接贴进论文的 Markdown 表
8. 单元测试要点
| 测什么 | 期望 | 对应测试 |
|---|---|---|
大样本正态,statistic=np.mean |
区间半宽 ≈ 2 × 1.96 × se,容差 10% |
test_bootstrap_ci_matches_normal_theory_on_large_n |
| 任意统计量(中位数,对数正态样本) | 区间包含样本中位数 | test_bootstrap_ci_works_for_arbitrary_statistic |
| 同种子两次 | low 完全相等 |
test_bootstrap_ci_is_reproducible |
| 不给种子 | 跟随 set_seed(决策 24) |
test_bootstrap_ci_follows_get_rng_when_no_seed_is_given |
estimate |
等于原样本上的 T(x) |
test_bootstrap_ci_reports_the_original_estimate |
confidence 0.80 → 0.95 |
区间单调变宽 | test_bootstrap_ci_interval_widens_with_confidence |
| 极差作为统计量 | high <= estimate(原子点塌陷) |
test_bootstrap_ci_works_for_a_custom_lambda |
| 五种容器 | DataFrame/Series/ndarray/list/tuple 结果一致 | test_bootstrap_ci_accepts_the_documented_containers |
method 非法 / "bca" |
ValueError / NotImplementedError |
test_bootstrap_ci_rejects_unknown_method |
9. MCM/ICM 用法
- 哪类题:任何要"给结论 + 给不确定性"的题 —— 评价类(排名稳定性)、预测类(区间预测)。
- 论文里怎么表述:"采用非参数 bootstrap(
B = 10000,seed = 42)估计中位数的 95% 置信区间。" - 图:预测曲线 + 阴影置信带(
viz.plot/viz.save_figure)。 - 踩坑提醒:必须报
B和 seed;说清用的是 percentile 还是 BCa(目前只有 percentile); 被问到"为什么不用 ±1.96σ"时就答"统计量是中位数,没有解析标准误"。
10. 待拍板(未定稿,Codex 不代选)
| # | 口径 | 现状 | 可选 |
|---|---|---|---|
| 1 | x 里的 NaN |
静默传进 T → nan 区间 |
像 confidence_interval 一样丢掉 / 直接报错 / 维持现状 |
| 2 | n_resamples <= 0 |
走到 np.percentile 抛 IndexError: index -1 is out of bounds |
入口 ValueError(必须 ≥ 1) |
| 3 | confidence 越界 |
由 numpy 抛 ValueError: Percentiles must be in the range [0, 100] |
入口 ValueError + 可读文案 |
| 4 | method="bca" |
docstring 承诺过,实测 NotImplementedError |
补 BCa 实现(S 档,约 25 行)/ 从 docstring 删掉 |
| 5 | bootstrap_statistic |
仍是 NotImplementedError,而 bootstrap_ci 把重采样循环内联了 |
抽出去让 bootstrap_ci 调它(易错点 16 的变体) |
口径 5 尤其值得做:
bootstrap_statistic的 docstring 与bootstrap_ci里那段循环说的是同一件事, 现在等于把同一段算法写了两遍,只实现了一遍。