Data Science Cross Validation

Cross validation estimates how well a model generalizes by repeatedly splitting the data into training and validation folds, so the reported performance does not depend on one lucky or unlucky split.

The Problem With a Single Train/Test Split

A single train_test_split() gives one accuracy number, but that number depends partly on which rows happened to land in the test set. Shuffle the split differently and the score can move around, especially with smaller datasets. Cross validation addresses this by testing the model on several different splits and reporting the average, giving a much more stable and trustworthy estimate of real-world performance.

How K-Fold Splitting Works

In k-fold cross validation, the dataset is divided into k equally sized folds (chunks). The model is trained k separate times: each time, one fold is held out as the validation set and the remaining k-1 folds are used for training. Every fold gets exactly one turn as the validation set, so every row in the dataset is used for validation exactly once and for training k-1 times. The final reported score is the average of the k individual validation scores.

  • Split the data into k folds of roughly equal size (a common choice is k=5 or k=10).
  • Round 1: train on folds 2-k, validate on fold 1.
  • Round 2: train on folds 1, 3-k, validate on fold 2.
  • ...continue until every fold has served as the validation set exactly once.
  • Average the k validation scores into a single cross-validation score.

Example: 5-fold cross validation with cross_val_score

from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score

X, y = load_iris(return_X_y=True)

model = LogisticRegression(max_iter=1000)
scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')

print('Score per fold:', scores)
print('Mean accuracy:', scores.mean())
print('Standard deviation:', scores.std())
Note: Looking at the standard deviation across folds, not just the mean, is valuable: a small standard deviation means the model performs consistently across different subsets of the data, while a large one suggests the model's performance is sensitive to which rows it happens to see.

Stratified K-Fold for Classification

For classification problems with imbalanced classes, plain k-fold splitting can accidentally create folds where a minority class is over- or under-represented. StratifiedKFold fixes this by preserving the original class proportions in every fold. scikit-learn's cross_val_score automatically uses stratified folds for classifiers, but you can pass a StratifiedKFold object explicitly for full control.

Example: Explicit StratifiedKFold

from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_breast_cancer

X, y = load_breast_cancer(return_X_y=True)

cv = StratifiedKFold(n_splits=10, shuffle=True, random_state=1)
model = LogisticRegression(max_iter=5000)

scores = cross_val_score(model, X, y, cv=cv)
print('Mean accuracy over 10 folds:', scores.mean())
k valueTrade-off
Small (e.g. k=3)Faster, but each training fold is smaller and estimates can be noisier
Large (e.g. k=10)More stable estimate, but k times slower since it fits k models
k = n (leave-one-out)Uses almost all data for training each round, very slow on large datasets
Note: Cross validation estimates generalization performance; it does not replace a final held-out test set. When you also tune hyperparameters (for example with grid search) using cross validation, keep a separate test set untouched until the very end to get an unbiased final number.