LABS  /  CROSS-VALIDATION

One exam,
or five?

Grade a student on one random exam and the grade can lie. Grade a model on one random split and the score can lie the same way — 0.79 on the split you got, 0.88 on the split you didn't. Rotate the held-out fold and the number stops moving.

THE GIST · 20 SECONDS

A single hold-out split scores your model on whichever rows happened to land in the test set — fragile, and it wastes those rows for training. K-fold cuts the data into k folds, trains on k−1 and tests on the one left out, rotating until every row has been tested exactly once. Report the mean ± std of the k scores. Cost: k trainings instead of one.

  • Foldone slice of the data
  • Hold-outone split, one score
  • Stdhow much it wobbles
  • the compute you pay

Move the k slider to repartition the 20 rows, then step the held-out fold along the row.

UNDER THE HOOD

What you just played, written down

A single hold-out gives you one number and no idea how much to trust it. Cross-validation gives you a number and its wobble — and spends every row twice.

The problem with a single hold-out set

The naive way to check a model is to carve off a chunk of the data as a test set, train on everything else, and score on the part it never saw. That beats scoring on the training data — which only measures how well the model memorised its own answers — but a single split still has two problems that don't go away just because the split was "random".

The first is fragility. The score depends entirely on which rows landed in the test set. Shuffle differently and you get a meaningfully different number with nothing about the model changed. On a large dataset that noise mostly washes out; on a small one it swings enough to make a mediocre model look good, or a good model look mediocre, purely by chance.

The second is waste. Whatever rows you set aside never help the model learn. On a small dataset that isn't a minor inefficiency — it's data you can't afford to throw away, held hostage to produce one score you don't fully trust.

How k-fold fixes both

  1. Split the dataset into k roughly equal folds.
  2. Train on k − 1 folds, test on the one left out, record that fold's score.
  3. Rotate: hold out a different fold, retrain from scratch, score again.
  4. Repeat until every fold has been the test set exactly once — k rounds, k scores.
  5. Report the mean, and the spread, of those k scores.

Across the full rotation every row is tested exactly once and trained on k − 1 times. The wasted-data problem simply disappears.

WHY AVERAGING WORKS

Any one fold can still be an easy or a hard test set — that randomness doesn't go away. But a fold that flatters the model on one round tends to be offset by a harder fold on another, so the errors partially cancel instead of compounding in one direction. And the spread is information too: five near-identical fold scores mean an average you can trust; five scores bouncing all over mean the model's true performance is genuinely uncertain — something a single hold-out would have hidden behind one confident-looking figure.

The whole thing in code

from sklearn.model_selection import (
    KFold, StratifiedKFold, GroupKFold,
    TimeSeriesSplit, cross_val_score)
import numpy as np

# the one-liner most people want
s = cross_val_score(model, X, y, cv=5, scoring="f1")
print(f"{s.mean():.3f} ± {s.std():.3f}")   # 0.836 ± 0.033

# the same thing, unrolled
kf = KFold(n_splits=5, shuffle=True, random_state=0)
scores = []
for tr, va in kf.split(X):
    model.fit(X[tr], y[tr])              # k trainings!
    scores.append(score(model, X[va], y[va]))
print(np.mean(scores), np.std(scores))

# imbalanced labels → keep the class ratio per fold
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
# correlated rows (same patient/user) → keep groups together
cv = GroupKFold(n_splits=5)     # .split(X, y, groups)
# temporal data → never train on the future
cv = TimeSeriesSplit(n_splits=5)
5-FOLD HERE0.836 ± 0.033stable, reproducible
ONE SPLIT HERE0.790 – 0.880same model, same data

The cost ladder

None of this is free. A hold-out trains one model; k-fold trains k. Every row of this table is computed from the same 20 fixed row-scores the lab above uses.

n = 20 rows · mean ± population std of the fold scores
kTrainingsTrain rowsReported
2210 (50%)0.836 ± 0.026
5516 (80%)0.836 ± 0.033
101018 (90%)0.836 ± 0.037
20 (LOOCV)2019 (95%)0.836 ± 0.077
LOOCV — THE EXTREME, k = n

Push k all the way to n and every single row takes a turn as the entire test set: n trainings, each on n − 1 rows. It's nearly unbiased — each model sees almost all the data — but expensive, and the individual fold scores are wildly noisy because each is measured on one row (the ± 0.077 above is the spread of single rows, not of the estimate). LOOCV is a small-data tool, not a default.

