Python & Data Science
Statistics Under review

Reference: Hypothesis Testing

Roster

ConceptDefinition / FormulaRangeWhen to use
Null hypothesis (H₀)The “nothing interesting” claim — no effect, no difference, equality holds.Any statistical hypothesisStarting point of every frequentist test.
Alternative hypothesis (H₁)The claim you are trying to find evidence for — an effect, a difference, a change.Any statistical hypothesisPaired with H₀; reject H₀ in favour of H₁.
Significance level (α)The false-positive rate you are willing to tolerate: P(reject H₀ | H₀ true).(0, 1), typically 0.01–0.10Set before seeing data.
p-valueP(test statistic ≥ observed | H₀ true). The probability of seeing data this extreme if nothing is going on.[0, 1]Compare to α to decide reject/fail to reject.
Type I error (α)Rejecting a true H₀ — a false positive.(0, 1)Controlled by choice of α.
Type II error (β)Failing to reject a false H₀ — a false negative.(0, 1)Controlled by sample size and effect size.
Statistical power1 − β = P(reject H₀ | H₁ true). The probability of detecting a real effect.(0, 1), target ≥ 0.80Use to size experiments; computed before data collection.
Test statisticA scalar function of data that measures deviation from H₀ (e.g., t, z, χ², F).Depends on testThe bridge between data and the sampling distribution.
Sampling distributionThe distribution the test statistic follows under H₀ (e.g., t{df}, standard normal, χ²{df}).Provides the p-value.
Effect sizeA standardised measure of the magnitude of an effect (Cohen’s d, Pearson r, η²).Depends on measureRequired for power calculations; complements p-values.
Bonferroni correctionAdjusted α’ = α / m for m simultaneous tests.(0, α]Conservative; controls family-wise error rate (FWER).
Benjamini-Hochberg (BH)Controls false-discovery rate (FDR): rank p-values, reject p_{(i)} ≤ (i/m)·q.(0, 1]Less conservative; preferred for large m.

The Neyman-Pearson ritual in five steps

Every frequentist hypothesis test — no matter the test statistic — follows the same five-step recipe. Understanding the procedure is more important than memorising any particular formula, because the procedure is what guarantees the error rates you signed up for.

  1. Pick a null hypothesis (H₀). This is the “baseline” or “boring” world: no difference, no effect, coefficient equals zero, model A and model B perform identically. The null is never “proven” — it is the thing you try to disprove.

  2. Pick an alternative hypothesis (H₁). This is what you are looking for. A one-sided alternative (“B > A”) is more powerful but requires a pre-registered justification; a two-sided alternative (“B ≠ A”) is the default unless you have a strong directional prior.

  3. Pick α. Before touching data, decide the false-positive rate you will tolerate. The convention is α = 0.05, but the right value depends on the cost of a false positive. If a false positive triggers a costly deployment, use α = 0.01 or lower.

  4. Compute the test statistic from data. Reduce your sample to a single scalar (t, z, χ², F, D) using the formula dictated by your test choice.

  5. Compare the statistic to its sampling distribution under H₀. The tail area beyond your statistic is the p-value. If p ≤ α, reject H₀; otherwise, fail to reject. The word “fail” is deliberate — you do not “accept” H₀, you merely withhold judgment.

The general form of a p-value is:

p=P ⁣(Ttobs    H0 true)p = P\!\Big(T \ge t_{\text{obs}} \;\Big|\; H_0 \text{ true}\Big)

where TT is the random test statistic under the null, and tobst_{\text{obs}} is the value computed from your actual sample. For a two-sided test:

p=P ⁣(Ttobs    H0)=2P ⁣(Ttobs    H0)p = P\!\Big(|T| \ge |t_{\text{obs}}| \;\Big|\; H_0\Big) = 2 \cdot P\!\Big(T \ge |t_{\text{obs}}| \;\Big|\; H_0\Big)

The decision rule is:

Reject H0    pα\text{Reject } H_0 \iff p \le \alpha

The four possible outcomes:

H₀ trueH₀ false
Reject H₀Type I error (α)Correct (power = 1 − β)
Fail to reject H₀Correct (1 − α)Type II error (β)

Power is derived from the alternative distribution:

