多重検定問題とBonferroni・FDR補正

ゲノム研究で20,000個の遺伝子について「この遺伝子は疾患と関連があるか」を検定したとします。有意水準 $\alpha = 0.05$ で各遺伝子を独立に検定すると、全ての遺伝子が実際には無関係であっても、$20{,}000 \times 0.05 = 1{,}000$ 個の遺伝子が「有意」と判定されてしまいます。

これが多重検定問題(multiple testing problem)です。検定を繰り返すほど、少なくとも1つの偽陽性が生じる確率が増大します。

逆に、この問題に対処するためにBonferroni補正を安直に適用すると、有意水準が $0.05/20{,}000 = 0.0000025$ と極めて厳しくなり、本当に関連のある遺伝子も見逃してしまいます。

多重検定の補正方法を理解すると、以下の場面で適切な判断ができます。

  • ゲノミクス: 数千から数万の遺伝子の同時検定
  • 脳科学: fMRIデータの数十万ボクセルの検定
  • A/Bテスト: 複数の指標を同時に評価する場合
  • 分散分析の事後検定: 複数の群間比較

本記事の内容

  • 多重検定問題の本質
  • ファミリーワイズ過誤率(FWER)の制御
  • 偽発見率(FDR)の制御
  • Bonferroni補正、Holm法、Benjamini-Hochberg法の比較
  • Pythonでの実装と可視化

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

多重検定問題の本質

問題の定式化

$m$ 個の帰無仮説 $H_{0,1}, \ldots, H_{0,m}$ を同時に検定します。

棄却しない 棄却する 合計
$H_0$ が真 $U$ (true negative) $V$ (false positive) $m_0$
$H_0$ が偽 $T$ (false negative) $S$ (true positive) $m – m_0$
合計 $m – R$ $R$ $m$

$V$ は偽発見の数(false discoveries)、$R$ は棄却の総数です。

個別の過誤率の膨張

各検定の有意水準が $\alpha$ なら、$m$ 回の独立な検定で少なくとも1つの偽陽性が出る確率は

$$ P(\text{少なくとも1つの偽陽性}) = 1 – (1-\alpha)^{m_0} \approx 1 – e^{-m_0\alpha} $$

$m_0 = 20$ で $\alpha = 0.05$ のとき、$1 – 0.95^{20} = 0.642$。20回検定するだけで、偽陽性が出る確率は64%にもなります。

この問題に対処する2つのアプローチを見ていきましょう。

FWER(ファミリーワイズ過誤率)の制御

定義

$$ \begin{equation} \text{FWER} = P(V \geq 1) = P(\text{少なくとも1つの偽陽性}) \end{equation} $$

FWERを $\alpha$ 以下に制御する方法:

Bonferroni補正

最もシンプルな方法。各検定の有意水準を $\alpha/m$ にします。

$$ \text{$H_{0,i}$ を棄却} \iff p_i < \frac{\alpha}{m} $$

理論的根拠: ボンフェローニの不等式 $P(\bigcup A_i) \leq \sum P(A_i)$ により、

$$ \text{FWER} = P\left(\bigcup_{i \in \text{true } H_0} \{p_i < \alpha/m\}\right) \leq \sum_{i \in \text{true } H_0} P(p_i < \alpha/m) = m_0 \cdot \frac{\alpha}{m} \leq \alpha $$

長所: 非常にシンプル。検定間の依存構造によらず有効

短所: 保守的すぎる(検出力が低い)。$m$ が大きいと本当の差も見逃す

Holm法(ステップダウン法)

Bonferroniの改良版で、p値を昇順に並べて段階的に棄却します。

  1. p値を昇順に並べる: $p_{(1)} \leq p_{(2)} \leq \cdots \leq p_{(m)}$
  2. $j = 1$ から始めて $p_{(j)} < \alpha/(m - j + 1)$ なら $H_{0,(j)}$ を棄却
  3. 初めて棄却できなくなったら、残りはすべて棄却しない

Holm法はBonferroniより常に検出力が高く(あるいは等しく)、FWERを厳密に $\alpha$ 以下に制御します。

FWERの限界

FWERは「1つでも偽陽性があってはならない」という厳しい基準です。大規模検定($m \gg 100$)では保守的すぎて、真の発見も見逃してしまいます。

より柔軟な基準として、偽発見率(FDR)の制御が提案されました。

FDR(偽発見率)の制御

定義

$$ \begin{equation} \text{FDR} = E\left[\frac{V}{R}\right] = E\left[\frac{\text{偽発見の数}}{\text{棄却の総数}}\right] \end{equation} $$

$R = 0$ のとき $V/R = 0$ と定義します。

