Concepts / Stratified Cross-Validation for Classification

Stratified Cross-Validation for Classification

Cross-validation is usually treated as a safe way to evaluate a model.

  • Programming
Interactive lab

Try it: K-Fold Cross-Validation and Model Selection

How hold-out and k-fold cross-validation choose among candidate models: each fold takes a turn as the validation set, the fold errors are averaged, the lowest average picks the model, and only then is the untouched test set used once — while tuning on the test set would report an optimistic number.

How it works

  1. Set 10 test points aside; they take no part in any choice.
  2. Optionally shuffle the 20 development points (seeded), then cut them into k folds (hold-out: one chunk validates, the rest trains).
  3. For every round, train each candidate polynomial on the other folds and measure its validation MSE on the held-out fold.
  4. Average each candidate's fold errors (with repeated shuffling: average the p run scores; p × k fits per candidate).
  5. Select the candidate with the lowest average, retrain it on all development points, and evaluate it once on the test set.
  6. For contrast, show which candidate the test set itself would have picked — that figure is leaked and optimistic.

Default run (12 steps): 20 development points (sorted by x as they arrived) and 10 test points set aside. Candidates: polynomial degree 0, 1, 2, 3, 4, 5, 6. Plan: 5-fold cross-validation, shuffled with seed 1 → 5 fits per candidate (p × k = 1 × 5), 35 in total. … 5-fold cross-validation picks degree 3 (score 0.0768); its honest test MSE is 0.0889. Picking by test error would report degree 4 at 0.0868 — a leaked, optimistic figure.

Simplified: One fixed toy dataset (20 development + 10 test points from sin(2πx) with noise) and polynomial least squares of degree 0–6 as the candidate models. Shuffling uses a seeded mulberry32 Fisher–Yates; folds are contiguous chunks like scikit-learn's KFold. Hold-out uses the first of k chunks as the validation set.

Educational simulation

Loading the simulation…

When a Sensible Default Quietly Fails

Cross-validation is usually treated as a safe way to evaluate a model. You split the data into folds, train on some folds, test on the remaining one, and repeat until every fold has been the test set once. A common assumption is that this procedure is robust to how the rows happen to be arranged. If there are enough folds, the thinking goes, the evaluation should be fair no matter what order the samples came in.

The source shows that this assumption is false. When the samples are ordered by label, a plain split can hand each fold a single class. The model is then evaluated on data it has never learned to recognize, and the resulting scores tell you very little about how the model would behave on new data. This article follows one such case step by step, then introduces the tools scikit-learn provides to take back control of how the data is divided.

The source is also direct about the usual situation. For most problems the defaults work well: regression uses standard k-fold, and classification uses stratified k-fold. The problem described here is not caused by those defaults. It appears when you have a concrete reason to choose a different strategy, such as reproducing someone else's published experiment that used standard k-fold on a classification dataset.

Predict the Fold Scores Before Looking

Before reading the trace, commit to a prediction. The iris dataset, as used in the source, is ordered so that each class occupies a contiguous block of rows. Suppose you split it into three folds with KFold, leave shuffling off, and use logistic regression as the model. What do you expect the three fold scores to look like?

What do you think happens?

With unshuffled 3-fold KFold on label-ordered iris, what do you expect the three fold scores to look like?

Reveal answer
Answer:

The scores are very poor. Each fold corresponds to one class, so the model is trained on the other two classes and tested on a class it has never seen, and nothing can be learned for that fold.

Plain KFold does not look at labels. It cuts the rows into consecutive pieces. Because the rows are already sorted by class, each piece contains one class, so the training and test distributions in each round are very different.

Where the Split Is Decided

The mechanism that gives you control is the cross-validation splitter. A splitter is an object that controls how the data is split during cross-validation. You pass it directly as the cv parameter to cross_val_score. Instead of asking for a number of folds, you hand over a configured splitter, and that splitter decides the folds itself. The number of folds can still be set this way, through the n_splits argument of KFold.

  1. 1
    Import the KFold class from the model_selection module and create an instance with the number of folds you want.
  2. 2
    Pass that object as the cv argument to cross_val_score, along with the estimator and the data.
  3. 3
    Read the returned array, which holds one score per fold.
python
Notes on the code
  • load_iris(return_X_y=True) returns the feature matrix X and the label vector y
  • KFold(n_splits=3) creates a splitter object; it does not split anything yet
  • cv=cv hands the configured splitter to cross_val_score, which decides the folds
EstimatorLogisticRegressionData X and yiris features and labelscross_val_scorepassed cv=splitterPer-fold scoresone score per foldSplitterKFold with n_splits=3modelX, ycvreturns
How do the estimator, the data, the splitter object, and the resulting per-fold scores connect when cross_val_score is called?

Tracing Unshuffled Folds Through the Ordered Labels

The source uses a deliberately bad configuration to check whether the splitter behaves as expected: three folds, no stratification, no shuffling, on a classification dataset ordered by label. The goal is to verify the claim that three-fold non-stratified cross-validation on iris is a very poor choice. Follow the rows in order.

Plain KFold splits by position. It takes the first block of consecutive rows as fold one, the next block as fold two, and the last block as fold three. Iris has 150 rows, with the classes stored as three contiguous blocks. Cutting those 150 rows into three consecutive pieces lines up each piece with one class, which is the exact outcome the source describes.