Power=P ⁣(reject H0    H1 true)=P ⁣(pα    H1)\text{Power} = P\!\Big(\text{reject } H_0 \;\Big|\; H_1 \text{ true}\Big) = P\!\Big(p \le \alpha \;\Big|\; H_1\Big)
Plain EnglishStatistical symbolPython equivalent
Significance levelαalpha = 0.05
False-negative rateβbeta = 1 - power
Power1 − βpower = 1 - beta
Effect size (two-sample means)Cohen’s dd = (mean1 - mean2) / pooled_sd
Sample meanxˉ\bar{x}x.mean()
Sample standard deviationsx.std(ddof=1)
Standard error of the meanσ/n\sigma/\sqrt{n}sem = x.std(ddof=1) / np.sqrt(n)

Decision tree: which test for which question

The single biggest source of hypothesis-testing errors in practice is using the wrong test for the question. The tree below maps data shape and question type to the correct test.

  • Is the question about a mean (or difference of means)?

    • One group, comparing to a known value → One-sample t-test (data approximately normal, n < 30) or z-test (n ≥ 30 or σ known).
    • Two independent groups → Welch’s two-sample t-test (unequal variances; the safe default) or Student’s t-test (if you verified equal variances with Levene’s test).
    • Two paired/matched groups (before vs after, same subjects) → Paired t-test.
    • Three or more groups → One-way ANOVA (if assumptions hold) or Kruskal-Wallis (non-parametric).
    • Data is heavily skewed or ordinal → Mann-Whitney U (independent) or Wilcoxon signed-rank (paired).
  • Is the question about variances or spread?

    • Comparing two variances → F-test (if normal) or Levene’s test / Bartlett’s test (more robust).
    • Testing a single variance against a constant → Chi-square test for variance.
  • Is the question about the shape of a distribution?

    • Comparing to a named theoretical distribution → Kolmogorov-Smirnov (KS) test (continuous distributions).
    • Comparing two empirical distributions → Two-sample KS test.
    • Better sensitivity in tails → Anderson-Darling test.
  • Is the question about independence or association between categorical variables?

    • Two categorical variables, contingency table → Pearson’s chi-square test of independence.
    • Small samples or expected cell counts < 5 → Fisher’s exact test.
    • Ordinal categories → Mantel-Haenszel test.
  • Is the question about whether residuals are autocorrelated (time series)?

    • Testing residual autocorrelation at multiple lags → Ljung-Box test (preferred; tests a group of lags jointly).
    • Testing autocorrelation at lag 1 only → Durbin-Watson test.
    • Testing for unit root (stationarity) → Augmented Dickey-Fuller (ADF) test.
  • Is the question about whether two models have different performance?

    • Paired accuracy/AUC across folds or segments → Paired t-test or McNemar’s test (binary outcomes) or 5×2 cross-validated paired t-test.

Worked example: a two-sample t-test with scipy.stats

The most common hypothesis test in data science is the two-sample t-test: “Is the mean of group A different from the mean of group B?” Below is a complete, runnable example.

import numpy as np
from scipy import stats

rng = np.random.default_rng(seed=42)

# Two groups: control (mean=50) and treatment (mean=53)
control  = rng.normal(loc=50, scale=10, size=100)
treatment = rng.normal(loc=53, scale=10, size=100)

# Welch's two-sample t-test (does NOT assume equal variances)
result = stats.ttest_ind(treatment, control, equal_var=False)

print(f"Mean control:    {control.mean():.3f}")
print(f"Mean treatment:  {treatment.mean():.3f}")
print(f"t-statistic:     {result.statistic:.4f}")
print(f"p-value:         {result.pvalue:.5f}")
print(f"95% CI for diff: {result.confidence_interval(0.95)}")

Expected output (will vary slightly with the RNG but reproducible at seed 42):

Mean control:    49.497
Mean treatment:  52.894
t-statistic:     2.7170
p-value:         0.00720
95% CI for diff: (0.930, 5.862)

With α = 0.05, since p = 0.007 ≤ 0.05, we reject the null that the means are equal. The 95% confidence interval for the difference does not include zero, which is consistent with the rejection.

