Multiple Comparisons › FWER-Controlling Methods

Michael BrenndoerferJanuary 9, 20266 min read
Machine Learning from ScratchAll chapters
Part 2 of 9Hypothesis Testing
Chapter 21Multiple Comparisons › FWER-Controlling Methods
Reading section 4 of 12 · FWER-Controlling Methods · 6 minSection 4 of 12 · 6 min

Book contents

Section 4 of 12 · 6 min read

Multiple Comparisons › FWER-Controlling MethodsOne section · Sections in this chapter4 / 12 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Bonferroni Correction

The Bonferroni correction is the simplest and most widely known method for controlling FWER. The idea is mathematically elegant: if you want the overall error rate to be at most α\alpha across mm tests, make each individual test more stringent.

The method: To test mm hypotheses while controlling FWER at level α\alpha:

  • Reject hypothesis ii if pi<α/mp_i < \alpha/m
  • Equivalently, compute adjusted p-values: p~i=min⁡(m⋅pi,1)\tilde{p}_i = \min(m \cdot p_i, 1)

Mathematical justification: The Bonferroni inequality states that for any events A1,…,AmA_1, \ldots, A_m:

P(⋃i=1mAi)≤∑i=1mP(Ai)P\left(\bigcup_{i=1}^{m} A_i\right) \leq \sum_{i=1}^{m} P(A_i)

Let AiA_i be the event that test ii produces a false positive. If we use threshold α/m\alpha/m for each test:

FWER=P(⋃i=1mAi)≤∑i=1mP(Ai)=∑i=1mαm=α\text{FWER} = P\left(\bigcup_{i=1}^{m} A_i\right) \leq \sum_{i=1}^{m} P(A_i) = \sum_{i=1}^{m} \frac{\alpha}{m} = \alpha

Properties:

  • Always valid (makes no assumptions about test dependence)
  • Very conservative, especially for large mm
  • Power decreases as mm increases
  • Simple to implement and explain
In[6]:
Code
def bonferroni_correction(p_values, alpha=0.05):
    """
    Apply Bonferroni correction for multiple comparisons.

    Parameters
    ----------
    p_values : array-like
        Raw p-values from multiple tests
    alpha : float
        Desired FWER level

    Returns
    -------
    adjusted_p : ndarray
        Bonferroni-adjusted p-values
    rejected : ndarray
        Boolean array indicating which hypotheses to reject
    """
    p = np.asarray(p_values)
    m = len(p)
    adjusted_p = np.minimum(p * m, 1.0)
    rejected = p < (alpha / m)
    return adjusted_p, rejected

Šidák Correction

The Šidák correction is slightly less conservative than Bonferroni and is exact when tests are independent.

The method: For mm independent tests at FWER level α\alpha:

  • Reject hypothesis ii if pi<1−(1−α)1/mp_i < 1 - (1-\alpha)^{1/m}

Mathematical justification: If tests are independent and we want FWER = α\alpha:

FWER=1−(1−αper-test)m=α\text{FWER} = 1 - (1-\alpha_{\text{per-test}})^m = \alpha

Solving for the per-test significance level:

αper-test=1−(1−α)1/m\alpha_{\text{per-test}} = 1 - (1-\alpha)^{1/m}

Comparison with Bonferroni: For m=20m = 20 tests at α=0.05\alpha = 0.05:

  • Bonferroni: α/m=0.0025\alpha/m = 0.0025
  • Šidák: 1−(1−0.05)1/20=0.002561 - (1-0.05)^{1/20} = 0.00256

The difference is small but Šidák is slightly more powerful. However, Šidák requires independence while Bonferroni is always valid.

In[7]:
Code
def sidak_correction(p_values, alpha=0.05):
    """
    Apply Šidák correction for multiple comparisons.
    Requires test independence for exact FWER control.
    """
    p = np.asarray(p_values)
    m = len(p)
    alpha_adj = 1 - (1 - alpha) ** (1 / m)
    # Adjusted p-values
    adjusted_p = 1 - (1 - p) ** m
    rejected = p < alpha_adj
    return adjusted_p, rejected

Holm's Step-Down Procedure

Holm's method (1979) is uniformly more powerful than Bonferroni while still controlling FWER. The key insight is that we can be less stringent on tests with larger p-values.

The algorithm:

  1. Order p-values: p(1)≤p(2)≤…≤p(m)p_{(1)} \leq p_{(2)} \leq \ldots \leq p_{(m)}
  2. For j=1,2,…,mj = 1, 2, \ldots, m:
    • If p(j)>α/(m−j+1)p_{(j)} > \alpha/(m-j+1), stop and reject all hypotheses with smaller p-values
    • Otherwise, continue to the next p-value
  3. If all p-values pass their thresholds, reject all hypotheses

Mathematical justification: The adjusted thresholds form a step-down sequence:

αm,αm−1,αm−2,…,α2,α\frac{\alpha}{m}, \frac{\alpha}{m-1}, \frac{\alpha}{m-2}, \ldots, \frac{\alpha}{2}, \alpha

The first comparison uses the Bonferroni threshold, but subsequent comparisons are less stringent. This maintains FWER control because once we fail to reject at step jj, we know that hypothesis has p>α/(m−j+1)p > \alpha/(m-j+1), and remaining hypotheses have even larger p-values.

In[8]:
Code
def holm_correction(p_values, alpha=0.05):
    """
    Apply Holm's step-down procedure for FWER control.

    More powerful than Bonferroni while maintaining FWER control.
    """
    p = np.asarray(p_values)
    m = len(p)

    # Get sorted indices
    sorted_idx = np.argsort(p)
    p_sorted = p[sorted_idx]

    # Calculate Holm thresholds
    thresholds = alpha / (m - np.arange(m))

    # Find first non-significant result
    rejected_sorted = np.zeros(m, dtype=bool)
    for j in range(m):
        if p_sorted[j] > thresholds[j]:
            break
        rejected_sorted[j] = True

    # Map back to original order
    rejected = np.zeros(m, dtype=bool)
    rejected[sorted_idx] = rejected_sorted

    # Calculate adjusted p-values
    adjusted_sorted = np.zeros(m)
    for j in range(m):
        adjusted_sorted[j] = min(
            max(p_sorted[: j + 1] * (m - np.arange(j + 1))), 1.0
        )
    adjusted_p = np.zeros(m)
    adjusted_p[sorted_idx] = adjusted_sorted

    return adjusted_p, rejected

Hochberg's Step-Up Procedure

Hochberg's method (1988) is similar to Holm but works in the opposite direction (step-up instead of step-down). It's more powerful than Holm but requires the assumption that tests are independent or positively correlated.

The algorithm:

  1. Order p-values: p(1)≤p(2)≤…≤p(m)p_{(1)} \leq p_{(2)} \leq \ldots \leq p_{(m)}
  2. For j=m,m−1,…,1j = m, m-1, \ldots, 1 (starting from largest):
    • If p(j)≤α/(m−j+1)p_{(j)} \leq \alpha/(m-j+1), reject all hypotheses with p-values ≤p(j)\leq p_{(j)}
    • Otherwise, continue to the next smaller p-value

When to use:

  • When tests are independent or positively correlated
  • When you want more power than Holm
  • Not valid for negatively correlated tests

Comparing FWER Methods

Out[9]:
Visualization
Visualization comparing different FWER correction methods showing their thresholds and rejections.
Comparison of FWER-controlling methods on the same set of 10 p-values. The step-down (Holm) and step-up (Hochberg) procedures use increasingly lenient thresholds than Bonferroni. In this particular example, Bonferroni and Holm both reject only the smallest p-value.