Fold 1rows 0 to 49, setosa onlyFold 2rows 50 to 99, versicoloronlyFold 3rows 100 to 149, virginicaonly
Which rows land in each fold when the data is sorted by class and split by position, and what class mix does each training set end up with?

Now look at what each round trains on. When fold one is the test set, the model is trained only on the versicolor and virginica rows. When fold two is held out, it is trained on setosa and virginica. Each round, the held-out fold is a class the model has never seen during training. That is the exact point where the result departs from a reasonable expectation: a fair evaluation needs each class to be represented in the training data, and here no fold provides that.

Why the Model Cannot Learn What It Is Tested On

Plain KFold does not look at labels at all. It only counts positions. That is why the fold boundaries say nothing about the classes they contain. When the rows are already sorted by label, the boundaries and the classes line up, and the scores stop measuring how well the model generalizes.

Shuffling as a Second Remedy

The source offers a remedy that does not require stratification: shuffle the data before splitting it. Shuffling removes the ordering of the samples by label. Shuffling is switched on with the shuffle parameter of KFold. Passing shuffle=True randomly reorders the samples before they are cut into folds, so each fold is likely to contain a mix of classes. The source reports that shuffling before splitting yields a much better result than the unshuffled run.

python
Notes on the code
  • shuffle=True randomly reorders the rows before they are cut into folds
  • random_state=42 fixes the random reordering so the same split is produced on every run

Shuffled and unshuffled runs differ in exactly one respect: whether the samples are reordered before they are split. Everything else in the setup can stay the same. That single change is what moves the class mix of each fold away from one class per fold.

Reproducibility and the random_state Setting

Shuffling introduces randomness, and randomness introduces variation between runs. Without a fixed seed, each run draws a different random ordering, which means each run creates different folds and therefore different scores. Fixing random_state makes the shuffle repeatable: the same seed produces the same fold assignment every time the code runs.

SettingFold assignmentReproducible between runs?
shuffle=FalseConsecutive row blocks in original orderYes, always the same
shuffle=True, no random_stateA different random reordering on each runNo, scores can change between runs
shuffle=True, random_state=42The same random reordering every timeYes, the same split is reproduced

How the three KFold configurations assign rows to folds

Plain KFold Versus Stratified Splitting

Shuffling is one remedy. Stratified splitting is another. The source's default for classification is stratified k-fold, and it differs from plain KFold because it looks at the labels. Plain KFold only cares about position. A stratified splitter arranges the folds so that each fold keeps roughly the same class proportions as the full dataset, so no fold ends up with one class in place of the others.

Plain fold 1setosa onlyStratified fold 1all three classes insimilar proportionPlain fold 2versicolor onlyStratified fold 2all three classes insimilar proportionPlain fold 3virginica onlyStratified fold 3all three classes insimilar proportion
How does the class composition of each test fold differ between plain KFold and StratifiedKFold, and why does that matter for accuracy?

The reason this matters for the score is that a test fold containing a class the training folds never saw cannot be classified correctly by a model that never learned that class. Stratification and shuffling both address this by making each training set resemble the full dataset, but they reach that goal differently. Stratification uses the labels directly, while shuffling relies on the random reordering to break up the class blocks.

Mistakes Beginners Make With Splitters

  • Common mistake

    Assuming enough folds makes the evaluation fair regardless of row order

    What it looks like

    Using KFold with many folds on iris sorted by class and expecting the scores to be reliable

    Why it fails

    Plain KFold splits by position, so sorted labels can still leave each fold with one class.

    Do this instead

    Shuffle the rows with shuffle=True, or use a stratified splitter when the labels are ordered.

  • Common mistake

    Writing KFold(n_splits=3) and expecting cross_val_score to shuffle

    What it looks like
    cv = KFold(n_splits=3) with no shuffle argument, then reading the result as a random split
    Why it fails

    shuffle defaults to off, so the folds are cut from the original row order.

    Do this instead

    Pass shuffle=True explicitly whenever you want a randomized split.

  • Common mistake

    Turning on shuffling without fixing random_state

    What it looks like

    Running the same KFold(shuffle=True) code twice and getting different scores

    Why it fails

    Each run draws a different random ordering, which produces different folds.

    Do this instead

    Set random_state to a fixed integer so the same split is reproduced.

  • Common mistake

    Changing the splitter but forgetting that the defaults are already appropriate for most problems

Choosing a Splitter for a Real Experiment

MEDIUM

A colleague gives you a classification dataset whose rows are sorted by label. They want a quick cross-validated estimate with three folds and say they do not care whether the result is reproducible. Using what you have learned, explain what would go wrong with plain KFold here, and write the KFold call you would use instead, including one argument that makes the split repeatable.

Hints
  • Ask what plain KFold uses to decide the folds: labels or positions?
  • Think about which argument turns on shuffling, and which argument fixes the random ordering.
  • Even if the colleague does not care about reproducibility, consider what happens to your own reruns.

Key Takeaways

  • A cross-validation splitter is an object such as KFold that controls how the data is divided, and it is passed as the cv parameter to cross_val_score.
  • Plain KFold splits by row position, so when the rows are ordered by label each fold can hold a single class, and the model is tested on a class it never learned.
  • Setting shuffle=True randomly reorders samples before splitting, which breaks up the label ordering and gives a much better result in the source example.
  • Fixing random_state makes the shuffle repeatable, so the same split and the same scores are produced on every run.
  • For most problems the defaults work well, with stratified k-fold for classification; a custom splitter is for cases where you have a concrete reason to deviate.

Related concepts

More concepts starting with S