ML Cross Validation

Cross validation repeatedly splits the data into training and validation folds so a model's performance estimate does not depend on the luck of a single train/test split.

Why a Single Train/Test Split Isn't Enough

A single train_test_split gives one accuracy number, but that number depends heavily on which rows happened to land in the test set. Reshuffle the split with a different random_state and the score can move noticeably, especially on small or imbalanced datasets, making it hard to tell whether a change to the model actually helped or the score just moved because of which rows were held out this time.

Cross validation fixes this by splitting the data into k roughly equal parts, called folds, and running k rounds of training and evaluation. In each round, one fold is held out as the validation set while the model trains on the remaining k-1 folds; over the k rounds every row gets used for validation exactly once. Averaging the k scores gives a much more stable estimate of how the model performs on unseen data than any single split could.

Example

import numpy as np
from sklearn.model_selection import KFold
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import make_classification

X, y = make_classification(n_samples=200, n_features=6, random_state=0)
kf = KFold(n_splits=5, shuffle=True, random_state=0)

fold_scores = []
for train_idx, test_idx in kf.split(X):
    model = LogisticRegression(max_iter=1000)
    model.fit(X[train_idx], y[train_idx])
    fold_scores.append(model.score(X[test_idx], y[test_idx]))

print('Fold scores:', fold_scores)
print('Mean accuracy:', np.mean(fold_scores))

cross_val_score and Choosing k

Scikit-learn's cross_val_score function automates the loop shown above: hand it an unfitted estimator, the full feature matrix and labels, and a number of folds, and it returns an array of scores, one per fold. For classification tasks, prefer StratifiedKFold (which cross_val_score uses by default for classifiers) so that each fold keeps roughly the same proportion of each class as the full dataset; plain KFold can otherwise create folds with very few or zero examples of a minority class.

Example

from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import make_classification

X, y = make_classification(n_samples=300, n_features=8, random_state=7)

model = RandomForestClassifier(n_estimators=150, random_state=7)
scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')

print('Scores per fold:', scores)
print('Mean +/- std: %.3f +/- %.3f' % (scores.mean(), scores.std()))
  • Shuffle the data before splitting (shuffle=True) unless it is already in random order, or folds can end up unbalanced
  • Use StratifiedKFold for classification so each fold keeps the same class proportions as the full dataset
  • Fit any scaler, encoder, or imputer inside a Pipeline so it is refit separately on each fold's training data
  • A larger k gives a less biased estimate but costs more compute and can increase variance between runs
k ValueBiasVarianceCompute Cost
k = 3Higher (less training data per fold)LowerLow
k = 5 or 10Balanced, the most common choiceBalancedModerate
k = n (leave-one-out)LowestHigherVery high

Example

from sklearn.model_selection import cross_validate, StratifiedKFold
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
from sklearn.datasets import make_classification

X, y = make_classification(n_samples=300, n_features=8, weights=[0.7, 0.3], random_state=3)

pipeline = make_pipeline(StandardScaler(), SVC(kernel='rbf', C=1.0))
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=3)

results = cross_validate(pipeline, X, y, cv=cv, scoring=['accuracy', 'f1'])

print('Accuracy per fold:', results['test_accuracy'])
print('F1 per fold:      ', results['test_f1'])
Note: cross_val_score is the fast path for a single metric. Reach for cross_validate when you need several metrics at once, timing information, or the fitted estimator from each fold.
Note: Never fit a scaler or encoder on the whole dataset before cross-validating. That lets information from each validation fold leak into training and produces an optimistically biased score; always fit preprocessing steps inside a Pipeline passed to cross_val_score.

Exercise: ML Cross Validation

What is the main purpose of cross-validation?