配对置换检验说明一次比较的参考分布怎样建立。但如果同时比较十个候选模型、五种指标和多个数据切片,最后只展示最小的那个 p 值,问题已经变了。

这一篇先固定“哪些检验属于同一组结论”,再定义至少错拒一次的概率,推导 Bonferroni 与 Holm 校正。我们使用合成 p 值检查计算和错误率边界;不会把校正当作修复无效检验或事后挑选的万能步骤。

先固定家族

报告哪些模型、指标与切片,不能只留下赢家。

再检查 p 值

每个真零假设下的 p 值都要满足有效性条件。

最后逐步校正

排序比较,第一次未通过便停止拒绝。

四个排序p值0.004、0.012、0.030、0.200,前两步通过Holm阈值,第三步停止;调整p值为0.016、0.036、0.060、0.200
阈值逐步放宽,停止规则不能跳过。 数字来自正文四个合成 p 值;颜色表示在 α=0.05 下的决策,不表示效应大小或模型质量。查看原图

先定义一个家族,再定义错误率

一次评估中,预先规定要据以作结论的一组零假设称为一个检验家族。家族可以是“同一数据集上四个新模型分别与基线比较”;也可以包含多个主要指标。应按研究问题事先规定,不能看完结果再把赢家单独称为一个家族。

记 V 为被错误拒绝的真零假设数量。**家族错误率 FWER=P(V≥1)**:只要错拒一次就算这次家族发生错误。它不是每个模型出错的概率,也不是“已拒绝结论中错误比例”的期望;后者属于 FDR 的讨论范围。R 官方 p.adjust 文档区分了这两类控制目标。

本文所需的单项条件是:对于每个真零假设 j,在其成立时,任意 t∈[0,1] 都有 P(p_j≤t)≤t,通常称为有效或超均匀 p 值。精确离散检验可以满足不等式而不恰好服从连续均匀分布。单次检验的交换性、抽样机制或模型假设若不成立,后续校正不能补救。

多次单独检验为什么会积累风险

若 m 个零假设都为真,而且 p 值独立且连续均匀,每项用阈值 α,则:

1
P(至少错拒一次) = 1 − (1−α)^m

例如 m=20、α=0.05,得到约 0.6415。这个公式需要独立性;共享测试集的多个模型未必满足。它用来展示问题,不是估算任意实际评估的错误率。

不需要独立性的保守办法来自并集界。每项改用 α/m,则:

1
2
3
P(至少错拒一个真零假设)
≤ Σ_{j 为真零假设} P(p_j≤α/m)
≤ m₀ α/m ≤ α

m₀ 是真零假设数量,未知但不超过总数 m。这就是 Bonferroni 控制的基本理由。等价的调整 p 值为 min(1,m p_j),再和原始 α 比较。

Holm 怎样逐步放宽阈值

把 m 个原始 p 值升序排列为 p_(1)≤…≤p_(m)。第 i 步与 α/(m−i+1) 比较:若通过便拒绝该零假设,继续下一步;第一次 p 值大于阈值时,停止,并保留它及后面全部零假设。本文用“≤”表示通过阈值,边界在代码中保持一致。

这与把每个排序值分别拿去比较不同。不能在某一步失败后,跳到更宽的阈值重新捞取后面的显著结果。

四个模型怎样手算调整 p 值

取四个合成 p 值 A=0.004、B=0.012、C=0.030、D=0.200,α=0.05。它们不是本站真实模型的实验结果,也不代表效果排名。

步骤 i 模型 原始 p 剩余数 m−i+1 Holm 阈值 累积调整 p 决策
1 A 0.004 4 0.0125 0.016 拒绝
2 B 0.012 3 约 0.016667 0.036 拒绝
3 C 0.030 2 0.025 0.060 停止,不拒绝
4 D 0.200 1 0.05 0.200 已停止,不再继续拒绝

Bonferroni 会给出 [0.016,0.048,0.120,0.800],本例也拒绝 A、B。Holm 对本例的拒绝数量没有增加,但为后两项给出更小的调整 p 值;不能为了演示“更强”而把 0.048 错写成大于 0.05。

一般的 Holm 调整值为:

