Concepts / Stratified k-fold cross-validation

Stratified k-fold cross-validation

Stratified k-fold cross-validation keeps the proportions between classes the same in each fold as in the whole dataset.

  • 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 Fair-Looking Split Gives Meaningless Scores

Most people meet k-fold cross-validation as a recipe. Cut the dataset into k folds, let each fold take a turn as the test set, train on the rest, and average the scores. The recipe says nothing about which samples land in which fold, so it is easy to assume that cutting the data into consecutive pieces is harmless. For a classifier, that assumption can break badly. A split that looks neutral can quietly make the evaluation meaningless, even when the model itself is perfectly good.

The lesson of this article is that the way data is cut into folds matters just as much as the model. The source uses the iris dataset to show the problem. Before you look at any output, try to reason about it yourself. Suppose the iris samples are arranged so that all samples of one class come first, then the next class, then the last class. If you cut that ordered sequence into three consecutive folds without shuffling or stratifying, what would each fold contain? Commit to an answer in your head before reading on.

What do you think happens?

With KFold(n_splits=3) and no shuffling, on iris sorted by class label, what does each fold contain?

  • A mix of all three classes in every fold

  • Only one class in each fold

  • Two classes in each fold, with the third class split across folds

Reveal answer
Answer:

Only one class in each fold

The samples are in three consecutive label blocks, and an unshuffled split assigns rows by position, so each consecutive block becomes one fold. When a fold is held out, the class it contains is absent from the training data.

Cutting Ordered Labels into Consecutive Folds

The source loads the iris target labels with scikit-learn and prints them directly, which makes the ordering visible. The labels come in three consecutive blocks: the first third of the samples is class 0, the second third is class 1, and the last third is class 2. Nothing about the classifier is wrong here. The problem is in the split, because a standard k-fold split assigns rows by their position in the dataset, not by their class.

Now imagine three-fold cross-validation on this ordered data. The first fold is made only of class 0. In the first split, the test set therefore holds only class 0, and the training set holds only classes 1 and 2. The next two splits have the same mismatch: the classes in the training and test sets are different in every split. A classifier trained on two folds has never seen the class that the held-out fold contains, so it has no fair chance to predict it.

The source reports that three-fold accuracy on this arrangement would be zero, which is far below what a working classifier achieves on iris. The zero is a symptom of the split, not of the classifier. Keep this in mind when a result looks wrong: the place to look first is which samples ended up in which fold.

Passing a Splitter Object to cross_val_score

The cv parameter of cross_val_score can take a plain number, which sets the number of folds and hides the splitting rule behind it. It can also accept a cross-validation splitter, which is an object that controls how the data is split. Instead of accepting whatever rule sits behind a number, you build the splitter yourself and hand it over. That gives you finer control over the splitting step, and it makes the rule visible in your code.

The splitter used in the source is the KFold class, which lives in the model_selection module. You import it and instantiate it with the number of folds you want, set through the n_splits argument. The instance is a reusable object, so it can be passed to cross_val_score directly through the cv argument. The call then uses exactly the folds this object produces, rather than building its own.

python
Output (expected)
[0. 0. 0.]
Notes on the code
  • KFold(n_splits=3) creates a splitter that makes three folds
  • cv=kfold hands that exact splitter to cross_val_score
  • The three printed scores are the end product of the three train-and-test rounds

Watch where the scores come from. Each score is produced by training on the samples in the other folds and testing on the held-out fold. The score list is the end product, so when it looks wrong, the cause is usually upstream, in which samples ended up in which fold. The zero output above is the result the source describes for unshuffled three-fold splitting on this ordered data. The model is not the place to look first.

Tracing a Nine-Sample Split Step by Step

A small example makes the divergence easy to see. Take nine samples with labels in three consecutive blocks: [0, 0, 0, 1, 1, 1, 2, 2, 2]. This dataset is invented for teaching, not taken from the source, but it has the same ordered structure as the iris example. Each class makes up one third of the data, so the overall class mix is even.

Worked example

Plain KFold(n_splits=3) on ordered labels

