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.
🎯 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.
- 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
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.
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
- 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
- 1733de Moivre's normal curveAbraham de Moivre publishes the first version of the normal distribution while working on gambling problems.
- 1810Laplace proves the CLTPierre-Simon Laplace shows why averages of many samples always look normal. The single most important theorem in applied statistics.
- 1908Student's t-testWilliam Gosset (‘Student’) at Guinness Brewery invents the t-test to compare beer batches with small samples. Guinness makes him publish under a pseudonym.
- 1925Fisher's p < 0.05Ronald Fisher publishes ‘Statistical Methods for Research Workers’. He picks 0.05 as a convenient threshold. A century later, we're still stuck with it.
- 1979Bootstrap · EfronBradley Efron invents the bootstrap — ‘just resample your data 10,000 times and see what happens’. Compute finally makes it practical.
- 2016ASA statement on p-valuesThe 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
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.
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.
For means → t-statistic. For proportions → z-statistic. For distributions → chi-square, KS, etc. Each has a known reference distribution under H₀.
The probability of seeing a statistic at least this extreme if H₀ were true.
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’.
‘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
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)
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
Compare categorical distributions
- e.g. is device-type breakdown the same?
- Works for k > 2 groups
- Watch expected counts ≥ 5
- scipy: chi2_contingency
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.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."
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".
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.
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.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.
- 1Under 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
- 2The 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
- 3A 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
- 4Stopping 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
- 5The 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 "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.
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.
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.
You need to decide on a feature. Fixed-horizon A/B test, sequential test with early stopping, or Bayesian decision analysis?
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
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.
(d) Production reality · 15 min
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%.
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.
Where this shows up in the rest of the plan
(e) Recall + stretch · 10 min
Explain-out-loud test
Teach these three, no notes, in under a minute each:
- Why does the Central Limit Theorem let us use ‘mean ± something’ almost everywhere?
- What does a p-value mean — and what does it definitely NOT mean?
- 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.