跳到正文

多重检验修正:Bonferroni、Holm 与 Benjamini-Hochberg

三个程序,是同一个问题的三种回答:你愿意为「一个假阳性都不要」付出多少统计功效。以及它们共同的盲点——没有一个会去看你的策略之间有多像。

第 1 篇 · 共 12 篇 约 13 分钟

你测了 100 个策略,其中 5 个的 p 值低于 0.05。常规解释是”5% 的策略在 95% 置信水平上显著”。这个解释是错的:做 100 次检验时,假阳性的期望数量正好就是 100×0.05=5100 \times 0.05 = 5。你观察到的现象,和”一个有优势的策略都没有”这个世界完全相容。

本文讲三个用来解决这件事的程序:Bonferroni、Holm、Benjamini-Hochberg。它们其实是同一个问题的三种回答,而那个问题是——你愿意为”一个假阳性都不要”付出多少统计功效?Bonferroni 付得最多,Holm 用同样的假设付得更少,BH 干脆换了个问题问。

推完这三个之后,还有一件更要紧的事:它们有一个共同的盲点,而这个盲点在量化场景里恰好最致命。

假阳性是怎么叠起来的

检验 mm 个原假设,每个都用显著性水平 α\alpha。在全局原假设成立、检验相互独立时,至少出现一个假阳性的概率是

P(at least one FP)=1−(1−α)mP(\text{at least one FP}) = 1 - (1 - \alpha)^m

代入 α=0.05\alpha = 0.05、m=100m = 100:

P=1−0.95100=1−0.0059=0.994P = 1 - 0.95^{100} = 1 - 0.0059 = 0.994

99.4%。做 100 次 5% 水平的检验,你几乎必然会看到至少一个”显著”结果,哪怕真相是什么都没有。m=1000m = 1000 时这个概率在小数点后好几位都是 9,假阳性成了确定事件。

这个概率有个名字,家族错误率(FWER):

FWER=P(at least one Type I error∣any true configuration)FWER = P(\text{at least one Type I error} | \text{any true configuration})

Bonferroni 和 Holm 控制的都是它。注意 FWER 是一个关于”有没有”的陈述,不是关于”有多少”的陈述——这个区别等下讲 BH 时会变成关键。

Bonferroni:把阈值除以次数

αBonferroni=αm\alpha_{Bonferroni} = \frac{\alpha}{m}

α=0.05\alpha = 0.05、m=100m = 100 时阈值是 0.0005,只有 p 值低于它才拒绝。

证明只有两行。设所有原假设都成立:

P(any rejection∣H0)=P(min⁡ipi≤αm∣H0)P(\text{any rejection} | H_0) = P\left(\min_i p_i \le \frac{\alpha}{m} \Big| H_0\right) ≤∑i=1mP(pi≤αm∣H0)=m⋅αm=α\le \sum_{i=1}^{m} P\left(p_i \le \frac{\alpha}{m} \Big| H_0\right) = m \cdot \frac{\alpha}{m} = \alpha

第二行用的是 Boole 不等式(并集上界)。这里有一个常被忽略的好处:这一步对 p 值之间的依赖结构不作任何要求,正相关、负相关、任意相关都成立。Bonferroni 是无条件有效的。

代价也正是从这里来的。并集上界在各事件互斥时取等号,而在正相关时松得厉害——几个策略同时给出小 p 值这件事高度重叠,被上界重复计了很多次。量化场景恰恰是重叠最严重的场景:两个在 SPY 上做动量的策略共享绝大部分信号,它们不是两次独立下注,是同一次下注的两种写法。Bonferroni 却按两次算。

给个数量感。假设你有 50 个策略,但里面真正独立的信号只有 5 个,其余都是变体。Bonferroni 给你的阈值是 0.05/50=0.0010.05/50 = 0.001,而”正确”的阈值应该按 5 次独立检验算,即 0.05/5=0.010.05/5 = 0.01。Bonferroni 严了整整 10 倍。