FDRは「棄却した中で偽陽性が占める割合の期待値」です。FWERが「1つでも偽陽性がある確率」を制御するのに対し、FDRは「偽陽性の割合」を制御します。

Benjamini-Hochberg法(BH法)

1995年にBenjaminiとHochbergが提案した方法で、FDRを $\alpha$ 以下に制御します。

  1. p値を昇順に並べる: $p_{(1)} \leq p_{(2)} \leq \cdots \leq p_{(m)}$
  2. $k^* = \max\left\{i : p_{(i)} \leq \frac{i}{m}\alpha\right\}$ を求める
  3. $p_{(1)}, \ldots, p_{(k^*)}$ に対応する帰無仮説をすべて棄却

BH法の直感

BH法は「p値が十分小さい仮説を上から順に棄却していく」手続きです。各p値に対する閾値 $i\alpha/m$ は $i$ に比例して増加するため、Bonferroniの一律の閾値 $\alpha/m$ より柔軟です。

Pythonで各補正法を比較しましょう。

Pythonでの実装と可視化

import numpy as np
import matplotlib.pyplot as plt
from scipy import stats

np.random.seed(42)

# シミュレーション設定
m = 1000  # 検定の数
m0 = 900  # 真の帰無仮説の数
m1 = m - m0  # 真に有意な検定の数
alpha = 0.05

# p値の生成
# H0が真: p ~ Uniform(0,1)
p_null = np.random.uniform(0, 1, m0)
# H0が偽: 効果ありのp値(小さい値に集中)
effect_size = 3.0
z_alt = np.random.normal(effect_size, 1, m1)
p_alt = 2 * (1 - stats.norm.cdf(np.abs(z_alt)))

p_values = np.concatenate([p_null, p_alt])
is_null = np.concatenate([np.ones(m0, dtype=bool), np.zeros(m1, dtype=bool)])

# シャッフル
idx = np.random.permutation(m)
p_values = p_values[idx]
is_null = is_null[idx]

# 補正方法の適用
# (1) 補正なし
reject_none = p_values < alpha

# (2) Bonferroni
reject_bonf = p_values < alpha / m

# (3) Holm
sorted_idx = np.argsort(p_values)
sorted_p = p_values[sorted_idx]
holm_thresholds = alpha / (m - np.arange(m))
reject_holm_sorted = np.zeros(m, dtype=bool)
for i in range(m):
    if sorted_p[i] <= holm_thresholds[i]:
        reject_holm_sorted[i] = True
    else:
        break
reject_holm = np.zeros(m, dtype=bool)
reject_holm[sorted_idx[reject_holm_sorted]] = True

# (4) Benjamini-Hochberg
bh_thresholds = np.arange(1, m+1) * alpha / m
reject_bh_sorted = sorted_p <= bh_thresholds
k_star = np.max(np.where(reject_bh_sorted)[0]) + 1 if np.any(reject_bh_sorted) else 0
reject_bh = np.zeros(m, dtype=bool)
reject_bh[sorted_idx[:k_star]] = True

# 結果の集計
def summarize(reject, is_null):
    V = np.sum(reject & is_null)   # 偽陽性
    S = np.sum(reject & ~is_null)  # 真陽性
    R = np.sum(reject)
    FDR = V / max(R, 1)
    Power = S / max(np.sum(~is_null), 1)
    return V, S, R, FDR, Power

results = {}
for name, reject in [('No correction', reject_none), ('Bonferroni', reject_bonf),
                      ('Holm', reject_holm), ('BH (FDR)', reject_bh)]:
    V, S, R, FDR, Power = summarize(reject, is_null)
    results[name] = {'V': V, 'S': S, 'R': R, 'FDR': FDR, 'Power': Power}

fig, axes = plt.subplots(1, 3, figsize=(16, 5.5))

# (a) p値と各閾値
ax = axes[0]
sorted_p_plot = np.sort(p_values)
rank = np.arange(1, m+1)

ax.scatter(rank, sorted_p_plot, s=2, alpha=0.3, color='gray')
ax.plot(rank, np.full(m, alpha), 'k--', linewidth=1.5, label=f'No correction ($\\alpha={alpha}$)')
ax.plot(rank, np.full(m, alpha/m), 'r-', linewidth=1.5, label=f'Bonferroni ($\\alpha/m$)')
ax.plot(rank, rank * alpha / m, 'b-', linewidth=2, label=f'BH ($i\\alpha/m$)')

# BHの棄却点を表示
if k_star > 0:
    ax.plot(k_star, sorted_p_plot[k_star-1], 'b*', markersize=15, zorder=5)
    ax.annotate(f'$k^* = {k_star}$', xy=(k_star, sorted_p_plot[k_star-1]),
                xytext=(k_star+50, sorted_p_plot[k_star-1]+0.05),
                fontsize=10, arrowprops=dict(arrowstyle='->', color='blue'))

