Free demoJob-oriented courses at Debugshala, Old Palasia, Indore. Freshers, non-IT and year-gap welcome.Book a free demo +91-8982385539

Cross-Validation Explained: K-Fold, Stratified and Time Series

Lesson 4 of 27 · 11 min · Updated 13 Nov 2026

Cross-validation explained with runnable scikit-learn code: k-fold, stratified, time series and group splits, leakage-free pipelines and how to read the scores.

Picture the selectors for a Ranji Trophy side choosing between two young batters from Madhya Pradesh. One scored 140 in a single practice match on a flat pitch at Holkar Stadium. The other scored 55, 62, 38, 71 and 49 across five matches on five different pitches.

One innings tells you about one pitch on one day. Five innings on five pitches tell you how the batter plays in general, and how much the scores swing.

A single train/test split is the one innings. Cross-validation is the five-match record.

What is cross-validation in machine learning?

Map the cricket story to model building:

  • Each match is one fold, a slice of your data the model has not trained on.
  • The runs scored in each match are the fold scores.
  • The batting average is the cross-validated score, and the spread of runs is its standard deviation.

Cross-validation is a resampling method that trains and evaluates a model several times on different train/validation splits of the same data and averages the results, giving a more reliable estimate of performance on unseen data than one split.

It sits inside the workflow from model evaluation metrics: you still keep a final test set aside, and cross-validation runs on the training portion to compare models and settings. It is also the main tool for spotting the problem described in overfitting and the bias-variance trade-off.

This tutorial uses scikit-learn 1.7, the version that runs in the browser on this site.

Why is one train/test split not enough?

A random split decides which rows land in the test set. With small or medium datasets, that luck of the draw moves the score a lot. Run the same model on the same 200 rows with ten different random splits:

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

# 200 loan applicants, 6 numeric features, label 1 = repaid on time
X, y = make_classification(n_samples=200, n_features=6, n_informative=3,
                           flip_y=0.1, random_state=42)

scores = []
for seed in range(10):
    X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=seed)
    model = LogisticRegression().fit(X_tr, y_tr)
    scores.append(model.score(X_te, y_te))

print("accuracy on 10 different random splits:")
print(np.round(scores, 3))
print(f"lowest {min(scores):.3f}, highest {max(scores):.3f}")

Nothing about the model changed, yet accuracy ranges from 0.80 to 0.95. If you had compared two models using one split each, a 5-point difference could be pure luck. That is the problem cross-validation solves.

How does k-fold cross-validation work?

K-fold is the standard method. Follow these steps:

  1. Shuffle the rows (unless the data has a time order).
  2. Cut them into k equal parts called folds. Five or ten is typical.
  3. Train on k−1 folds and score on the one left out.
  4. Repeat until every fold has been the validation fold exactly once.
  5. Report the mean of the k scores and their standard deviation.
K-fold cross-validation diagram: five rounds, each using a different fold as the validation set and the other four folds for training

In 5-fold cross-validation every row is used for validation exactly once and for training four times. The final score is the average of the five fold scores.

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

X, y = make_classification(n_samples=200, n_features=6, n_informative=3,
                           flip_y=0.1, random_state=42)

kf = KFold(n_splits=5, shuffle=True, random_state=0)
for i, (train_idx, val_idx) in enumerate(kf.split(X), start=1):
    print(f"fold {i}: train on {len(train_idx)} rows, validate on {len(val_idx)} rows")

scores = cross_val_score(LogisticRegression(), X, y, cv=kf, scoring="accuracy")
print("fold scores:", np.round(scores, 3))
print(f"cross-validated accuracy = {scores.mean():.3f} +/- {scores.std():.3f}")

The result, about 0.88 ± 0.04, is a far more honest summary than any single number from the previous block. The ± part tells you how much to trust a small gap between two models.

Types of cross-validation and when to use each

Plain k-fold assumes rows are independent and shuffled freely. Real data often breaks that assumption, and scikit-learn has a splitter for each case.

Splitter How it splits Use it when
KFold Random equal folds Regression on independent rows
StratifiedKFold Keeps class ratio in every fold Any classification, especially imbalanced
TimeSeriesSplit Train on the past, validate on the next block Sales, prices, sensor readings by date
GroupKFold All rows of one group stay in one fold Many rows per customer, patient or shop
LeaveOneOut Each row alone is a validation set Very small datasets (under ~50 rows)
RepeatedStratifiedKFold Stratified k-fold run several times with new shuffles Small data where you want a steadier average