实际后果不只是”少拒绝几个”。能穿过 0.001 这道门的,只有整批结果里最极端的那几个离群值,而在回测里,最极端的结果恰恰是最可能来自过拟合的那些。把门槛抬到极端,筛出来的就是极端。

Bonferroni 唯一的优点是简单,以及那个无条件成立的保证。检验只有五个的时候用它没问题,五十个就该换。

Holm:免费的改进

Holm-Bonferroni 逐步下降程序对 p 值排序后,给每个位置分配不同的阈值:

  1. 排序:p(1)≤p(2)≤…≤p(m)p_{(1)} \le p_{(2)} \le \ldots \le p_{(m)}。
  2. 从 k=1k = 1 开始逐个检查 p(k)≤α/(m−k+1)p_{(k)} \le \alpha / (m - k + 1)。
  3. 一旦某个 kk 不满足就停下,拒绝所有 i<ki < k 的 H(i)H_{(i)}。

第 3 步的”停下”必须照字面执行。Holm 在第一次失败处终止,即使排在后面的某个 p 值恰好满足它自己那条阈值,也不再拒绝。这一点和下面的 BH 正好相反,混起来会让程序失去 FWER 保证——这是实现 Holm 时最常见的 bug。

阈值的分母从 mm 一路减到 1:最小的 p 值面对的阈值和 Bonferroni 一模一样(α/m\alpha/m),之后每拒绝一个,剩下的假设就少一个,分母跟着变小、阈值跟着放松。所以 Holm 的功效永不低于 Bonferroni——Bonferroni 能拒绝的,Holm 一定也能拒绝,反过来不成立。

而这个改进是免费的:Holm 和 Bonferroni 一样,在任意依赖结构下都把 FWER 控制在 α\alpha 以内,不需要独立性假设。既然假设一样、功效更强、代价只是排个序,实践中就没有理由再用裸的 Bonferroni。

举例。m=5m = 5,α=0.05\alpha = 0.05,p 值为 0.002,0.012,0.016,0.038,0.300.002, 0.012, 0.016, 0.038, 0.30。

Bonferroni 对所有检验用同一个阈值 0.05/5=0.010.05/5 = 0.01,只有 0.0020.002 通过,拒绝 1 个。

Holm 逐个看:

  • p(1)=0.002p_{(1)} = 0.002,阈值 0.05/5=0.010.05/5 = 0.01,通过,拒绝。
  • p(2)=0.012p_{(2)} = 0.012,阈值 0.05/4=0.01250.05/4 = 0.0125,通过,拒绝。
  • p(3)=0.016p_{(3)} = 0.016,阈值 0.05/3=0.01670.05/3 = 0.0167,通过,拒绝。
  • p(4)=0.038p_{(4)} = 0.038,阈值 0.05/2=0.0250.05/2 = 0.025,不通过,停。

拒绝 3 个。同样的数据、同样的 FWER 保证,多找出两个候选。

Holm 顺带还给你一个排好序的拒绝列表,最小的 p 值是最强候选、第二小是第二强,做优先级排序时直接可用。

Benjamini-Hochberg:换一个问题

在检验很多、而且你预期里面确实有一批是真的场景下(基因研究、因子发现),FWER 这个目标太苛刻了。“一个假阳性都不能有”意味着几乎什么都拒绝不了,于是真发现和假发现一起被埋掉。

Benjamini 和 Hochberg(1995)的做法是换掉目标函数,改控错误发现率(FDR):

FDR=E[false positivestotal rejections]FDR = E\left[\frac{\text{false positives}}{\text{total rejections}}\right]

差别在于 FWER 限制的是”出现一个或更多假阳性”的概率,FDR 限制的是假阳性在你的拒绝集合里占的期望比例。FDR 明确允许假阳性存在,只要求它们别占太大份额。这不是把标准放松,是换了一个衡量单位。

