Search Tech Journey

Find topics, journeys and posts

6-month learning plan22 / 130
back to blog
mathbeginner 50m read

S022 · Statistics — CLT, Hypothesis Testing, Confidence Intervals

The three ideas that let you turn noisy data into defensible claims: the Central Limit Theorem, p-values, and confidence intervals — with the mistakes that get careers cancelled.

📐MathM02 · Math Foundations· Session 022 of 130 90 min

🎯 Read a p-value, a confidence interval, and an A/B test dashboard without lying to yourself — and simulate all three from scratch in Python.

Why this session exists

Every A/B test result you'll ever ship, every ML model comparison you'll ever defend, every "is this metric change real?" Slack thread — they all rest on three ideas: the Central Limit Theorem (why averages of noisy stuff behave), hypothesis testing (how you decide if a difference is real), and confidence intervals (how you communicate uncertainty). Skip this session and you'll spend the next decade being technically wrong in high-stakes meetings — the kind of wrong that ships a bad checkout flow to 100M users.

You will be able to
  • Explain the Central Limit Theorem in one sentence and demonstrate it with 20 lines of Python.
  • State the null and alternative hypothesis for any A/B test you're asked to evaluate.
  • Compute a p-value and a 95% confidence interval for a difference in means — and interpret both without the four classic mistakes.
  • Spot p-hacking, peeking, and multiple-comparison abuse in a colleague's analysis before it ships.
  • Know when to use a t-test, when to use a proportion z-test, and when to just bootstrap.

Prerequisites

  • S021 · Probability — random variables, expectation, variance.
  • S019 · Linear Algebra Basics — vector operations (used to summarise a sample).
  • Comfort with numpy from S005–S009 Python foundations.


(a) Intuition · 5 min

Statistics is how noisy measurements become confident claims
🌍 Real world

You're a coffee roaster. You claim your new bean batch is stronger than the old one. You brew one cup of each, taste them — the new one feels stronger. Are you sure? Maybe you were more caffeinated when you tasted the new one. Maybe the water temperature drifted 2 °C.

So you brew 100 cups of each, shuffle them, taste blind. Now the average strength difference is a real signal, not a lucky sip. That's statistics: the mathematics of separating signal from noise when every single measurement is unreliable.

💻 Code world

You changed the checkout button from blue to green. Conversion went from 3.1% to 3.4% over one day. Real improvement or lucky Tuesday? A hypothesis test answers exactly that: how surprised should I be by this difference if the true conversion rate didn't change at all?

The p-value is your surprise meter. The confidence interval is your "I'm 95% sure the true lift is between X and Y." The CLT is what makes both of them work no matter how weird the underlying data looks.

The three big ideas, one sentence each

