Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

🎲 Resampling Strategies

As future data scientists, you probably know how expensive data collection is. Time, money, and experimental design all limit how much data we get, so we cannot afford to set aside a large part of it just for testing. At the same time, we need to know how well a model will do on new data.

Resampling methods solve this by splitting the same data many times into training and test sets and combining the results. You already saw why this is necessary in the bias-variance exercise: depending on the random train/test split, the test MSE of the very same model varied between 0.20 and 0.40.

The data

We will use the famous Iris dataset, which contains 150 samples from three species of the iris plant (iris setosa, iris virginica and iris versicolor). The data contains four features: the length and the width of the sepals and petals (in centimeters).

The three iris species setosa, versicolor and virginica, with petals and sepals labelled

Figure 1:The three iris species in the dataset. Image source: @naidubhavya06 on Medium.

Loading...
<Figure size 640x480 with 1 Axes>

The goal of our model is to predict the species of a flower from the two features shown in the plot (sepal length and width). What kind of problem is this?

Loading...
Loading...
Loading...
Loading...

Validation Sets

The simplest resampling strategy is to split the dataset into two parts:

Validation set approach

Figure 2:The validation set approach splits the dataset into a training and a validation set.

The training and validation set neither need to be of equal size nor do they need to be contiguous blocks in the data. train_test_split() shuffles the rows before splitting, so the sorted species are not a problem here. Let’s try the validation set approach on the Iris data:

  1. Define features and target data

  1. Split the data into training and validation samples

  1. Fit the model (we use a support vector classifier, which you will learn about later in the seminar)

  1. Evaluate model performance

0.85

The score() method returns the accuracy of our predictions. In this case, our algorithm correctly predicted the species of the flower in 85% of cases.

Loading...
Loading...
Loading...
Loading...

How much does that 85% actually mean?

The number above came out of exactly one split, chosen by random_state=42. There is nothing special about 42, so what happens if we repeat the same procedure with 500 different random splits?

Source
mean  0.792
std   0.044
range 0.633 – 0.917
<Figure size 700x400 with 1 Axes>

The spread is large. Depending only on the random seed, the same model reaches anywhere between 63% and 92% accuracy. Our 85% was a lucky split, and a single split should never be reported as the performance of a model.

Try it yourself: the split ratio matters too. Is it better to train on 80% of the data and validate on 20%, or the other way round? Think about what you expect, then complete the code below with the test sizes 0.2, 0.5 and 0.8 and run it. To keep luck out of the comparison, the code averages over 200 random splits for each ratio:

import numpy as np
from sklearn import datasets, svm
from sklearn.model_selection import train_test_split

# Load the data: sepal length and width as features, species as target
df = datasets.load_iris(as_frame=True).frame
X = df[["sepal length (cm)", "sepal width (cm)"]]
y = df["target"]

test_sizes = ...  # TODO: fill in the test sizes as a list

for test_size in test_sizes:
    accuracies = []
    for seed in range(200):
        X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=test_size, random_state=seed)
        accuracies.append(svm.SVC(kernel='linear').fit(X_tr, y_tr).score(X_te, y_te))

    print(f"train on {1 - test_size:.0%} / validate on {test_size:.0%}   ->   mean accuracy = {np.mean(accuracies):.3f}, std = {np.std(accuracies):.3f}")
Show answer

With more training data, the model is slightly better on average. With only 20% of the data for validation, however, the accuracy varies most between splits, because each estimate is based on just 30 flowers. The validation set approach cannot escape this tradeoff: every observation we use for validation is missing from training, and the other way round.

Cross-Validation (CV)

K-fold CV

To get a more reliable estimate, we split the data several times and use every observation for validation exactly once.

In k-fold CV, the dataset is divided into kk equally sized folds. In each round, one fold is the validation set and the remaining k−1k-1 folds form the training set. This is repeated kk times, so that every fold is used for validation once. The average accuracy across the kk folds is our estimate of the model’s performance.

Cross validation

Figure 3:K-fold cross-validation splits the dataset into kk equally sized folds. Each fold is used once as the validation set, while the model is trained on the remaining k−1k-1 folds.

