4. Bootstrap基本算法与重采样机制
非参数Bootstrap的核心思想可以用一句话概括:把样本当作总体,从样本中有放回地反复抽样,用这些“重采样样本”的统计量分布去近似原统计量的抽样分布。这句话里有两个关键点:一是“把样本当作总体”,这对应经验分布;二是“有放回地抽样”,这决定了重采样样本与原始样本的关系。理解这两点,才能正确实现Bootstrap。
经验分布:Bootstrap的“总体替身”
设原始样本为 x = (x₁, x₂, …, xₙ),来自某个未知分布 F。我们想估计某个统计量 θ = T(F) 的抽样分布,但 F 未知,无法直接抽样。Bootstrap的做法是构造经验分布 F̂ₙ:它在每个观测值 xᵢ 上放置 1/n 的概率质量。也就是说,F̂ₙ 是一个离散分布,取值就是样本中的 n 个点,每个点的概率相等。
从 F̂ₙ 中抽取一个样本,等价于从原始 n 个观测值中独立地、有放回地抽取 n 次。每次抽取时,每个原始观测值被选中的概率都是 1/n。由于是有放回,同一个观测值可能被抽中多次,也可能一次都没被抽中。一个重采样样本记为 x* = (x*₁, x*₂, …, x*ₙ),其中每个 x*ᵢ 都来自原始样本。
F̂ₙ 是Bootstrap的理论支点:它用样本的离散均匀分布替代未知的 F。样本量 n 越大,F̂ₙ 越接近 F,Bootstrap近似通常越好。一次重采样的完整拆解
下面用一个具体的小样本手动走一遍。假设我们测量了5名学生的某次测验成绩(单位:分):
x = (72, 85, 90, 78, 88) n = 5
我们关心的统计量是样本均值 θ̂ = x̄。原始样本均值为 (72+85+90+78+88)/5 = 82.6。现在执行一次Bootstrap重采样:
- 建立抽样池:把5个观测值放入池中,每个值对应一个“球”,共5个球,每个球被抽中的概率为
1/5 = 0.2。 - 有放回抽5次:每次抽一个球、记录数值、放回池中,再抽下一次。假设这次抽到的索引序列为
(2, 5, 2, 1, 4),对应数值为(85, 88, 85, 72, 78)。注意索引2出现了两次,索引3(数值90)一次都没出现——这正是有放回抽样的典型特征。 - 计算重采样统计量:
x̄* = (85+88+85+72+78)/5 = 81.6。 - 记录结果:把
x̄* = 81.6存入Bootstrap统计量集合。
重复上述过程 B 次(例如 B = 1000 或 B = 10000),得到 B 个重采样均值 x̄*₁, x̄*₂, …, x̄*_B。这 B 个值的经验分布,就是原均值 x̄ 抽样分布的Bootstrap近似。基于这个分布,可以计算标准误(B 个值的样本标准差)、百分位置信区间(取2.5%和97.5%分位数)等。
算法伪代码与实现要点
把上面的过程写成通用伪代码,结构非常清晰:
输入: 原始样本 x[1..n], 统计量函数 T, 重采样次数 B
输出: B 个 Bootstrap 统计量
for b = 1 to B:
x_star = 空数组
for i = 1 to n:
j = 从 {1, 2, …, n} 中等概率有放回抽取一个索引
x_star.append(x[j])
theta_star[b] = T(x_star)
返回 theta_star[1..B]
实现时有几个容易出错的细节值得注意。第一,抽样必须独立且等概率,每次抽取都从完整的 n 个索引中选,不能“抽一个少一个”。第二,重采样样本的容量应等于原样本容量 n,这是标准非参数Bootstrap的约定;容量不同会改变统计量的方差尺度。第三,统计量函数 T 应保持与原始分析完全一致,包括缺失值处理、加权方式等,否则Bootstrap分布与目标统计量不匹配。第四,B 的选择:估计标准误时 B = 200 通常已够用,百分位置信区间建议 B ≥ 1000,需要更稳定的分位数时可取 B = 10000。
用代码手动实现一次
下面用Python的NumPy手动实现,不依赖任何Bootstrap库,便于看清每一步:
import numpy as np
x = np.array([72, 85, 90, 78, 88])
n = len(x)
rng = np.random.default_rng(2024)
# 一次重采样
idx = rng.integers(0, n, size=n) # 有放回抽 n 个索引
x_star = x[idx]
theta_star_1 = x_star.mean()
print(idx, x_star, theta_star_1)
# 示例输出: [1 4 1 0 3] [85 88 85 72 78] 81.6
# B 次重采样
B = 1000
theta_star = np.empty(B)
for b in range(B):
idx = rng.integers(0, n, size=n)
theta_star[b] = x[idx].mean()
se_boot = theta_star.std(ddof=1)
ci = np.percentile(theta_star, [2.5, 97.5])
print(se_boot, ci)
上述输出中的具体数值取决于随机种子,属于示例结果,不是固定实测值。读者运行时应关注三件事:idx 中是否有重复索引、x_star 是否只包含原始样本中的值、theta_star 的分布是否大致以原始统计量为中心。若 idx 出现越界或 x_star 出现原始样本之外的值,说明抽样逻辑有误。
重采样机制带来的两个直觉
其一,重采样样本的均值围绕原始统计量波动。 因为每次抽样都是从原始样本中等概率抽取,重采样样本的期望均值等于原始样本均值。所以 B 个 x̄* 的分布中心应接近 x̄ = 82.6,其离散程度反映 x̄ 的抽样不确定性。
其二,重采样样本的构成与原始样本不同。 一个有放回样本中,平均约有 1 − 1/e ≈ 63.2% 的原始观测值至少出现一次,其余约36.8%不出现。这个比例是Bootstrap的固有性质,也解释了为什么重采样样本之间会有差异——正是这种差异承载了抽样变异的信息。
掌握这套机制后,后续的标准误估计、百分位区间、BCa区间都只是在这 B 个统计量上做不同的加工。先把“经验分布—有放回抽样—统计量集合”这条链路走通,是理解一切Bootstrap变体的基础。