Your model scored 84.3%; the baseline scored 83.9%. Your teammate says ship it. Is that a real improvement, or is it a coin flip dressed up as a decision? This lesson is the statistical machinery that separates signal from the jitter of a finite test set — standard errors, confidence intervals, the bootstrap, paired tests, power, and the multiplicity traps that manufacture fake wins.
You just spent two weeks fine-tuning a question-answering model. You run it against your 1,000-question benchmark: 84.3%. The old production model scored 83.9% on the same kind of benchmark. A colleague glances at the dashboard, sees the bigger number, and says the two words that start most bad launches: "ship it."
Everyone's instinct is the same — higher number, better model. The whole point of this chapter is to break that instinct, gently and permanently. Because that 0.4-point "win" might be a real improvement, or it might be nothing at all: the statistical equivalent of one basketball team beating another by a single free throw and declaring itself the better franchise.
Here is the crack in the reasoning. Your test set is a sample — a finite handful drawn from an effectively infinite universe of possible questions of the same kind. You did not test the model on every question a user could ever ask; you tested it on the 1,000 you happened to collect. If you had drawn a different 1,000 questions of the same difficulty, both scores would have come out different. The only question that matters is: would they have moved by more than 0.4 points?
Picture ten parallel universes. In each, your team collected its own random 1,000-question test set from the same source. In each universe you run both models and record the two accuracies. In our universe A won by 0.4. But in universe 3 maybe B won by 0.6; in universe 7 they tied; in universe 9 A won by 1.5. The 0.4-point gap flips sign in several universes — and that, precisely, is what people mean when they wave their hands and say "noise."
Let us name two ideas informally now, and formalize them across the lesson. Sampling error is the amount a score jumps around simply because the test set is finite — the width of the spread across those parallel universes. And the comparison that actually decides anything is delta versus noise: a gap only means something when it is large compared to that jitter.
Here is a back-of-envelope number to hold in your head (we derive it properly in Chapter 1). For a single accuracy near 84% measured on N = 1,000 examples, the jitter is about ±1.2 points. That is three times larger than the 0.4-point gap we got excited about. The gap is a pebble; the noise is a boulder rolling past it.
This is not a beginner's mistake that professionals outgrow. Teams have reverted genuinely-better models and shipped genuinely-worse ones because nobody asked "is this delta bigger than the noise?" It is common enough at the frontier that in November 2024 a researcher at Anthropic published a whole methodology note, Adding Error Bars to Evals, precisely because top labs kept making this exact error on their own benchmarks.
Play with the simulation below before reading on. Both dots are drawn from a fixed truth: model A has 84.3% real skill, baseline B has 83.9%. Each "Redraw test set" resamples a fresh N-question exam and replots both observed accuracies, keeping a fading trail of past draws. Watch the two clouds — at small N they overlap almost completely, and the worse model wins constantly. Drag N up and watch the clouds slowly pull apart.
Two truths: A = 84.3% and B = 83.9%. Each redraw resamples an N-example test set and plots both observed accuracies as dots on the score axis, leaving ghost trails. The tally counts how often the worse model (B) won. At N=100 the clouds are enormous; at N=10,000 they separate.
Now let us put arithmetic under what your eyes just saw. Treat the two scores as coming from independent test sets of the same kind, so the question is purely: is the 0.4-point gap distinguishable from noise?
Setup. Model A: 843/1000 = 84.3%. Baseline B: 839/1000 = 83.9%. Different random test sets of the same kind, so treat them as independent. Is the gap distinguishable from noise?
Step 1 — jitter of A's score. The formula (derived next chapter) is SE = √(p(1−p)/N).
SEA = √(0.843 × 0.157 / 1000) = √(0.132351 / 1000) = √(0.000132351) = 0.011504 — about 1.15 points.
Step 2 — jitter of B's score.
SEB = √(0.839 × 0.161 / 1000) = √(0.000135079) = 0.011622 — about 1.16 points.
Step 3 — jitter of the DIFFERENCE. Independent variances add, so the SE of A−B is the square root of the sum of the two variances (not the sum of the SEs):
SEdiff = √(SEA² + SEB²) = √(0.000132351 + 0.000135079) = √(0.000267430) = 0.016353 — about 1.64 points.
Step 4 — compare the gap to the jitter. A standardized score, or z, is "how many noise units is the gap?":
z = (0.843 − 0.839) / 0.016353 = 0.004 / 0.016353 = 0.245. The gap is a quarter of one noise unit.
Step 5 — the p-value. The two-sided p-value is the probability of seeing a gap at least this large if the models were truly equal:
p = 2 × P(Z > 0.245) = 0.807. If A and B were exactly equally good, you'd see a gap this big 81% of the time. This result says nothing.
Let us reproduce that in code — first the "parallel universes" by brute force, then the one-line formula, and confirm they agree.
python — the parallel universes, by simulation import numpy as np rng = np.random.default_rng(0) N = 1000 draws = 10000 # 10,000 parallel universes # each universe: a fresh N-question exam for each model accA = rng.binomial(N, 0.843, draws) / N accB = rng.binomial(N, 0.839, draws) / N print("baseline B wins:", np.mean(accB > accA).round(3)) # -> ~0.40 : the WORSE model tops the chart ~40% of the time
python — the one-line formula (same answer) import numpy as np from scipy import stats se_diff = np.sqrt(0.843*0.157/1000 + 0.839*0.161/1000) p = 2 * stats.norm.sf(0.004 / se_diff) print(round(se_diff,6), round(p,3)) # 0.016353 0.807
Notice the two agree: the simulation's "40% of the time the worse model wins" and the formula's "p = 0.807 that a gap this big is pure luck" are two views of the same fact. The gap is inside the noise.
Here is the arc of the lesson. First we quantify the jitter (the standard error, Ch1). Then we put honest brackets on a single score (confidence intervals, Ch2), and get brackets for any metric — F1, BLEU, win rate — with no formula (the bootstrap, Ch3). We learn to compare two models properly (paired tests, Ch4), decide how big a test set we even need (power, Ch5), and survive the temptation of trying twenty configs (multiplicity, Ch6). Chapter 7 collides all of it in a live leaderboard simulator that lies in real time, and Chapter 8 splits the noise itself into its sources.
To decide anything, we first need a number for that jitter. Where does ±1.2 points come from — and why does quadrupling the test set only halve it? That is Chapter 1.
We watched the score jitter with our own eyes. Now we put a number on the jitter — and derive, from scratch, why quadrupling your test set only halves it. This one formula is the workhorse of the entire lesson.
Reframe the eval as a sequence of coin flips. On each test example the model is either right (1) or wrong (0). The accuracy is simply the average of those N zero-or-one outcomes. That single reframing is the whole trick: accuracy is a sum of random things, and sums of random things have completely understood behavior.
Before we go further, separate two quantities people constantly confuse. The estimand is p, the model's TRUE accuracy over the whole distribution of possible questions — a number you can never observe. The estimate is p-hat (written p̂), the accuracy you actually measured on your N questions — the number on your dashboard. The estimate wobbles around the estimand. Statistics is the discipline of saying how much.
Derive the variance of a single flip from scratch. Let X be 1 with probability p and 0 otherwise — a Bernoulli variable. Its mean is E[X] = p. Its variance is E[X²] − (E[X])². Because X is 0 or 1, X² = X, so E[X²] = p. Therefore the variance is p − p² = p(1−p).
Plug in a real number to feel it: at p = 0.85, the per-flip variance is 0.85 − 0.7225 = 0.1275. Notice this quantity peaks at p = 0.5 (a 50/50 coin is maximally uncertain) and shrinks toward 0 as p approaches 0 or 1 (a model that is right 99% of the time barely varies). Uncertainty is largest exactly when the model is most undecided.
Now average N independent flips. Two facts do all the work. First, variances of independent variables add: the variance of the sum of N flips is N × p(1−p). Second, dividing a quantity by a constant c divides its variance by c². We divide the sum by N to get the average, so we divide the variance by N². Net: Var(p̂) = N p(1−p) / N² = p(1−p)/N.
The standard error — the typical distance p-hat lands from p — is the square root of that variance:
Stare at the N under the square root, because it governs the economics of every eval you will ever run. Because N sits inside a square root, precision improves only as 1/√N. To halve your error bar you must quadruple your test set. To get 10× the precision you need 100× the examples — and 100× the labeling or inference cost. This single fact is why eval precision is genuinely expensive, and why Chapter 8's compute budgeting matters so much.
Why is the sampling distribution shaped like a bell? The Central Limit Theorem says that for a reasonable N, the average of many independent things is approximately normal — a bell curve centered at the true p with width SE. That is why the "±2 SE covers about 95%" rule works. Hold onto the word approximately: in Chapter 2 we will watch this approximation break when p is extreme or N is small.
Plug in real scales to build muscle memory. At 85% accuracy: SE is about ±1.6 points at N=500, ±1.1 at N=1000, ±0.5 at N=5000. Look at that first number and reread the last three papers you saw claim an "improvement." Many of those deltas are smaller than the ±1.6 that a 500-example benchmark simply cannot resolve.
One caveat that sets up several later chapters: this formula assumes the examples are independent draws. If your benchmark has ten questions all about the same document, those ten share a fate — get the document wrong and you likely miss all ten. That clustering inflates the true variance beyond p(1−p)/N. Anthropic's November 2024 error-bars note makes exactly this correction with clustered standard errors; the mental model is that your effective N is smaller than your nominal N.
Left: a stream of 0/1 outcomes filling one test set (green=correct, red=wrong). Right: a histogram of observed accuracies across many simulated test sets, with the theoretical bell √(p(1−p)/N) overlaid and the ±1 SE / ±2 SE bands shaded. Hit "2× N" and watch the histogram narrow by only √2 — the 1/√N law made tactile.
Setup. Your model got 425/500 = 85% on a 500-example benchmark. Compute the standard error by hand, then see what N=2000 would buy.
Step 1 — per-example variance. p(1−p) = 0.85 × 0.15 = 0.1275.
Step 2 — variance of the average. Divide by N: 0.1275 / 500 = 0.000255.
Step 3 — standard error. SE = √(0.000255) = 0.015969 — about ±1.60 points.
Step 4 — rough 95% range. p̂ ± 1.96 SE = 0.85 ± 1.96 × 0.015969 = 0.85 ± 0.0313 → roughly 81.9% to 88.1%. Your "85%" is honestly "85 ± 3".
Step 5 — quadruple to N=2000. SE = √(0.1275/2000) = √(0.00006375) = 0.007984 — exactly HALF of 0.015969. Four times the data, twice the precision. The 1/√N law in the flesh: 0.015969 / 0.007984 = 2.0.
python — from scratch, every intermediate import numpy as np outcomes = np.array([1]*425 + [0]*75) # 425 right, 75 wrong N = outcomes.size phat = outcomes.mean() # 0.85 var1 = phat * (1 - phat) # 0.1275 per-flip variance var_mean = var1 / N # 0.000255 se = var_mean ** 0.5 # 0.015969 print(phat, var1, round(se,6), round(1.96*se,4)) # Monte-Carlo check: empirical std of accuracies should match the formula rng = np.random.default_rng(0) sims = rng.binomial(N, phat, 100000) / N print("empirical SE:", round(sims.std(),6)) # ~0.0160
python — the library one-liner from scipy import stats print(stats.sem(outcomes)) # standard error of the mean # or the proportion shortcut: math.sqrt(p*(1-p)/N)
We can now quantify the jitter of a single score. The next step is to wrap it into the thing every eval report should show and almost none do: an honest interval.
We have a point score and a noise scale. Combine them and you get the single most under-used artifact in evaluation: the interval. A number without an interval is a claim without evidence attached; a number with one tells the reader how far to trust it.
Define a confidence interval (CI) by its GUARANTEE, not its formula. A 95% CI is a procedure: if you reran the whole experiment — drew a fresh test set — 100 times and built the interval each time, about 95 of those 100 intervals would contain the true p. The interval is the random thing; the truth is fixed. This is subtle and worth slowing down for.
The standard recipe, called the Wald or normal-approximation interval, is p̂ ± 1.96 × √(p̂(1−p̂)/N). Where does 1.96 come from? It is the point on the bell curve that leaves 2.5% of the area in each tail, so 95% sits in the middle. In your head, "2" is a fine shortcut for 1.96.
Now the reading that separates careful practitioners from casual ones. The RIGHT reading: "the procedure that produced this interval captures the truth 95% of the time." The WRONG reading: "there is a 95% probability the true accuracy is inside THIS interval." The distinction sounds pedantic — until you notice that a fixed interval either contains the fixed truth or it does not; there is no probability left once both are fixed. The 95% describes the method, across many hypothetical reruns.
Here is where the normal approximation quietly breaks: small N, or p near 0 or 1 — which is exactly the regime of the most interesting modern evals. A hard agentic benchmark where a model solves 17 of 20 tasks. A safety eval where the violation rate hovers near zero. In these corners the Wald recipe produces nonsense. Concretely, the Wald interval for 17/20 pokes above 100% accuracy — a bracket that includes impossible values.
The Wilson interval fixes this by asking a smarter question. Instead of approximating a bell curve around p-hat, it asks: "which true values of p would make my observation unsurprising?" — it inverts the test. The result has two beautiful properties. The center shrinks toward 0.5 (a built-in skepticism about extreme small-sample scores), and the width accounts for uncertainty in the variance itself. It stays inside [0, 1] by construction.
The rule of thumb for practice: Wald is fine when both N p̂ and N(1−p̂) exceed about 10; below that, or any time the interval nears 0 or 1, reach for Wilson. If you need guaranteed conservative coverage — a regulated safety claim — use Clopper-Pearson, which is wider but never under-covers. Wilson is the default in statsmodels and most eval libraries precisely because it behaves well across the whole range.
This is not academic hygiene — the field moved. OpenAI's SimpleQA-style and swe-bench-style reports and Anthropic's error-bars methodology (Nov 2024) publish CIs; when HuggingFace relaunched the Open LLM Leaderboard v2 in June 2024 it re-ran and normalized scores explicitly because small-N subscores were being over-read as rankings.
100 repeated experiments. Each row draws a fresh test set from a hidden true p (the gold line), builds a CI, and paints it green if it captures the line, red if it misses. Set p=0.95, n=20 and toggle Wald → Wilson: Wald misses far more than 5% of the time (and spills past 100%) while Wilson holds near 95%.
Setup. Your agent solved 17 of 20 hard tasks (p̂ = 0.85). Build the 95% Wald interval, watch it break, then build Wilson by hand.
Step 1 — Wald. SE = √(0.85×0.15/20) = √(0.006375) = 0.079844. Interval: 0.85 ± 1.96×0.079844 = 0.85 ± 0.156494 → [0.6935, 1.0065]. The upper end is 100.6% accuracy — impossible. The approximation has failed.
Step 2 — Wilson ingredients. z = 1.96, so z² = 3.8416; n = 20; z²/(2n) = 3.8416/40 = 0.096040; z²/n = 0.192080; z²/(4n²) = 3.8416/1600 = 0.002401.
Step 3 — Wilson center. (p̂ + z²/(2n)) / (1 + z²/n) = (0.85 + 0.096040) / 1.192080 = 0.946040 / 1.192080 = 0.793604. It moved from 0.85 toward 0.5 — built-in small-sample skepticism.
Step 4 — Wilson half-width. numerator = z × √(p̂(1−p̂)/n + z²/(4n²)) = 1.96 × √(0.006375 + 0.002401) = 1.96 × √(0.008776) = 1.96 × 0.093680 = 0.183613. Divide by the same 1.192080: half-width = 0.154028.
Step 5 — Wilson interval. 0.793604 ± 0.154028 → [0.6396, 0.9476]. Sane (inside [0,1]), honest (17/20 is compatible with true skill anywhere from 64% to 95%), and pointedly NOT centered on 85%.
python — Wilson from scratch, every intermediate import math def wilson(k, n, z=1.96): phat = k / n zz = z * z center = (phat + zz / (2*n)) / (1 + zz/n) half = z * math.sqrt(phat*(1-phat)/n + zz/(4*n*n)) / (1 + zz/n) return center - half, center + half print(wilson(17, 20)) # (0.6396, 0.9476) # and watch Wald misbehave: phat, n = 0.85, 20 se = math.sqrt(phat*(1-phat)/n) print(phat - 1.96*se, phat + 1.96*se) # 0.6935 1.0065 (>1!)
python — the library one-liner from statsmodels.stats.proportion import proportion_confint print(proportion_confint(17, 20, method='wilson')) # -> (0.6396..., 0.9476...) same answer
Wilson handles proportions beautifully. But your dashboard also shows F1, BLEU, and judge win-rates — metrics with no clean closed-form SE. For those we need a tool that needs no formula at all: the bootstrap.
The formula SE = √(p(1−p)/N) works for accuracy because accuracy is an average of independent 0/1 outcomes. But your dashboard also shows F1, BLEU, and judge win-rates. Where do their error bars come from? Chapter 1's derivation gives no answer — and that is the wall this chapter climbs over.
Why do those metrics resist the formula? F1 is a ratio of sums (2·precision·recall over their sum) — it does not decompose into a per-example average. BLEU is a geometric mean of clipped n-gram precisions multiplied by a brevity penalty, computed over the whole corpus at once. Neither is "the mean of N independent things," so no closed-form standard error exists in general.
The bootstrap idea, in one sentence: we cannot rerun reality to get fresh test sets, but we can fake fresh test sets by resampling our one test set with replacement. Each fake set is N draws from the empirical distribution — our best available stand-in for the true distribution — so the spread of the metric across fake sets mimics its spread across real reruns of the world.
Make "with replacement" concrete. Each resample draws N examples from the original N, but after drawing an example we put it back, so some examples get picked twice or three times and others get picked zero times. On average about 36.8% of the original examples are left out of any given resample — that number is (1−1/N)N, which converges to 1/e as N grows. Every fake universe is a slightly reweighted version of your data.
The recipe, mechanically:
Why does this work, honestly rather than by hand-waving? The fluctuation of the metric across bootstrap resamples mimics its fluctuation across true resamples of the world, because the empirical distribution converges to the true one as data accrues. This is called the plug-in principle — we plug the empirical distribution in where the (unknown) true distribution belongs. It is a principled approximation, not magic.
Be clear about what the bootstrap cannot do. It never invents examples you did not collect. If your 500-example set contains no adversarial phrasings, no bootstrap interval will reflect uncertainty about adversarial phrasings. The bootstrap quantifies sampling noise within your distribution; it is silent about distribution shift. It reveals how little you know from the data you have — not what you failed to collect.
Resample the RIGHT unit. For a QA benchmark, resample questions. For a dialogue benchmark, resample whole conversations, not individual turns — turns within a conversation share user, topic, and context, so they are correlated. For a multi-document eval, resample documents. Resampling below the independence unit pretends you have more independent data than you do, and gives intervals that are too narrow — the same clustering trap from Chapter 1.
You will meet this everywhere in the wild: HELM publishes bootstrapped CIs; machine-translation shared tasks have used paired bootstrap resampling since Koehn (2004); lm-eval-harness exposes a bootstrap standard error for aggregate metrics. And the paired version of the bootstrap returns as our best model-comparison tool in the very next chapter.
Top: the real test set as tiles (correct / wrong). Each crank draws N tiles with replacement — duplicates stack, omitted tiles gray out — computes the resample's accuracy, and drops a dot into the histogram below. The 2.5% and 97.5% markers slide as the histogram fills; the final CI is a bracket.
Setup. A toy test set of n=10 with correctness [1,1,0,1,1,1,0,1,1,0] — observed accuracy 7/10 = 0.70. First three resamples fully by hand, then B=10,000 in code.
Step 0 — real metric. (1+1+0+1+1+1+0+1+1+0)/10 = 7/10 = 0.70.
Step 1 — resample 1. draw indices with replacement [3,0,7,7,2,9,4,1,5,8] → values [1,1,1,1,0,0,1,1,1,1] → sum 8 → mean 0.80. Index 7 appeared twice; index 6 was never drawn.
Step 2 — resample 2. indices [6,6,1,3,0,2,9,5,4,8] → values [0,0,1,1,1,0,0,1,1,1] → sum 6 → mean 0.60. Index 6 (a wrong answer) appeared twice — a pessimistic universe.
Step 3 — resample 3. indices [0,1,2,3,4,5,6,7,8,9] (every example once, rare but legal) → mean 0.70, the original.
Step 4 — the fake universes. Three gave 0.80, 0.60, 0.70. Repeat 10,000 times (seeded): the 2.5th percentile is 0.40, the 97.5th is 1.00.
Step 5 — read it. The 95% percentile bootstrap CI is [0.40, 1.00]. Ten examples tell you almost nothing — which the bootstrap reports honestly, echoing Chapter 2's small-N warning. Sanity check: bootstrap std ≈ 0.144 matches the formula √(0.7×0.3/10) = 0.145.
python — a 12-line bootstrap, any metric import numpy as np, math data = np.array([1,1,0,1,1,1,0,1,1,0]) print(data.mean()) # 0.70 for idx in ([3,0,7,7,2,9,4,1,5,8], [6,6,1,3,0,2,9,5,4,8], [0,1,2,3,4,5,6,7,8,9]): print(data[np.array(idx)].mean(), end=' ') # 0.8 0.6 0.7 print() rng = np.random.default_rng(42) boots = np.array([rng.choice(data, size=10, replace=True).mean() for _ in range(10000)]) print(np.percentile(boots,2.5), np.percentile(boots,97.5), round(boots.std(),3), round(math.sqrt(0.7*0.3/10),3)) # -> 0.4 1.0 0.144 0.145
python — the library one-liner from scipy.stats import bootstrap res = bootstrap((data,), np.mean, n_resamples=10000, confidence_level=0.95, method='percentile') print(res.confidence_interval) # ~ (0.4, 1.0)
The bootstrap gives one model's error bar for any metric. But the question that actually decides launches is a comparison — and there is a magic trick hiding in the fact that both models took the same exam.
Here is a magic trick. The same 2-point gap, the same 1,000 examples — one test says p=0.22, the other says p=0.023. Nothing changed but the method. The difference is simply noticing that both models took the same exam. This chapter is that trick, fully explained.
First, hypothesis testing honestly, in one beat. The null hypothesis is "the two models are equally accurate." The p-value is the probability of seeing a gap at least this large IF the null were true. It is NOT the probability the null is true, and 0.05 is a social convention, not a law of physics.
The pairing insight. When models A and B answer the SAME questions, most questions come out identical — both right on the easy ones, both wrong on the hard ones. That shared component is pure noise for the comparison: it tells you the questions were easy or hard, not which model is better. Pairing subtracts it out. An unpaired test pays for that shared difficulty twice, once in each model's variance.
Build the 2×2 disagreement table. Label each question by how the two models did: both-correct (a), A-only-correct (b), B-only-correct (c), both-wrong (d). Here is the crucial fact — the entire comparison lives in b versus c, the discordant cells. The a and d cells, however enormous, contain zero information about which model is better.
| B correct | B wrong | |
|---|---|---|
| A correct | a = both right | b = A only |
| A wrong | c = B only | d = both wrong |
McNemar's test from scratch. Under the null, each discordant example is a fair coin — equally likely to favor A or B — so b should be about (b+c)/2. The statistic (|b−c|−1)² / (b+c) measures how far the observed split is from fair, and it follows a chi-square (χ²) distribution with 1 degree of freedom. The "−1" is a continuity correction — a small adjustment for using a smooth curve to approximate discrete counts. When b+c is small, skip the approximation entirely and use the exact binomial tail.
Now run the magic trick with numbers. 1,000 examples: a=810, b=45, c=25, d=120. Marginals: A = 85.5%, B = 83.5%. The unpaired two-proportion z-test gives z=1.24, p=0.216 — "not significant." McNemar on the very same data: χ²=5.16, p=0.023 — significant. Same data, same 2-point gap. The paired test simply refuses to be distracted by the 930 agreements.
The paired bootstrap generalizes this beyond accuracy. Resample QUESTION INDICES (with replacement), recompute BOTH models' metrics on the same resampled indices, and record the delta. The percentile CI of those deltas — or the fraction of resamples where the delta flips sign — is your answer. It works for F1, BLEU, win rate, anything. The key is resampling the same indices for both systems, so the shared difficulty cancels.
Practical guidance. Same test set → always pair: McNemar for accuracy on 0/1 outcomes, paired bootstrap for everything else. Different test sets → you are stuck with unpaired, and you need far more data. And always report the CI on the DELTA, not just a p-value star, so readers see effect size and uncertainty in the same glance.
Field note: Anthropic's Adding Error Bars to Evals (Nov 2024) explicitly recommends paired analyses for model comparisons on shared eval sets, and MT/LLM tooling (sacrebleu, lm-eval-harness) implements paired bootstrap for exactly this reason. Judge-based win rates get their own paired treatment in GenAI Eval.
A grid of test questions colored by outcome: both right, both wrong, A-only, B-only. Slide the disagreement counts and watch the unpaired z-test p-value and the McNemar p-value diverge. The agreement dots dim, because they carry no comparison information. Hit "Load worked example" for 810/45/25/120.
Setup. Models A and B on the same 1,000 questions: both correct on 810, A-only on 45, B-only on 25, both wrong on 120. Is A really better?
Step 1 — marginal accuracies. A = (810+45)/1000 = 85.5%; B = (810+25)/1000 = 83.5%. Gap = 2.0 points.
Step 2 — the WRONG (unpaired) test. SEdiff = √(0.855×0.145/1000 + 0.835×0.165/1000) = √(0.000123975 + 0.000137775) = √(0.000261750) = 0.016179. z = 0.020/0.016179 = 1.236 → p = 0.216. Verdict: "no evidence."
Step 3 — the paired view. Only b=45 and c=25 matter. Under the null each of these 70 discordant questions is a fair coin, expected split 35/35.
Step 4 — McNemar statistic. χ² = (|45−25|−1)² / (45+25) = (20−1)² / 70 = 361/70 = 5.157.
Step 5 — p-value from χ²(1 df). p = 0.0232. Cross-check with the exact binomial (is 45-of-70 surprising for a fair coin?): pexact = 2·P(X ≥ 45 | n=70, p=0.5) = 0.0225. Both agree: significant at 0.05.
Step 6 — the moral in numbers. The unpaired noise scale (0.0162) was inflated by the 930 agreement questions; pairing removed them, and the SAME 2-point gap went from p=0.216 to p=0.023 — roughly a 10× drop in p, purely from using the right test.
python — McNemar from scratch, then paired bootstrap import math, numpy as np from scipy import stats b, c = 45, 25 chi2 = (abs(b - c) - 1)**2 / (b + c) # 5.157 p_chi = stats.chi2.sf(chi2, 1) # 0.0232 p_exact = 2 * stats.binom.sf(max(b,c)-1, b+c, 0.5) # 0.0225 print(round(chi2,3), round(p_chi,4), round(p_exact,4)) # paired bootstrap on the delta (works for any metric): rng = np.random.default_rng(0) A = np.array([1]*855 + [0]*145) # stand-in per-example correctness B = np.array([1]*835 + [0]*165) idx = np.arange(1000) deltas = [ (A[s].mean() - B[s].mean()) for s in (rng.choice(idx, 1000, replace=True) for _ in range(2000)) ] print(np.percentile(deltas, [2.5, 97.5])) # CI on the delta
python — the library one-liner from statsmodels.stats.contingency_tables import mcnemar tbl = [[810, 45], [25, 120]] print(mcnemar(tbl, exact=True)) # pvalue ~ 0.0225 # or: scipy.stats.binomtest(45, 70, 0.5)
Pairing wrings the most out of the data you have. But no test can see a gap it is too small to resolve. How many examples do you actually need? That is power — Chapter 5.
Chapter 4 taught you to test properly. But a proper test on too few examples is a microscope with the lens cap on — you will see nothing and conclude "no difference." This chapter is about whether your eval could ever have seen the effect you are hunting.
Two ways a test can fail. A false alarm — declaring a gap that is not there — happens with probability α (alpha), which we set at 0.05 by convention. A miss — failing to detect a gap that IS there — happens with probability β (beta). The power of a test is 1−β: the chance you catch a real effect. Convention aims for 80% or more.
Build the intuition geometrically. Under the null, the observed gap is a bell curve centered at 0 with width SEdiff. Under the alternative (a real gap δ), it is a bell curve centered at δ. Power is an overlap question: how much of the alternative's curve lands beyond the significance threshold. Small δ or big SE → the curves overlap heavily → detection is a coin flip.
Derive the sample-size formula from that picture. To reject reliably, the true gap δ must exceed roughly (zα/2 + zβ) standard errors. Squaring and solving for n gives:
Every symbol earns its place. 1.96 (that is zα/2) buys the 5% false-alarm rate. 0.8416 (zβ) buys 80% detection. The variance sum is the noise. And δ² in the denominator is the killer.
That δ² denominator is the headline of the whole chapter: halving the gap you want to detect QUADRUPLES the required test set. Detecting 5-point gaps is cheap; detecting 1-point gaps costs about 19,000 examples; detecting 0.5-point gaps costs about 78,000. This single fact explains most eval frustration you will ever feel.
Run the flagship numbers (below). To detect 85% vs 86% with 80% power you need about 19,500 examples per model. Now hold that against reality: MMLU's smallest subjects have around 100 questions, GSM8K's test split is 1,319 problems, HumanEval is 164. Per-subject "wins" on such slices are astrology.
Flip the formula into a diagnostic — the minimum detectable effect (MDE): given your N, what is the smallest gap you can see with 80% power? For N=1,000 per model at ~85% accuracy, the MDE is about 4.4 points. Any claimed improvement below your MDE deserves the response: "your eval cannot support that claim."
What underpowered comparisons do to a whole FIELD is worth pausing on. Only lucky-large deltas cross the significance bar, so published and leaderboard gaps are systematically inflated — a phenomenon called the winner's curse — and follow-up reproductions "mysteriously" shrink. This needs no fraud, just small N and selective attention. It sets up Chapter 6 directly.
Practical levers before you buy more data: pairing (Ch4) shrinks the effective noise and can cut required N dramatically; stratifying by difficulty helps; and for generative models, more samples per prompt reduces decode variance (Ch8). Online-experiment power with real users, and CUPED-style variance reduction, live in Experiment Design — same math, different battlefield.
Two bell curves: null at 0, alternative at δ. The area of the alternative past the threshold fills in as POWER. Drag δ and N; watch power climb. The readout tells you the N required for 80% power at the current gap — and how brutally it grows as δ shrinks.
Setup. How many test examples per model to detect 85% vs 86% (one point) with two-sided α=0.05 and 80% power?
Step 1 — the two z multipliers. zα/2 = 1.9600 (5% false alarm, two-sided), zβ = 0.8416 (80% power). Sum = 2.8016; squared = 7.8490.
Step 2 — the noise term. p1(1−p1) + p2(1−p2) = 0.85×0.15 + 0.86×0.14 = 0.1275 + 0.1204 = 0.2479.
Step 3 — the effect term. δ² = (0.86 − 0.85)² = (0.01)² = 0.0001.
Step 4 — assemble. n = 7.8490 × 0.2479 / 0.0001 = 1.94578 / 0.0001 = 19,458 examples per model (exact 19,457.4; round up).
Step 5 — sanity checks. A 2-point gap (δ² = 0.0004) needs n ≈ 4,865 — a quarter, confirming the 1/δ² law. And the MDE at n=1,000: δ = √(7.8490×0.2479/1000) = √(0.0019458) = 0.0441 — you can only see 4.4-point differences.
python — required_n from scratch, with an empirical check import math, numpy as np from scipy import stats def required_n(p1, p2, alpha=0.05, power=0.8): za = stats.norm.ppf(1 - alpha/2) # 1.96 zb = stats.norm.ppf(power) # 0.8416 varsum = p1*(1-p1) + p2*(1-p2) return (za + zb)**2 * varsum / (p2 - p1)**2 n = required_n(0.85, 0.86) print(math.ceil(n)) # 19458 # empirical: simulate at that N, fraction of z-tests that reject ~ 0.80 rng = np.random.default_rng(0); N = math.ceil(n); hits = 0 for _ in range(10000): a = rng.binomial(N, 0.86)/N; bb = rng.binomial(N, 0.85)/N se = math.sqrt(a*(1-a)/N + bb*(1-bb)/N) if abs(a-bb)/se > 1.96: hits += 1 print(hits/10000) # ~0.80
python — the library one-liner from statsmodels.stats.power import NormalIndPower from statsmodels.stats.proportion import proportion_effectsize es = proportion_effectsize(0.86, 0.85) print(NormalIndPower().solve_power(es, power=0.8, alpha=0.05))
Power protects a single comparison. But real teams do not run one test — they sweep twenty configs and report the best. That is a different, sneakier way to fool yourself, and it is Chapter 6.
You tried 20 prompt variants and one beat baseline at p=0.03. Champagne? Before you pop it, compute the probability that pure noise hands you at least one such "win." The answer is going to be uncomfortable.
This is the garden of forking paths, quantified. Each test at α=0.05 has a 5% false-alarm rate. But run 20 independent tests where NOTHING works, and P(at least one false win) = 1 − 0.9520 = 64%. The dashboard almost guarantees a champagne moment. The family of tests, not each individual test, is what needs the 5% guarantee.
Name the everyday versions of this sin, because you commit them without noticing: sweeping 20 hyperparameter configs and reporting the best; evaluating on 15 benchmark subsets and highlighting the 2 that improved; rerunning a flaky eval "to be sure" and keeping the good run; trying 5 judge prompts and picking the favorable one. All are multiple comparisons wearing different clothes.
Bonferroni, the blunt instrument. To keep the FAMILY false-alarm rate at 5% across m tests, require each individual test to clear α/m. For 20 configs: 0.05/20 = 0.0025. It is simple, assumption-free, and it works — but it is brutally conservative when m is large, because it guards against even a single false positive anywhere in the family.
Benjamini-Hochberg (BH), the practical instrument, controls the false discovery rate (FDR) instead: rather than "probably zero false wins," it promises "at most ~5% of my declared wins are false." Recipe: sort the m p-values ascending, find the LARGEST k such that p(k) ≤ (k/m)·α, and reject hypotheses 1..k. Early p-values face a near-Bonferroni bar; later ones face progressively gentler bars — a rising staircase rather than a flat wall.
Seed variance is the same disease in disguise. Train the identical config with 5 random seeds and scores land at [84.1, 83.5, 84.8, 83.9, 84.4] — mean 84.14, std 0.49. Reporting your best seed (84.8) against a rival's mean is benchmark shopping against yourself. The honest unit of comparison is mean ± std over seeds. "Quantifying Variance in Evaluation Benchmarks" (Madaan et al., June 2024) measured exactly this and found seed noise swamps many claimed gaps.
The field-scale consequence, why leaderboard tops are inflated: hundreds of teams submit to the same benchmark, and the top slot is selected as the MAX over many noisy scores, so it is biased upward even if everyone is honest — the winner's curse from Chapter 5, now at ecosystem scale. The Leaderboard Illusion study (April 2025) documented the industrial version: labs privately testing many Chatbot Arena variants and surfacing only the best.
Defenses that actually work: preregister the ONE primary metric and test before running the sweep (demote everything else to exploratory); hold out a confirmation set the sweep never touches and validate the single winner on it once; apply BH to any screen you do report; and always report seed mean±std. The release-gate mechanics that operationalize this live in Regression Testing & Quality Gates.
m config cards, each secretly having ZERO true improvement. Each runs a simulated eval and shows a p-value; cards crossing the current threshold light up gold with "WINNER." Uncorrected mode almost always crowns fake winners. Switch to Bonferroni or BH and watch the bar rise (BH as a staircase over the sorted p-values) and the fake winners flick off.
Setup. (a) false-win probability for 20 null configs; (b) BH on 10 sorted p-values [0.001, 0.004, 0.012, 0.021, 0.041, 0.09, 0.13, 0.22, 0.45, 0.78] at α=0.05; (c) seed mean±std for [84.1, 83.5, 84.8, 83.9, 84.4].
Step a — family false-alarm rate. P(at least one of 20 fires under the null) = 1 − (0.95)20 = 1 − 0.3585 = 0.6415. A 64% chance of fake champagne. Bonferroni bar: 0.05/20 = 0.0025 per test.
Step b1 — BH thresholds (k/m · α for k=1..10): 0.005, 0.010, 0.015, 0.020, 0.025, 0.030, 0.035, 0.040, 0.045, 0.050.
Step b2 — compare. 0.001 ≤ 0.005 PASS; 0.004 ≤ 0.010 PASS; 0.012 ≤ 0.015 PASS; 0.021 ≤ 0.020 FAIL; 0.041 ≤ 0.025 FAIL; the rest fail.
Step b3 — step-up. The LARGEST passing k is 3, so reject hypotheses 1–3 — declare exactly 3 wins. Bonferroni (bar 0.005) would declare 2 (0.001, 0.004); naive per-test 0.05 would declare 5.
Step c1 — seed mean. (84.1+83.5+84.8+83.9+84.4)/5 = 420.7/5 = 84.14.
Step c2 — deviations & squares. −0.04, −0.64, +0.66, −0.24, +0.26; squares 0.0016, 0.4096, 0.4356, 0.0576, 0.0676; sum = 0.9720.
Step c3 — sample std (divide by n−1=4). 0.9720/4 = 0.2430; std = √(0.2430) = 0.493. Report "84.14 ± 0.49 over 5 seeds" — and note this seed noise alone is bigger than Chapter 0's 0.4-point "win."
python — Benjamini-Hochberg from scratch + seed report import numpy as np print(round(1 - 0.95**20, 4)) # 0.6415 def benjamini_hochberg(ps, alpha=0.05): order = np.argsort(ps) m = len(ps) passing = [i+1 for i, j in enumerate(order) if ps[j] <= (i+1)/m * alpha] kmax = max(passing) if passing else 0 return set(order[:kmax]) # indices of rejections ps = [0.001,0.004,0.012,0.021,0.041,0.09,0.13,0.22,0.45,0.78] print(len(benjamini_hochberg(ps))) # 3 seeds = np.array([84.1,83.5,84.8,83.9,84.4]) print(round(seeds.mean(),2), round(seeds.std(ddof=1),3)) # 84.14 0.493
python — the library one-liner from statsmodels.stats.multitest import multipletests reject, p_adj, _, _ = multipletests(ps, alpha=0.05, method='fdr_bh') print(reject.sum()) # 3
Every trap so far — jitter, weak tests, underpowered slices, selection bias — converges on one artifact everyone trusts and almost nobody error-bars: the leaderboard. Let's watch one lie in real time.
Everything from Chapters 1–6 collides here, in the single artifact your whole industry stares at and almost never error-bars: the leaderboard. This is the showcase. YOU control the truth — the models' real skills — and you get to watch the rankings lie.
Set the scene: 8 models. In the default preset, all eight have IDENTICAL true accuracy 85%. Every "Run eval" draws a fresh N-example test set per model and ranks them. The podium reshuffles every single run. Gold medals are being awarded by a random number generator, and the medal ceremony looks exactly as authoritative either way.
Why are ranks noisier than scores? A rank is an order statistic — a max/ordering over several noisy quantities. Each pairwise ordering flips whenever the score gap between two models is small relative to SEdiff, and with 8 models there are 28 pairs, so SOME pair flips almost every run. The rank-1 slot is claimed by whoever got luckiest — the winner's curse from Chapter 5, made visible.
The CI-overlap lens makes it honest. Turn on confidence intervals and at N=500 all eight bars overlap heavily, saying "these are statistically indistinguishable" — yet the RANK column keeps confidently printing 1 through 8. The number that looks most authoritative (the rank) carries the least statistical support on the page.
Play the three sliders as three lessons. (1) N drives everything — churn melts as N grows (Chapter 1's law). (2) More models = more churn at fixed gaps (Chapter 6's multiplicity). (3) The true-gap slider shows the detectability threshold — gaps below about 2 SEdiff churn, gaps above lock in (Chapter 5's power).
Now the real world, 2024–2025. The Leaderboard Illusion study (April 2025) showed Chatbot Arena's dynamics rewarded labs that privately tested many variants and surfaced the best — Chapter 6's max-selection, industrialized. The Llama 4 Maverick incident (April 2025) — the Arena-submitted variant differed from the released model — showed rank-optimizing pressure in action. And GSM1k (May 2024) showed several models dropping many points on freshly rewritten test items: some leaderboard positions were measuring memorization, a bias no error bar catches.
What an honest leaderboard looks like: a score with a CI per model; ranks only where separations are significant (cluster ties into bands); paired tests on shared test sets for adjacent entries; multi-run averages for stochastic decoding. Some of this exists — HELM ships CIs; the Open LLM Leaderboard aggregates carefully — but rank-1 headlines still ignore all of it.
Carry three questions into every leaderboard you ever read: how big is N, how big are the gaps in SE units, and how many models or configs were tried? You now own the math behind all three.
Left: a live leaderboard — medal ranks, score bars with optional CI whiskers, and a gold "true skill" tick per model. Each run resamples every test set and re-ranks. Right: a rank-history strip painting each model's rank over recent runs — identical-skill models leave a churning noise pattern; separated models leave clean bands. Readouts: rank churn and P(true-best ranked #1). To break it: set the gap to 0, crank down N, and watch the podium become a slot machine.
Setup. Model A has TRUE accuracy 86%, model B has TRUE accuracy 85%. Each is evaluated on its OWN independent N=500 test set. How often does the leaderboard put the WORSE model (B) on top?
Step 1 — variance of each observed score. A: 0.86×0.14/500 = 0.1204/500 = 0.0002408. B: 0.85×0.15/500 = 0.1275/500 = 0.000255.
Step 2 — SE of the observed difference (independent). SEdiff = √(0.0002408 + 0.000255) = √(0.0004958) = 0.022267 — about 2.2 points of noise on a 1-point true gap.
Step 3 — the flip probability. B tops A when (B̂ − Â) > 0. That difference is approximately normal with mean −0.01 and SD 0.022267, so P(flip) = Φ(−0.01/0.022267) = Φ(−0.449) = 0.327.
Step 4 — read it out loud. The worse model wins the leaderboard roughly 1 time in 3. A coin flip is 1 in 2 — this "benchmark comparison" is barely better than flipping a coin.
Step 5 — the N cure. At N=5,000, SEdiff = √(0.86×0.14/5000 + 0.85×0.15/5000) = √(0.00004958) = 0.007041, so P(flip) = Φ(−0.01/0.007041) = Φ(−1.420) = 0.078. Ten times the eval data takes the lie from 33% to 8% — and a Monte Carlo of 100k simulated runs confirms it (~0.328 at N=500).
python — the leaderboard simulator + flip probability import math, numpy as np from scipy import stats for N in (500, 5000): sed = math.sqrt(0.86*0.14/N + 0.85*0.15/N) print(N, round(sed,6), round(stats.norm.cdf(-0.01/sed),3)) # 500 0.022267 0.327 | 5000 0.007041 0.078 rng = np.random.default_rng(0) a = rng.binomial(500, 0.86, 100000)/500 b = rng.binomial(500, 0.85, 100000)/500 print(round(np.mean(b > a) + 0.5*np.mean(b == a), 3)) # ~0.328
The leaderboard treated "noise" as a single blob. But rerun the same model on the same test set with a different seed or temperature and the score still moves. Time to split the noise into its parts.
So far "noise" meant test-set sampling. But rerun the SAME model on the SAME test set with a different training seed or a different temperature and the score moves anyway. The single blob called "noise" is actually a stack of independent sources, and the compute budget should attack the biggest one — not the most convenient one.
Enumerate the sources an LLM eval actually has. (1) Training-seed variance — retrain the same config, get a different model (weight init, data order, dropout). (2) Test-set sampling variance — Chapter 1's √(p(1−p)/N). (3) Decode-sampling variance — temperature > 0 makes the SAME model give different answers per run. Plus honorable mentions: prompt-template sensitivity, quantization/hardware nondeterminism, and judge variance for LLM-judged metrics.
The key mathematical fact that makes this tractable: independent noise sources add in VARIANCE, not in standard deviation. Total variance = varseed + vartest + vardecode; total std = √ of the sum. The consequence is huge: the total is dominated by the largest term, and shrinking a small term is nearly invisible. The square-root arithmetic makes "fixing the wrong noise" provably futile.
How to MEASURE each term with a designed grid: train S seeds; for each seed run the eval R times with sampling on. Variance across seeds (of per-seed means) estimates seed noise; variance across repeats within a seed estimates decode noise; the analytic p(1−p)/N estimates test-set noise. This is a one-way random-effects ANOVA (analysis of variance) wearing ML clothes — Madaan et al. (June 2024) ran exactly this design across popular benchmarks and found seed variance rivals or exceeds many published deltas.
Averaging attacks each term differently — this is the budgeting lever. Averaging over s seeds divides varseed by s (test/decode untouched). Enlarging the test set k-fold divides vartest by k. Averaging r decode runs (or r samples per prompt) divides vardecode by r. And each remedy has a different unit price: a seed costs a full training run; test examples cost labeling; decode repeats cost only inference.
The temptation of a deterministic eval and its price: temperature=0 sets vardecode to zero and makes evals reproducible — but greedy decoding measures a slightly different quantity than the sampled behavior users actually experience, and single-sample pass@1 at t=0 can misrank models whose sampled distributions differ. The sensible middle ground is sampling multiple generations and averaging (the pass@k machinery itself belongs to GenAI Eval).
Reporting discipline that follows directly: state WHICH sources your error bar includes. "acc 84.1 ± 0.5 (test-set CI, single seed, t=0)" and "acc 84.1 ± 1.2 (3 seeds × 5 decode runs, all sources)" are different claims — most papers publish the first and implicitly defend the second. Regression gates need the ALL-sources number or they will fire on seed noise (Regression Testing owns that machinery).
Left: a stacked bar of the three variance components (seed / test-set / decode), with the TOTAL error bar above as a bracket whose width is the √ of the stack. A wallet (100 units) with three buys: +1 seed (cost 30), +500 test (cost 10), +1 decode repeat (cost 2). Purchases shrink their component live; the ghost bracket remembers your starting width. "Auto-optimize" greedily spends the wallet — and usually ignores seeds.
Setup. seed-to-seed std 0.6 points, test-set SE 1.0 point, decode-sampling std 0.4 points (independent). Compute the total error bar, then compare averaging 3 seeds vs quadrupling the test set.
Step 1 — convert every std to a variance. seed 0.6² = 0.36; test 1.0² = 1.00; decode 0.4² = 0.16.
Step 2 — total variance (independent sources add). 0.36 + 1.00 + 0.16 = 1.52. Total std = √(1.52) = 1.233 points. Note it is NOT 0.6+1.0+0.4 = 2.0 — stds don't add.
Step 3 — upgrade A: average 3 seeds (three full training runs). seed variance → 0.36/3 = 0.12. New total = √(0.12 + 1.00 + 0.16) = √(1.28) = 1.131. Three expensive retrainings bought 0.10 points of precision.
Step 4 — upgrade B: quadruple the test set (labeling/inference only). test variance → 1.00/4 = 0.25. New total = √(0.36 + 0.25 + 0.16) = √(0.77) = 0.877. A 0.36-point improvement — 3.5× the gain of upgrade A, at a fraction of the cost.
Step 5 — the rule this proves. Compare the COMPONENTS first (1.00 dominates 0.36 dominates 0.16), attack the largest. Even eliminating decode noise entirely (0.16 → 0) only moves the total from 1.233 to √(1.36) = 1.166.
python — variance decomposition, four scenarios import math vs, vt, vd = 0.36, 1.00, 0.16 # seed, test, decode variances print(round(math.sqrt(vs + vt + vd), 3)) # 1.233 — all sources print(round(math.sqrt(vs/3 + vt + vd), 3)) # 1.131 — average 3 seeds print(round(math.sqrt(vs + vt/4 + vd), 3)) # 0.877 — 4x test set print(round(math.sqrt(vs + vt), 3)) # 1.166 — kill decode entirely
python — recover the components from a designed S×R grid import numpy as np rng = np.random.default_rng(0) S, R = 8, 6 # seeds, decode repeats per seed seed_eff = rng.normal(0, 0.6, S) # true seed std 0.6 grid = seed_eff[:, None] + rng.normal(0, 0.4, (S, R)) seed_means = grid.mean(axis=1) print("seed var est:", round(seed_means.var(ddof=1), 3)) # ~ 0.36 + small print("decode var est:", round(grid.var(axis=1, ddof=1).mean(), 3)) # ~ 0.16
You now own the full toolkit. The next chapter drills it into interview reflexes; the last files everything into one cheat sheet and points to where each thread continues.
Eval-statistics questions are staff-interview gold because they expose whether a candidate reasons about uncertainty or just reads dashboards. This chapter drills the reflexes and then hands you ten worked Q&A and three debates to argue both sides of.
The meta-skill interviewers probe: given any eval claim, instinctively locate N, the noise scale, the pairing structure, and the number of comparisons tried. Rehearse the 10-second triage — "delta vs 2×SEdiff, paired or not, how many shots were taken?"
Reflex 1 — the back-of-envelope SE. Memorize SE = √(p(1−p)/N) and its worst case √(0.25/N) = 0.5/√N. Instant anchors: N=100 → ±5pt; N=1,000 → ±1.6pt; N=10,000 → ±0.5pt. Producing these without a calculator is the single highest-value move in this topic.
Reflex 2 — the pairing question. "Same test set?" If yes, the disagreement cells b and c are everything (McNemar); paired bootstrap handles any metric. Practice re-deriving (|b−c|−1)²/(b+c) from "discordant items are fair coins under the null" — interviewers love the derivation more than the formula.
Reflex 3 — power sizing on a whiteboard. n ≈ 8 × variance-sum / δ² for 80% power at α=0.05 (since (1.96+0.84)² ≈ 7.85 ≈ 8). One-point gap near 85% → n ≈ 8×0.25/0.0001 ≈ 20k. Say the number BEFORE they ask.
Reflex 4 — multiplicity instinct. Hear "we tried k variants" and immediately compute 1−0.95k out loud; know Bonferroni (α/m) as the conservative bar and BH's staircase as the practical one; know the confirmation-set escape hatch.
Reflex 5 — variance accounting. Any single-run claim gets the three-source checklist (seed / test-set / decode); components add in variance; attack the biggest. Anchor: seed noise alone rivals many published gaps (Madaan et al., 2024).
How to structure answers at staff level: state the framework first ("this is a paired comparison with multiplicity"), do ONE crisp calculation, name the correction, then land on a DECISION recommendation. Interviewers grade the landing, not the formula recall.
Flashcards. The canvas deals a scenario ("acc 90%, N=200"); drag the slider to guess the 95% CI half-width; on Reveal the true interval animates in around the score and scores your gut (within 25% = staff-level, within 50% = solid, else recalibrate). Build the 0.5/√N anchors until they're automatic.
Setup. "Our agent hits 90% on a 200-task suite; the old one hit 87%. Real?" Answer it with numbers.
Step 1 — SE of the new score. √(0.9×0.1/200) = √(0.09/200) = √(0.00045) = 0.0212 → ±2.1 points.
Step 2 — noise on the difference (unpaired). √(0.9×0.1/200 + 0.87×0.13/200) = √(0.00045 + 0.0005655) = √(0.0010155) = 0.0319 → ±3.2 points. The observed gap is 3.0 — LESS than one noise unit, far short of the ~2 units needed.
Step 3 — the pairing rescue. If both agents ran the SAME 200 tasks, ask for the disagreement counts. If the split is 14 vs 4 among 18 discordants, the exact binomial gives p = 2·P(X ≥ 14 | n=18, p=0.5) = 0.031 — significant where the unpaired view was not.
Step 4 — the spoken landing. "Unpaired, 90 vs 87 on 200 tasks is one sigma — noise. Give me the paired disagreement table; if it splits like 14–4 it's real (p≈0.03). Otherwise we need roughly 4× the tasks to adjudicate a 3-point gap properly."
python — the eval_triage helper you'd keep in your snippets import math from scipy import stats def eval_triage(k1, n1, k2, n2, discordant=None): p1, p2 = k1/n1, k2/n2 se_diff = math.sqrt(p1*(1-p1)/n1 + p2*(1-p2)/n2) z = (p1 - p2) / se_diff print(f"unpaired: gap={p1-p2:.3f} SE={se_diff:.4f} z={z:.2f} p={2*stats.norm.sf(abs(z)):.3f}") if discordant: b, c = discordant p_ex = 2 * stats.binom.sf(max(b,c)-1, b+c, 0.5) print(f"paired (McNemar exact): {b} vs {c}, p={p_ex:.3f}") eval_triage(180, 200, 174, 200, discordant=(14, 4)) # unpaired: gap=0.030 SE=0.0319 z=0.94 p=0.347 # paired (McNemar exact): 14 vs 4, p=0.031
Expand each for a staff-grade answer and a follow-up the interviewer will actually ask next.
First quantify the noise floor: SE of each score is √(0.84×0.16/1000) ≈ 1.16 points, so the difference has SE ≈ 1.64 points unpaired — the observed 0.4-point gap is a quarter of a noise unit, p ≈ 0.81. Before concluding, I'd check whether both models ran on the SAME 1,000 examples: if so, pairing via McNemar on the disagreement table can be dramatically more powerful and might rescue the comparison. I'd also ask how many candidate models were compared to pick this one — if it's the best of several, selection bias inflates the gap further. Then I'd compute what the eval CAN detect: at N=1,000 the MDE at 80% power is roughly 4 points, so a 0.4-point true improvement is invisible to this suite by construction. Recommendation: either pair and re-test, or accept that shipping requires non-statistical grounds (cost, latency, qualitative wins) and say so explicitly.
Follow-up: If the paired disagreement table shows 52 vs 48, what do you conclude and what do you tell the team? (Answer: b+c=100, split 52/48 is utterly unremarkable for a fair coin — p ≈ 0.76; tell them it's a tie and the suite can't see this gap.)
Each example's correctness is Bernoulli(p): mean p, variance E[X²]−(E[X])² = p − p² = p(1−p). The accuracy is the mean of N independent such variables; independent variances add, and dividing the sum by N divides variance by N², so Var(p̂) = p(1−p)/N and SE = √(p(1−p)/N). Worst case p=0.5 gives SE = 0.5/√N, yielding the anchors: N=100 → ±5 points, N=1,000 → ±1.6 points, N=10,000 → ±0.5 points (and the ±2SE 95% half-widths: 10, 3, 1). The key assumption is independence — clustered questions (same document, same template) violate it and require clustered standard errors, which shrink the effective N. These anchors let me triage any claimed delta in seconds: a gap below one anchor unit is noise until proven otherwise.
Follow-up: How do the anchors change if 10 questions are generated per source document? (Answer: effective N drops toward number-of-documents; if the 10 are highly correlated, treat N as ~N/10, so error bars roughly triple.)
Use Wilson when N is small or p-hat is near 0 or 1 — the practical trigger is N·p̂ or N·(1−p̂) below ~10, and ALWAYS for safety-style rates near zero. The Wald interval approximates around p-hat and uses p-hat's own variance estimate, which fails two ways: it can cross 0 or 1 (17/20 gives an upper bound of 100.6%), and at p̂=0 it collapses to zero width, claiming perfect certainty from finite data. Wilson inverts the hypothesis test instead — it asks which true p values would make the observation unsurprising — producing a center pulled toward 0.5 by a z²/2n term and a width that stays inside [0,1]. For 17/20 it gives [64%, 95%], an honest statement that 20 tasks can't distinguish a 65% agent from a 94% one. For guaranteed conservative coverage (regulatory settings), Clopper-Pearson; otherwise Wilson is the default in statsmodels for good reason.
Follow-up: Your red-team eval shows 0 jailbreaks in 300 attempts. What upper bound do you report? (Answer: the "rule of three" — upper 95% bound ≈ 3/300 = 1%; report "0 observed, but consistent with up to ~1% true rate.")
BLEU has no per-example decomposition — it's a geometric mean of corpus-level clipped n-gram precisions times a brevity penalty — so there's no closed-form SE; the tool is the bootstrap. Resample the corpus's sentences (or documents, if sentences cluster) with replacement B=1,000+ times, recompute corpus BLEU per resample, take the 2.5th/97.5th percentiles. The classic botch is computing sentence-level BLEU and averaging — that's a different, worse-behaved metric, not corpus BLEU with error bars. The second botch is resampling below the independence unit (segments when documents are the correlated unit), which gives intervals that are too narrow. For comparing two MT systems, the standard since Koehn (2004) is the PAIRED bootstrap: resample the same sentence indices for both systems and examine the distribution of the BLEU delta, or count sign flips.
Follow-up: Your bootstrap CI for the BLEU delta is [−0.1, +0.9], mean +0.4. What do you report? (Answer: "+0.4 BLEU, 95% CI [−0.1, +0.9] — not significant; the interval straddles zero, so we can't claim an improvement.")
Both are correct answers to different questions. The unpaired test treats the two accuracies as independent measurements, so its noise term includes the full example-difficulty variance TWICE — but the models took the same exam, so difficulty is shared and cancels in the comparison. McNemar conditions on the agreements (which contain zero information about which model is better) and tests only the discordant split — 45 vs 25 against a fair coin — capturing the true uncertainty of the paired design. Believe McNemar: it matches how the data was actually generated. The general lesson is that pairing is free statistical power whenever it's available (a 10× p-value improvement on identical data here), and the paired bootstrap extends the same idea to any metric. The unpaired test isn't wrong math; it's the right math for an experiment we didn't run.
Follow-up: When would McNemar and the unpaired test roughly agree? (Answer: when the models rarely agree — low a+d, so the shared-difficulty savings vanish — or at very large N where both have ample power.)
n = (zα/2 + zβ)² · (p1(1−p1)+p2(1−p2)) / δ² = (1.96+0.8416)² · (0.1275+0.1204) / 0.0001 ≈ 19,500 examples per model — call it 20k, and note the brutal 1/δ² scaling that makes 0.5-point detection cost ~78k. Three shrinkers: (1) pairing — consecutive versions agree on the vast majority of examples, and a McNemar/paired-bootstrap design's effective noise depends only on the discordant rate, often cutting required N severalfold; (2) variance reduction outside sampling — averaging multiple decode runs and fixing the harness removes non-test-set noise (and stratified sampling by difficulty helps at the margin); (3) relax the target honestly — gate on a 2-point MDE (needing only ~5k) and explicitly declare sub-2-point changes statistically invisible. The anti-pattern is running the underpowered suite anyway and reading non-significance as safety.
Follow-up: Your suite is 2,000 examples and can't grow. What's its MDE? (Answer: δ = √(7.85×0.25/2000) ≈ 0.031 → ~3.1 points; gate only on 3+ point changes, label the rest smoke tests.)
The failure mode is max-selection under multiplicity: with 48 tests, the chance at least one null config reaches p ≤ 0.01 is 1−0.9948 ≈ 38%, so "best of sweep at p=0.01" is weak evidence, and its effect size is inflated by the winner's curse. Bonferroni controls the family-wise error rate by requiring p ≤ 0.05/48 ≈ 0.001 — safe but harsh, and this fails it. Benjamini-Hochberg controls the false discovery rate via the staircase p(k) ≤ k·α/m — more powerful when several configs are truly good, the right choice for screening. What I'd actually do is neither alone: treat the sweep as exploration, then run the ONE winning config against baseline on a held-out confirmation set the sweep never touched, with fresh seeds, as a single preregistered test. If p=0.01 reproduces there, it means what it says. This exploration/confirmation split is cheaper than correcting across 48 tests and produces a cleaner launch narrative.
Follow-up: The confirmation run comes back p=0.06 and the team says "so close — let's just add more data to it." Respond. (Answer: that's optional-stopping p-hacking; either preregister the new N up front or accept the confirmation failed — you can't peek and extend.)
Three dominant sources: training-seed variance (retrain same config → different model), test-set sampling variance (the √(p(1−p)/N) term), and decode-sampling variance (temperature > 0 → different outputs per run); plus prompt-template sensitivity and judge variance where applicable. Measure with a designed grid: S seeds × R decode repeats on the fixed test set — between-seed variance of per-seed means estimates seed, within-seed variance across repeats estimates decode, the analytic formula gives the test term (one-way random-effects ANOVA; Madaan et al. 2024 ran it at scale and found seed noise rivals many published deltas). Because independent variances ADD, total std is the √ of the sum, so the total is dominated by the largest component — the budget rule is to compare components and buy down the biggest: decode repeats are cheapest (inference only), test examples next (labeling), seeds most expensive (full training). Spending on a minority term is provably near-useless: eliminating a 0.4-point component under a 1.0-point one moves the total bar by ~5%.
Follow-up: Leadership wants ONE number per model. What number and footnote? (Answer: report the all-sources mean±std, e.g. "84.1 ± 1.2 (3 seeds × 5 samples, incl. test-set CI)," and footnote which sources it includes.)
Ranks are order statistics of noisy scores: a pairwise ordering flips whenever the true gap is small relative to SEdiff, and a 1-point-worse model tops a 500-example comparison about a third of the time. With many models, SOME adjacent pair flips almost every refresh, so rank churn is the norm even with static skills — and the #1 slot specifically is a max over noise, biased upward. Real incidents show the human amplifiers: The Leaderboard Illusion (April 2025) documented labs privately testing many variants on Chatbot Arena and surfacing only the best — best-of-k selection laundered into a single public rank; the Llama 4 Maverick incident (April 2025) involved an Arena-submitted variant that differed from the released model; GSM1k (May 2024) showed some models dropping double digits on freshly rewritten items, meaning certain rankings partly measured memorization. The trustworthy reading: check N, check whether adjacent CIs overlap, check how many submissions were tried, and treat rank #1 vs #3 as a tie unless a paired test says otherwise.
Follow-up: If you ran a public leaderboard, what submission policy would prevent variant-shopping? (Answer: cap submissions per org, require pre-registration of the exact model, publish CIs and rank-as-ties, and rotate a private held-out slice.)
Everything in the sampling-statistics toolkit conditions on the test distribution being (a) the distribution you care about and (b) genuinely unseen. It cannot detect contamination — GSM1k showed models scoring high on memorized items while dropping sharply on fresh rewrites, with tight CIs around the inflated numbers. It cannot detect distribution shift — a CI on yesterday's traffic says nothing about next month's users. It cannot detect metric invalidity — optimizing a proxy that diverges from the outcome you want (Goodhart), which is metric-design's territory. And it cannot detect harness bugs — a mis-parsed answer format produces precisely repeatable wrong scores. So the mature workflow pairs statistical rigor WITH validity practices: periodic fresh test items, contamination checks, qualitative reads of discordant examples, and metric audits. Precision and validity are orthogonal axes; this lesson maxes out one of them.
Follow-up: Design a cheap quarterly "validity audit" for a benchmark you gate releases on. (Answer: rewrite 50 items from scratch, re-score top models, and flag any that drop > 2× their CI — a contamination smoke test for ~one day of work.)
The reflexes are yours. The last chapter files every tool into a one-screen cheat sheet and shows where each thread continues across the family.
You started with "84.3 vs 83.9 — real or noise?" You can now answer it five different ways. Let's file the tools, draw the decision tree, and point to where each thread continues.
Retell the lesson as one story: a score is a random variable (Ch1) → honest brackets via Wilson (Ch2) → brackets for any metric via bootstrap (Ch3) → comparisons via pairing (Ch4) → test sets sized via power (Ch5) → sweeps disciplined via multiplicity corrections (Ch6) → leaderboards read with rank-skepticism (Ch7) → noise budgeted via decomposition (Ch8). One toolkit, one thread.
Tap a branch to walk from "what are you claiming?" down to the right tool, its formula, and the chapter to revisit. Visited paths stay lit. Hit "Quiz me" to be dealt a random scenario and score whether you tap the right leaf.
Setup. What test-set size guarantees a ±1-point 95% CI for ANY accuracy — the worst case?
Step 1 — worst-case variance. p(1−p) is maximized at p=0.5, giving 0.25. Any other accuracy has SMALLER variance, so sizing for p=0.5 covers all cases.
Step 2 — require half-width 0.01. 1.96 × √(0.25/N) = 0.01.
Step 3 — solve. √(0.25/N) = 0.01/1.96 = 0.005102; square: 0.25/N = 0.00002603; N = 0.25/0.00002603 = 9,604.
Step 4 — the memorable form. N = (1.96/0.01)² × 0.25 = 196² × 0.25 = 38,416 × 0.25 = 9,604. Call it 10k: "ten thousand examples buys a ±1-point guarantee, anywhere on the scale."
Step 5 — scale it. ±2 points → N=2,401 (a quarter); ±0.5 points → N=38,416 (four times). The 1/δ² law, one last time.
| Quantity | Formula | When it breaks / when to use |
|---|---|---|
| Standard error of accuracy | SE = √(p(1−p)/N) | Assumes independent examples; clustered data inflates true variance |
| Worst-case SE (anchor) | 0.5/√N | N=100→±5pt, 1k→±1.6pt, 10k→±0.5pt |
| Wald CI | p̂ ± 1.96·SE | Only when N·p̂ and N·(1−p̂) both > ~10 |
| Wilson CI | center (p̂+z²/2n)/(1+z²/n) | Small N or p near 0/1; the default |
| Bootstrap CI | percentiles of B resamples | Any non-decomposable metric; resample the independence unit! |
| McNemar (paired accuracy) | χ² = (|b−c|−1)²/(b+c) | Same test set; use exact binomial when b+c small |
| Sample size for 80% power | n ≈ 8·varsum/δ² | Halving δ quadruples n |
| Multiplicity | Bonferroni α/m; BH k·α/m | Any time you tried more than one thing |
| Variance adds | σtot = √(σseed²+σtest²+σdecode²) | Variances add, stds don't; attack the biggest term |
The decision tree in prose. One model → CI (Wilson if small/extreme, bootstrap if the metric is exotic). Two models, same test set → McNemar / paired bootstrap. Two models, different test sets → unpaired z, and check power first. Many configs → correct for multiplicity or confirm on held-out data. Any single-run claim → variance decomposition.
Honest limitations of everything taught here. All of it quantifies SAMPLING uncertainty within a fixed distribution — none of it detects distribution shift, test-set contamination (GSM1k showed models dropping points on freshly rewritten items, May 2024), metric gaming, or a metric that measures the wrong thing entirely. Statistics tells you the measurement is precise; it cannot tell you it is the right measurement.
Further reading, all load-bearing above: Miller, Adding Error Bars to Evals (arXiv 2411.00640, Nov 2024); Madaan et al., Quantifying Variance in Evaluation Benchmarks (June 2024); Singh et al., The Leaderboard Illusion (April 2025); Zhang et al., GSM1k, A Careful Examination of LLM Performance on Grade School Arithmetic (May 2024); Koehn, Statistical Significance Tests for Machine Translation Evaluation (2004).
"An approximate answer to the right question is worth a good deal more than an exact answer to the wrong question." — John Tukey
An error bar is exactly that: approximate, honest, and worth more than any exact-looking point score.