Line by line:

  1. rng = np.random.default_rng(seed=42) — a modern NumPy Generator (preferred over the legacy np.random.seed global-state approach). The seed makes the example reproducible.

  2. rng.normal(loc=50, scale=10, size=100) — draws 100 samples from a normal distribution with mean 50 and standard deviation 10. The treatment group has mean 53, so the true effect size is Cohen’s d ≈ 0.3 (a small-to-medium effect).

  3. stats.ttest_ind(treatment, control, equal_var=False) — this is Welch’s t-test, which does not assume the two groups have equal variance. This is the safer default over equal_var=True (Student’s t-test). Student’s test inflates Type I error when variances are unequal; Welch’s correction adjusts the degrees of freedom downward to compensate.

  4. result.statistic — the t-statistic, computed as:

    t=xˉ1xˉ2s12/n1+s22/n2t = \frac{\bar{x}_1 - \bar{x}_2}{\sqrt{s_1^2/n_1 + s_2^2/n_2}}

    Under H₀, this follows a t distribution with Welch-Satterthwaite degrees of freedom.

  5. result.pvalue — the two-sided p-value: the area in both tails of the t distribution beyond ±|t|.

  6. result.confidence_interval(0.95) — the 95% confidence interval for the mean difference. A useful sanity check: if the CI excludes zero, the two-sided test rejects at α = 0.05 (these are equivalent statements).

What this tells you: With 100 samples per group and a true effect of 3 units (SD = 10), the test has enough power to reliably detect the difference. Re-run with size=30 and the p-value will frequently exceed 0.05 — this is the underpowered pattern described in the corpus article on statistical power.


Worked example: Bonferroni vs. Benjamini-Hochberg

When you run m hypothesis tests simultaneously, the probability that at least one is a false positive balloons. If each test has a 5% false-positive rate, running 20 independent tests gives a 1 − (0.95)²⁰ ≈ 64% chance of at least one false positive. This is the multiple comparisons problem.

Two corrections are standard:

  • Bonferroni divides α by m and tests each p-value against α/m. It controls the family-wise error rate (FWER) — the probability of any false positive among all m tests. Extremely conservative: as m grows, almost nothing survives.

  • Benjamini-Hochberg (BH) controls the false-discovery rate (FDR) — the expected proportion of false positives among the rejected tests. Sort p-values ascending, and reject all tests where p(i)imqp_{(i)} \le \frac{i}{m} \cdot q, with q the desired FDR (typically 0.05 or 0.10).

import numpy as np
from scipy import stats

rng = np.random.default_rng(seed=7)

# 100 tests, 10 true alternatives, 90 nulls
m = 100
true_effects = 10

# Generate p-values: ~Beta(alpha,1) for true effects, Uniform(0,1) for nulls
p_nulls   = rng.uniform(0, 1, size=m - true_effects)
p_trues   = rng.beta(1, 20, size=true_effects)  # skewed small
pvals = np.concatenate([p_trues, p_nulls])

# --- Bonferroni ---
alpha_bonf = 0.05 / m
bonf_reject = pvals <= alpha_bonf
print(f"Bonferroni threshold: {alpha_bonf:.5f}")
print(f"Bonferroni rejections: {bonf_reject.sum()}")

# --- Benjamini-Hochberg ---
q = 0.05
ranked = np.sort(pvals)
thresholds = (np.arange(1, m + 1) / m) * q
# Find the largest k where p_(k) <= k/m * q, reject all up to k
k_max = np.max(np.where(ranked <= thresholds)[0]) if np.any(ranked <= thresholds) else 0
bh_reject = pvals <= ranked[k_max] if k_max > 0 else np.zeros(m, dtype=bool)
print(f"BH threshold (largest surviving p): {ranked[k_max]:.5f}")
print(f"BH rejections: {bh_reject.sum()}")

Expected output:

Bonferroni threshold: 0.00050
Bonferroni rejections: 1
BH threshold (largest surviving p): 0.00099
BH rejections: 3

BH finds more discoveries (3 vs. 1) because it tolerates a small fraction of false positives among the rejections rather than guarding against any false positive. For exploratory data analysis, genomics, or any high-throughput setting, BH is almost always the better choice. Bonferroni is appropriate when a single false positive is catastrophic (e.g., regulatory submissions, safety-critical systems).

Why the thresholds differ so dramatically:

  • Bonferroni’s threshold is α/m=0.05/100=0.0005\alpha / m = 0.05 / 100 = 0.0005. Only p-values below 0.05% survive. With 10 true effects drawn from a Beta(1, 20) distribution, most true p-values are small but only the very smallest dips below that bar — so Bonferroni rejects just 1 of the 10 real effects.

  • BH’s threshold is adaptive. The largest p-value that survives is p(k)kmqp_{(k)} \le \frac{k}{m} \cdot q. With q = 0.05 and k = 3, the ceiling is 0.05×3/100=0.00150.05 \times 3 / 100 = 0.0015 — and the actual 3rd-smallest p-value (0.00099) clears it. That cutoff is already about double Bonferroni’s fixed 0.0005 cutoff, which is why BH rejects 3 tests where Bonferroni rejects only 1. The key insight: BH’s effective threshold rises as you find more discoveries, making it far less conservative when many true effects exist.