程序如下:

  1. 排序:p(1)≤p(2)≤…≤p(m)p_{(1)} \le p_{(2)} \le \ldots \le p_{(m)}。
  2. 找到最大的 kk,使得 p(k)≤(k/m)⋅αp_{(k)} \le (k/m) \cdot \alpha。
  3. 拒绝所有 i≤ki \le k 的 H(i)H_{(i)}。

注意第 2 步是”最大的 kk“,不是”第一次失败就停”。BH 是逐步上升(step-up)程序:中间某个 p(j)p_{(j)} 越过了自己的阈值不要紧,只要后面还有更大的 kk 满足条件,jj 照样被拒绝。这和 Holm 的停止规则是相反的,两者不能互换。

用上面那组 p 值(0.002,0.012,0.016,0.038,0.300.002, 0.012, 0.016, 0.038, 0.30,m=5m = 5,α=0.05\alpha = 0.05)走一遍,阈值序列是 0.01,0.02,0.03,0.04,0.050.01, 0.02, 0.03, 0.04, 0.05:

  • k=1k=1:0.002≤0.010.002 \le 0.01,满足。
  • k=2k=2:0.012≤0.020.012 \le 0.02,满足。
  • k=3k=3:0.016≤0.030.016 \le 0.03,满足。
  • k=4k=4:0.038≤0.040.038 \le 0.04,满足。
  • k=5k=5:0.30≤0.050.30 \le 0.05,不满足。

最大的满足者是 k=4k = 4,拒绝 4 个。同一组数据上,Bonferroni 1 个、Holm 3 个、BH 4 个——这个 1/3/4 的阶梯就是三种目标函数的价格表。

BH 实际把 FDR 控制在 α⋅m0/m\alpha \cdot m_0 / m,其中 m0m_0 是真原假设的数量。当 m0<mm_0 < m,也就是确实有一部分假设为假时,BH 比名义水平还要保守一点。这既是它在筛选场景里的优势,也留下了改进空间(下面 Storey 那一节就在做这件事)。

有一个前提必须点明,因为量化场景经常踩:BH 的 FDR 保证需要检验之间独立,或者满足一个叫 PRDS(正回归依赖)的条件。策略之间正相关通常落在 PRDS 里,勉强还行;但如果依赖结构复杂或者含负相关,保证就不再成立。Benjamini 和 Yekutieli(2001)给了一个在任意依赖下都成立的版本,代价是把阈值再除以 ∑i=1m1/i\sum_{i=1}^{m} 1/i(m=100m = 100 时约 5.2)。这个代价很重,重到很多人宁可去论证自己的场景满足 PRDS。

三个程序,一份代码

import numpy as np


def bonferroni(pvals, alpha=0.05):
    """Bonferroni correction. Returns a boolean mask of rejections."""
    return np.array(pvals) <= (alpha / len(pvals))


def holm(pvals, alpha=0.05):
    """Holm-Bonferroni step-down: stop at the FIRST failure."""
    m = len(pvals)
    sorted_idx = np.argsort(pvals)
    sorted_p = np.array(pvals)[sorted_idx]
    thresholds = alpha / (m - np.arange(m))      # alpha/m, alpha/(m-1), ..., alpha/1
    rejected = np.zeros(m, dtype=bool)
    for i, (p, th) in enumerate(zip(sorted_p, thresholds)):
        if p <= th:
            rejected[sorted_idx[i]] = True
        else:
            break                                 # step-down: halt here
    return rejected


def benjamini_hochberg(pvals, alpha=0.05):
    """Benjamini-Hochberg step-up: take the LARGEST k that passes."""
    m = len(pvals)
    sorted_idx = np.argsort(pvals)
    sorted_p = np.array(pvals)[sorted_idx]
    thresholds = (np.arange(1, m + 1) / m) * alpha
    max_k = -1
    for k in range(m):
        if sorted_p[k] <= thresholds[k]:
            max_k = k                             # step-up: keep scanning
    rejected = np.zeros(m, dtype=bool)
    if max_k >= 0:
        rejected[sorted_idx[:max_k + 1]] = True
    return rejected


