0Pricing
Machine Learning Academy · Lesson

Stratified and Time-Series Cross-Validation

Learners will apply StratifiedKFold to maintain class ratios and TimeSeriesSplit to avoid future data leaking into past folds in time-ordered datasets.

Stratified and Time-Series Cross-Validation is a free Machine Learning Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Machine Learning Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

When Standard K-Fold Fails

Standard KFold splits data randomly without regard for class distribution or temporal order. This creates two serious problems: (1) for imbalanced datasets, some folds may have very few or no minority class examples, making fold-to-fold variation extreme; (2) for time-ordered data, training on future examples to predict the past creates data leakage that inflates CV scores far beyond real-world performance. Specialised CV strategies solve these problems without sacrificing honest evaluation.

Stratified K-Fold: Preserving Class Ratios

StratifiedKFold ensures that each fold contains approximately the same proportion of each class as the full dataset. For example, if 10% of your data is fraudulent, each fold will have approximately 10% fraud cases. This prevents the scenario where one fold has no fraud cases, making it impossible for the classifier to learn to detect fraud in that round. Use StratifiedKFold whenever you have more than 2 classes or significant class imbalance.

from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_breast_cancer
import numpy as np

X, y = load_breast_cancer(return_X_y=True)
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(RandomForestClassifier(random_state=42), X, y, cv=skf)
print('Stratified 5-fold:', np.round(scores, 4))
print('Mean:', round(scores.mean(), 4), 'Std:', round(scores.std(), 4))

Verifying Stratification

You can verify that StratifiedKFold maintains class ratios by inspecting the target distribution in each fold. For each test fold, the proportion of positive examples should match the overall positive rate. Comparing the class distributions from standard KFold and StratifiedKFold on an imbalanced dataset clearly shows why stratification matters — fold class ratios can differ by 20%+ without stratification.

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

# Imbalanced data: 90% negative, 10% positive
np.random.seed(42)
y = np.array([0]*90 + [1]*10)
X = np.random.randn(100, 5)

for name, cv in [('KFold', KFold(n_splits=5, shuffle=True, random_state=42)),
                  ('StratifiedKFold', StratifiedKFold(n_splits=5, shuffle=True, random_state=42))]:
    ratios = [y[test].mean() for _, test in cv.split(X, y)]
    print(f'{name} positive rates per fold:', np.round(ratios, 2))

cross_val_score Uses Stratification Automatically

When you pass cv=5 (an integer) to cross_val_score for classification tasks, scikit-learn automatically uses StratifiedKFold. For regression, it uses plain KFold. This means most users get stratification for free without explicitly constructing a StratifiedKFold object. Only override this default when you need custom split behaviour (e.g., group splits, time-series splits, or a different number of folds with shuffling).

Time Series Data: The Unique Challenge

Financial, sensor, weather, and many other real-world datasets are ordered in time. A key property of time-series models: predictions must be made using only past data. If you train a model on data from April and use it to predict January values, you are training on the future — this is called temporal data leakage. Even standard K-Fold CV violates this: when fold 3 is the test set and fold 4 is in the training set, future data predicts the past. TimeSeriesSplit prevents this.

TimeSeriesSplit: Always Train on the Past

TimeSeriesSplit creates folds where training data always precedes test data chronologically. In each split, the training set expands by including the previous test fold. For example, with 5 splits: split 1 trains on the first 20% and tests on the next 20%, split 2 trains on the first 40% and tests on the next 20%, and so on. This ensures no future information enters the training set and the CV score reflects realistic temporal generalisation.

from sklearn.model_selection import TimeSeriesSplit
import numpy as np

n = 100
X = np.random.randn(n, 5)
y = np.random.randn(n)

tscv = TimeSeriesSplit(n_splits=5)
for fold, (train_idx, test_idx) in enumerate(tscv.split(X, y), 1):
    print(f'Fold {fold}: train [{train_idx[0]}-{train_idx[-1]}], '
          f'test [{test_idx[0]}-{test_idx[-1]}]')

Using TimeSeriesSplit with cross_val_score

Pass a TimeSeriesSplit object directly to the cv parameter of cross_val_score. The resulting scores measure how well the model generalises when trained on earlier data and evaluated on later data — which is exactly the real-world use case. Note that the training sets grow in size across folds, so earlier folds have fewer training examples and may show lower scores — this is expected, not a bug.

from sklearn.model_selection import TimeSeriesSplit, cross_val_score
from sklearn.ensemble import GradientBoostingRegressor
import numpy as np

# Simulated time-series: price = trend + noise
np.random.seed(42)
n = 200
X = np.arange(n).reshape(-1, 1) + np.random.randn(n, 1) * 5
y = 0.5 * np.arange(n) + np.random.randn(n) * 10

tscv = TimeSeriesSplit(n_splits=5)
scores = cross_val_score(GradientBoostingRegressor(n_estimators=50, random_state=42),
                          X, y, cv=tscv, scoring='r2')
