Lesson 10 · Statistics for data analysts
Multiple Comparisons & P-Value Corrections | Statistics #10
Video ten of the 15-part series: what happens to your real false-positive rate once you stop running one test and start running many. Real Sample Superstore…
- CourseStatistics for data analysts
- Lesson10 of 15
- Video13 min
- FormatJupyter notebook · 8 code cells
- Data1 dataset
What you'll learn
Datasets used in this lesson
Save these next to the notebook. In Google Colab, upload them with the 📁 icon on the left first.
- superstore_sales.csv715.5 KB
📓 Full notebook
Download .ipynbStatistics for Data Analysts, Video 10: Multiple Comparisons#
- Video ten of the 15-part series: what happens to your real false-positive rate once you stop running one test and start running many.
- Real Sample Superstore order data throughout, including 136 real pairwise sub-category comparisons.
- Let's get into it.
Before You Start#
- Open a new Jupyter Notebook in VS Code and select your Python interpreter as the kernel.
- Install statsmodels if you don't have it yet:
pip install statsmodels. - Place superstore_sales.csv in the same folder as this notebook.
import pandas as pd
import numpy as np
from scipy import stats
from itertools import combinations
from statsmodels.stats.multitest import multipletests
from statsmodels.stats.multicomp import pairwise_tukeyhsd
df = pd.read_csv('superstore_sales.csv')
print(df.shape)
Part 1: The Problem, in Theory and in Real Data#
for k in [1, 5, 10, 20, 50, 136]:
fwer = 1 - (0.95) ** k
print(f'{k} independent tests at alpha=0.05: at least one false positive with probability {fwer:.1%}')
office = df[df['Category'] == 'Office Supplies']['Sales'].values
rng = np.random.default_rng(seed=9)
shuffled = rng.permutation(office)
null_groups = np.array_split(shuffled, 20)
null_pairs = list(combinations(range(20), 2))
null_pvals = np.array([stats.ttest_ind(null_groups[i], null_groups[j])[1] for i, j in null_pairs])
print(f'Real null-world pairs tested: {len(null_pairs)} (all drawn from the same real Office Supplies population)')
print(f'Real false positives at raw alpha=0.05: {(null_pvals < 0.05).sum()}')
Part 2: Running It for Real - 136 Sub-Category Comparisons#
subcats = df['Sub-Category'].unique()
pairs = list(combinations(subcats, 2))
pvals = np.array([stats.mannwhitneyu(df[df['Sub-Category'] == a]['Sales'], df[df['Sub-Category'] == b]['Sales']).pvalue for a, b in pairs])
print(f'Real sub-categories: {len(subcats)}')
print(f'Real pairwise comparisons: {len(pairs)}')
print(f'Real significant at raw alpha=0.05: {(pvals < 0.05).sum()} of {len(pairs)}')
Part 3: The Bonferroni Correction#
bonferroni_alpha = 0.05 / len(pairs)
bonf_significant = pvals < bonferroni_alpha
print(f'Real Bonferroni-adjusted alpha: {bonferroni_alpha:.6f}')
print(f'Real significant after Bonferroni: {bonf_significant.sum()} of {len(pairs)}')
flipped = [(pairs[i], pvals[i]) for i in range(len(pairs)) if pvals[i] < 0.05 and pvals[i] >= bonferroni_alpha]
print(f'Real pairs significant raw but NOT after Bonferroni: {len(flipped)}')
for pair, p in sorted(flipped, key=lambda x: x[1])[:5]:
print(f' {pair}: raw p={p:.5f}')
Part 4: Tukey's HSD and the Benjamini-Hochberg FDR#
tukey_data = df[['Sales', 'Sub-Category']].dropna()
tukey_result = pairwise_tukeyhsd(tukey_data['Sales'], tukey_data['Sub-Category'], alpha=0.05)
print(tukey_result.summary().as_text()[:1200])
reject_bh, pvals_bh, _, _ = multipletests(pvals, alpha=0.05, method='fdr_bh')
print(f'Real significant after Benjamini-Hochberg FDR correction: {reject_bh.sum()} of {len(pairs)}')
print(f'Real significant after Bonferroni: {bonf_significant.sum()} of {len(pairs)}')
print(f'Real significant, raw uncorrected: {(pvals < 0.05).sum()} of {len(pairs)}')
Part 5: Choosing a Correction#
- Bonferroni: simplest, most conservative, controls the chance of even one false positive across the whole family of tests. Good default when a single false alarm would be costly.
- Tukey's HSD: purpose-built for comparing every pair of groups after an ANOVA-style design; use it specifically for that all-pairs case.
- Benjamini-Hochberg (FDR): less conservative, controls the expected proportion of false positives among the significant results rather than eliminating false positives entirely. Common choice when screening many real comparisons and some real true effects are expected, like this sub-category analysis.
- Whichever correction you pick, decide on it before looking at the real results, exactly the same discipline as the pre-registration and sample-size planning from video eight.
Wrap-Up: What You Learned#
- Why the family-wise error rate grows with the number of tests, in theory and confirmed with a real null-world simulation.
- Running 136 real pairwise sub-category comparisons and seeing how the significant count shifts under different corrections.
- The Bonferroni correction, and the specific real borderline pairs it flips from significant to not.
- Tukey's HSD, purpose-built for all-pairs comparisons, and the Benjamini-Hochberg FDR correction as a middle ground.
- A practical guide for choosing a correction method before looking at real results.
- Video eleven shifts from comparing groups to modeling relationships directly, starting with simple linear regression on real mtcars data. Subscribe so it lands automatically see you there.
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