pvals = [0.003, 0.0055, 0.006, 0.02, 0.03, 0.20, 0.35, 0.50, 0.70, 0.90]
print(f"Bonferroni rejects: {bonferroni(pvals).sum()}")
print(f"Holm rejects:       {holm(pvals).sum()}")
print(f"BH rejects:         {benjamini_hochberg(pvals).sum()}")

这组 m=10m = 10 的 p 值输出还是 1、3、4。两个循环之间那个 break 和 max_k 的差别,就是 step-down 和 step-up 的全部实现差异,也是整篇文章里最容易写错的两行。

选哪一个

检验数量少(五个以内)、或者你处在需要向监管方交代的确认性分析里,Bonferroni 够用,它的无条件保证和一行实现在这种场合是优点。检验数量到了几十个,就该换 Holm——它假设一样、功效更高,没有理由不换。真正需要想清楚的是 Holm 和 BH 之间的取舍,而这个取舍不是技术问题,是你打算拿结果干什么。

如果这一批检验的输出会直接决定资金投向,你要的是 FWER:宁可漏掉几个真的,也不要在名单里混进一个假的,因为你只会挑一个去部署,而混进来的那个可能正好是你挑中的。如果这一批检验只是在圈定候选、后面还有独立的确认环节(样本外测试、另一个市场上的复现),那 FDR 更合适:你能容忍名单里有 5% 是假的,因为下一关会把它们筛掉,而 FWER 的严格会让你在第一关就丢掉太多真信号。

一句话:FWER 用于终局决策,FDR 用于漏斗上游。

还有一种情况值得单独说:从一批候选里挑一个策略部署。这时上面三个方法都不完全对路。BH 或 Holm 给你的仍然是一个多元素的名单,而”从名单里挑最好的那个”这个动作本身又引入了新的选择偏差,多重检验修正对此一言不发。去膨胀夏普比率和回测过拟合概率(本模块后面两篇)就是为这个决策设计的。

它们共同的盲点

现在说那件更要紧的事。上面三个程序有一个共同点:它们的输入只有 p 值和检验数量 mm,从头到尾没有看过你的检验之间有多像。

Bonferroni 按 mm 平分,Holm 按排序调整分母,BH 按排序缩放阈值——三者用的都是排序位置这一个信息,而策略之间 0.95 的相关性和 0.05 的相关性,在它们眼里长得一模一样。

这在生物统计里问题不大,那里的检验往往接近独立。在量化里这是核心问题:50 个动量变体提供的独立信息可能只相当于三五个检验,而这三个程序会一致地把它当成 50 个。Holm 相对 Bonferroni 的那点改进是从排序里榨出来的,不是从依赖结构里榨出来的,所以面对一批高度雷同的策略时,它同样会过度保守。

要利用依赖结构,就得让程序真的去看数据,而不只是看 p 值。这正是 Romano-Wolf 逐步下降法用 bootstrap 做的事,也是它成为下一篇主题的原因。

Storey 的 π0\pi_0 估计

顺着 BH 那个”α⋅m0/m\alpha \cdot m_0/m 比名义水平更保守”的观察往下走,Storey(2002)提出把 π0=m0/m\pi_0 = m_0/m 估出来,再用它把阈值放大回去:

  1. 估计真原假设占比。常用估计量是 π^0=#{pi>λ}/(m(1−λ))\hat{\pi}_0 = \#\{p_i > \lambda\} / (m(1 - \lambda)),其中 λ\lambda 通常取 0.5——想法是大 p 值那一段几乎全是真原假设贡献的,据此外推总量。
  2. 用修正阈值 p(k)≤(k/(mπ^0))⋅αp_{(k)} \le \big(k / (m \hat{\pi}_0)\big) \cdot \alpha。