The false-discovery rate guarantee: BH guarantees that E[false positives among rejections/total rejections]qE[\text{false positives among rejections} / \text{total rejections}] \le q. If you reject 3 tests, you expect at most 0.05×3=0.150.05 \times 3 = 0.15 false discoveries on average — i.e., almost certainly all 3 of your discoveries are real.

Using statsmodels for production code:

from statsmodels.stats.multitest import multipletests

# Bonferroni
_, p_bonf, _, _ = multipletests(pvals, alpha=0.05, method='bonferroni')
print(f"Bonferroni rejections: {(p_bonf <= 0.05).sum()}")

# Benjamini-Hochberg
_, p_bh, _, _ = multipletests(pvals, alpha=0.05, method='fdr_bh')
print(f"BH rejections: {(p_bh <= 0.05).sum()}")

The power curve: why underpowered experiments lie

Power is the probability of detecting an effect that actually exists. It depends on three things: the effect size, the sample size n, and α. The power curve plots power against n for a fixed effect size and α, showing how detection probability climbs as you collect more data.

The classic failure mode: an underpowered experiment (n too small for the effect you’re trying to detect) produces a non-significant result, and you conclude “no effect.” But the test was never capable of detecting that effect in the first place. Worse, among the significant results that do survive, effect sizes are inflated (the “winner’s curse” or “type M error”).

import numpy as np
from scipy import stats

def compute_power(effect_size, n_per_group, alpha=0.05, two_sided=True):
    """Power of a two-sample t-test given n_per_group observations in each group."""
    # Non-centrality parameter
    ncp = effect_size * np.sqrt(n_per_group / 2)
    df = 2 * n_per_group - 2
    if two_sided:
        crit = stats.t.ppf(1 - alpha / 2, df)
        power = 1 - stats.nct.cdf(crit, df, ncp) + stats.nct.cdf(-crit, df, ncp)
    else:
        crit = stats.t.ppf(1 - alpha, df)
        power = 1 - stats.nct.cdf(crit, df, ncp)
    return power

# Power curves for three effect sizes (n = observations per group)
ns = np.arange(10, 501, 10)
for d, label in [(0.2, "small (d=0.2)"), (0.5, "medium (d=0.5)"), (0.8, "large (d=0.8)")]:
    powers = [compute_power(d, n) for n in ns]
    n_80 = next(n for n, p in zip(ns, powers) if p >= 0.80)
    print(f"{label}: need n={n_80} per group for 80% power")

Expected output:

small (d=0.2): need n=400 per group for 80% power
medium (d=0.5): need n=70 per group for 80% power
large (d=0.8): need n=30 per group for 80% power

The pattern is unambiguous: small effects require enormous samples. A Cohen’s d of 0.2 (a “small” effect by convention) needs ~400 observations per group (~800 total, since both arms need the same size) to reach 80% power. Many real-world A/B tests are attempting to detect effects of 0.01–0.05 in relative conversion rate — which demands tens of thousands of users per arm.

For a two-sample z-test (large n), power is:

Power=Φ ⁣(δσ/n/2z1α/2)+Φ ⁣(δσ/n/2z1α/2)\text{Power} = \Phi\!\Big(\frac{\delta}{\sigma/\sqrt{n/2}} - z_{1-\alpha/2}\Big) + \Phi\!\Big(-\frac{\delta}{\sigma/\sqrt{n/2}} - z_{1-\alpha/2}\Big)

where δ\delta is the true mean difference, σ\sigma is the common standard deviation, and Φ\Phi is the standard normal CDF. For a one-sided test, the second term drops and z1α/2z_{1-\alpha/2} becomes z1αz_{1-\alpha}.

The sample size formula for 80% power at significance α (two-sided, large n) is:

n=2σ2(z1α/2+z1β)2δ2n = \frac{2\sigma^2 (z_{1-\alpha/2} + z_{1-\beta})^2}{\delta^2}

where z1β=0.84z_{1-\beta} = 0.84 for 80% power and z1α/2=1.96z_{1-\alpha/2} = 1.96 for α = 0.05, giving:

n2σ2(1.96+0.84)2δ2=15.7σ2δ2n \approx \frac{2 \sigma^2 \cdot (1.96 + 0.84)^2}{\delta^2} = \frac{15.7 \, \sigma^2}{\delta^2}

