Cross-validation estimates how well a model will perform on new data by training and testing it several times on different splits. In k-fold cross-validation the training data is divided into k parts (folds); each fold takes a turn as the validation set while the model trains on the other k − 1, giving k scores instead of one:
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import cross_val_score
scores = cross_val_score(LinearRegression(), X, y, cv=5, scoring="neg_mean_absolute_error")
print(-scores.mean(), scores.std())The average shows expected performance and the spread shows how stable it is. This lesson covers the fold procedure, choosing a splitter that matches your data, choosing a metric that matches the decision, and wrapping preprocessing in a pipeline so each fold stays leak-free. The examples predict attendance at community sports sessions. It builds on training and testing datasets.
Fold procedure
With cv=5, scikit-learn:
- splits the data into five folds;
- for each fold, fits a fresh copy of the model on the other four and scores it on that fold;
- returns five scores.
from sklearn.model_selection import cross_validate
results = cross_validate(model, X, y, cv=5, scoring="neg_mean_absolute_error",
return_train_score=True)
print(results["test_score"], results["train_score"])Look at every fold score, not just the mean or the best one. Widely varying scores mean performance depends heavily on which rows the model sees, often a sign of too little data or a few unusual records. A large gap between training and validation scores indicates overfitting.
Cross-validation is for comparing and tuning models using training data. It doesn't replace a final test set: if you use cross-validation scores to choose among many models, the winning score is slightly optimistic, so confirm the final choice once on held-out test data.
Appropriate splitters
The splitter decides which rows end up together, and must match how the data was produced:
| Splitter | Use when |
|---|---|
KFold(shuffle=True) | rows are independent (default for regression is KFold without shuffling) |
StratifiedKFold | classification; keeps class proportions in each fold (default for classifiers) |
GroupKFold | rows come in groups (same venue, patient, user) that must not be split |
TimeSeriesSplit | rows are ordered in time; each fold trains on the past and validates on the future |
from sklearn.model_selection import TimeSeriesSplit
for train_idx, val_idx in TimeSeriesSplit(n_splits=3).split(X):
print(train_idx.min(), train_idx.max(), "->", val_idx.min(), val_idx.max())Using plain k-fold on time-ordered data lets models train on future sessions and validate on past ones, producing scores that won't hold in real forecasting. Using it on grouped data lets near-identical rows from the same venue appear in both training and validation. Pass the splitter object as cv=, and for grouped splitters pass groups= too.
Metrics
The metric should match the decision the model supports:
| Task | Metric | scoring= |
|---|---|---|
| regression, errors in real units | mean absolute error | "neg_mean_absolute_error" |
| regression, large errors especially bad | root mean squared error | "neg_root_mean_squared_error" |
| regression, share of variance explained | R² | "r2" |
| classification, balanced classes | accuracy | "accuracy" |
| classification, rare positive class | F1, recall, precision | "f1", "recall" |
| ranking quality of probabilities | ROC AUC | "roc_auc" |
scikit-learn always maximises scores, so error metrics are returned negated: a score of −3.2 means an MAE of 3.2. Flip the sign when reporting.
A single overall number can hide failure on an important subgroup: a model may predict weekday sessions well and weekend sessions badly. Check errors per subgroup that matters, and always compare with a baseline such as DummyRegressor evaluated with the same folds.
Pipelines
Preprocessing steps that learn from data must be refitted inside each fold. A pipeline does that automatically:
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import Ridge
pipeline = make_pipeline(StandardScaler(), Ridge(alpha=1.0))
scores = cross_val_score(pipeline, X, y, cv=5, scoring="neg_mean_absolute_error")In each fold, the scaler is fitted on that fold's training rows only, then applied to its validation rows. Scaling the whole dataset first, then cross-validating, lets every validation fold influence the scaling, which leaks information. The same applies to imputing missing values, selecting features and encoding categories.
Pipelines also make hyperparameter search safe: GridSearchCV(pipeline, {"ridge__alpha": [0.1, 1, 10]}, cv=5) tunes the model while keeping preprocessing inside the folds. Parameter names use the step name, two underscores and the parameter.
Independent practice and a worked check
Task: compare attendance predictors with an appropriate splitter, fold scores, a baseline and an untouched final test set.
from sklearn.dummy import DummyRegressor
from sklearn.linear_model import Ridge
from sklearn.metrics import mean_absolute_error
from sklearn.model_selection import TimeSeriesSplit, cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
X = [[t, w] for t, w in [(12, 0), (14, 1), (9, 0), (16, 1), (18, 0), (11, 1),
(20, 0), (15, 1), (22, 0), (13, 1), (19, 0), (24, 1)]]
y = [21, 34, 19, 38, 30, 33, 36, 41, 40, 38, 39, 52]
X_dev, y_dev, X_test, y_test = X[:9], y[:9], X[9:], y[9:] # last 3 sessions held out
cv = TimeSeriesSplit(n_splits=3)
for name, est in [("baseline", DummyRegressor()), ("ridge", make_pipeline(StandardScaler(), Ridge()))]:
folds = -cross_val_score(est, X_dev, y_dev, cv=cv, scoring="neg_mean_absolute_error")
print(name, [round(float(f), 1) for f in folds])
final = make_pipeline(StandardScaler(), Ridge()).fit(X_dev, y_dev)
print("final test MAE:", round(mean_absolute_error(y_test, final.predict(X_test)), 1))Fold scores guide the choice; the last three sessions are scored once at the end. For the worked check, preprocessing goes inside the cross-validation pipeline to fit it separately on each training fold.
Reference and next practice
scikit-learn's cross-validation guide and scoring parameter reference cover every splitter and metric. Continue with clustering with k-means. Practise with the Python exercises and Python quiz.
Cross-Validation and Evaluation Summary
- Fold procedure: Cross validation fits on training folds and evaluates on their held out counterparts.
- Appropriate splitters: Stratified, grouped and temporal splitters express different observation assumptions.
- Metrics: A metric should match the outcome and operational consequences.
- Pipelines: A pipeline fits preprocessing inside each training fold before evaluating that fold.
Cross-Validation and Evaluation Examples
Each example shows the code, the output it produces and what each part does.
Cross-Validation and Evaluation in a small program
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import cross_val_score
X = [[n] for n in range(12)]
y = [3 * n + 2 for n in range(12)]
scores = cross_val_score(LinearRegression(), X, y, cv=3, scoring="neg_mean_absolute_error")
print(len(scores))
print(round(float(-scores.mean()), 6))3
0.0Line-by-line explanation
- Lines 3 to 4Perfectly linear data: y = 3x + 2.
- Line 5Three folds, scored with negative mean absolute error.
- Lines 6 to 7Three scores, and an average error of 0 because a straight line fits this data exactly. Real data never gives a perfect zero.
Comparing a model with a baseline across folds
- Ridge beats the baseline in every fold under both splitters. The baseline's time-series errors grow in later folds because attendance keeps rising, which an average of earlier sessions can't follow.
from sklearn.dummy import DummyRegressor
from sklearn.linear_model import Ridge
from sklearn.model_selection import KFold, TimeSeriesSplit, cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
# features: temperature (°C), weekend (0/1), weeks since launch; target: attendance
X = [[t, w, n] for n, (t, w) in enumerate([(12, 0), (14, 1), (9, 0), (16, 1), (18, 0), (11, 1),
(20, 0), (15, 1), (22, 0), (13, 1), (19, 0), (24, 1)])]
y = [21, 34, 19, 38, 30, 33, 36, 41, 40, 38, 39, 52]
model = make_pipeline(StandardScaler(), Ridge(alpha=1.0))
baseline = DummyRegressor(strategy="mean")
for name, cv in [("shuffled 4-fold", KFold(4, shuffle=True, random_state=0)),
("time series", TimeSeriesSplit(n_splits=4))]:
for label, est in [("baseline", baseline), ("ridge", model)]:
scores = -cross_val_score(est, X, y, cv=cv, scoring="neg_mean_absolute_error")
print(f"{name:<16} {label:<8} MAE per fold {[round(float(s), 1) for s in scores]} mean {scores.mean():.1f}")shuffled 4-fold baseline MAE per fold [8.1, 8.0, 3.6, 6.9] mean 6.6
shuffled 4-fold ridge MAE per fold [1.1, 1.7, 1.6, 1.0] mean 1.4
time series baseline MAE per fold [3.5, 9.3, 7.5, 12.5] mean 8.2
time series ridge MAE per fold [1.6, 2.6, 1.2, 0.9] mean 1.6Line-by-line explanation
- Line 12Scaling sits inside the pipeline, so it's refitted within every fold.
- Lines 15 to 17The same two splitters are applied to both estimators, so their scores are directly comparable.
- Line 18Error scores are negated by scikit-learn; the minus sign turns them back into positive MAE values.
Common Cross-Validation and Evaluation Mistakes
Preprocessing the whole dataset before cross-validation
X_scaled = StandardScaler().fit_transform(X)
cross_val_score(Ridge(), X_scaled, y, cv=5) # each fold's validation rows shaped the scalingcross_val_score(make_pipeline(StandardScaler(), Ridge()), X, y, cv=5)Reporting only the mean of negated scores
print(cross_val_score(model, X, y, cv=5, scoring="neg_mean_absolute_error").mean())
# -4.2: looks like a negative error and hides the fold-to-fold spreadmae = -cross_val_score(model, X, y, cv=5, scoring="neg_mean_absolute_error")
print(f"MAE {mae.mean():.1f} ± {mae.std():.1f} attendees over {len(mae)} folds")Cross-Validation and Evaluation in Real Projects
Cross-validation in practice
Data scientists use cross-validation to compare feature sets, model types and hyperparameters on the same footing, usually through GridSearchCV or RandomizedSearchCV. Forecasting teams use rolling-origin (time series) validation that mirrors how models are retrained and used each week. Healthcare and education projects use grouped folds so evaluation reflects new patients or new schools.
Reports present the mean and spread across folds next to a baseline, then confirm the chosen model once on an untouched test set. That two-stage approach is what makes a reported number trustworthy.
Cross-Validation and Evaluation FAQ
What is k-fold cross-validation?
A method that splits data into k folds, trains on k − 1 of them and validates on the remaining one, repeating so each fold is used for validation once. The k scores are averaged to estimate performance and their spread shows stability.
How many folds should I use?
Five or ten is standard. More folds use more data for training each time but take longer and can give noisier individual scores. For very small datasets, leave-one-out or repeated k-fold can help; for time series use TimeSeriesSplit.
Why are cross_val_score results negative?
scikit-learn treats every score as "higher is better", so error metrics are negated: neg_mean_absolute_error returns −MAE. Multiply by −1 before reporting.
Do I still need a test set if I use cross-validation?
Yes, when you use cross-validation to choose between models or settings. The chosen configuration's cross-validation score is slightly optimistic; a final evaluation on untouched test data gives an unbiased estimate.
What is the difference between cross_val_score and cross_validate?
cross_val_score returns one array of scores for one metric. cross_validate can compute several metrics at once, return training scores and fit times, and optionally return the fitted estimators.
Cross-Validation and Evaluation Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Why include preprocessing inside the cross validation pipeline?
Which review concern is valid for cross-validation and evaluation?
Cross-Validation and Evaluation Quiz
2 questions to check what you learned.
Why include preprocessing inside the cross validation pipeline?
- To inspect the final test target
- To fit it separately on each training fold
- Python ignores the distinction
Which statement correctly describes metrics?
- A metric should match the outcome and operational consequences.
- Only the example output matters, not the rule
- Every operation has the same behavior