feat(stats): bootstrap_ci 落地(L0 10/12,S 档)+ 卡片;修掉 rng 绕过 get_rng 与 lambda 名字死分支
## 审查结论 算法移植是对的:大样本正态下百分位区间半宽与解析 `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 的变体)
This commit is contained in:
1 parent
b70a96dd0c
commit
a1cc26e042
7 files changed
+373
-20
No files matched your search
@@ -10,6 +10,10 @@ from __future__ import annotations
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..core import Result, as_array, get_rng
|
||||
|
||||
__all__ = ["bootstrap_ci", "bootstrap_statistic"]
|
||||
|
||||
|
||||
@@ -42,23 +46,46 @@ def bootstrap_ci(
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x:
|
||||
样本:`pandas.DataFrame` / `pandas.Series` / `numpy.ndarray` / list / tuple。
|
||||
走 `core.as_array(x, numeric_only=True, ndim=1)` —— DataFrame 只取数值列,再摊平成一串样本。
|
||||
statistic:
|
||||
任意把样本映射成标量的函数,默认 `np.mean`。
|
||||
换成 `np.median` / `np.std` / 自定义的 `lambda s: s.max() - s.min()` 都能用,
|
||||
这是 bootstrap 相对于 `mean ± z·s/√n` 的核心优势。
|
||||
confidence:
|
||||
置信水平,取 `(0, 1)` 开区间,默认 0.95。
|
||||
n_resamples:
|
||||
重采样次数,默认 10000。论文里必须写明。
|
||||
method:
|
||||
`"percentile"`:直接取经验分布的 α/2 与 1-α/2 分位(最常用,默认);
|
||||
`"bca"`:偏差校正 + 加速,区间更准但慢,样本有偏时用。
|
||||
`"bca"`:偏差校正 + 加速,区间更准但慢,样本有偏时用 —— **尚未实现**,传了抛
|
||||
`NotImplementedError`。
|
||||
random_state:
|
||||
随机种子。传 `None` 时复用 `core.get_rng` 的**模块级生成器且会推进**,
|
||||
所以同一段脚本里连调两次结果不同;要完全可复现就显式给种子,或先 `core.set_seed`。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`params={"estimate", "low", "high", "confidence", "n_resamples"}`。
|
||||
`Result`:`params={"estimate", "low", "high", "confidence", "n_resamples"}`;
|
||||
`metrics["statistic_name"]` 是统计量的名字(自定义 lambda 时是 `"custom"`)。
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
`x` 不是受支持的容器,或内容转不成数值(由 `as_array` 抛)。
|
||||
ValueError
|
||||
`DataFrame` 里一列数值都没有;`method` 不在支持列表内。
|
||||
NotImplementedError
|
||||
`method="bca"`。
|
||||
|
||||
Notes
|
||||
-----
|
||||
* 重采样时必须**按块处理**,不要一次生成 `(10000, n)` 的整矩阵(n 大时会爆内存)。
|
||||
* 样本量 n < 10 时 bootstrap 不可靠,先补数据。
|
||||
* 比赛写论文时要注明:n_resamples、seed、用的是 percentile 还是 BCa。
|
||||
* 比赛写论文时要注明:n_resamples、seed、用的是 percentile 还是 BCa(目前只实现了 percentile)。
|
||||
* **缺失值目前不处理**:`x` 里的 NaN 会直接进统计量,`np.mean` 于是给出 nan 区间。
|
||||
`confidence_interval` / `t_test` 都是先丢掉 NaN 的,两边口径尚未统一。
|
||||
|
||||
Examples
|
||||
--------
|
||||
@@ -67,5 +94,57 @@ def bootstrap_ci(
|
||||
>>> r.params["low"] < 3 < r.params["high"]
|
||||
True
|
||||
"""
|
||||
# TODO(L0) ★ S 档:配完整函数卡片
|
||||
raise NotImplementedError
|
||||
if statistic is None:
|
||||
statistic = np.mean
|
||||
|
||||
rng = get_rng(random_state)
|
||||
|
||||
arr, _ = as_array(x=x, dtype=float, ndim=1)
|
||||
|
||||
original_estimate = float(statistic(arr))
|
||||
|
||||
n = len(arr)
|
||||
batch_size = 1000
|
||||
boot_stats = []
|
||||
|
||||
for i in range(0, n_resamples, batch_size):
|
||||
current_batch = min(batch_size, n_resamples - i)
|
||||
indices = rng.integers(0, n, size=(current_batch, n))
|
||||
batch_samples = arr[indices]
|
||||
|
||||
if statistic in [np.mean, np.median, np.std, np.min, np.max]:
|
||||
batch_stats = statistic(batch_samples, axis=1)
|
||||
else:
|
||||
batch_stats = np.array([statistic(s) for s in batch_samples])
|
||||
|
||||
boot_stats.extend(np.asarray(batch_stats).ravel().tolist())
|
||||
|
||||
boot_stats = np.array(boot_stats)
|
||||
|
||||
if method == 'percentile':
|
||||
alpha = 1 - confidence
|
||||
lower = np.percentile(boot_stats, alpha / 2 * 100)
|
||||
upper = np.percentile(boot_stats, (1 - alpha / 2) * 100)
|
||||
elif method == 'bca':
|
||||
# TODO(L1): BCa = 偏差校正 + 加速,属于 S 档、留给用户自己补的算法。
|
||||
raise NotImplementedError("BCa 还没实现,先用 method='percentile'")
|
||||
else:
|
||||
raise ValueError(f"不支持的方法:{method},仅支持'percentile'")
|
||||
|
||||
# lambda 也有 __name__(值恒为 "<lambda>"),所以只判断 hasattr 会让 "custom" 这支变成死代码。
|
||||
name = getattr(statistic, "__name__", None)
|
||||
statistic_name = "custom" if name in (None, "<lambda>") else str(name)
|
||||
|
||||
return Result(
|
||||
name="bootstrap_ci",
|
||||
params={
|
||||
"estimate": original_estimate,
|
||||
"low": float(lower),
|
||||
"high": float(upper),
|
||||
"confidence": confidence,
|
||||
"n_resamples": n_resamples
|
||||
},
|
||||
metrics={
|
||||
"statistic_name": statistic_name,
|
||||
}
|
||||
)
|
||||
Reference in new issue
Block a user