π^0\hat{\pi}_0 明显小于 1 时(也就是相当一部分假设确实为假),这比 BH 更有功效;π^0\hat{\pi}_0 接近 1 时它退化回 BH。

量化金融里很少用它,原因是它把方法的表现押在了 π^0\hat{\pi}_0 上,而这个估计对 λ\lambda 的选择敏感,在 mm 只有几十的时候还很不稳。50 到 100 个策略的规模上,BH 已经够用,多这一层估计带来的不确定性未必划算。

逐步下降、逐步上升,以及闭合检验

前面提过 Holm 是 step-down、BH 是 step-up,这两个词值得单独解释一下,因为它们背后是一个一般性的权衡。

逐步下降从最小 p 值往上走,遇到第一个不满足就停。逐步上升从最大 p 值往下走(或者等价地,扫完全部再取最大的满足者),中间的失败不影响后面的成功。

在 FWER 这一侧,Holm(step-down)和 Hochberg(step-up)是一对。功效上 Hochberg 更强:它拒绝的假设是 Holm 的超集。但这个优势不是白拿的——Hochberg 的有效性建立在 Simes 不等式上,需要独立性或 PRDS,而 Holm 在任意依赖下都成立。所以选择又回到那个老问题:你愿不愿意为多几个拒绝,去承担一个关于依赖结构的假设。在策略高度相关、依赖结构又说不清楚的量化场景里,Holm 那个无条件保证通常更值钱。

在 FDR 这一侧,step-up 的 BH 是标准做法,step-down 的 FDR 版本存在但很少用。

再往上还有一层,叫闭合检验:把 mm 个原假设的全部 2m−12^m - 1 个非空子集都拿出来,对每个子集检验它的交集原假设,只有当一个原假设在所有包含它的子集上都被拒绝时才最终拒绝它。这个框架能给出 FWER 控制的一般性证明,Holm 其实就是它的一个特例——对每个交集假设用 Bonferroni 做局部检验,化简之后正好得到 Holm 的阈值序列。换更强的局部检验(比如用 Simes 检验,得到的就是 Hommel 程序)能得到更高的功效,代价是计算量。

代价有多大?m=50m = 50 时 250≈10152^{50} \approx 10^{15} 个子集,直接枚举是不可能的。所以实践中的甜点区间就是 Holm 和 Romano-Wolf:它们都用了排序结构,又都不用付完整闭合检验的账。

因子研究里的阈值

多重检验最有名的一次实际应用,是 Harvey、Liu 和 Zhu(2016)对金融因子文献的清算。他们的论点是:考虑到几十年里被试过的因子数量,传统的 t>2t > 2 显著性门槛低得不合理。

把 Bonferroni 直接套上去就能看出问题的量级。双侧检验、α=0.05\alpha = 0.05 时,mm 个因子对应的阈值是 Φ−1(1−α/(2m))\Phi^{-1}\big(1 - \alpha/(2m)\big):

因子数量 mm双侧 Bonferroni 的 t 阈值
11.96
203.02
1003.48
3163.78
10004.06

单看这张表,只要研究者集体试过一百个以上的因子,t=2.5t = 2.5 就完全够不上显著。

Harvey、Liu 和 Zhu 最终给出的建议门槛是 t>3.0t > 3.0。值得注意的是这个数字低于同规模的 Bonferroni 阈值,而不是高于——原因是他们并没有停在 Bonferroni 上,还算了 Holm 和 Benjamini-Yekutieli 型的 FDR 阈值,后者比 FWER 阈值宽松;同时他们也承认自己统计到的因子数量是已发表因子的下界,无从知道有多少因子被试过又没被写出来。t>3.0t > 3.0 是在这些考量之间取的一个可操作的整数。