Stratified k-fold for classification

When one class is rare, random folds can end up with very few or none of it. A fold with zero fraud cases cannot measure fraud recall at all.

Python
import numpy as np
from sklearn.model_selection import KFold, StratifiedKFold

# 100 insurance claims, only 10 are fraud (label 1)
y = np.array([1] * 10 + [0] * 90)
X = np.zeros((100, 1))                      # features don't matter for splitting

for name, cv in [("KFold", KFold(5, shuffle=True, random_state=3)),
                 ("StratifiedKFold", StratifiedKFold(5, shuffle=True, random_state=3))]:
    counts = [int(y[val].sum()) for _, val in cv.split(X, y)]
    print(f"{name:>16}: fraud cases in each validation fold = {counts}")

Plain KFold gives one fold no fraud at all. StratifiedKFold puts exactly 2 in each. When you pass an integer such as cv=5 with a classifier, scikit-learn already uses stratified folds, but without shuffling. Passing your own splitter makes the behaviour explicit. Rare classes get a full treatment in handling imbalanced data.

Time series split for dated data

If you shuffle a year of sweet-shop sales, the model trains on December and is tested on March. In real life you never know the future when you predict, so this score is too optimistic.

Time series cross-validation diagram: an expanding training window of past months followed by a validation block of the next two months

TimeSeriesSplit always trains on earlier months and validates on the months that come right after, so the model never sees the future.

Python
import numpy as np
from sklearn.model_selection import TimeSeriesSplit

# 12 months of sales for a sweet shop, Jan to Dec
months = np.array(["Jan", "Feb", "Mar", "Apr", "May", "Jun",
                   "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"])
tscv = TimeSeriesSplit(n_splits=4, test_size=2)

for i, (tr, te) in enumerate(tscv.split(months), start=1):
    print(f"split {i}: train {months[tr][0]}-{months[tr][-1]} ({len(tr)} months)"
          f" -> test {', '.join(months[te])}")

The training window grows each round, and validation always comes later. For more on dated data, see time series analysis basics.

Group k-fold when rows are not independent

Suppose a hospital in Indore gives you 400 scans from 40 patients, 10 scans each. Scans from the same patient look alike. If one patient's scans land in both train and validation, the model can recognise the patient instead of learning the condition.

Python
import numpy as np
from sklearn.model_selection import KFold, GroupKFold, cross_val_score
from sklearn.neighbors import KNeighborsClassifier

rng = np.random.default_rng(0)
# 40 patients, 10 scans each. Each patient has a personal "signature",
# and the label (condition yes/no) is per patient, unrelated to the features.
patients = np.repeat(np.arange(40), 10)
signature = rng.normal(0, 3, (40, 4))
X = signature[patients] + rng.normal(0, 0.3, (400, 4))
y = rng.integers(0, 2, 40)[patients]

model = KNeighborsClassifier(n_neighbors=3)
plain = cross_val_score(model, X, y, cv=KFold(5, shuffle=True, random_state=0))
grouped = cross_val_score(model, X, y, cv=GroupKFold(5), groups=patients)
print(f"KFold (same patient in train and test): {plain.mean():.3f}")
print(f"GroupKFold (patients kept apart):       {grouped.mean():.3f}")

The labels here are random, so no honest model can beat about 50%. Plain KFold reports 99% because the model simply matches each scan to its patient's other scans. GroupKFold reports about 56%, the truth.

Quick check

In 5-fold cross-validation on 500 rows, how many rows are in each validation fold?

Quick check

You are predicting next month's electricity demand from 3 years of daily readings. Which splitter fits?

Quick check

A model scores 0.99 with KFold but 0.60 with GroupKFold grouped by customer. What is the likely cause?

How do you avoid data leakage during cross-validation?

Leakage happens when information from the validation fold reaches training. The usual culprit is preprocessing on the full dataset first: fitting a scaler, an imputer or a feature selector on all rows, then cross-validating. The validation rows have already influenced the training step.

The fix is a Pipeline. When you pass a pipeline to cross_validate, scikit-learn re-fits every step on each fold's training rows only.

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

X, y = make_classification(n_samples=300, n_features=8, n_informative=4,
                           random_state=1)