1
2
3
在排序后的第 i 项:
adjusted_(i) = min(1, max_{1≤j≤i} [(m−j+1) p_(j)])
最后映射回原始模型顺序。

这里的累积最大值不能省略。例如 p=[0.020,0.021,0.022],未经累积的乘积为 [0.060,0.042,0.022],竟然随排序下降;Holm 调整后应全部为 0.060。它保留了“前面失败,后面停止”的一致性。

为什么真零假设任意相关也能控制

假定家族中有 m₀≥1 个真零假设。若 Holm 发生了错误拒绝,考虑排序中第一个被拒绝的真零假设,位置为 k。它之前至多有 m−m₀ 个假零假设,所以 k≤m−m₀+1,剩余数 m−k+1≥m₀。

它被拒绝意味着 p_(k)≤α/(m−k+1)≤α/m₀。由于它也是全部真零假设中最小的 p 值,错误事件包含于“至少一个真零假设的 p≤α/m₀”。对 m₀ 个真零假设使用并集界,概率不超过 α。若 m₀=0,则 V 恒为零。

这个论证没有使用各 p 值独立。它需要的是家族固定、每个真零假设的 p 有效,以及按规则停止。Holm 的原始论文提出逐步拒绝方案;R 官方文档也列出 Holm 的家族错误率控制与依赖适用范围。这里“任意相关”不等于“任意错误的 p 值也能用”。

Holm 至少包含 Bonferroni 的拒绝集合:若某项 p≤α/m,排在它前面的值也都≤α/m,而各步阈值不小于 α/m,所以不会在到达它之前停止。有些输入会多拒绝,有些输入会一样。

例如 p=[0.01,0.02,0.03]、α=0.05 时,Bonferroni 调整为 [0.03,0.06,0.09],只拒绝第一项;Holm 调整为 [0.03,0.04,0.04],三项都通过。这里比较的是决策规则,并不表示未观测的真零假设数量已知。

精确网格实验怎样分开独立与依赖

为让有限枚举可见,下面实验使用 α=0.20,仅为演示,不是建议把真实评估阈值改大。每个真零假设的 p 从 {1/20,2/20,…,1} 等概率取值;该分布超均匀,因为其分布函数为 floor(20t)/20≤t。

四个独立 p 值共有 20⁴=160000 种等概率组合。全部零假设为真时,Holm 发生至少一次拒绝恰好等价于第一步通过,因此这一个实验可以按最小值直接计数,不必每个组合都完整排序。

全部零假设为真 未校正 FWER Holm FWER
四个独立网格 p 值 94464/160000=0.5904 29679/160000=0.18549375
四个完全相同的网格 p 值 4/20=0.20 1/20=0.05

第二行让四个 p 都等于同一个 U,只有 20 种等概率组合;它说明不能把独立公式机械用于相关检验。两行都不超过 Holm 的目标 0.20,但这些特定枚举本身不证明所有分布上的控制;一般保证来自上一节的证明。

完整标准库实现

校正使用 Fraction,避免手算阈值附近的二进制浮点误差。建议给小数传字符串;若传 float,代码按其十进制字符串解释,并不恢复浮点数在产生前可能丢失的信息。实现拒绝缺失值,不会悄悄删除检验项来缩小家族。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import itertools
import json
from fractions import Fraction


def unit(value, name):
if isinstance(value, bool) or not isinstance(value, (int, float, str, Fraction)):
raise ValueError(name + " must be numeric")
try:
result = value if isinstance(value, Fraction) else Fraction(str(value))
except (ValueError, ZeroDivisionError) as error:
raise ValueError(name + " must be a finite rational number") from error
if not 0 <= result <= 1:
raise ValueError(name + " must lie in [0,1]")
return result


def holm(pvalues, alpha="0.05"):
if not isinstance(pvalues, (list, tuple)) or not pvalues:
raise ValueError("a nonempty fixed family is required")
p = [unit(value, "p") for value in pvalues]
alpha = unit(alpha, "alpha")
if not 0 < alpha < 1:
raise ValueError("require 0 < alpha < 1")
m = len(p)
order = sorted(range(m), key=lambda i: (p[i], i))
adjusted = [Fraction(0)]*m
running = Fraction(0)
for rank, index in enumerate(order):
running = max(running, (m-rank)*p[index])
adjusted[index] = min(Fraction(1), running)
return [{"index": i, "raw": str(p[i]), "adjusted": str(adjusted[i]),
"reject": adjusted[i] <= alpha} for i in range(m)]


