Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

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
  • k×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.

THE PICTURE

The picture: one fold out per round

roundfold scoreF1rows 1-4F2rows 5-8F3rows 9-12F4rows 13-16F5rows 17-20 1testtrain on 16 rows0.790 2test0.810 3test0.840 4test0.860 5test0.880 report: mean 0.836 ± 0.033 over 5 trainings one hold-out reports whichever fold it drew: 0.790 to 0.880
This is the lab’s rotate act drawn flat, for k = 5 on the same 20 rows. The held-out fold walks along the diagonal, so after five rounds every row has been tested exactly once and trained on four times. The five fold scores sit 0.09 apart from lowest to highest. That spread is exactly what a single hold-out hides, because it only ever sees one of them.
BY THE NUMBERS

Worked example: the five rounds, row by row

These are the lab’s fixed row scores, so you can check every number against the widget. With k = 5 each fold is four consecutive rows. A fold’s score is the mean of its four row scores.

RoundHeld outRow scores in that foldFold scoreRunning mean
1F1 (rows 1–4)0.72, 0.81, 0.88, 0.753.16 ÷ 4 = 0.7900.790
2F2 (rows 5–8)0.93, 0.68, 0.85, 0.783.24 ÷ 4 = 0.8100.800
3F3 (rows 9–12)0.79, 0.91, 0.86, 0.803.36 ÷ 4 = 0.8400.813
4F4 (rows 13–16)0.95, 0.74, 0.89, 0.863.44 ÷ 4 = 0.8600.825
5F5 (rows 17–20)0.83, 0.97, 0.90, 0.823.52 ÷ 4 = 0.8800.836

The mean is 0.836. The spread depends on which formula you use. Divide the squared deviations by k and you get the population std, 0.033, which is what the page reports and what np.std gives by default. Divide by k − 1 and you get the sample std, 0.037, which is the default in pandas. Say which one you used. Also look at the row scores inside each fold: F2 holds both 0.93 and 0.68. Fold scores are averages, so they move less than single rows do, and that is why the LOOCV row of the cost ladder shows the much wider ± 0.077.

WATCH FOR

Common mistakes

  • Preprocessing before the split

    Fit a scaler, an imputer, feature selection or a vocabulary on all the rows, then cross-validate, and every validation fold has already shaped the features. Put the preprocessing and the model in one scikit-learn Pipeline and pass that to cross_val_score, so each fold fits its own.

  • Oversampling before the split

    Duplicate or synthesise minority rows (SMOTE, for example) and then split, and copies of the same row land on both sides. The model is then tested on near-duplicates of rows it trained on. Resample inside each training fold only, and never touch the validation fold.

  • Leaving KFold unshuffled on sorted data

    scikit-learn’s KFold does not shuffle unless you pass shuffle=True. If the file is sorted by label, region or date, each fold becomes one slice of that ordering, and the scores measure the sort, not the model. Shuffle with a fixed random_state, unless the order is time, in which case use a time-series split.

  • Calling fold noise a win

    Model B scores 0.842 and model A 0.836, with a fold std of 0.033. That gap is far smaller than the wobble. Score both models on the same folds, compare them fold by fold, and only believe a gap that holds on most folds.

  • Shipping one of the fold models

    Cross-validation estimates how well your recipe works. Each fold model saw only 80% of the data. Once the recipe is chosen, refit it on all the rows and ship that. The CV score is the estimate for the refit model. GridSearchCV does this refit for you by default.

IN THE WILD

Where this shows up in production and interviews

  • Hyperparameter search

    GridSearchCV and RandomizedSearchCV run a full k-fold for every candidate. A grid of 20 settings with cv=5 is 100 training runs, plus one refit at the end. This is where the k× cost from the ladder above actually hurts, and why people shrink the grid before they shrink k.

  • Stacking and out-of-fold predictions

    cross_val_predict returns, for every row, the prediction from the one model that did not train on it. Those out-of-fold predictions can train a second-level model without leakage. Training it on in-sample predictions instead teaches it to trust overfit base models.

  • Small LLM eval sets

    Say you have 200 labelled examples and are choosing between prompts. That is the small-data case this page is about. Compare the candidates on the same folds or resamples and report the spread. If you also tuned the prompt on those examples, keep a held-back slice to report on.

  • The interview version

    “You have 2,000 labelled rows. How do you evaluate?” A strong answer covers four points. Use stratified 5-fold, grouped by user if users repeat. Fit preprocessing inside the folds. Report the mean ± std. Use nested CV, or a final untouched test set, if the same data picked the hyperparameters.

QUICK CHECK

Did it stick?

See every answer, with the reasoning
  1. A model scores 0.79 on one 80/20 split and 0.88 on a different 80/20 split of the same data. What changed?

    Answer: Nothing but which rows landed in the test set — a single hold-out score is high-variance — Nothing about the model changed. The score of a single hold-out depends entirely on which rows you happened to hold out, and on a small dataset that alone can move the number by several points. Averaging over k rotating folds is what removes that luck.

  2. In 5-fold cross-validation, how many times is each row used for training?

    Answer: 4 times — it is in the training set for every fold except the one where it is held out — Each row sits in exactly one fold. That fold is held out for one of the five rounds and is part of the training set for the other four, so every row is trained on k − 1 = 4 times and tested exactly once.

  3. What does cross-validation cost compared with a single hold-out?

    Answer: Roughly k times the compute, because you train k models — Each fold requires a fresh training run, so k-fold is roughly k× the training cost. That is why it is worth it on scarce data or when the score drives a decision, and often skipped on very large datasets where one split is already low-variance.

  4. Your rows are daily sales figures with a timestamp. Why is plain shuffled k-fold wrong here?

    Answer: It trains on future rows and tests on past ones, leaking information you would never have in production — Shuffling destroys temporal order, so a fold can train on December and test on January. The reported score depends on information the model could not possibly have at prediction time. A time-series split only trains on a prefix and validates on what comes next.

  5. You pick the best learning rate by its 5-fold score, then report that same score. What is wrong?

    Answer: The score is optimistic: the hyperparameter choice already saw every fold, so it needs nested CV to be honest — Selecting on the folds makes those folds part of the fitting process. Nested CV puts an inner CV loop inside each outer training fold to choose hyperparameters, and scores the whole pipeline on the outer fold — data no part of the tuning ever touched.

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 k×, 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
NextGradient Descent: The Descent Closest lab to this one

Explore the topic

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

More Labs