P-Value Correction for Multiple Tests#
Family-Wise Error Rate (FWER): the probability of rejecting the null hypothesis when it is actually true while performing multiple hypotheses tests.
We often need do account for multiple tests by adjusting the \(\alpha\) to keep the type 1 error rate down. Common methods to adjusting \(\alpha\) include:
Bonferroni: When strict control over false positives is paramount or when number of tests is relatively small.
Holm-Bonferroni: Balances power and strictness. Default for most business/scientific scenarios.
False Discovery Rate (FDR): For exploratory analyses or many tests. Useful when maximizing discovery outweighs risk of false positives.
How Each Method is Calculated#
For each of the examples, assume we have a family of three tests. The test is considered statistically significant when \(\text{p-value} \leq \alpha \)
Test |
P-value |
\( \alpha \) |
Statistically Significant? |
|---|---|---|---|
1 |
0.01 |
0.05 |
Yes ✅ |
2 |
0.02 |
0.05 |
Yes ✅ |
3 |
0.05 |
0.05 |
Yes ✅ |
Bonferroni Correction#
Divide \(\alpha\) by the number of tests (\(m\)) to get a \(\alpha_{adjusted}\) :
Test |
P-value |
Adjusted \(\alpha\) |
Statistically Significant? |
|---|---|---|---|
1 |
0.01 |
0.05/3 = 0.0167 |
Yes ✅ |
2 |
0.02 |
0.05/3 = 0.0167 |
No ❌ |
3 |
0.05 |
0.05/3 = 0.0167 |
No ❌ |
Holm-Bonferroni Correction#
Compare each p-value against decreasing alpha thresholds.
Order p-values smallest to largest: \( p_{(1)} \le p_{(2)} \le ... \le p_{(m)} \)
For the ith test, the adjusted alpha is:
Test |
P-value |
Adjusted \(\alpha\) |
Statistically Significant? |
|---|---|---|---|
1 |
0.01 |
0.05/3 = 0.0167 |
Yes ✅ |
2 |
0.02 |
0.05/2 = 0.025 |
Yes ✅ |
3 |
0.05 |
0.05/1 = 0.05 |
No ❌ |
False discovery rate (FDR)#
There are a few different methods to control the FDR. Here is the BH step-up procedure:
Order p-values smallest to largest: \( p_{(1)} \le p_{(2)} \le ... \le p_{(m)} \)
For each p-value, compute the threshold:
Test |
P-value |
Adjusted \(\alpha\) |
Statistically Significant? |
|---|---|---|---|
1 |
0.01 |
(1/3)*0.05 = 0.0167 |
Yes ✅ |
2 |
0.02 |
(2/3)*0.05 = 0.0333 |
Yes ✅ |
3 |
0.05 |
(3/3)*0.05 = 0.05 |
Yes ✅ |
There are several methods available on statsmodels.
Benjamini–Hochberg: Proven for independent test statistics, and has been shown to be robust to violations of the independence assumption.
Benjamini–Yekutieli: A more conservative extension that controls for correlated tests.
Matti Pirinen has great lecture notes online that dive deeper into the details of FDR.
Multiple Tests Example#
Three independent two-sample t-tests are performed comparing random data for three groups
import numpy as np
import scipy.stats as stats
from statsmodels.stats.multitest import multipletests
rng = np.random.default_rng()
alpha = 0.05
sample_size = 100
n_tests = 3
data1 = rng.standard_normal(sample_size)
data2 = rng.standard_normal(sample_size)
data3 = rng.standard_normal(sample_size)
t_stat1, p_val1 = stats.ttest_ind(data1, data2)
t_stat2, p_val2 = stats.ttest_ind(data1, data3)
t_stat3, p_val3 = stats.ttest_ind(data2, data3)
p_values = [p_val1, p_val2, p_val3]
print(f"Original p-values: {p_val1:.4f}, {p_val2:.4f}, {p_val3:.4f}")
print("\nBonferroni Correction Results:")
rejected, pvals_corrected, _, _ = multipletests(p_values, alpha=0.05, method='bonferroni')
for i, (orig_p, adj_p, rej) in enumerate(zip(p_values, pvals_corrected, rejected), start=1):
status = 'Rejected' if rej else 'Not rejected'
print(f"\tTest {i}: Original p = {orig_p:.4f}, Adjusted p = {adj_p:.4f}, {status}")
print("\nFDR with Benjamini-Hochberg Procedure:")
rejected, pvals_corrected, _, _ = multipletests(p_values, alpha=0.05, method='fdr_bh')
for i, (orig_p, adj_p, rej) in enumerate(zip(p_values, pvals_corrected, rejected), start=1):
status = 'Rejected' if rej else 'Not rejected'
print(f"\tTest {i}: Original p = {orig_p:.4f}, Adjusted p = {adj_p:.4f}, {status}")
print("\nFDR with Benjamini-Yekutieli Procedure:")
rejected, pvals_corrected, _, _ = multipletests(p_values, alpha=0.05, method='fdr_by')
for i, (orig_p, adj_p, rej) in enumerate(zip(p_values, pvals_corrected, rejected), start=1):
status = 'Rejected' if rej else 'Not rejected'
print(f"\tTest {i}: Original p = {orig_p:.4f}, Adjusted p = {adj_p:.4f}, {status}")
Original p-values: 0.5700, 0.6228, 0.9499
Bonferroni Correction Results:
Test 1: Original p = 0.5700, Adjusted p = 1.0000, Not rejected
Test 2: Original p = 0.6228, Adjusted p = 1.0000, Not rejected
Test 3: Original p = 0.9499, Adjusted p = 1.0000, Not rejected
FDR with Benjamini-Hochberg Procedure:
Test 1: Original p = 0.5700, Adjusted p = 0.9342, Not rejected
Test 2: Original p = 0.6228, Adjusted p = 0.9342, Not rejected
Test 3: Original p = 0.9499, Adjusted p = 0.9499, Not rejected
FDR with Benjamini–Yekutieli Procedure:
Test 1: Original p = 0.5700, Adjusted p = 1.0000, Not rejected
Test 2: Original p = 0.6228, Adjusted p = 1.0000, Not rejected
Test 3: Original p = 0.9499, Adjusted p = 1.0000, Not rejected