Hypothesis Testing
A procedure for asking whether chance alone could plausibly have produced what you observed — and a p-value is the answer to that question, not to the one you probably wanted.
Why Does This Exist?
Your new model scores two points higher. Is that a real improvement or a lucky test set?
You cannot answer by staring at the numbers, and the cost of getting it wrong runs both ways: ship a model that is no better and you have spent a deployment for nothing; discard one that genuinely is better and you have thrown away a real gain. Hypothesis testing is the formal procedure for making that call, and it is what sits underneath every A/B test and every "statistically significant" claim in a paper.
The reason it needs its own page rather than a formula is that the p-value answers a different question from the one most people think it answers. It is not the probability your result is a fluke, and it is not the probability the null hypothesis is true. Those misreadings drive real decisions, so the distinction is worth the page.
Think of It Like This
A coin you suspect is loaded
Someone hands you a coin and claims it is fair. You flip it ten times and get eight heads.
You do not start by asking "is the coin loaded?" — you cannot answer that directly. Instead you ask a question you can answer: if the coin really were fair, how often would ten flips give eight or more heads? That is a calculation, and the answer is about 5.5%.
Now you have a decision, not a proof. Eight heads is on the unusual side for a fair coin, but not extraordinary — it happens about one time in eighteen. So you either accept that fairness is still plausible, or you decide the evidence is strong enough to act on. Both are defensible, and neither is certainty.
Two features of this reasoning matter more than the arithmetic. You assumed fairness and asked whether the data embarrasses that assumption — you never computed the probability the coin is loaded. And "not embarrassing enough" is not the same as "the coin is fair"; ten flips simply cannot detect a mild bias.
How It Actually Works
The procedure has five steps and they run in this order for a reason.
- State the null hypothesis — the boring explanation, usually "no difference". And the alternative .
- Choose , your tolerance for false alarms, before looking at the data. Conventionally 0.05.
- Compute a test statistic — a single number measuring how far the data sits from what predicts.
- Compute the p-value — the probability of a statistic at least this extreme if were true.
- Decide. If , reject . Otherwise fail to reject.
What a p-value is, precisely
In plain words: assuming there is no real effect, how often would chance alone produce a result this striking?
Read the conditional carefully, because the direction is the whole thing. It is , not . Three specific things a p-value is therefore not:
- Not the probability that is true. Getting that requires a prior and Bayes' theorem.
- Not the probability your result is a fluke.
- Not a measure of effect size. A microscopic, useless difference produces a tiny p-value given enough data.
Two ways to be wrong
- Type I error — rejecting a true . A false positive. Its rate is , which you chose.
- Type II error — failing to reject a false . A false negative, rate . Power is : the chance of detecting a real effect.
The trade is unavoidable. Lowering to reduce false alarms raises and costs you power. The only way to improve both at once is more data.
This is why "not significant" must never be reported as "no difference". An underpowered test that returns nothing has measured nothing, and with the sample sizes typical of ML evaluation, underpowered is the normal state.
Failing to reject is not accepting
is never proven. You either have enough evidence against it or you do not, and "not enough" covers both "there is no effect" and "there is an effect and my test was too weak to see it". Distinguishing those requires a power calculation, or a confidence interval — which tells you what effect sizes remain plausible, and is strictly more informative than the p-value.
Which test, for ML work
- Two proportions, independent samples — two-proportion z-test. Comparing accuracy across separate test sets.
- Two proportions, same examples — McNemar's test. This is the right one for comparing two models on one test set, and it is much more powerful because it looks only at the examples where the models disagree, which is where the information about the difference lives.
- Two means — the t-test, or Welch's variant when the variances differ. Welch is the safer default.
- No distributional assumption — a permutation test. Shuffle the labels many times and see how often chance beats your observed gap. Assumption-free and always available.
Worked example
Model A gets 87% and model B gets 89%, each on 1,000 examples. is that their true accuracies are equal.
Pool the two under — if they are the same, the best estimate uses all the data:
The standard error of the difference:
The test statistic:
Since 1.376 is below the 1.96 cutoff, this does not reach significance. The two-sided p-value is .
In plain words: if the two models were genuinely equally good, you would see a gap this large about 17% of the time — roughly one experiment in six. That is far too common to treat as evidence.
Note how this agrees with the confidence-interval view: 87% ± 2.1 and 89% ± 2.1 overlap heavily, and the test confirms the same conclusion. They are one calculation.
What would settle it? Holding the 2-point gap fixed and solving for the that pushes past 1.96 gives roughly per model — so about double the data, and that is only to reach the threshold, not to have decent power.
Show Me the Code
import numpy as npfrom scipy import stats
def two_proportion_z(x1: int, n1: int, x2: int, n2: int) -> tuple[float, float]: p_pool = (x1 + x2) / (n1 + n2) # pooled under H0 se = np.sqrt(p_pool * (1 - p_pool) * (1 / n1 + 1 / n2)) z = (x2 / n2 - x1 / n1) / se return float(z), float(2 * (1 - stats.norm.cdf(abs(z))))
z, p = two_proportion_z(870, 1000, 890, 1000)print(round(z, 3), round(p, 3)) # -> 1.376 0.169 not significant
# Doubling the data on the same 2-point gap barely clears the bar.print(np.round(two_proportion_z(1740, 2000, 1780, 2000), 4)) # -> [1.946 0.0517]
# Paired case: same test set, so use McNemar on the DISAGREEMENTS only.# 40 examples B got right and A wrong; 20 the other way.print(round(float(stats.binomtest(40, 60, 0.5).pvalue), 4)) # -> 0.0154 significantThe z of 1.376 and p of 0.169 match the hand calculation. The last line is the important one: the same 2-point gap, analysed as paired data, reaches significance — because pairing removes the variance the models share.
Watch Out For
Reading a p-value as the probability you are wrong
"p = 0.03, so there is a 3% chance this is a fluke" is the single most common statistical error in technical writing, and it inverts the conditional.
is not . Converting between them requires a prior on how likely the effect was to begin with, and when that prior is low the difference is enormous. If only 10% of your candidate improvements are genuinely real — a realistic rate for hyperparameter tweaks — then among results significant at with 80% power, roughly a third are false positives. Not 5%.
That is the same base-rate arithmetic as a rare-disease test, and it explains why "significant" findings so often fail to replicate.
Two practical habits. Report the effect size and its confidence interval, not just the p-value: "recall improved 1.5 points, 95% CI [0.2, 2.8]" is a statement a reader can act on, and it also makes clear when a significant result is too small to matter. And treat significance as a filter, not a conclusion — the question after "is it real" is always "is it big enough to be worth the deployment".
Testing repeatedly and stopping when it turns significant
Peeking at results as data accumulates and stopping the moment guarantees a false positive eventually, even with no real effect at all.
The reason is that the p-value fluctuates as data arrives. Check it once and your false-positive rate is the 5% you chose. Check it after every hundred observations and you are running many correlated tests, and the chance of crossing the threshold at some point approaches 1. Continuous monitoring of a fixed-sample test can push the effective false-positive rate above 20%.
This is the most common failure in ML A/B testing, and it does not feel like cheating — it feels like being attentive. The dashboard updates, someone notices the line has crossed, and the experiment is called.
Three correct approaches. Fix the sample size in advance from a power calculation, and do not look until you reach it. Use a sequential test designed for continuous monitoring — group sequential methods with alpha spending, or a Bayesian approach where updating as data arrives is legitimate by construction. Or use always-valid confidence sequences, which several experimentation platforms now implement for exactly this reason.
The related trap is the garden of forking paths: trying several metrics, segments, or windows and reporting the one that reached significance. That is multiple comparisons whether or not anyone counted the tests.
The Quick Version
- State and before looking at the data. Then compute a statistic, a p-value, and decide.
- . Not , not the chance of a fluke, not an effect size.
- Type I is a false positive at rate ; Type II is a false negative at rate . Power is .
- Lowering costs power. Only more data improves both.
- "Not significant" is not "no difference." An underpowered test has measured nothing.
- is never accepted, only not rejected.
- A confidence interval is strictly more informative: it shows both significance and effect size.
- Same test set → McNemar's test, which is far more powerful than treating the samples as independent.
- Permutation tests need no distributional assumption and are always available.
- 87% vs 89% on 1,000 examples gives , — one experiment in six by chance.
- Never peek and stop at significance. Fix in advance, or use a sequential method built for it.
What to Read Next
- Confidence Intervals is the same arithmetic, reported in a more useful form.
- Multiple Comparisons is what happens when you run more than one of these.
- Central Limit Theorem supplies the normal approximation the z-test uses.
- Bootstrap and Resampling tests statistics that have no formula.
- Bayes' Theorem is what converting a p-value into a belief actually requires.
- Statistical Power and Sample Size is the type II error rate turned into a number you compute before running the test.
- Definitions worth a look: Normal Distribution, Standard Deviation, and Random Variable.
- Related interview question: What is maximum likelihood estimation?