Assign nine label-sorted samples to three consecutive folds and check whether each fold matches the overall class mix of one third per class.

  1. 1
    Step 1: Count the overall class mix

    There are three samples of class 0, three of class 1, and three of class 2. Each class is one third of the nine samples, so a fair fold should hold one sample of each class.

  2. 2
    Step 2: Apply the plain split by position

    A standard k-fold split takes rows by position. Fold 1 gets positions 0 to 2, fold 2 gets positions 3 to 5, and fold 3 gets positions 6 to 8. Because the labels are sorted, fold 1 is all class 0, fold 2 is all class 1, and fold 3 is all class 2.

  3. 3
    Step 3: Find the step where the mix diverges

    The divergence happens at Step 2, when rows are assigned by position. The overall mix is one third per class, but each fold holds only one class. When fold 2 is the test set, the training data holds only classes 0 and 2, so the classifier never sees class 1 during training.

Result

Each fold is a single class. The plain split diverges from the expected class mix at the moment rows are cut by position, not when the model is trained.

Sample 0class 0Sample 1class 0Sample 2class 0Sample 3class 1Sample 4class 1Sample 5class 1Sample 6class 2Sample 7class 2Sample 8class 2
Which classes end up in each fold when consecutive positions are cut into three folds. Each fold holds exactly one class, so every held-out fold removes a class from training.

Read the diagram as a trace. Each fold is a consecutive run of positions, and because the labels are sorted, each run holds one class. No fold contains the full class mix, so no single test fold tells you how the classifier handles the other two classes. Trace this to the step that caused it: the position-based assignment, not the classifier.

How Stratification Keeps Class Proportions

Stratified k-fold cross-validation fixes the problem by changing what the split must preserve. The rule is that the proportions between classes are the same in each fold as they are in the whole dataset. The split is still into k folds, and each fold still takes a turn as the test set. What changes is that each fold is built to mirror the class composition of the full data, rather than whatever happens to sit in a block of rows.

Rule

Here $n_{c,f}$ is the number of samples of class $c$ in fold $f$, $n_f$ is the size of fold $f$, $n_c$ is the number of samples of class $c$ in the whole dataset, and $N$ is the total number of samples. The rule says that for each class, its share inside a fold should match its share in the full dataset. The approximately-equal sign is deliberate. Real datasets do not always divide evenly, so the goal is that each fold is representative, not that every fold has an identical count.

The source gives a simple ratio to make this concrete. If 90% of the samples belong to class A and 10% belong to class B, stratified cross-validation ensures that in each fold, 90% of the samples are class A and 10% are class B. The composition of every fold is anchored to the composition of the whole dataset, which is the property a fair test set needs.

Fold 1only class 0Fold 1one of each classFold 2only class 1Fold 2one of each classFold 3only class 2Fold 3one of each class
How the class content of each fold differs when the same label-sorted samples are split plainly versus with stratification. In the stratified split, every fold mirrors the one-third-per-class mix of the whole dataset.

The stratified column in the comparison is the generated nine-sample example, where each class is one third of the data, so every fold gets exactly one sample of each class. With real data the counts rarely divide this neatly, and each fold only needs to approximate the overall proportions.

Why Stratification Is the Usual Choice for Classifiers

The source recommends stratified k-fold cross-validation as the usual choice for evaluating a classifier, because it gives more reliable estimates of generalization performance. The reasoning is concrete. Suppose only 10% of the samples belong to class B. With standard k-fold, one fold can easily contain only samples of class A. Using that fold as the test set tells you little about overall performance, because the classifier was never asked to handle class B on that split.

Stratification matters most for imbalanced classification, where a standard split can produce a test fold that says little about the minority class. Scikit-learn uses stratified k-fold for classification by default. The stratified counterpart of the KFold splitter is available in the same model_selection module, so the choice is about which splitter you pass to cv, not about changing the rest of your code.

Shuffling with shuffle and random_state

The source offers two remedies for the ordered-label failure. The first is stratifying the folds. The second is to shuffle the data before cutting the folds, so that the ordering of samples by label is removed. Shuffling is controlled through the shuffle parameter of KFold. Setting shuffle=True randomizes the order of the samples before the split. The source reports that shuffling before splitting gives a much better result than the unshuffled three-fold run, and it attributes that improvement to removing the label ordering.

python
Notes on the code
  • shuffle=True randomizes the sample order before the folds are cut
  • random_state=42 fixes the shuffle so the same split is produced on every run
  • Any fixed integer works; what matters is that the same value is reused
Before
kfold = KFold(n_splits=3)

Rows are cut in their stored order, so label-sorted data gives one class per fold.

After
kfold = KFold(n_splits=3, shuffle=True, random_state=42)

Rows are reordered first, so a fold drawn from the shuffled sequence is far more likely to contain more than one label, and the same random_state reproduces the same split.

