Cross-Validation (CV)

Cross-validation splits the data into \(k\) folds, trains on \(k-1\) of them and tests on the one left out, rotating until every fold has served as the test set once, then averages the \(k\) scores. A single train/test split gives one number that depends on which rows happened to land where; cross-validation averages that away.

What the number refers to

Cross-validation trains \(k\) different models and never keeps any of them. What gets deployed is refit on all the data, and that model has no cross-validated score of its own.

So the estimate belongs to the procedure: this algorithm, with these hyperparameters and this preprocessing, applied to a dataset of about this size. That is usually what you want when comparing two approaches, and it is not the same as an estimate of the thing you shipped. It is also slightly pessimistic for that thing, since each fold’s model trained on less data than the final one.

How much the number moves

import numpy as np
from sklearn.model_selection import cross_val_score, KFold
from sklearn.tree import DecisionTreeClassifier

rng = np.random.default_rng(1)
model = DecisionTreeClassifier(random_state=0)

def one_dataset(n=200, seed=7):
    r = np.random.default_rng(seed)
    x = r.normal(size=(n, 4))
    return x, (r.uniform(size=n) < 1 / (1 + np.exp(-x[:, 0]))).astype(int)

x, y = one_dataset()
across_seeds = [cross_val_score(model, x, y, cv=KFold(5, shuffle=True, random_state=s)).mean()
                for s in range(50)]
print(f"one dataset, 5-fold over 50 split seeds  min {min(across_seeds):.4f}  "
      f"max {max(across_seeds):.4f}  sd {np.std(across_seeds, ddof=1):.4f}")

means, naive = [], []
for _ in range(150):
    xx = rng.normal(size=(200, 4))
    yy = (rng.uniform(size=200) < 1 / (1 + np.exp(-xx[:, 0]))).astype(int)
    scores = cross_val_score(model, xx, yy, cv=KFold(10, shuffle=True, random_state=0))
    means.append(scores.mean())
    naive.append(scores.std(ddof=1) / np.sqrt(10))
true_sd = float(np.std(means, ddof=1))
print(f"10-fold: true sd of the estimate {true_sd:.4f}  "
      f"naive SE from the folds {np.mean(naive):.4f}  ratio {true_sd / np.mean(naive):.2f}")
# one dataset, 5-fold over 50 split seeds  min 0.4700  max 0.6150  sd 0.0330
# 10-fold: true sd of the estimate 0.0476  naive SE from the folds 0.0341  ratio 1.40

The first line takes one dataset and one model and varies only the shuffling seed. Five-fold cross-validation returns anything from 0.4700 to 0.6150. Nothing about the data or the model differs between those runs.

The second line addresses the error bar usually quoted alongside — the standard deviation across folds divided by \(\sqrt{k}\). Measured against the estimate’s true spread over fresh datasets, that figure is too small by a factor of 1.40 here. The reason is that the folds are not independent: any two training sets share most of their rows, so their errors are correlated and the usual formula for the standard error of a mean does not apply.

Bengio and Grandvalet show why a universal unbiased variance estimator for K-fold CV is unavailable without further assumptions. That does not establish a universal one-percentage-point significance threshold. Compare models using their paired performance differences, an uncertainty procedure appropriate to the sampling dependence, and the scale that matters for the application.

Which variant

VariantWhen
k-fold, \(k = 5\) or \(10\)The default. Enough folds to average out the split, few enough to afford
Stratified k-foldA common classifier default that approximately preserves class proportions. Respect time and group constraints first; stratification can also shrink the observed fold-to-fold variability
Leave-one-outk=n. Requires n fits and trains each model on nearly all observations. Its error dependence and uncertainty depend on the learner and distribution; nearly identical training sets do not imply almost perfectly correlated held-out errors
Time-basedAnything with a time order. Shuffling lets the model train on the future — see time-based splits
GroupedRepeated measurements on the same patient, user or device. Rows from one group must not straddle the split
NestedWhen hyperparameters are tuned. An inner loop tunes, an outer loop scores, so the reported number is not the one that was optimized

The last row is the one most often skipped. Choosing a model by its cross-validated score and then reporting that score describes the best of many draws, not the procedure — the same reason a maximum over noisy estimates is biased upward.

What it cannot rescue

Cross-validation protects against one thing: a lucky or unlucky single split. It does nothing about a preprocessing step fitted on all the data before the folds were drawn, which leaks the test set into training and can turn pure noise into an apparently strong model — that is measured under k-fold cross-validation. Nor does it help when the evaluation data does not look like production data, which no resampling scheme can detect.

References


Discover more from Insightful Data Lab

Subscribe to get the latest posts sent to your email.

Similar Posts

Questions, corrections, or additional insights?

This site uses Akismet to reduce spam. Learn how your comment data is processed.