无论用哪一档,结论方向是一致的:大量 tt 值落在 2 到 3 之间的已发表因子,撑不过任何一种多重检验修正。这也是他们那篇论文最常被引用的含义——已发表的因子里有相当比例可能是假阳性。

顺着这个思路可以画一条发现曲线:横轴是显著性门槛,纵轴是通过门槛的因子数量。一个典型的因子研究项目可能长这样:p<0.05p < 0.05 时留下 30 个因子,p<0.01p < 0.01 时 12 个,p<0.001p < 0.001 时 5 个,Bonferroni 修正后 2 个。这条曲线把”发现数量”和”严谨程度”之间的取舍画在了同一张图上,比任何单个门槛都更有信息量——它让读者自己看到,你的结论对门槛有多敏感。

验证你的实现

多重检验程序的实现 bug(比如把 Holm 的 break 写成了 BH 的扫描)不会报错,只会悄悄给你更多拒绝。有一个便宜的 sanity check:模拟纯零数据。

生成 1000 组”谁都没有优势”的数据,每组跑一遍你的程序。Bonferroni 和 Holm 应该在大约 5% 的模拟里出现至少一个拒绝,也就是名义 FWER;BH 应该让拒绝集合里的假阳性比例平均在 5% 左右。观测值和名义水平差得明显(尤其是偏高),八成不是运气,是实现有问题。

同一套模拟顺便还能告诉你功效:把真信号加进去,看各程序分别找回多少。这比读文献里的功效比较更贴近你自己的数据结构。

修正之外

多重检验修正是给统计问题的统计解药。它管不了前视偏差、容量限制、状态切换和实现细节——一个干干净净通过 Holm 修正的策略,照样可能因为这四样里的任何一样在实盘失败。

它也不告诉你哪些策略有真实优势。它告诉你的是:在把检验次数计入之后,哪些策略的收益还能和基准区分开。这是部署的必要条件,不是充分条件。真正的流程是拿修正后的候选名单去过样本外测试、容量分析、状态分析和带实现摩擦的回测,最后再由 DSR 或 PBO 处理”从名单里挑一个”这个动作本身带来的偏差。

报告的时候,“我测了 100 个策略,5 个显著”是不完整的。读者需要检验数量 mm、用的什么检验统计量、原始 p 值、修正方法、修正后的 p 值或调整后的显著性水平、被拒绝的假设列表,以及结果对 mm 的敏感性——把 m=50m = 50、m=100m = 100、m=200m = 200 都算一遍列出来。最后这一项尤其重要,因为 mm 往往不是一个客观事实,而是一个判断:你到底”试过”多少个策略?改一次参数算不算一次试验?调一次特征呢?把这个判断的影响摊开给读者看,比替他们选一个数要诚实得多。

参考文献

  • Bonferroni, C. E. (1936). “Teoria Statistica delle Classi e Calcolo delle Probabilità.” Pubblicazioni del R Istituto Superiore di Scienze Economiche e Commerciali di Firenze, 8, 3–62.
  • Holm, S. (1979). “A Simple Sequentially Rejective Multiple Test Procedure.” Scandinavian Journal of Statistics, 6(2), 65–70.
  • Benjamini, Y. and Hochberg, Y. (1995). “Controlling the False Discovery Rate: A Practical and Powerful Approach to Multiple Testing.” Journal of the Royal Statistical Society B, 57(1), 289–300.
  • Benjamini, Y. and Yekutieli, D. (2001). “The Control of the False Discovery Rate in Multiple Testing under Dependency.” Annals of Statistics, 29(4), 1165–1188.
  • Storey, J. D. (2002). “A Direct Approach to False Discovery Rates.” Journal of the Royal Statistical Society B, 64(3), 479–498.
  • Harvey, C. R., Liu, Y., and Zhu, H. (2016). “…and the Cross-Section of Expected Returns.” Review of Financial Studies, 29(1), 5–68。大多数已发表因子可能是假阳性的实证证据。