Shuffling introduces randomness, and uncontrolled randomness makes results hard to reproduce. The source is direct about this: if you shuffle the data, you also need to fix random_state to get a reproducible shuffling. Without a fixed random_state, each run of cross_val_score would use a different split and so could give a different result. The source adds that this might not be a problem in every setting, but it can be surprising.

The same reasoning helps you reproduce someone else's results. If you know the number of folds, the shuffle setting, and the random_state, you have pinned down the split. Using the same number of folds is the detail the source emphasizes when describing reproduction. Recording those settings alongside a score is what makes the score meaningful to someone else.

Choosing a Splitting Strategy for the Task

Stratification is about class proportions, so it applies to classification. For regression, scikit-learn uses standard k-fold cross-validation by default. The source notes that one could try to make each fold representative of the different values a regression target takes, but this is not a commonly used strategy and would surprise most users. The practical lesson is to match the splitting strategy to the kind of prediction being evaluated.

Prediction task?ClassificationStratified k-folddefault for classifiersReproducing someoneelse's results?Match their folds,shuffle, random_stateRegressionStandard k-folddefault for regressionclassificationregressionstratify by classstandard by defaultcheck the purposeyes
Which path leads to stratified k-fold, standard k-fold, or matching a specific earlier setup. The deciding question is the type of prediction, and whether a result needs to be reproduced exactly.

The flowchart encodes the source's guidance, with one caveat. The source says the defaults of standard k-fold for regression and stratified k-fold for classification work well for most use cases. It also names a situation where people often reach for a different strategy: reproducing someone else's results with standard, non-stratified k-fold on a classification dataset. Whether that choice is harmful depends on how the samples are ordered, which is the part learners tend to miss.

Mistakes Beginners Make with Splitters

  • Common mistake

    Assuming the default splitting strategy always works

    What it looks like

    Running an unshuffled KFold on label-sorted iris and trusting a score of zero on every fold as a skill estimate.

    Why it fails

    The defaults work for most use cases, but the way the data is cut into folds decides whether the evaluation is fair. For classifiers, a fold with only one class tests nothing about the other classes.

    Do this instead

    Check the class mix inside each fold, and use stratified k-fold for classifiers unless you have a specific reason to match a different setup.

  • Common mistake

    Tuning the model when the scores look wrong

    What it looks like

    Changing the model's settings after seeing three identical zero scores.

    Why it fails

    A uniform bad score is a symptom of the split, not of the classifier. Tuning the model will not fix which samples landed in each fold.

    Do this instead

    Trace the result back to the splitting step and inspect which samples and labels ended up in each fold.

  • Common mistake

    Shuffling without fixing random_state

    What it looks like

    Setting shuffle=True but omitting random_state, then getting different scores on each run.

    Why it fails

    Each run produces a different split, so results cannot be reproduced.

    Do this instead

    Pass a fixed random_state alongside shuffle=True and record the fold count, shuffle setting, and random_state with the score.

  • Common mistake

    Expecting stratification to matter for regression

    What it looks like

    Passing a stratified splitter to a regression evaluation and expecting class-balanced folds.

    Why it fails

    Stratification keeps class proportions, which is a classification concept. Regression uses standard k-fold by default.

    Do this instead

    Match the splitter to the prediction task: stratified for classification, standard for regression.

Practice: Trace the Split Before Trusting the Score

MEDIUM

Labels are sorted as [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2] (twelve samples, four per class). You run KFold(n_splits=3) with no shuffling. Without running any code, predict which classes each fold contains, and identify the step where the plain split diverges from the overall class mix of one third per class.

Hints
  • Plain KFold assigns rows by position, so the three folds are consecutive runs of four positions each.
  • Compare the class content of each fold with the overall mix of four samples per class.
  • Ask what a classifier trained on the other two folds would have seen when predicting the held-out fold.

Key Takeaways

  • A standard k-fold split assigns rows by position, so when the data is ordered by class label, folds can contain only one class and the evaluation can be meaningless.
  • Passing a KFold object with n_splits to cross_val_score through the cv parameter makes the splitting rule explicit and reusable.
  • Stratified k-fold cross-validation keeps the class proportions in each fold the same as in the whole dataset, which is why it is the usual default for evaluating classifiers.
  • Shuffling with shuffle=True removes label ordering, and a fixed random_state makes the shuffled split reproducible.
  • For regression, standard k-fold is the default, so match the splitting strategy to the prediction task and record the split settings with any score you report.

Related concepts

More concepts starting with S