Let’s try it on our data:

Average accuracy:      0.807
Individual accuracies: [0.9   0.767 0.767 0.833 0.767]

Let’s see what happens without shuffling:

Without shuffling: [1.    0.8   0.3   0.767 0.2  ] → mean 0.613

The third fold contains only versicolor and the fifth fold only virginica flowers, and their accuracy drops to 0.3 and 0.2. The mean of 0.61 is caused by the row order alone. Whenever the row order of your data has some structure (sorted by group, recorded by session, ordered in time), shuffling or a stratified splitter matters more than the choice of kk.

For classification, it is usually even better to use StratifiedKFold, which keeps the class proportions the same in every fold. If you pass a plain integer to cross_val_score, scikit-learn does this automatically:

Average accuracy: 0.807

scikit-learn offers many more splitters for different kinds of data. The scikit-learn user guide on cross-validation gives a good overview, with a visualization of each splitter.

If we are interested in the individual models, we can also run the training and evaluation explicitly, which allows us to save them:

Accuracy per fold: [0.9   0.767 0.767 0.833 0.767]

Try it yourself: complete the code below with kk = 2, 5, 10, 20 and 50 folds and run it. What do you think is a good tradeoff?

from sklearn import datasets, svm
from sklearn.model_selection import KFold, cross_val_score

# Load the data: sepal length and width as features, species as target
df = datasets.load_iris(as_frame=True).frame
X = df[["sepal length (cm)", "sepal width (cm)"]]
y = df["target"]

ks = ...  # TODO: fill in the numbers of folds as a list

for k in ks:
    cv = KFold(n_splits=k, shuffle=True, random_state=42)
    s = cross_val_score(svm.SVC(kernel='linear'), X, y, cv=cv)
    print(f"k = {k:>2}   mean accuracy = {s.mean():.3f}   "
          f"std across folds = {s.std():.3f}   ({k} model fits)")
Show answer

The mean barely changes once k≥5k \ge 5, while the standard deviation across folds keeps growing. With more folds, each validation fold is smaller, so the individual fold scores get noisier, even though their average stays stable. Beyond k=5k = 5 or 10, the extra model fits buy you very little.

Leave-one-out CV (LOOCV)

LOOCV is a special case of k-fold cross-validation, where kk equals the number of observations. The model is trained on all but one observation and validated on the remaining one. This is repeated for every observation, so each one is used for validation exactly once.

LOOCV gives an almost unbiased estimate, but it needs one model fit per observation. Also, each fold score can only be 0 or 1, because there is just a single observation to classify. In code, we only need to change the splitter from KFold() to LeaveOneOut():

Average accuracy:      0.800
Individual accuracies: [0. 1.]  (only 0 or 1 possible)
Number of fits:        150

Bootstrapping

Bootstrapping estimates how much a result would vary if we had collected a different dataset. Instead of a single training set, we create many new datasets by sampling with replacement from the original data. Each bootstrap sample has the same size as the original dataset, but some observations appear more than once, while others do not appear at all.

For each bootstrap iteration:

  1. A new sample (the bootstrap sample) is drawn from the data.

  2. The model is trained on this bootstrap sample.

  3. The observations that were not drawn, called out-of-bag (OOB) samples, are used to test the model.

Cross-validation is usually the better choice to estimate predictive performance. Bootstrapping is mostly used to quantify uncertainty, for example of model parameters or of a performance estimate.

Let’s run 200 bootstrap iterations and look at the distribution of the OOB accuracies:

Mean OOB accuracy:   0.790
Middle 95% of runs:  0.686 – 0.877
<Figure size 700x400 with 1 Axes>

The mean OOB accuracy is very close to the 5-fold estimate (0.81), because both are computed on data the model did not see during training. The histogram adds what a single number cannot show: depending on which flowers end up in the training data, the accuracy varies roughly between 69% and 88%.

Source
Mean fraction of distinct observations: 0.636
Theoretical value 1 - (1 - 1/n)^n:      0.633

Test your understanding of this session:

Loading...
Loading...
Loading...
Loading...