X[:, 0] *= 1000                                 # one feature in rupees, others small

pipe = make_pipeline(StandardScaler(), SVC())   # scaler is re-fitted inside every fold
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
res = cross_validate(pipe, X, y, cv=cv, scoring=["accuracy", "f1"],
                     return_train_score=True)

for key in ["train_accuracy", "test_accuracy", "test_f1"]:
    print(f"{key:>15}: {res[key].mean():.3f}")

cross_validate returns several metrics at once, plus training scores. A training accuracy of 0.96 against a validation accuracy of 0.92 is a small, healthy gap. A gap of 20 points would point to overfitting. Scaling itself is covered in data preprocessing: scaling and encoding.

What happens after cross-validation?

Cross-validation scores a recipe (model type plus settings), not a final model. Once you have picked the best recipe:

  1. Re-train it on the whole training set.
  2. Score it once on the held-out test set.
  3. Ship that model.

The five fold models are thrown away. When you also use cross-validation to pick settings, you are doing hyperparameter tuning, the next topic in this series, and GridSearchCV automates exactly these steps.

Common cross-validation mistakes

  • Shuffling time-ordered data. Use TimeSeriesSplit, or your score will look better than real forecasting performance.
  • Ignoring groups. Rows from the same customer, patient or device must stay in one fold.
  • Preprocessing before splitting. Fit scalers, imputers and selectors inside a pipeline.
  • Reporting only the mean. Always show the standard deviation. 0.88 ± 0.01 and 0.88 ± 0.09 are very different results.
  • Tuning on the test set after cross-validation. The test set is for one final check.

Choosing the right splitter is easier when someone reviews your setup on real project data, which is part of the practice in Debugshala's machine learning classes in Old Palasia. You can also solve cross-validation MCQs on Debugshala Test.

Key takeaways

  • One random split gives a noisy score; cross-validation averages several splits for a steadier estimate.
  • K-fold uses each row for validation once and for training k−1 times. Five or ten folds is the usual choice.
  • Match the splitter to the data: stratified for classes, time series for dates, group for repeated entities.
  • Report mean and standard deviation, and treat differences within one standard deviation as ties.
  • Wrap preprocessing in a Pipeline so nothing leaks from the validation fold.
  • After choosing a recipe, re-train on all training data and test once.
Take the free Machine Learning quiz10 quick questions with explanations. No sign-up needed, and you can come back to this lesson after.

FAQ

What is the main purpose of cross-validation?

The main purpose of cross-validation is to estimate how well a model will perform on unseen data, using only the data you already have. It repeats training and validation across several splits and averages the results, so you can compare models and settings without being fooled by one lucky or unlucky split.

How many folds should I use in k-fold cross-validation?

Five or ten folds is the standard choice. Five is faster and works well for most datasets; ten gives a slightly less biased estimate when data is limited. With very large datasets, even three folds or a single validation split can be enough, because each fold already holds plenty of rows.

What is the difference between k-fold and stratified k-fold?

K-fold splits rows into folds at random. Stratified k-fold also keeps the proportion of each class the same in every fold, so a 10% minority class stays near 10% everywhere. Use stratified k-fold for classification, especially when one class is rare.

Does cross-validation prevent overfitting?

Cross-validation does not prevent overfitting by itself, but it detects it. A large gap between training and validation scores, or a model whose validation score drops as it gets more complex, shows overfitting. You then fix it with simpler models, regularisation or more data, using cross-validation to confirm the fix.

Can I learn cross-validation and model validation at Debugshala in Indore?

Yes. Debugshala runs a Machine Learning course at its Old Palasia campus in Indore, taught in English and Hindi, where model validation is practised on project datasets. Fresh graduates, non-IT learners and people with a year gap can join, and you can attend a free demo class before paying anything.

From the team behind this site

Go further with the Data Science & AI Course at Debugshala

These tutorials are written by Debugshala, a training and placement institute at Old Palasia, Indore. The classroom course adds live mentoring, real projects and interview preparation.

  • Offline classes at Old Palasia, Indore
  • Free demo class before you pay
  • Non-IT, fresher and year-gap friendly
  • Placement assistance

700+ students placed · 200+ hiring partners · 4.9★ Google rating

Model Evaluation Metrics: Accuracy, F1, ROC-AUC, RMSE and MoreRidge, Lasso and Elastic Net: Regularization in Python