This is the formula underlying most A/B test sample-size calculators. The ratio δ/σ\delta / \sigma is exactly Cohen’s d, so:

n15.7d2n \approx \frac{15.7}{d^2}
Plain EnglishStatistical symbolPython equivalent
True effect (mean difference)δ\deltadelta = mean_A - mean_B
Pooled standard deviationσ\sigmasigma = pooled_std(x1, x2)
Effect sized=δ/σd = \delta / \sigmad = delta / sigma
Critical z-value for αz1α/2z_{1-\alpha/2}stats.norm.ppf(1 - alpha/2)
Non-centrality parameterλ=dn/2\lambda = d\sqrt{n/2}ncp = d * np.sqrt(n/2)
Power1β1 - \betapower = 1 - stats.nct.cdf(...)

Edge cases and common mistakes

  1. “The p-value is the probability that H₀ is true.” No. The p-value is computed assuming H₀ is true. It is P(data | H₀), not P(H₀ | data). Confusing these is the base-rate fallacy and is the single most common statistical error in published research.

  2. Using a two-sided test when a one-sided test was pre-registered (or vice versa). One-sided tests have more power but require a directional hypothesis declared before data inspection. Switching to one-sided after seeing the data doubles the false-positive rate. Always pre-register your sidedness.

  3. Assuming equal variances without checking. Student’s t-test (the default in many stats packages) assumes both groups share the same population variance. If they don’t, Type I error inflates. Use Welch’s t-test (equal_var=False) unless you have a reason not to.

  4. Interpreting failure to reject as “no effect.” Non-significance does not mean the effect is zero — it means your test lacked the precision to distinguish the effect from zero. Always report the confidence interval alongside the p-value; a wide CI around zero tells you the test was underpowered, not that the effect is absent.

  5. Multiple comparisons without correction. Running 20 A/B metrics without correction gives a ~64% chance of at least one false positive. If you are checking many outcomes, use BH (for discovery) or Bonferroni (for confirmatory).

  6. Peeking at data and stopping early. Repeatedly checking p-values and stopping when p < 0.05 inflates the false-positive rate far above α. This is optional stopping, and it is one of the most damaging mistakes in online experimentation. Use sequential testing procedures (e.g., always-valid p-values, group sequential designs) if you need to peek.

  7. Using the wrong test for the data shape. Applying a t-test to severely skewed data with n = 8. The t-test assumes the sampling distribution of the mean is approximately normal; for small n and skewed data, this fails. Use a non-parametric test (Mann-Whitney, Wilcoxon) or transform the data.

  8. Confusing practical significance with statistical significance. With n = 1,000,000, any tiny difference is statistically significant. A 0.001% lift in conversion rate can yield p < 0.001 but may not justify the engineering cost of shipping the change. Always pair p-values with effect-size estimates and business-impact calculations.

  9. Testing residuals for autocorrelation with the wrong lag structure. Using Durbin-Watson when you care about multiple lags; Durbin-Watson only tests lag-1 autocorrelation. Use Ljung-Box for joint multi-lag testing.

  10. Forgetting that chi-square requires expected cell counts ≥ 5. With sparse contingency tables, Pearson’s chi-square approximation breaks down. Use Fisher’s exact test (2×2) or simulate the p-value (simulate_p_value=True in scipy.stats.chi2_contingency).


Cross-references


Further reading

Foundational papers:

  • Fisher, R. A. (1925). Statistical Methods for Research Workers. Oliver and Boyd. — The original articulation of significance testing and the p-value.
  • Neyman, J., & Pearson, E. S. (1933). “On the problem of the most efficient tests of statistical hypotheses.” Philosophical Transactions of the Royal Society of London, Series A, 231, 289–337. — The Neyman-Pearson lemma; the formal framework of α, β, and the rejection region.
  • Benjamini, Y., & Hochberg, Y. (1995). “Controlling the false discovery rate: a practical and powerful approach to multiple testing.” Journal of the Royal Statistical Society, Series B, 57(1), 289–300. — The BH procedure; the most-cited paper in multiple-comparisons statistics.
  • Wasserstein, R. L., & Lazar, N. A. (2016). “The ASA statement on p-values: context, process, and purpose.” The American Statistician, 70(2), 129–133. — The community’s official response to p-value misuse.

Library documentation:

Kaggle competition / dataset:

  • Kaggle: A/B Testing datasets — A collection of A/B test datasets ideal for practising two-sample tests, power calculations, and multiple-comparison corrections on realistic conversion-rate data.

Looking for something else?

Search every article by title, summary or topic.