Cross-Validation
k-fold, stratified, leave-one-out and nested CV, with the bias–variance trade-off in choosing k.
Assumes you know
Cross-Validation
Intuition first
A single validation split gives you one number, and that number depends on which rows happened to land in it. With a few hundred validation examples, the luck of the draw can shift accuracy by several points — enough to pick the wrong model.
Cross-validation removes the luck by rotating the role of the validation set. Split the data into parts. Train on of them, validate on the one left out, and repeat until every part has served as validation exactly once. Average the scores.
You get two things out of it. A more stable estimate, because it averages over different splits. And a sense of how unstable your model is, because the spread of the scores tells you how much the answer depends on the sample.
The price is times the compute.
k-fold cross-validation
where is the error on fold from the model trained without it.
| Symbol | Meaning | Read aloud |
|---|---|---|
| k | Number of folds | k |
| n | Number of examples | n |
| R̂⁽ʲ⁾ | Error measured on fold j | R hat superscript j |
| CVₖ | Cross-validated error, averaged over folds | C V k |
| SE | Standard error across folds, a measure of stability | standard error |
Choosing k
The choice is itself a bias–variance trade-off, on the estimate rather than the model.
| k | Training set per fit | Bias of estimate | Variance of estimate | Cost |
|---|---|---|---|---|
| 2 | 50% of data | High (pessimistic) | Low | 2 fits |
| 5 | 80% | Moderate | Moderate | 5 fits |
| 10 | 90% | Low | Moderate | 10 fits |
| n (LOOCV) | Nearly none | Often high | fits |
Small is pessimistic. Each model trains on much less than the full dataset, so it performs worse than the model you will eventually ship on 100% of the data. The estimate is biased upward in error.
Large reduces that bias but the estimates become highly correlated — the training sets overlap almost completely — so averaging them removes less variance than the count suggests.
Leave-one-out cross-validation
Setting leaves out one example at a time.
Nearly unbiased, since each fit uses examples. Two problems in practice.
First, cost: model fits. Second, and less obvious, the variance of the estimate is often higher than 10-fold, because the fitted models are nearly identical — they differ by one example — so their errors are strongly correlated and averaging them cancels little noise.
Why LOOCV can have high varianceAdvanced
Consider averaging random variables each with variance and pairwise correlation :
As the first term vanishes but the second tends to . So the variance of the average is floored by the correlation, no matter how many terms you average.
For LOOCV the fitted models overlap in of their training points, so is very close to 1 and almost no variance reduction occurs. For 10-fold the training sets overlap in about 8/9 of their rows — still correlated, but less so.
There is a genuine exception worth knowing: for linear models fitted by least squares, LOOCV has a closed form that costs a single fit,
where is the -th diagonal entry of the hat matrix . When it is this cheap, the cost objection disappears — which is why LOOCV is standard for ridge regression and almost nowhere else.
Variants you will need
Stratified k-fold — preserves class proportions in each fold. Always use this for classification; it strictly reduces the variance of the estimate at no cost.
Grouped k-fold — keeps all rows sharing a key (patient, user, document) in the same fold. Required whenever rows are not independent.
Time series split — trains on the past, validates on the immediately following period, and never lets a fold see data from after its validation window.
Report the spread, not just the mean
The standard error across folds tells you whether a difference between two models is real:
Solved problem 1 · Is model B actually better?
Two models are compared with 5-fold cross-validation. Accuracies per fold:
- Model A: 0.812, 0.795, 0.834, 0.801, 0.818
- Model B: 0.826, 0.781, 0.858, 0.793, 0.847
Which should you choose?
Step 1 — mean for Model A
Step 2 — mean for Model B
B is ahead by .
Step 3 — spread for Model A
Deviations from :
Step 4 — spread for Model B
Deviations from :
Step 5 — standard errors
Step 6 — compare the difference to its uncertainty
The difference is . A rough standard error for the difference:
The observed gap of is only standard errors — well inside noise.
Answer
The 0.9-point advantage is not statistically meaningful: it is about half a standard error of the difference. Model B is also more than twice as variable across folds ( versus ), meaning its performance depends heavily on which data it sees.
Choose Model A — equal performance within noise, and substantially more stable. When two models tie, prefer the one with lower variance, and after that the simpler one.
Nested cross-validation
If you use cross-validation to select hyperparameters, the resulting CV score is optimistically biased for the same reason a reused validation set is — selection has contaminated it.
Nested CV fixes this with two loops: an inner loop that selects hyperparameters, and an outer loop that evaluates the whole selection procedure on data the inner loop never saw.
import numpy as np
from sklearn.model_selection import GridSearchCV, StratifiedKFold, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
from sklearn.datasets import make_classification
X, y = make_classification(n_samples=600, n_features=15, random_state=0)
pipe = Pipeline([("scale", StandardScaler()), ("svc", SVC())])
grid = {"svc__C": [0.1, 1, 10, 100], "svc__gamma": ["scale", 0.01, 0.1]}
inner = StratifiedKFold(n_splits=5, shuffle=True, random_state=1)
outer = StratifiedKFold(n_splits=5, shuffle=True, random_state=2)
# Inner loop picks C and gamma; outer loop scores the whole procedure.
search = GridSearchCV(pipe, grid, cv=inner, scoring="accuracy")
scores = cross_val_score(search, X, y, cv=outer, scoring="accuracy")
print(f"nested CV accuracy: {scores.mean():.4f} ± {scores.std(ddof=1)/np.sqrt(len(scores)):.4f}")
print(f"per fold: {np.round(scores, 4)}")The cost is fits — here . Expensive, and the correct thing to do when reporting a number that has to be trusted.
Exercise 1
You have 80 labelled examples — expensive medical annotations. Would you use a single 80/20 split, 10-fold CV, or LOOCV? Justify.
Show solutionHide solution
Not a single split. A 20% validation set is 16 examples; accuracy on 16 examples has a standard error of roughly
Ten percentage points of noise makes model comparison meaningless, and you cannot afford to sacrifice 16 of 80 examples from training.
10-fold CV is the right default. Each fit trains on 72 examples, every example is used for validation exactly once, and you get a spread across folds to judge stability. Cost is trivial at this scale.
LOOCV is defensible — 80 fits is nothing — and gives slightly less bias. But the fold estimates are near-perfectly correlated, so the variance of the estimate is typically no better than 10-fold, and each fold score is 0 or 1 which makes the spread uninformative. The exception is a linear model with the closed-form shortcut, where LOOCV is free.
Also worth doing at this sample size: repeated stratified 10-fold (say 5 repeats with different seeds), averaging the 50 fold scores, to reduce dependence on one particular partition.
Exercise 2
Explain why cross-validation scores are not an unbiased estimate of the model you finally deploy, even when done correctly.
Show solutionHide solution
Two separate reasons.
Training set size. Each CV fit trains on of the data, while the deployed model is refitted on 100%. Since error generally decreases with more data, the CV estimate is pessimistic for the deployed model. With each fit sees 80% of the data, so the gap can be material on small datasets and is negligible on large ones.
Selection. If any choice — hyperparameters, feature set, algorithm — was made by comparing CV scores, the winning score is optimistically biased, because it is a maximum over noisy estimates. This pushes in the opposite direction to the first effect.
The two biases partially cancel, which is convenient and also means you cannot rely on either dominating. If you need a trustworthy number, use nested CV for the estimate and plain CV for the selection, and report the nested figure.
Next: Hyperparameter Search, which is what the inner loop above was doing, done deliberately.