print('TimeSeriesSplit R² scores:', np.round(scores, 4))
print('Mean R²:', round(scores.mean(), 4))

GroupKFold: Preventing Sample Leakage

Some datasets have multiple samples per entity — multiple ECGs per patient, multiple transactions per user, multiple images per scene. If the same patient appears in both training and test folds, the model learns patient-specific patterns rather than general patterns. GroupKFold ensures all samples from the same group (patient, user, scene) are in the same fold, preventing this within-group leakage. Pass group identifiers to the groups parameter of cross_val_score.

from sklearn.model_selection import GroupKFold, cross_val_score
from sklearn.linear_model import LogisticRegression
import numpy as np

np.random.seed(42)
n = 100
X = np.random.randn(n, 5)
y = np.random.randint(0, 2, n)
groups = np.repeat(np.arange(10), 10)  # 10 patients, 10 samples each

gkf = GroupKFold(n_splits=5)
scores = cross_val_score(LogisticRegression(max_iter=1000), X, y, cv=gkf, groups=groups)
print('GroupKFold scores:', np.round(scores, 4))
print('Mean:', round(scores.mean(), 4))

Expanding Window vs Sliding Window for Time Series

TimeSeriesSplit uses an expanding window: the training set grows with each split. An alternative is a sliding window: use only the most recent N days/months for training, discarding older data. Sliding windows are better when the relationship between features and target changes over time (concept drift) and older data is no longer representative. scikit-learn does not provide a sliding window CV out of the box, but you can implement it using a custom CV generator or TimeSeriesSplit with max_train_size.

from sklearn.model_selection import TimeSeriesSplit
import numpy as np

# Sliding window: fix the training size
tscv = TimeSeriesSplit(n_splits=4, max_train_size=50)  # max 50 training samples
X = np.random.randn(200, 3)
for fold, (train_idx, test_idx) in enumerate(tscv.split(X), 1):
    print(f'Fold {fold}: train size={len(train_idx)}, '
          f'test [{test_idx[0]}-{test_idx[-1]}]')

Choosing the Right CV Strategy

A simple decision guide: (1) Classification with balanced classes → standard KFold or cross_val_score integer (auto-stratified); (2) Classification with imbalance → StratifiedKFold explicitly; (3) Time-ordered data → TimeSeriesSplit; (4) Multiple samples per entity → GroupKFold; (5) Imbalanced + grouped → StratifiedGroupKFold (scikit-learn 0.24+). Getting the CV strategy wrong can make a poorly generalising model look great or a good model look mediocre — always match the CV method to your real-world deployment scenario.

Walk-Forward Validation for Production

In real production time-series systems, models are retrained periodically and evaluated on the subsequent period. Walk-forward validation simulates this: train on months 1-6, evaluate on month 7; train on months 1-7, evaluate on month 8; and so on. This is equivalent to TimeSeriesSplit with gap parameter to add a buffer between train end and test start (to avoid leakage from closely adjacent time points). The resulting score sequence also reveals whether model performance is stable over time or degrading.

from sklearn.model_selection import TimeSeriesSplit
import numpy as np

# Simulate 24 months of data
n_months = 24
np.random.seed(42)
X = np.random.randn(n_months, 5)
y = np.random.randn(n_months)

# Walk-forward: 12 train periods, 1 test each
tscv = TimeSeriesSplit(n_splits=12, test_size=1)
for fold, (tr, te) in enumerate(tscv.split(X), 1):
    if fold <= 3 or fold == 12:
        print(f'Fold {fold:2d}: train months 1-{len(tr)}, test month {te[0]+1}')

Quick Check

Test your understanding of Stratified and Time-Series cross-validation from this lesson.

Lesson Recap

In this lesson you learned: StratifiedKFold preserves class proportions across folds for imbalanced classification, TimeSeriesSplit prevents temporal leakage by always training on past data, and GroupKFold prevents within-group leakage when multiple samples belong to the same entity. Next up we compare Grid Search and Random Search for hyperparameter optimisation.

Frequently asked questions

Is the “Stratified and Time-Series Cross-Validation” lesson free?

Yes — the full text of “Stratified and Time-Series Cross-Validation” is free to read here on the web, and the Machine Learning Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Machine Learning Academy course, upgrade to CoddyKit PRO.

What will I learn in “Stratified and Time-Series Cross-Validation”?

Learners will apply StratifiedKFold to maintain class ratios and TimeSeriesSplit to avoid future data leaking into past folds in time-ordered datasets. You practise Machine Learning Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Machine Learning Academy?

No prior experience is required. Machine Learning Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Stratified and Time-Series Cross-Validation” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Machine Learning Academy lesson?

Yes. Every Machine Learning Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. K-Fold Cross-Validation: Splitting Without Leaking
  2. Stratified and Time-Series Cross-Validation
  3. Grid Search vs Random Search
  4. Nested Cross-Validation: Selecting and Evaluating Simultaneously
← Back to Machine Learning Academy