ax.set_xlabel('Rank $i$', fontsize=12)
ax.set_ylabel('$p_{(i)}$', fontsize=12)
ax.set_title('Sorted p-values and Thresholds', fontsize=13)
ax.legend(fontsize=8)
ax.grid(True, alpha=0.3)
ax.set_xlim(0, 200)  # 最初の200個に拡大
ax.set_ylim(0, 0.15)

# (b) 結果の比較
ax = axes[1]
methods = list(results.keys())
x_pos = np.arange(len(methods))
width = 0.25

tp = [results[m]['S'] for m in methods]
fp = [results[m]['V'] for m in methods]
fn = [np.sum(~is_null) - results[m]['S'] for m in methods]

ax.bar(x_pos - width, tp, width, color='green', alpha=0.7, label='True Positive')
ax.bar(x_pos, fp, width, color='red', alpha=0.7, label='False Positive')
ax.bar(x_pos + width, fn, width, color='orange', alpha=0.7, label='False Negative')

for j, m_name in enumerate(methods):
    fdr = results[m_name]['FDR']
    ax.text(j, max(tp[j], fp[j], fn[j]) + 5, f'FDR={fdr:.3f}',
            ha='center', fontsize=8, fontweight='bold')

ax.set_ylabel('Count', fontsize=12)
ax.set_title('Detection Results by Method', fontsize=13)
ax.set_xticks(x_pos)
ax.set_xticklabels(methods, fontsize=8)
ax.legend(fontsize=9)
ax.grid(True, alpha=0.3, axis='y')

# (c) FDR vs Power のトレードオフ
ax = axes[2]
alpha_range = np.linspace(0.001, 0.3, 50)
fdr_bonf = []
power_bonf = []
fdr_bh = []
power_bh = []

for a in alpha_range:
    # Bonferroni
    rej = p_values < a / m
    V, S, R, fdr, pwr = summarize(rej, is_null)
    fdr_bonf.append(fdr)
    power_bonf.append(pwr)

    # BH
    sorted_idx_a = np.argsort(p_values)
    sorted_p_a = p_values[sorted_idx_a]
    bh_thresh = np.arange(1, m+1) * a / m
    rej_sorted = sorted_p_a <= bh_thresh
    k = np.max(np.where(rej_sorted)[0]) + 1 if np.any(rej_sorted) else 0
    rej_bh = np.zeros(m, dtype=bool)
    rej_bh[sorted_idx_a[:k]] = True
    V, S, R, fdr, pwr = summarize(rej_bh, is_null)
    fdr_bh.append(fdr)
    power_bh.append(pwr)

ax.plot(fdr_bonf, power_bonf, 'r-', linewidth=2, label='Bonferroni')
ax.plot(fdr_bh, power_bh, 'b-', linewidth=2, label='BH (FDR)')
ax.plot([0, 1], [0, 1], 'k:', alpha=0.3)
ax.set_xlabel('False Discovery Rate', fontsize=12)
ax.set_ylabel('Power (True Positive Rate)', fontsize=12)
ax.set_title('FDR vs Power Trade-off', fontsize=13)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
ax.set_xlim(0, 0.3)

plt.tight_layout()
plt.savefig('multiple_testing.png', dpi=150, bbox_inches='tight')
plt.show()

このグラフから多重検定補正の効果がわかります。

  1. 左図: p値を小さい順に並べたものと各補正法の閾値です。Bonferroni(赤線)は極めて低い一定の閾値、BH法(青線)は順位に比例して増加する閾値を使います。BH法の閾値の方が柔軟であり、より多くの仮説を棄却できます

  2. 中央図: 各手法での真陽性・偽陽性・偽陰性の数です。補正なしでは偽陽性が約45個と多すぎます。BonferroniとHolmはFDRが低いですが偽陰性(見逃し)が多く、BH法はFDRを制御しつつも高い検出力を維持しています

  3. 右図: FDRと検出力のトレードオフを示しています。BH法はBonferroniより効率的であり、同じFDR水準でより高い検出力を達成しています

まとめ

本記事では、多重検定問題と主要な補正法を解説しました。

  • 多重検定を行うと、個別の有意水準では偽陽性が大量に発生する
  • FWER制御(Bonferroni, Holm)は「1つでも偽陽性がある確率」を制御する。保守的だが安全
  • FDR制御(BH法)は「棄却した中の偽陽性の割合」を制御する。大規模検定で実用的
  • BH法はBonferroniより常に高い検出力を持ち、ゲノミクスなどの大規模検定の標準手法

次のステップとして、以下の記事も参考にしてください。