def grid_experiment():
# Four true nulls; integer indices represent p = k/20 exactly.
grid, m, alpha = 20, 4, Fraction(1, 5)
raw_limit = (alpha*grid).__floor__()
first_limit = (alpha*grid/m).__floor__()
raw_count = corrected_count = 0
for row in itertools.product(range(1, grid+1), repeat=m):
smallest = min(row)
raw_count += smallest <= raw_limit
corrected_count += smallest <= first_limit
shared_raw = shared_corrected = 0
for k in range(1, grid+1):
shared_raw += Fraction(k, grid) <= alpha
shared_corrected += any(item["reject"] for item in holm([Fraction(k, grid)]*m, alpha))
total = grid**m
return {"independent": {"total": total, "raw_count": raw_count,
"holm_count": corrected_count,
"raw_fwer": str(Fraction(raw_count, total)),
"holm_fwer": str(Fraction(corrected_count, total))},
"shared": {"total": grid, "raw_count": shared_raw,
"holm_count": shared_corrected,
"raw_fwer": str(Fraction(shared_raw, grid)),
"holm_fwer": str(Fraction(shared_corrected, grid))}}


if __name__ == "__main__":
print(json.dumps({"four_models": holm(["0.004", "0.012", "0.030", "0.200"]),
"monotone_example": holm(["0.020", "0.021", "0.022"]),
"grid": grid_experiment()}))

四模型的 adjusted 依次为 2/125、9/250、3/50、1/5,即 0.016、0.036、0.060、0.200。只有前两项 reject 为 true。网格计数应与上表一致。

m 项校正的排序需要 O(m log m) 时间,扫描与结果映射为 O(m),额外空间 O(m)。网格实验为 O(m·G^m) 时间,额外迭代空间 O(m);这里只用 G=20、m=4,不把枚举当作大规模统计工具。

上述校正复杂度按有理数比较和算术操作的次数计;Fraction 的分子分母位数越大,单次操作本身也会更贵。代码没有把任意精度算术当成固定时间的生产保证。

上述校正复杂度按有理数比较和算术操作的次数计;Fraction 的分子分母位数越大,单次操作本身也会更贵。代码没有把任意精度算术当成固定时间的生产保证。

验证程序 tests/verify-holm-article.cjs 对 500 个随机家族使用独立的闭合检验参考,穷举 30,964 个非空交集,核对 2,248 个假设的调整值与决策;另检查 8,000 个真假零假设混合配置、20 个相关网格配置、平局和重排不变性及 14 类非法输入。它没有用这些有限例子替代一般 FWER 证明,也没有声称已核实某个真实业务检验的 p 值有效性。

校正没有替你解决什么

容易误解的情况 正确边界
校正后 p=0.04 不是“零假设有 4% 概率为真”
没有拒绝零假设 不是证明模型相同,应同时报告效应差值与不确定性
只把挑出来的赢家交给函数 遗漏了选择过程,不能声称控制整个搜索的 FWER
同一测试集不断看结果直到显著 固定家族的一次性校正不能自动覆盖可选停止
把组内相关的行当独立样本 单项 p 值可能已无效,校正不会修复它
有些指标其实是事后探索 明确区分探索和确认,另用独立数据确认结论
希望控制发现中的错误比例 那是 FDR 目标,不能把 Holm 的 FWER 结论换个名字
自检:为什么 p=[0.020,0.021,0.022] 不能只保留最后一项?

第一步阈值为 0.05/3,0.020 已未通过,Holm 必须停止。直接把最后一项与 0.05 比较会破坏逐步规则;累积最大值使三个调整 p 都为 0.060,与停止决策一致。

继续阅读配对 Bootstrap时,可以把“差值有多大、区间多宽”和“在预先规定的多个检验中是否拒绝”分开报告;RAG 回归评测的关键题与分组退步也不应只压缩成一个最小 p 值。