Three ideas that unlock all of applied statistics
  • Central Limit Theorem — the average of many independent samples is approximately normally distributed, no matter what the original distribution looks like. This is the miracle that makes everything else work.
  • Hypothesis testing — assume nothing changed (the null), compute how likely you'd see data this extreme by chance (the p-value), and reject the null if the answer is small enough.
  • Confidence interval — a range that would contain the true value in 95% of hypothetical repeats of the experiment. NOT ‘a 95% chance the true value is in this range for this one experiment’ (a Bayesian would say that; a frequentist won't).

A quick history so you know why the world looks like this

  1. 1733
    de Moivre's normal curve
    Abraham de Moivre publishes the first version of the normal distribution while working on gambling problems.
  2. 1810
    Laplace proves the CLT
    Pierre-Simon Laplace shows why averages of many samples always look normal. The single most important theorem in applied statistics.
  3. 1908
    Student's t-test
    William Gosset (‘Student’) at Guinness Brewery invents the t-test to compare beer batches with small samples. Guinness makes him publish under a pseudonym.
  4. 1925
    Fisher's p < 0.05
    Ronald Fisher publishes ‘Statistical Methods for Research Workers’. He picks 0.05 as a convenient threshold. A century later, we're still stuck with it.
  5. 1979
    Bootstrap · Efron
    Bradley Efron invents the bootstrap — ‘just resample your data 10,000 times and see what happens’. Compute finally makes it practical.
  6. 2016
    ASA statement on p-values
    The American Statistical Association issues a formal warning: stop treating p < 0.05 as a magic yes/no line. Every industry ignored it.

(b) Visual walkthrough · 15 min

The Central Limit Theorem in one picture

The population can be any shape — a mess of spikes, a long tail, whatever. Sample it, take the average, do that over and over. The distribution of those averages is a smooth bell curve. That is why "mean plus or minus a bit" works for almost every metric on Earth.

The anatomy of a hypothesis test

11
State H₀ and H₁

Null: ‘the button colour has no effect on conversion’. Alternative: ‘the two conversion rates differ’ (two-sided) or ‘green > blue’ (one-sided). Pick one BEFORE looking at data.

22
Pick a significance level α

By convention 0.05. This is your acceptable false-positive rate — you're willing to be wrong 5% of the time in the ‘there was no effect but we shipped a change’ direction.

33
Compute the test statistic

For means → t-statistic. For proportions → z-statistic. For distributions → chi-square, KS, etc. Each has a known reference distribution under H₀.

44
Compute the p-value

The probability of seeing a statistic at least this extreme if H₀ were true.

55
Compare p to α

p < α → reject H₀ (‘the effect is statistically significant’). p ≥ α → fail to reject (‘we don't have enough evidence’). Note: this is NOT ‘there is no effect’.

66
Also report the effect size + CI

‘Significant’ can still mean ‘tiny and boring’. Always report the estimated effect (e.g. +0.3% conversion) and its 95% CI (e.g. [+0.1%, +0.5%]).

When to use which test

Two-sample t-test

Compare means of two groups

  • e.g. average session length A vs B
  • Assumes roughly normal data
  • Small samples OK (that's the whole point)
  • scipy: ttest_ind(a, b)
Two-proportion z-test

Compare rates / conversions

  • e.g. conversion rate control vs treatment
  • Assumes n·p and n·(1−p) ≥ 10
  • Standard for A/B testing
  • statsmodels: proportions_ztest
Chi-square test

Compare categorical distributions

  • e.g. is device-type breakdown the same?
  • Works for k > 2 groups
  • Watch expected counts ≥ 5
  • scipy: chi2_contingency
Bootstrap

When in doubt, resample

  • Works for ANY statistic — median, quantile, whatever
  • No distributional assumptions
  • Just compute + resample 10,000 times
  • The 21st-century default

The four interpretations that will get you fired

What p and CI do NOT mean

p = 0.03 does NOT mean ‘97% chance the effect is real’
It means ‘if there were truly no effect, we'd see data this extreme 3% of the time’. Different sentence entirely.
mistake 1
p = 0.20 does NOT mean ‘no effect’
It means ‘we don't have enough evidence to reject H₀’. Absence of evidence isn't evidence of absence — you might just need more samples.
mistake 2
‘95% CI = [1.2, 4.8]’ does NOT mean ‘95% chance the true value is in [1.2, 4.8]’
The true value either is or isn't in that interval (it's fixed, unknown). ‘95%’ refers to the METHOD: 95% of intervals built this way contain the truth.
mistake 3
‘p < 0.05, so we ship’ ignores effect size
With n = 10M, a 0.001% lift in a metric will be ‘significant’ and completely worthless to your business. Always report the point estimate + CI, not just p.
mistake 4

Common misconception
✗ What most people think

"p < 0.05 means there's a 95% chance the effect is real, and p = 0.20 means there's probably no effect. The p-value tells me how likely my hypothesis is."

✓ What is actually true

A p-value is P(data at least this extreme | the null is true). It is a statement about the data given a hypothesis, not about the hypothesis given the data. It cannot tell you the probability your effect is real — that requires a prior, which the p-value never uses. And a large p-value means "insufficient evidence", never "no effect".

Why the myth is so sticky

Because the conditional is reversed in exactly the direction that makes the convenient interpretation feel natural, and because the threshold ritual — significant or not — encourages treating 0.05 as a boundary between true and false. The narrow case where the wrong reading is roughly harmless is a well-powered test of a plausible hypothesis, where a significant result usually is real. It breaks badly when the prior is low or the power is small. Testing many implausible hypotheses at 0.05 means most of your "significant" findings are false positives, for exactly the same base-rate reason a rare-disease test misleads. And an underpowered test that finds nothing has told you almost nothing — with low power, most real effects would have been missed, so "not significant" is not evidence of absence.

Prove it to yourself

Run twenty tests on data with no effect at all, and watch significance appear:

import numpy as np
from scipy import stats
rng = np.random.default_rng(0)

hits = 0
for _ in range(20):                      # 20 'metrics', all pure noise
    a, b = rng.normal(size=100), rng.normal(size=100)
    if stats.ttest_ind(a, b).pvalue < 0.05:
        hits += 1
print(hits)   # ~1 significant result, from nothing. That is what 5% means.
From first principles
Start with the question

Why does peeking at an A/B test and stopping when it hits significance inflate the false positive rate far above 5%? Each individual test is still a valid 5% test — derive why the sequence is not.

  1. 1
    Under the null hypothesis, the observed difference between groups is a random walk: it wanders as samples accumulate, with no drift.
    forced by · with no true effect, each new observation nudges the estimate in a random direction
  2. 2
    The significance boundary shrinks as n grows (it scales like 1/√n), but the walk still fluctuates, so the estimate repeatedly approaches and retreats from the boundary.
    forced by · the standard error shrinks while the walk keeps moving; both are in play simultaneously
  3. 3
    A fixed-horizon test controls the probability of being outside the boundary at one predetermined moment. That is the guarantee it makes and the only one.
    forced by · the 5% was computed for a single evaluation of a single statistic
  4. 4
    Stopping at the first crossing instead evaluates the boundary at every point along the walk, so the relevant probability becomes "does the walk ever cross?" rather than "is it outside at time T?"
    forced by · an optional stopping rule turns one question into a maximum over many correlated questions
  5. 5
    The probability of ever crossing is strictly and substantially larger than the probability of being across at any fixed time — and with unlimited peeking it approaches certainty, since a random walk with no drift will eventually exceed any fixed boundary.
    forced by · a maximum over many chances exceeds any single chance, and unbounded random walks are recurrent
⇒ Therefore

Therefore "stop when significant" is guaranteed to eventually produce significance even when nothing is happening. The false positive rate is not 5%; it is a function of how often you looked, and with enough looks it goes to 100%.

And note what this predicts, along with the fixes: you must fix the sample size in advance from a power calculation, or use a method explicitly designed for continuous monitoring — sequential testing with alpha spending, or always-valid confidence sequences — which buy that freedom by using wider boundaries. It also predicts the identical problem in a different costume: testing twenty metrics on one experiment is peeking across metrics instead of across time, and needs the same correction. And it predicts that "the test was going to be significant, we just stopped early" is not a defence — it's a description of the bias.

Mental modelSignal, noise, and how many times you looked

Every measured difference is signal plus noise. A hypothesis test asks one question: could noise alone plausibly have produced something this large? The p-value is the answer, and it says nothing about how big the signal is or whether it matters.

So three separate numbers must be reported together, and each answers a different question. The effect size: how big, and does anyone care? The confidence interval: what range is consistent with this data? The p-value: could noise have done this? Reporting only the third — the one that answers the least useful question — is how a statistically significant 0.1% improvement ends up justifying a quarter of engineering work.

  • Compute the required sample size before running the test, from the minimum effect worth detecting. If you can't name that minimum, you aren't ready to run the experiment.
  • Report the confidence interval, not just the p-value. "+2.1% [−0.3%, +4.5%]" tells the reader everything the p-value would have, plus the magnitude and the uncertainty.
  • Multiple comparisons need correction, whether across metrics, segments, variants, or time. Twenty independent looks at α=0.05 gives roughly a 64% chance of at least one false positive.
  • Statistical significance is not practical significance. With enough traffic, any nonzero difference becomes significant — which makes effect size the only question left worth asking.
🔔 Fires when you see

Fire this model the moment you see: a test stopped early because it "hit significance" · a dashboard of twenty metrics with one starred · "no significant difference" used as proof of equivalence · a segment analysis discovered after the fact · a p-value reported with no effect size · any experiment with no pre-registered sample size.

The tradeoff

You need to decide on a feature. Fixed-horizon A/B test, sequential test with early stopping, or Bayesian decision analysis?

Fixed-horizon frequentist test
+ you gain rigorous and universally understood, with a clearly controlled error rate; the sample-size calculation forces an explicit conversation about what effect size actually matters
− you pay you must wait for the full horizon even when the result is obvious in either direction, and mid-flight peeking invalidates it — which people do anyway, quietly
pick when the decision is high-stakes and one-shot, traffic is sufficient to hit the required n in reasonable time, and the result will be scrutinised by others
Sequential / always-valid testing
+ you gain you may monitor continuously and stop as soon as evidence is sufficient, which cuts average test duration substantially and lets you kill genuinely harmful variants immediately
− you pay wider boundaries mean a larger maximum sample size for the same power — you pay in worst case for flexibility in the average case; harder to explain and easier to implement wrongly
pick when you run many tests and average duration is the bottleneck, or a bad variant causes real user harm that you cannot leave running to a fixed horizon
Bayesian decision analysis
+ you gain gives you the quantity you actually wanted — P(B is better than A) and the expected loss of choosing wrongly — and incorporates prior knowledge from previous experiments instead of discarding it
− you pay the prior is a real choice that must be defended, and a badly chosen one biases the conclusion; error-rate guarantees are not the frequentist ones stakeholders may expect; requires more statistical literacy in the room
pick when you have genuine prior information from comparable past experiments, or the decision needs an expected-cost framing rather than a binary significant/not verdict
What a senior engineer actually does

For most product experimentation the binding constraint is not the statistical framework — it is discipline. Pre-register the metric, the sample size, and the decision rule before the test starts, and most of the classic failures disappear regardless of which method you chose.

Where the framework does matter is organisational: a fixed-horizon test that everyone peeks at is worse than a sequential test designed for peeking, because at least the second one is honest about what people are actually going to do. Pick the method that matches the behaviour you will really get, not the behaviour you wish you had.


(c) Hands-on · 25 min

Run this file top to bottom. It simulates the CLT, runs a t-test, computes a bootstrap confidence interval, and reproduces the classic p-hacking mistake in ~120 lines.

"""stats_playground.py — CLT, t-test, bootstrap, p-hacking demo.
 
Run with:  uv run stats_playground.py
Deps:     numpy, scipy, matplotlib (only for the CLT figure — comment out if headless)
"""
from __future__ import annotations
import numpy as np
from scipy import stats
 
rng = np.random.default_rng(42)
 
 
# ------------------------------------------------------------------
# 1) Central Limit Theorem — averages of an ugly distribution
# ------------------------------------------------------------------
def demo_clt(n_per_sample: int = 30, n_samples: int = 10_000) -> None:
    """Sample from an exponential (very skewed) and take the mean 10,000 times."""
    means = rng.exponential(scale=2.0, size=(n_samples, n_per_sample)).mean(axis=1)
    print(f"[CLT] population mean=2.0  sample-of-means mean={means.mean():.3f}")
    print(f"[CLT] theoretical SE=σ/√n={2.0/np.sqrt(n_per_sample):.3f}  observed={means.std():.3f}")
    # If you have matplotlib, uncomment to see the bell curve:
    # import matplotlib.pyplot as plt
    # plt.hist(means, bins=60); plt.title("Distribution of sample means"); plt.show()
 
 
# ------------------------------------------------------------------
# 2) Two-sample t-test — is treatment better than control?
# ------------------------------------------------------------------
def demo_ttest() -> None:
    control = rng.normal(loc=100, scale=15, size=200)      # e.g. session length in seconds
    treatment = rng.normal(loc=104, scale=15, size=200)    # small true lift of 4s
    t, p = stats.ttest_ind(treatment, control, equal_var=False)   # Welch's t-test
    diff = treatment.mean() - control.mean()
    print(f"[t-test] observed diff={diff:+.2f}s  t={t:.2f}  p={p:.4f}  -> {'REJECT H0' if p<0.05 else 'fail to reject'}")
 
 
# ------------------------------------------------------------------
# 3) Bootstrap 95% CI for the difference in means
# ------------------------------------------------------------------
def bootstrap_ci(a: np.ndarray, b: np.ndarray, n_boot: int = 10_000, alpha: float = 0.05) -> tuple[float, float, float]:
    diffs = np.empty(n_boot)
    n_a, n_b = len(a), len(b)
    for i in range(n_boot):
        ra = rng.choice(a, size=n_a, replace=True)
        rb = rng.choice(b, size=n_b, replace=True)
        diffs[i] = rb.mean() - ra.mean()
    lo, hi = np.quantile(diffs, [alpha/2, 1 - alpha/2])
    return diffs.mean(), lo, hi
 
 
def demo_bootstrap() -> None:
    control = rng.normal(loc=100, scale=15, size=200)
    treatment = rng.normal(loc=104, scale=15, size=200)
    est, lo, hi = bootstrap_ci(control, treatment)
    print(f"[bootstrap] est diff={est:+.2f}s  95% CI=[{lo:+.2f}, {hi:+.2f}]  contains 0? {lo <= 0 <= hi}")
 
 
# ------------------------------------------------------------------
# 4) p-hacking demo — test 20 metrics with no real effect
# ------------------------------------------------------------------
def demo_phacking(n_metrics: int = 20, n_per_group: int = 100) -> None:
    """Run 20 A/B tests where the truth is that NOTHING changed. Count false positives."""
    false_positives = 0
    for i in range(n_metrics):
        a = rng.normal(size=n_per_group)
        b = rng.normal(size=n_per_group)     # same distribution — truth: no effect
        _, p = stats.ttest_ind(a, b)
        if p < 0.05:
            false_positives += 1
            print(f"  metric #{i:02d}: p={p:.3f}  <-- 'SIGNIFICANT' (but it isn't)")
    print(f"[p-hacking] {false_positives}/{n_metrics} tests came out ‘significant’ despite zero true effect.")
    print("            Expected number of false positives at α=0.05: n·α =", n_metrics * 0.05)
 
 
# ------------------------------------------------------------------
# 5) Peeking — early-stopping inflates false positives
# ------------------------------------------------------------------
def demo_peeking(n_final: int = 2000, n_peeks: int = 20, n_experiments: int = 1000) -> None:
    hits = 0
    for _ in range(n_experiments):
        a = rng.normal(size=n_final)
        b = rng.normal(size=n_final)          # again, truth: no effect
        step = n_final // n_peeks
        for k in range(step, n_final + 1, step):
            _, p = stats.ttest_ind(a[:k], b[:k])
            if p < 0.05:
                hits += 1
                break                          # ‘we won! ship it!’
    rate = hits / n_experiments
    print(f"[peeking] false-positive rate with {n_peeks} peeks: {rate:.1%}  (should be ~5%!)")
 
 
if __name__ == "__main__":
    demo_clt()
    demo_ttest()
    demo_bootstrap()
    demo_phacking()
    demo_peeking()

Anatomy of the script

What each block teaches

demo_clt
Draws from an exponential (a very non-normal distribution) and shows that the mean-of-means is normal with SE = σ/√n. This is the theoretical result you'll rely on for every t-test.
CLT
demo_ttest
Welch's t-test — the safer default when the two groups may have different variances. Returns t and p; reject H₀ if p < 0.05.
t-test
bootstrap_ci
Resample WITH replacement 10,000 times, recompute the difference of means, take the 2.5 and 97.5 percentiles. Zero distributional assumptions.
resampling
demo_phacking
20 tests with no real effect → we expect ≈1 false positive by chance (20·0.05). This is the ‘green jelly beans’ problem.
multiple testing
demo_peeking
Runs the same test 20 times over the course of the experiment. Naive early-stopping inflates the false-positive rate to ~15–25% instead of 5%. This is why platforms enforce fixed sample sizes.
sequential
Try itFeel what ‘underpowered’ means — see how sample size changes p

Change the sample sizes in demo_ttest:

control = rng.normal(loc=100, scale=15, size=25)
treatment = rng.normal(loc=104, scale=15, size=25)

Re-run 10 times. Note how often you get p < 0.05 vs p > 0.05 — with n=25, you'll fail to detect the real 4-second lift most of the time.

💡 Hint · Halve n_per_group repeatedly (100 → 50 → 25 → 10). Watch the p-value climb even though the true effect never changed. That's ‘lack of statistical power’ — you can't reject H₀ because you don't have enough data, not because the effect isn't real.

(d) Production reality · 15 min

War story Airbnb · 2014· 2014early-stopping bias killed months of experiments
🔥 What broke

Airbnb's experimentation platform let PMs watch results live and ‘call the test’ when it hit p < 0.05. Sounds smart — ship winners faster.

Data science later measured that ~30% of ‘winners’ under this rule were pure noise. Peeking + early-stopping had inflated the false-positive rate 5× above the nominal 5%.

🧯 The fix
They rebuilt the platform to lock the sample size before the test starts and hide interim p-values entirely. Where sequential monitoring was needed, they switched to always-valid p-values (Wald's SPRT / mSPRT) so you can peek without inflating error.
🎓 Lesson to steal
Naive t-test + peek = broken. Either fix the sample size up front, or use a sequential test designed for continuous monitoring.
Post-mortem
War story Microsoft Bing · 2013–2019· 2019tens of thousands of A/B tests
🔥 What broke

Bing's team documented dozens of "obviously good" changes that A/B tested as significantly bad: cleaner UI reduced engagement, faster search reduced revenue on some pages, etc.

Turned out most were metric definition bugs — but a real subset showed genuine, counter-intuitive user behaviour that would have shipped as ‘wins’ under any less-rigorous framework.

🧯 The fix
Bing built one of the industry's most-cited experimentation guides ("Trustworthy Online Controlled Experiments") mandating: pre-registered hypotheses, guardrail metrics, minimum-detectable-effect calculations up front, and mandatory A/A tests to validate the platform itself.
🎓 Lesson to steal
Your instinct is not a statistical test. Every product change that ‘obviously helps’ has a 30–40% chance of being flat or negative in a real A/B test.
Post-mortem
War story Every ML team · every weekreproducibility crisis
🔥 What broke
A team reports their new model is 0.4% better on a benchmark. They ship it. Three weeks later a colleague can't reproduce the improvement — the ‘win’ was within one standard deviation of the seed-to-seed variance.
🧯 The fix
Report benchmark metrics with **confidence intervals across seeds** (or better, a paired bootstrap CI on the per-example scores). If the CI includes 0, you don't have a real improvement — you have a lottery ticket.
🎓 Lesson to steal
Model benchmarks without confidence intervals are storytelling, not science. The same statistics that govern A/B tests apply to ML leaderboards.

Where this shows up in the rest of the plan

Statistics feeds every ‘is this real?’ moment
S087 · Model Evaluation
Cross-validation, held-out test sets — same CLT machinery, applied to loss values.
S089 · Bias & Fairness
Every fairness claim is a hypothesis test across demographic slices.
S097 · A/B Testing for ML
How to ship models with the same rigour as UI changes.
S110 · Distributed Systems Consistency
SLO ‘p99 latency \<200ms’ is a statistical claim about a distribution.
S123 · Metric Design
Why picking the RIGHT metric matters more than the p-value you compute on it.
S128 · Postmortems & Incident Analysis
‘Was this outage anomalous?’ is a hypothesis test against historical variance.

(e) Recall + stretch · 10 min

Recall — click to reveal · click to reveal
★ = stretch question

Explain-out-loud test

Teach these three, no notes, in under a minute each:

  1. Why does the Central Limit Theorem let us use ‘mean ± something’ almost everywhere?
  2. What does a p-value mean — and what does it definitely NOT mean?
  3. Give one concrete way peeking at an A/B test breaks it, and one way to fix that.

What comes next

Hub: The 6-Month Learning Plan


Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.