Picking k, and the four splits that aren't plain k-fold

HOW TO PICK k

5 and 10 are the conventions

k = 5 is the cheap default: five trainings, each on 80% of the data. k = 10 trains on 90%, so each model is closer to the one you'd ship (less pessimistic bias) — at double the compute, and with folds that overlap more in their training sets, so the k scores are more correlated and the std understates the true uncertainty. Rule of thumb: 10 when data is scarce and training is cheap, 5 when training is expensive, a single hold-out when the dataset is big enough that split noise is already negligible.

IMBALANCED CLASSES

Stratified k-fold

With 3% positives, a random 5-fold split can hand one fold almost no positives — that fold's score is then noise, and a fold with zero positives can make the metric undefined outright. Stratified k-fold samples within each class so every fold carries the same class ratio as the full dataset. For classification it should be your default; scikit-learn already uses it when you pass an integer cv to a classifier.

CORRELATED SAMPLES

Grouped k-fold

If rows come in clusters — twelve scans per patient, many sessions per user, many chunks per document — a plain split puts some of a group in train and the rest in validation. The model then recognises the group, not the pattern, and the score is inflated. Grouped k-fold keeps every row of a group on one side of the line. If your rows share an identity, that identity is the unit you must split on.

TEMPORAL DATA

Time-series split

Plain k-fold shuffles, which means fold 1 can be tested on January while the model trained on December — it leaks the future. Production never has that luxury, so the score is unreproducible optimism. A time-series split only ever trains on a prefix and validates on the slice that follows, growing the window forward. Anything with a timestamp, a trend, or drift wants this, not KFold.

⚠ Don't report the folds you tuned on

Tune a learning rate by picking whichever value scored best across your 5 folds, then report that best score, and the number is optimistic — the choice has already seen every fold. Nested CV fixes it: an inner CV loop inside each outer training fold picks the hyperparameters, and the outer fold scores the whole pipeline on rows no part of the tuning touched. Same rule as a leaked test set, one level up.

↔ When to skip it

On a large dataset the test split is already big enough that its noise is small, so one hold-out is a perfectly serviceable estimate at a fifth of the cost. CV earns its keep when data is scarce or when the score drives a decision — tuning, or picking a champion model — because there you can't afford to chase one split's luck.

★ Report the spread

"0.84" is a claim. "0.836 ± 0.033 over 5 folds" is a measurement. The second one tells a reader how much of your improvement is real and how much is fold noise — and it's the difference between an eval you can defend in review and a number someone will quietly fail to reproduce.

QUICK CHECK

Did it stick?

FAQ

Cross-validation, answered

What is k-fold cross-validation?

Split the data into k folds and train k times — each round trains on k−1 folds and scores on the one left out, rotating until every fold has been the test set exactly once. Average the k scores. Every row ends up used for both training and testing.

Why isn't a single hold-out test set enough?

Its score depends entirely on which rows landed in the test set. In the lab above the same model on the same data reports 0.790 or 0.880 purely by which fifth you held out. You also lose those rows from training.

What value of k should I use?

5 and 10 are the conventions. k=5 is five trainings on 80% of the data; k=10 is ten trainings on 90%, slightly less biased but more expensive and with more correlated folds. Large datasets often don't need CV at all.

What is LOOCV?

K-fold with k = n — every row is its own test set, so you train n models on n−1 rows each. Nearly unbiased, expensive, and each fold score is measured on a single row so the fold-to-fold spread is huge. A small-data tool.

How much extra compute does it cost?

About , since you train k models instead of one. That's the whole trade: k times the compute for a low-variance number you can act on.

When do I need stratified, grouped, or time-series splits?

Stratified for imbalanced classes (keeps the class ratio per fold), grouped when rows share an identity like a patient or user (keeps a group on one side), and a time-series split for anything temporal — plain k-fold trains on the future and leaks it.

What is nested cross-validation?

Tuning on the same folds you report makes the reported score optimistic. Nested CV runs an inner CV inside each outer training fold to choose hyperparameters, and scores the full pipeline on the outer fold — data the tuning never saw.

Finished this one? 0 / 61 Labs done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More Labs