Math Relearn/ Track 2 · Uncertainty/ Module 05

Statistics: trusting your numbers

Every accuracy, conversion rate and latency number you report is one draw from a noisy process. Statistics is the tooling for the sentence "0.93 beats 0.91" — estimators, intervals, tests, experiments, and the bias–variance decomposition that decides whether your model generalises at all.

On this page
    Track 2 · Uncertainty

    05 · Statistics: trusting your numbers

    ⏱ ~1 h 30📎 NumPy / scikit-learn🎯 Defend or reject a metric change with evidence, not vibes.

    You already know how to compute a number. The hard part of production is knowing whether the number means anything: whether 0.93 really beats 0.91, whether last week's dip was real, whether the model you shipped is better than the one it replaced. That is a statistics problem, and this module is the working kit for it.

    By the end you can

    • Separate a population parameter ($\mu$, $\sigma$) from a sample statistic ($\bar x$, $s$) and say what random sampling buys you — and what bias creeps in without it.
    • Explain what an estimator is, why $s^2$ divides by $n-1$, and how bias² + variance scores an estimator.
    • Compute a standard error and a 95% confidence interval, and interpret the interval the correct way — no Bayesian smuggling.
    • Run a hypothesis test and then fix it: effect size, power, multiple comparisons, peeking, practical significance.
    • Design the parts of an A/B test that actually protect the conclusion: randomisation, unit, metric hierarchy, MDE.
    • Derive the bias–variance decomposition and act on it with learning curves, cross-validation and regularization.

    Why this module exists

    In production the computation is never the bottleneck — subtracting 0.91 from 0.93 takes one line. The bottleneck is the claim attached to it. Both numbers came from finite data, so both are estimates with error bars, and two estimates can differ purely because the draws differed. Statistics is the discipline that tells you when a difference is signal and when it is the weather.

    Engineers get burned in both directions. Ship noise: "we improved conversion by 0.4 points!" — no, you resampled, and the effect vanished the next week. Miss real effects: "the test showed nothing" — your test was powered to detect a 5-point swing and you were looking for 0.3, so nothing was guaranteed to show up. One wastes engineering time; the other quietly leaves money and quality on the table for a year.

    The same machinery holds up the evaluation half of machine learning. A test set is a sample. Cross-validation is a sampling distribution of a score. Overfitting is variance. Regularization is a bias–variance trade. Learn this module and Modules 04, 06 and 08 get noticeably easier, because you will recognise the inferential skeleton under their algorithms.

    Every metric you report is one draw from a noisy process; statistics is how you say what the metric really is, and whether a difference is real.
    Pass 1

    Remember — retrieve before you read

    Six questions. Write answers before you scroll

    Cold warm-up

    1. Two symbols, one of which you compute and one the universe has: the population mean and the sample mean. Write both, and say which is which.
    2. The standard error is $s/\sqrt n$. What does the $\sqrt n$ do? If you quadruple your data, what happens to the error?
    3. Define a p-value in one sentence, with the conditional probability spelled out ("given that …").
    4. Write the bias–variance decomposition from memory, and name what each term is a property of.
    5. Why must the test set stay untouched until the very last look?
    6. Why does the sample variance divide by $n-1$ instead of $n$? What is the expectation of the version that divides by $n$?
    1. Population mean vs sample mean — which symbol is which?
    $\mu$ is the parameter, $\bar x$ is the statistic. $\mu$ (and $\sigma$) belong to the whole population — the number the universe has, which you will never observe directly. $\bar x$ (and $s$) belong to the sample you actually collected: computable, but different every time you redraw. The whole module is about the gap between them: estimators close it, standard errors measure it, confidence intervals describe it.
    2. What does the $\sqrt n$ in the standard error do?
    It makes the error shrink, but slowly. $\mathrm{SE} = s/\sqrt n$ means quadrupling your data halves the error, and multiplying by 100 divides it by 10. Diminishing returns are brutal: the last decimal place costs 100× the data of the first. This is also why "just collect more data" fixes noise (variance) but never fixes a wrong model (bias).
    3. What is a p-value, technically?
    Probability of data at least this extreme, assuming the null hypothesis is true. $p = P(\text{data this extreme} \mid H_0)$. It is a statement about the data given a hypothesis — never about the hypothesis given the data. It is not the probability the null is true, not the probability the result is a fluke, and not a measure of effect size.
    4. The bias–variance decomposition, from memory
    $\mathbb{E}[(y-\hat f(x))^2] = \mathrm{Bias}^2[\hat f(x)] + \mathrm{Var}[\hat f(x)] + \sigma^2$. The three terms are squared systematic error of the average fit, scatter of the fits across retrained datasets, and irreducible noise in the data itself. Section 9 rebuilds it term by term with a bullseye picture and code you can watch break.
    5. Why must the test set stay untouched?
    Because the moment you make a decision with it, it becomes validation data. Every peek, every hyperparameter tweak chosen "because test looked bad", every feature added after checking test error converts test error into a tuned quantity — optimistically biased by exactly the amount you looked. One final look, at the end, on data nobody's decisions touched.
    6. $n-1$ vs $n$ — why the correction?
    Because $\bar X$ is computed from the same data, it eats one degree of freedom. $\mathbb{E}\left[\frac1n\sum(X_i-\bar X)^2\right] = \frac{n-1}{n}\sigma^2$ — the naive version is biased low by a factor $(n-1)/n$. Dividing by $n-1$ makes it unbiased. Section 3 shows the variance argument in two lines.
    Pass 2

    Understand — the shape of the idea

    Eleven sections: from what you can observe to what you may claim

    1 · Why this module is the one that pays the salary

    Ask what a data team actually delivers and the honest answer is not models, it is claims: this ranking is better, this change raised retention, this anomaly is real. Each claim is a comparison between two noisy numbers. Statistics is the tooling that turns "the number went up" into "the number went up because of the change, and here is how sure we are".

    It protects you from the two expensive mistakes. Shipping noise: you A/B test, see +0.4%, ship, and the next cohort reverts — because you never computed an interval and the interval would have contained zero. Missing real effects: you run a test on 500 users, see $p = 0.12$, conclude "no difference", and lock in an inferior design for a year — the test had no power to see the effect that was there. Both failures are invisible without this material.

    How to think about it

    Treat every number your team reports as a measurement with an instrument, and every instrument has tolerance. If you would not ship a latency monitor that reported one sample, do not ship a metric comparison that reports one sample either. The three questions to ask of any number in a review: what is it an estimate of, how wide is its error bar, and what would have to be true for it to be wrong?

    2 · Population vs sample, statistic vs parameter

    The population is everything you want to generalise to — all users, all requests, all future inputs. The sample is the finite pile of observations you actually have. Numbers computed from the population are parameters; numbers computed from the sample are statistics. You never see a parameter, so you use a statistic as its estimator.

    ObjectSymbolLives whereExample
    Population mean$\mu$The world; unobservabletrue conversion rate of all users
    Population sd$\sigma$The world; unobservabletrue spread of per-user spend
    Sample mean$\bar x$Your dataconversions / impressions in the log
    Sample sd$s$Your dataspread of spend in the log
    Estimator$\hat\theta$A recipe: data in, number out$\bar x$ estimates $\mu$; $s$ estimates $\sigma$

    Statistics are only trustworthy if the sample is a random draw from the population — roughly, every member had a known, non-zero chance of being included, and inclusion did not depend on the answer. Also distinguish the sampling frame (the list you drew from: opted-in users, logged events, US phone numbers) from the population you claim to describe (all users, all customers, all citizens). Frames are rarely the population, and the gap is a bias no sample size fixes.

    Without random sampling you get selection bias — the sample is systematically different from the target. Survivorship bias in logged data is the ML-flavoured version: your training data is a sample of the world, and often a biased one. Clicks only exist for items that were shown; labels only exist for cases a human reviewed; purchases only exist for users who were not churned last month. The model learns the policy that generated the log, not the world.

    Dataset shift — the sample you trained on is not the sample you deploy on

    You trained on last quarter's logs and deployed into next quarter's world. That is a different sample from a different distribution: user mix changed, seasonality moved, your own model changed what gets shown (so the logging policy shifted too). No amount of math hides this — a perfectly unbiased estimator of last quarter is still an estimator of the wrong thing. Ask, always: from which distribution was this sample drawn, and is that the distribution in production? This is covariate / prior / concept shift, and it is why offline metrics and online metrics disagree.

    3 · Estimators and their quality

    An estimator $\hat\theta$ is a function of the sample that guesses a parameter $\theta$. Many functions could do the job — $\bar x$ estimates $\mu$, but so does the sample median, or the midpoint of the largest and smallest values. So estimators compete on quality, and there are four standard axes.

    1. Unbiasedness — the estimator is centred on the truth on average: $\mathbb{E}[\hat\mu] = \mu$. Over many samples it does not systematically err high or low. (Unbiasedness is about the average over samples, not about any single answer.)
    2. Variance — how much the estimator jitters between samples. Unbiased but wildly noisy is not useful.
    3. Consistency — as $n \to \infty$ the estimator converges to the parameter. The sample mean is consistent; "the largest observed value" generally is not.
    4. Efficiency — among unbiased estimators, who has the smallest variance.

    They are combined into one score, the mean squared error of the estimator — and it splits exactly like the model decomposition you will meet in section 9:

    $$\mathrm{MSE}(\hat\theta) = \mathbb{E}\big[(\hat\theta - \theta)^2\big] = \underbrace{\big(\mathbb{E}[\hat\theta] - \theta\big)^2}_{\text{bias}^2} + \underbrace{\mathrm{Var}[\hat\theta]}_{\text{variance}}$$ an estimator is wrong in two ways: systematically off-centre, or jittery around the centre

    Why $n-1$. The variance estimator $\frac1n\sum(X_i-\bar X)^2$ is biased because $\bar X$ was fitted to the same data: it is closer to each $X_i$ than the true $\mu$ is, so the squared deviations come out too small. Two lines:

    $$\sum_{i=1}^{n}(X_i-\bar X)^2 = \sum_{i=1}^{n}(X_i-\mu)^2 - n(\bar X-\mu)^2$$ centre the data at $\mu$ instead of $\bar X$ and one leftover term pops out

    Taking expectations, with $\mathbb{E}[\sum(X_i-\mu)^2] = n\sigma^2$ and $\mathbb{E}[(\bar X-\mu)^2] = \sigma^2/n$, gives the promised answer:

    $$\mathbb{E}\left[\frac{1}{n}\sum_{i=1}^{n}(X_i-\bar X)^2\right] = \frac{1}{n}\left(n\sigma^2 - n\cdot\frac{\sigma^2}{n}\right) = \frac{n-1}{n}\,\sigma^2$$ one degree of freedom was spent estimating $\bar X$, so dividing by $n-1$ restores unbiasedness: $s^2$

    Maximum likelihood — Module 04's estimator — is (asymptotically) efficient: in the large-sample limit the MLE attains the smallest variance any unbiased estimator can have, the Cramér–Rao bound. Stated here, not proved: the practical rule is that MLE is the default you reach for, and small-sample corrections are the exception.

    Bias and variance of an estimator and bias–variance of a model are the same idea at two scales — section 9 builds the model version.

    4 · Sampling distributions, CLT, standard error, confidence intervals

    $\bar x$ is itself a random variable — redraw your sample and you get a different $\bar x$. The sampling distribution is the distribution of that variable, and the central limit theorem says that for large $n$ it is approximately normal regardless of the shape of the data:

    $$\bar x \;\sim\; \mathcal{N}\!\left(\mu,\ \frac{\sigma^2}{n}\right) \quad\text{approximately, for large } n$$ the sample mean concentrates around the truth, with a spread that shrinks as $\sqrt n$

    Because $\sigma$ is unknown we estimate it with $s$, and the standard deviation of an estimator is called the standard error:

    $$\mathrm{SE} = \frac{s}{\sqrt n}$$ the width of the ruler you are measuring with — not the spread of the data, but the spread of the mean

    The 95% confidence interval is then the point estimate plus and minus about two standard errors:

    $$\bar x \;\pm\; 1.96\,\mathrm{SE}$$ a range built from this sample that would capture $\mu$ in 95% of repeated samples

    The $\sqrt n$ controls how fast you get precision: quadruple the data and the interval halves; multiply the data by 100 and you gain exactly one decimal place. There is no way around that arithmetic — it is why "we need 100× the traffic to resolve a 0.01 difference" is a scheduling statement, not pessimism.

    What "95%" attaches to — a top-tier misconception

    Wrong: "there is a 95% probability that $\mu$ lies in this interval." Once the interval has been computed, $\mu$ is either inside it or not; the probability is 0 or 1. Right: the 95% belongs to the procedure. If you repeated the whole experiment — new sample, new $\bar x$, new interval — 100 times, about 95 of those intervals would contain $\mu$. The interval is fixed by the data; the coverage claim is about the recipe. The "95% probability the parameter is in here" reading is the Bayesian credible interval, which starts from a prior over $\mu$ and conditions on the data — a different, legitimate object, but not what $\bar x \pm 1.96\,\mathrm{SE}$ means. If you want the Bayesian sentence, compute the Bayesian interval; do not rename the frequentist one.

    5 · Hypothesis testing, done properly

    A test is a decision procedure in five steps, and it is worth being able to recite them:

    1 · Null $H_0$"nothing is going on" → 2 · Alternative $H_1$what you'd ship → 3 · Test statisticdata → number → 4 · p-value$P(\text{data} \mid H_0)$ → 5 · Compare to $\alpha$0.05 by convention

    For two conversion rates the statistic is the pooled z-score — the observed difference divided by how big a difference pure chance would produce under $H_0$:

    $$z = \frac{\hat p_2 - \hat p_1}{\sqrt{\hat p\,(1-\hat p)\left(\frac{1}{n_1}+\frac{1}{n_2}\right)}}, \qquad \hat p = \frac{x_1+x_2}{n_1+n_2}$$ difference over noise: the same "signal divided by error" shape as every statistic you will ever use

    The p-value is the probability, assuming $H_0$ is true, of observing data at least this extreme. If $p \lt \alpha = 0.05$, reject $H_0$. That is the machinery. Now the four corrections that separate a professional report from a naive one:

    ClaimVerdict
    $p = 0.03$: 3% chance the null is trueWrong. p is $P(\text{data}\mid H_0)$, not $P(H_0\mid\text{data})$ — the direction of the conditional is flipped
    $p = 0.03$: the effect is bigWrong. p shrinks with $n$; it says nothing about magnitude. Report the effect size and its CI
    $p = 0.03$: the result will replicateNot guaranteed. Replication depends on power and on how the study was run
    $p = 0.03$: data this extreme is rare under $H_0$Right — that is the definition, and nothing more

    Significance ≠ practical significance. With $n = 1{,}000{,}000$ observations of a rate near 50%, $\mathrm{SE} = \sqrt{0.5\cdot0.5/10^6} = 0.05$ percentage points, so a difference of 0.1 points gives $z = 2$ and $p \approx 0.046$: statistically significant, worth nothing. As $n$ grows the test will find something for every trivial difference — the p-value answers "is it exactly zero?", and the honest answer for almost any real system is yes, it is not exactly zero.

    Absence of evidence is not evidence of absence. A non-significant result means "not enough data to distinguish this from noise", which is the power problem: $1-\beta$ is the probability of detecting a real effect of a given size. Power rises with effect size and with $n$, and the sample size you need scales as the inverse square of the effect:

    $$n \;\propto\; \frac{1}{\delta^{2}}$$ to halve the effect you can detect, gather 4× the data — this is the budget formula behind every test plan

    Multiple comparisons / p-hacking. Test 20 features at $\alpha = 0.05$ and you expect one false positive by construction; the chance of at least one is $1 - 0.95^{20} \approx 64\%$. Two standard fixes: Bonferroni divides the threshold by the number of tests ($0.05/20 = 0.0025$), which controls the chance of any false positive — conservative and simple. False discovery rate control (Benjamini–Hochberg) instead limits the share of your discoveries that are false: sort the p-values $p_{(1)} \le \dots \le p_{(m)}$, find the largest $k$ with $p_{(k)} \le k\alpha/m$, and declare the first $k$ real — better when you are screening many candidates and can tolerate a few misses.

    What to put in the report

    The reportable object is never a bare p-value. It is the effect size with a confidence interval — "conversion +0.42 points, 95% CI [+0.11, +0.73]" — plus the pre-registered decision rule and the power analysis that set the sample size. The interval tells a colleague everything the p-value cannot: the direction, the plausible magnitude, and whether your study could even see an effect that small.

    6 · A/B testing, as it is actually done

    An A/B test is a randomised experiment, and randomisation is the whole trick. It is the one mechanism that makes treatment and control exchangeable: everything else about the users — motivation, device, timezone, latent churn risk — is, on average, split evenly between arms, so any remaining difference in the outcome is attributable to the treatment rather than to who ended up in which arm. Observational comparisons have to model their way to that property; randomisation gets it by construction.

    1 · Pick the unituser, not request → 2 · Set the metricsprimary / secondary / guardrail → 3 · Plan from the MDEsample size = $1/\delta^2$ → 4 · Run to horizonno peeking → 5 · Analyse onceeffect + CI
    • Unit of randomisation = the unit that experiences the treatment. Randomise by user, not by request: the same person on both arms gets overlapping, self-inconsistent experiences (interference), and their requests are not independent, which quietly invalidates the standard error. One user, one arm, hashed from a stable id.
    • Metric hierarchy. One primary metric that decides the ship, defined before launch. Secondary metrics that explain why. Guardrail metrics that can veto — latency, error rate, unsubscribe rate. A win on a secondary metric with a guardrail breached is not a win.
    • Sample size from the MDE. Decide the smallest effect worth detecting ($\delta$), then size the test for it at 80% power: for a baseline near 8.5% and $\delta = 1$ percentage point that is about 12,200 users per arm; for $\delta = 0.1$ point, roughly 100× that. If you cannot afford that, your test cannot answer the question you are asking.
    • Stop at the pre-set horizon. Peeking daily and stopping at the first $p \lt 0.05$ inflates false positives — in Block 2's simulation, 40 daily looks turn a 5% false-positive rate into 30%. Fixes: a fixed horizon (simplest), or sequential testing — SPRT / always-valid p-values — which spends the error budget as you look and lets you stop early legitimately.
    • Variance reduction. CUPED regresses the metric on each user's pre-experiment value and uses the residual, which can cut variance (and therefore sample size) substantially on metrics with strong user-level autocorrelation — same idea as conditioning, one sentence, big win.

    Common failure modes, in the order they bite: novelty effects (users react to what is new, the lift decays over days — plot the effect by cohort day, not just pooled); seasonality (a test spanning a weekend or a sale is confounded with the calendar — run whole cycles); Simpson's paradox in segments (the aggregate wins while every segment loses, or the reverse — section 7 builds the numbers, and the cause is always the same: the mixture of segments differs between arms, so the pooled number is a confounded average); and sample ratio mismatch (if the split is not what you asked for, something upstream is broken and the result is void).

    7 · Correlation, causation, and the difference that pays

    Pearson correlation measures the strength of a linear relationship between two variables:

    $$r = \frac{\sum_{i}(x_i-\bar x)(y_i-\bar y)}{\sqrt{\sum_{i}(x_i-\bar x)^2\ \sum_{i}(y_i-\bar y)^2}}$$ average co-movement, normalised so that $r \in [-1, 1]$ and $r = 1$ means a perfect straight line

    Its limitations are worth memorising because they are the ones people trip over: it sees only linear structure (a perfect parabola gives $r \approx 0$), it is extremely sensitive to outliers (one wild point can invent or destroy a correlation), and it is symmetric — $r(x,y) = r(y,x)$, so it can never tell you which way an arrow points. The only road from correlation to causation is design, not more correlation.

    A confounder, concretely. Ice-cream sales and drowning deaths correlate; ice cream causes neither — summer (temperature) drives both. The same shape appears in product data: premium users convert better, but is it the premium badge or because motivated users buy premium? A confounder is a third variable that influences both the treatment and the outcome, so the treatment and outcome move together even if there is no arrow between them.

    Simpson's paradox, worked. A hospital compares a new treatment with the old one, patients arrive at differing severity, and severity is not randomised:

    SubgroupControlTreatmentWinner
    Mild cases60 / 70 = 85.7%18 / 20 = 90.0%treatment
    Severe cases1 / 2 = 50.0%5 / 8 = 62.5%treatment
    Pooled61 / 72 = 84.7%23 / 28 = 82.1%control

    The treatment wins in both subgroups and loses overall. The reason is a confounded mixture: the control arm is 97% mild cases (70 of 72) while the treatment arm is only 71% mild (20 of 28), and mild cases recover far more often. Pooling mixes the subgroups with different weights, and the weights — not the treatment — produce the reversal. The fix is to compare within strata or reweight both arms to a common mixture.

    Randomised experiments are the causal gold standard because randomisation breaks every backdoor path at once. Observational studies must assume their way there, and the two assumptions have names: ignorability (conditional on the covariates you measured, there is no unmeasured confounder — every backdoor path from treatment to outcome is blocked) and positivity (every type of unit has a non-zero chance of each treatment; if some subgroup never receives the treatment, its counterfactual is unidentified). Drawn as a DAG in words: $Z \to T$, $Z \to Y$, $T \to Y$ — the path $T \leftarrow Z \to Y$ is a backdoor, and conditioning on $Z$ blocks it; conditioning on a collider (a variable caused by both $T$ and $Y$) does the opposite and opens a path that was closed.

    Prediction and intervention are different jobs

    An ML model is a prediction machine: it will happily exploit whatever correlates with the label, including variables that are proxies for a policy or a confounder. That works while the world stays the same, and fails the moment you act on the prediction — approve more loans to people with feature X and the relationship between X and repayment can disappear or invert. Correlation is what you need to predict; causation is what you need to decide. When your model's output becomes an input to the system, you have moved from the left column to the right, and the correlation you trained on is no longer safe.

    8 · Linear regression as a statistical model, not just an algorithm

    Scikit-learn sells linear regression as a predictor. The statistical version is a generative claim about how the data were produced:

    $$y = X\beta + \varepsilon, \qquad \varepsilon \sim \mathcal{N}(0, \sigma^2)\ \text{i.i.d.}$$ the outcome is a linear combination of the features plus independent Gaussian noise

    Take that model seriously and you discover that ordinary least squares is exactly maximum likelihood under Gaussian noise (Module 04) — minimising $\sum_i (y_i - x_i^\top\beta)^2$ is maximising the log-likelihood. What the statistical framing adds is the ability to say how uncertain the coefficients are, which is what turns a fitted line into a claim.

    AssumptionIn one lineWhat breaks when it fails
    Linearity$y$ is a linear function of the featuresResiduals vs fitted show a curve; coefficients are an average slope that fits nowhere. Fix: add terms / transform
    IndependenceResiduals don't depend on each otherTime series or repeated measures: standard errors are too small, p-values too optimistic. Split by unit or time
    HomoscedasticityResidual variance is constant across fitted valuesInference is still roughly centred but SEs are wrong; use robust SEs or transform $y$
    No perfect collinearity$X$ has full rank$\beta$ is not identified — infinitely many coefficient vectors fit equally well (Module 02: rank)
    Normal errorsNeeded only for small-sample $t$/F inferenceLarge $n$: the CLT rescues you. Small $n$: intervals and p-values are off

    Residual analysis is the diagnostic. Plot residuals against fitted values: a funnel shape means heteroscedasticity, a U-shape means you left out nonlinearity, runs and clumps mean dependence. A model with a bad $R^2$ can be honest; a model with a beautiful $R^2$ and structured residuals is lying about its form — Block 4 fits a line to a parabola and the residuals correlate $+0.94$ with $x^2$ while the correct quadratic gives $+0.00$.

    $R^2$ vs adjusted $R^2$. $R^2$ is the share of variance explained and it can only go up when you add a feature — even a useless one absorbs noise. The adjusted version charges rent per parameter:

    $$R^2_{\text{adj}} = 1 - (1-R^2)\,\frac{n-1}{n-p-1}$$ $p$ predictors: every feature that does not earn its keep pushes the score down

    Two cautions to keep with it. Extrapolation: a model is only trustworthy inside the support of its training data — fitting a line to housing prices in the 100k–500k range says nothing about 2M, and no amount of fit-quality inside the range changes that. Multicollinearity: highly correlated features do not damage predictions but they inflate the variance of individual coefficients, so signs flip and magnitudes swing between runs — the matrix is nearly rank-deficient, which is Module 02's rank condition showing up as noisy inference.

    9 · Bias–variance for models

    Ask one question: if I retrained this model on a different sample of the same size, how different would the prediction be — and how far is it from the truth on average? The answer decomposes into exactly three terms:

    $$\mathbb{E}\big[(y-\hat f(x))^2\big] = \mathrm{Bias}^2[\hat f(x)] + \mathrm{Var}[\hat f(x)] + \sigma^2$$ expected squared error = systematic error² + how much the fit jitters across datasets + noise
    • $\mathrm{Bias}^2[\hat f(x)] = \big(\mathbb{E}[\hat f(x)] - f(x)\big)^2$ — the squared distance between the average fit over all possible training sets and the truth. High bias means the model's idea of the world is wrong in the same direction every time: too simple, wrong shape, missing features. It does not go away with more data.
    • $\mathrm{Var}[\hat f(x)] = \mathbb{E}\big[(\hat f(x) - \mathbb{E}[\hat f(x)])^2\big]$ — how far individual fits scatter around that average. High variance means the model chases the particular sample it happened to see: swap the training set and the prediction moves. It does shrink with more data.
    • $\sigma^2$ — the noise in the outcome itself. No model, of any complexity, removes it. This is the floor under your test error.

    The picture to keep is a bullseye. Each training set produces one shot. High bias, low variance: the shots land in a tight knot — consistent, and consistently in the wrong place (systematic offset). High variance, low bias: the shots scatter widely around the centre — on average right, but any single shot could be anywhere. Low bias, low variance: a tight knot in the bullseye — that is the goal, and you pay for every step towards it.

    Underfitting is too much bias, overfitting is too much variance — and the only three levers are model flexibility, training data size, and regularization.

    Why the levers work the way they do: a more flexible model (higher-degree polynomial, deeper tree, unregularised net) can represent more shapes, so the average fit can get closer to the truth — bias falls — but it also has more freedom to fit each sample's accidents, so variance rises. More data constrains those accidents: every extra point is another vote against the sample-specific wiggle, so variance falls while bias stays exactly where it was. Irreducible noise moves for nobody. Block 3 runs the whole story on noisy sine data: at degree 1 train and test error are both high (bias), by degree 5 test error has hit the floor, and from degree 7 train error keeps falling (0.053) while test error explodes (0.036 → 5.8 → 38 → 8036) — variance, all of it.

    Learning curves are the empirical view: plot train and validation error against training-set size. High variance shows as a large, slowly closing gap between the curves (more data would help — go get it). High bias shows as both curves already close together and already high (more data will not help — change the model). Reading that gap correctly tells you which of the three levers to pull, and it is the single most useful diagnostic in applied ML.

    10 · Generalisation, validation and regularization

    The test set exists to estimate the error of the final model on unseen data. The validation set exists because something has to absorb the damage of all your decisions: hyperparameters, feature choices, stopping time, model family. The test set stops being a test set the first time you look at it to make a decision — it has become validation, and its number is now optimistically biased by exactly as much as you looked.

    k-fold cross-validation spends small data better: split into $k$ folds, train on $k-1$, score the held-out fold, repeat so every point is scored once, average. What it estimates is the expected performance of a model trained on $(k-1)/k$ of your data — a slightly pessimistic proxy for the final model trained on all of it — plus a spread across folds that tells you how much the score moves with the sample. It is a model-selection instrument; it is not a substitute for a final held-out number, because every choice you made was made against CV scores.

    Leakage is the bug that makes everything look fine: scaling or imputing before splitting (the scaler saw the test data), duplicate rows or the same user appearing in both splits (the model recognises rather than generalises), and temporal leakage in time series — training on last month to predict last month. Time-ordered data must be split by time, always: train on the past, validate on the future, because that is the direction the world arrives in.

    Regularization is variance reduction with a bias budget. Ridge and lasso add a penalty that shrinks coefficients towards zero — you accept a little bias in exchange for a large drop in coefficient variance, which is exactly the trade of section 9 moved into parameter space. In the MAP language of Module 04, ridge is a Gaussian prior on the weights and lasso a Laplace one: same objective, Bayesian justification. Early stopping does the same job implicitly — stopping before convergence keeps the model in the low-variance region of its training path.

    When cross-validation is the wrong tool

    Random $k$-fold assumes folds are exchangeable, and three common situations break that. Time series: random folds leak the future into the past — use walk-forward or blocked splits. Grouped data (the same patient, user, document, or device in many rows): random folds let the model see the group in training and test — use grouped folds so a group lives in exactly one fold. Distribution shift: CV estimates performance on the current mixture; it says nothing about next quarter. In all three, a well-designed holdout that mirrors how the data will actually arrive beats any amount of random shuffling.

    11 · Metrics and their error bars

    Choosing a metric is choosing what you will optimise, and each of the standard ones is blind to something:

    MetricUse it whenBlind spot
    Accuracyclasses roughly balanced, errors cost the same99% accuracy on a 1% positive rate = "always negative"
    Precision / recallerrors have different costs — spam (precision) vs cancer screening (recall)Each is one side of a threshold; pick the threshold from the cost, not from habit
    ROC-AUCyou care about ranking quality across all thresholdsCan look good under heavy imbalance; says nothing about calibration
    Log-lossyou care about the probability you assigned, not just the labelPunishes confident mistakes hard; sensitive to label noise
    Calibrationthe number is a probability someone will act onSays nothing about ranking — you can be calibrated and useless

    ROC-AUC has a probabilistic reading worth internalising: AUC is the probability that a randomly chosen positive example is scored above a randomly chosen negative one. 0.5 is random ordering, 1.0 is perfect. It measures ranking, not thresholding and not probability quality. Log-loss measures the quality of the probabilities themselves and has the property Module 06 builds from scratch: it is a proper scoring rule, meaning the way to minimise your expected score is to report your true probability — which is exactly what you want a training loss to do.

    Calibration is the claim that "0.8 means 80%". Check it with a reliability curve (or reliability diagram): bucket predictions by confidence and plot observed frequency against predicted. Perfect calibration is the diagonal; below it you are overconfident, above it underconfident. A model can be superbly ranked (high AUC) and badly calibrated at the same time — which is why ranking metrics and probability metrics answer different questions.

    Never compare two metrics without an error bar on the difference — the difference is itself an estimator.

    That last point is the module in miniature. If 0.912 → 0.918 looks like a 0.6-point win, the honest question is whether that difference survives resampling the test set. The clean way to answer it is a paired bootstrap: because both models scored the same examples, you resample examples (not predictions), recompute the difference each time, and read the 2.5th and 97.5th percentiles. The pairing matters — it removes the noise shared by both models and gives a much tighter interval than treating the two scores as independent. The exercise below does exactly this on 40,000 predictions.

    Pass 3

    Implement — run it, break it

    Type each block, then change one number and predict the effect

    Block 1 · Bootstrap confidence interval vs the normal approximation

    The bootstrap is statistics without formulas: resample the data you have, recompute the statistic, repeat 10,000 times, and read off the percentiles. It works for any statistic — including ones (like a median or an AUC) with no tidy textbook standard error.

    import numpy as np
    rng = np.random.default_rng(0)
    
    x = rng.lognormal(0.0, 1.0, size=500)     # skewed data: the median's own sampling
    n = x.size                                # distribution is skewed too
    B = 10000                                 # bootstrap resamples
    idx = rng.integers(0, n, size=(B, n))     # B resamples of size n, with replacement
    
    med = np.median(x)
    boot = np.median(x[idx], axis=1)          # 10 000 medians of resampled data
    lo, hi = np.percentile(boot, [2.5, 97.5])
    se = boot.std(ddof=1)                     # bootstrap standard error
    print(f"median {med:.3f}  bootstrap [{lo:.3f}, {hi:.3f}]  normal [{med-1.96*se:.3f}, {med+1.96*se:.3f}]")
    # -> median 0.951  bootstrap [0.842, 1.079]  normal [0.828, 1.074]
    # the percentile interval sits to the right and is asymmetric about the estimate:
    # gaps of -0.109 / +0.128, because the sampling distribution inherits the skew.
    
    mu = x.mean()                             # same data, a symmetric statistic
    bm = x[idx].mean(axis=1)
    lo2, hi2 = np.percentile(bm, [2.5, 97.5])
    se2 = x.std(ddof=1) / np.sqrt(n)          # textbook SE for the mean
    print(f"mean {mu:.3f}  bootstrap [{lo2:.3f}, {hi2:.3f}]  normal [{mu-1.96*se2:.3f}, {mu+1.96*se2:.3f}]")
    # -> mean 1.598  bootstrap [1.441, 1.779]  normal [1.425, 1.771]   (agree to ~1%)

    Where they disagree is the lesson: for a symmetric statistic the two agree to within rounding, and for a skewed one the normal approximation is symmetric when the truth is not. Percentile bootstrap costs nothing extra and respects the shape.

    Block 2 · A/B test by hand, then calibrate it 500 times

    import numpy as np
    from math import erfc, sqrt
    
    def ztest(x1, n1, x2, n2):
        """Two-proportion pooled z-test -> (diff, z, two-sided p, SE)."""
        p1, p2 = x1/n1, x2/n2
        pp = (x1 + x2)/(n1 + n2)              # pooled rate: what H0 assumes
        se = sqrt(pp*(1-pp)*(1/n1 + 1/n2))
        z = (p2 - p1)/se                      # difference over chance-level noise
        return p2 - p1, z, erfc(abs(z)/sqrt(2)), se
    
    n = 5000
    r = np.random.default_rng(42)
    a, b = r.binomial(n, 0.08), r.binomial(n, 0.09)   # a REAL 1pp lift
    d, z, p, se = ztest(a, n, b, n)
    print(f"arms {a/n:.4f} vs {b/n:.4f}  diff {d:+.4f}  z {z:.2f}  p {p:.4f}  CI [{d-1.96*se:+.4f}, {d+1.96*se:+.4f}]")
    # -> arms 0.0780 vs 0.0928  diff +0.0148  z 2.65  p 0.0081  CI [+0.0038, +0.0258]
    
    r2 = np.random.default_rng(7)
    hit = sum(ztest(r2.binomial(n,0.08), n, r2.binomial(n,0.09), n)[2] < 0.05 for _ in range(2000))
    print("power 8% vs 9% at n=5000:", round(hit/2000, 3))       # -> 0.448 (a real lift, often missed)
    
    r3 = np.random.default_rng(1005)                             # now NO lift at all
    fp = sum(ztest(r3.binomial(n,0.08), n, r3.binomial(n,0.08), n)[2] < 0.05 for _ in range(500))
    print(f"false positives: {fp}/500 = {fp/500:.3f}")            # -> 25/500 = 0.050  (calibrated!)
    
    def peek(seed, days=40, per_day=500, R=2000):
        """Peek daily and stop at the first p < 0.05, when the null is true."""
        g = np.random.default_rng(seed); fired = 0
        for _ in range(R):
            ca = cb = 0
            for t in range(1, days+1):
                ca += g.binomial(per_day, 0.08); cb += g.binomial(per_day, 0.08)
                if ztest(ca, per_day*t, cb, per_day*t)[2] < 0.05:
                    fired += 1; break
        return fired / R
    print("40 daily looks:", round(peek(1), 3), " one look at the end:", round(peek(1, days=1, per_day=20000), 3))
    # -> 40 daily looks: 0.301   one look at the end: 0.05

    Three numbers to carry out of this block. The test is calibrated: with no real difference it still fired 25 times in 500 — that is what $\alpha = 0.05$ means, and Block 2 lets you watch it happen. It is also underpowered at this sample size: a real 1pp lift is caught only 45% of the time, so "the test showed nothing" is mostly a sample-size statement. And peeking destroys the calibration: 40 looks and you are celebrating a false win 30% of the time.

    Block 3 · Bias–variance, demonstrated

    import numpy as np
    import matplotlib.pyplot as plt
    
    rng = np.random.default_rng(3)
    f = lambda t: np.sin(2*np.pi*t)
    NOISE, N_TRAIN, R = 0.3, 30, 50           # noise sd, train size, repetitions
    grid = np.linspace(0, 1, 100)             # fixed test grid, noise-free
    y_true = f(grid)
    DEG = np.arange(1, 13)
    
    train = np.zeros((R, DEG.size)); test = np.zeros((R, DEG.size))
    preds = np.zeros((R, DEG.size, grid.size))
    for r in range(R):                        # a NEW training set every repetition
        xt = rng.uniform(0, 1, N_TRAIN)
        yt = f(xt) + rng.normal(0, NOISE, N_TRAIN)
        for k, d in enumerate(DEG):
            beta = np.polyfit(xt, yt, d)
            train[r, k] = np.mean((np.polyval(beta, xt) - yt)**2)
            preds[r, k] = np.polyval(beta, grid)
            test[r, k] = np.mean((preds[r, k] - y_true)**2)
    
    mean_pred = preds.mean(0)                 # the AVERAGE model over repetitions
    bias2 = ((mean_pred - y_true)**2).mean(1) # how far the average fit sits from truth
    var = preds.var(0, ddof=1).mean(1)        # how far the fits scatter around it
    
    print("deg   train     test     bias^2      var   bias^2+var")
    for k in range(DEG.size):
        if DEG[k] in (1, 3, 5, 7, 9, 11):
            print(f"{DEG[k]:3d} {train[:,k].mean():8.4f} {test[:,k].mean():9.4f}"
                  f" {bias2[k]:9.4f} {var[k]:9.4f} {bias2[k]+var[k]:10.4f}")
    #  deg   train     test     bias^2      var   bias^2+var
    #    1   0.2645   0.2242    0.2035   0.0211     0.2246
    #    3   0.0782   0.0239    0.0058   0.0185     0.0243
    #    5   0.0704   0.0360    0.0007   0.0360     0.0367
    #    7   0.0644   5.8316    0.1563   5.7911     5.9475
    #    9   0.0593  38.0266    0.7456  38.0418    38.7874
    #   11   0.0534 8035.99  166.4049 8030.19   8196.59
    plt.plot(DEG, train.mean(0), label="train MSE")
    plt.plot(DEG, test.mean(0), label="test MSE")
    plt.plot(DEG, bias2, label="bias^2"); plt.plot(DEG, var, label="variance")
    plt.xscale("linear"); plt.yscale("log"); plt.legend(); plt.show()

    Read the columns: bias² + variance matches the test error to within a couple of percent — the decomposition is an identity, not a metaphor. Degree 1 is dominated by bias (0.20 of 0.22), degree 5 is the sweet spot (bias² 0.0007, variance 0.0360), and from degree 7 the variance term swallows everything while train error happily keeps dropping. Train error is monotone in complexity; test error is not. That divergence is what overfitting is.

    Block 4 · Residual analysis and honest validation

    import numpy as np
    from sklearn.datasets import make_regression
    from sklearn.linear_model import LinearRegression
    from sklearn.model_selection import KFold, cross_val_score
    
    X, y = make_regression(n_samples=250, n_features=100, n_informative=8,
                           noise=50, random_state=0)
    n, p = X.shape
    m = LinearRegression().fit(X, y)
    r2 = m.score(X, y)                        # resubstitution score: always flattering
    adj = 1 - (1 - r2) * (n - 1) / (n - p - 1)
    cv = cross_val_score(LinearRegression(), X, y,
                         cv=KFold(5, shuffle=True, random_state=0), scoring="r2")
    print(f"train R2 {r2:.4f} | adjusted R2 {adj:.4f} | 5-fold CV R2 {cv.mean():.4f} +/- {cv.std():.4f}")
    # -> train R2 0.9673 | adjusted R2 0.9454 | 5-fold CV R2 0.8662 +/- 0.0320
    
    resid = y - m.predict(X)
    print(f"residual mean {resid.mean():+.2e}, sd {resid.std(ddof=p+1):.2f} (true noise sd 50)")
    # -> residual mean -5.91e-15, sd 48.98 (true noise sd 50)
    
    # a WRONG model leaves a PATTERN in the residuals, not just a worse score
    rng = np.random.default_rng(0)
    x = np.linspace(-3, 3, 200); yy = x**2 + rng.normal(0, 1, 200)
    r1 = yy - np.polyval(np.polyfit(x, yy, 1), x)      # fit a line to a parabola
    r2c = yy - np.polyval(np.polyfit(x, yy, 2), x)     # fit the right shape
    print(f"line  : corr(resid, x^2) = {np.corrcoef(r1, x**2)[0,1]:+.3f}, resid sd {r1.std(ddof=2):.3f}")
    print(f"curve : corr(resid, x^2) = {np.corrcoef(r2c, x**2)[0,1]:+.3f}, resid sd {r2c.std(ddof=3):.3f}")
    # -> line  : corr(resid, x^2) = +0.940, resid sd 2.831
    # -> curve : corr(resid, x^2) = +0.000, resid sd 0.965

    Two lessons in the numbers. The train score (0.967) flatters the model by a tenth of a point against honest 5-fold CV (0.866), and adjusted $R^2$ (0.945) only partly corrects — it charges a flat rent per feature and cannot know that 92 of those features were useless. And the residual pattern detects a wrong model that the score alone would not: correlation with the missing term is +0.94 for the straight line, 0.00 once you fit the right shape.

    Exercise · ship it or not? 15 minutes
    • Setup. Model B scores 0.918 where model A scored 0.912, both evaluated on the same 40,000 test predictions. Build the paired outcome table (both right / only A / only B / both wrong) with counts 36,000, 480, 720, 2,800 — that gives exactly 0.912 and 0.918.
    • Bootstrap the difference. The per-example contribution to the difference is $d_i \in \{-1, 0, +1\}$; resample those 40,000 values 5,000 times, take the mean each time, and read the 2.5th and 97.5th percentiles. Expected: [+0.0043, +0.0077].
    • Compare with the unpaired bootstrap (resample the two score vectors independently): [+0.0023, +0.0098] — more than twice as wide, because it throws away that the same examples were scored twice.
    • Decide. The interval excludes zero, so the improvement is not noise — now argue the practical side: is +0.6 points worth the engineering and the risk? Statistics answers only the first half; you still have to answer the second.
    import numpy as np
    rng = np.random.default_rng(0)
    n = 40000
    counts = np.array([0.900, 0.012, 0.018, 0.070]) * n   # both right / only A / only B / both wrong
    cell = np.repeat(np.arange(4), np.rint(counts).astype(int))
    rng.shuffle(cell)
    a = (cell == 0) | (cell == 1)                         # model A correct
    b = (cell == 0) | (cell == 2)                         # model B correct
    print(f"acc_A {a.mean():.4f}  acc_B {b.mean():.4f}  diff {b.mean()-a.mean():+.4f}")
    # -> acc_A 0.9120  acc_B 0.9180  diff +0.0060
    
    dvec = b.astype(int) - a.astype(int)                  # -1, 0, +1 per example
    diffs = np.empty(5000)
    for i in range(5000):                                 # resample EXAMPLES, both models together
        diffs[i] = dvec[rng.integers(0, n, n)].mean()
    print("paired 95% CI:", np.round(np.percentile(diffs, [2.5, 97.5]), 4))   # -> [0.0043 0.0077]
    
    diffs2 = np.empty(5000)                               # unpaired: forget they shared examples
    for i in range(5000):
        diffs2[i] = b[rng.integers(0, n, n)].mean() - a[rng.integers(0, n, n)].mean()
    print("unpaired 95% CI:", np.round(np.percentile(diffs2, [2.5, 97.5]), 4))  # -> [0.0023 0.0098]
    Checkpoint

    Prove it, then move on

    Answer first, then open the feedback

    Multiple choice — instant feedback

    1. You run a test and get $p = 0.05$. What does that number actually mean?

    2. A 95% confidence interval for $\mu$ comes out as [4.1, 5.3]. Which statement is correct?

    3. A colleague says: "I ran 10-fold cross-validation, so I don't need a held-out test set for my final number." What is true?

    4. New model: accuracy 0.8000 → 0.8012 on 5,000,000 test examples (2.5M per arm), $p \approx 0.0008$. Ship it?

    5. Your model underfits badly: high training error and high test error. What does 10× more training data do?

    6. You check your A/B test every day and stop as soon as $p \lt 0.05$, when there is genuinely no difference. Roughly how often will you declare a win?

    Open questions — answer aloud, no notes

    1. Your model's accuracy went 0.912 → 0.918 on 40,000 test predictions. Argue for shipping and against shipping, three sentences each.
    2. State the correct interpretation of a 95% confidence interval without using the word "probability" for the parameter.
    3. Why does $s^2$ divide by $n-1$? Give the expectation argument in two lines.
    4. Write the bias–variance decomposition from memory. Which of the three terms does more data move, and which does a more flexible model move?
    5. What is the difference between the sampling frame and the population? Give an example from your own log data where they are not the same.
    6. Your A/B test's aggregate wins while every segment loses. In one sentence, what is wrong with the aggregate?
    7. Why can't a p-value tell you the probability that the null hypothesis is true? Which direction would you need, and what would it cost?
    8. When is $k$-fold cross-validation the wrong tool? Name two situations and the split you would use instead.

    Re-answer all eight in 1 day, then 3 days, then 7 days — from memory, in writing. If question 4 or 6 comes back fuzzy, re-read only that section.

    Continue in your ML notebook

    The theory above has a hands-on twin in the companion notebook: ML module 04 · workflow & honest evaluation is where the train/validation/test discipline, leakage checks and honest baselines are worked through on real datasets; and ML module 12 · tuning, validation & error analysis is where cross-validation, search strategies and error slices get built. Read this module for why a number can be trusted, that one for how to produce it without fooling yourself.