Resample with rsample
The risk committee has one question about your loan-default model: how accurate is it, really? In Lesson 3 you bundled the recipe and the model into a workflow, split the 240 applications 75/25, scored the held-out quarter, and got 82% accuracy. You write that down. Then a colleague reshuffles the very same data, takes a fresh 75/25 split, and gets 76%. Same model, same applicants, same code. Which number do you put in the report?
The honest answer is neither, because a single split rests its whole verdict on one slice of luck. This lesson replaces that one coin-flip with resampling: score the model on many splits and read the average, with a measure of how much it wobbles. The rsample package builds those splits for you.
By the end of this lesson you will be able to:
- Explain why one train/test split gives an unreliable estimate of how good a model is
- Build cross-validation folds and bootstrap resamples with rsample
- Run your whole workflow across every resample and read the average score and its spread
Prerequisites: you can run R and use the |> pipe, and you can bundle a recipe and a model into a workflow and fit it (Lesson 3).
One split is a roll of the dice
Let us prove the wobble rather than assert it. Here is the lender's loan book again, rebuilt right here so this page runs on its own. Each row is one applicant; defaulted is what we are trying to predict.
Now fit the same logistic model on twenty different random 75/25 splits and record the accuracy each time. Nothing about the model changes between runs. Only the luck of which rows land in the test set changes.
Read that again. The identical model scored as low as 57% and as high as 75%, purely because of which applicants happened to sit in the test set. If you had run it once and stopped, you would have walked into that meeting with a number that was off by up to nine points in either direction, and no way to know it.
Why does the score swing?
You fit the same logistic-regression model twenty times and its test accuracy ranged from 57% to 75%. What is the swing actually telling you?
Use every row as a test row
Cross-validation kills the luck by not choosing one split at all. Split the data into \(k\) equal parts, called folds. Take fold 1 as the validation set, train on the other \(k-1\) folds, and score. Then let fold 2 be the validation set and repeat, and so on, until every fold has had exactly one turn as the holdout. You get \(k\) scores, and every row is validated exactly once.
The single number you report is their average. If \(e_j\) is the score on fold \(j\), the cross-validated estimate is
\[ \widehat{\text{Err}}_{\text{CV}} = \frac{1}{k} \sum_{j=1}^{k} e_j \]
and because it is an average of \(k\) draws, its standard error, how much that average itself would wobble if you reran it, is
\[ \mathrm{SE} = \frac{s}{\sqrt{k}} \]
where \(s\) is the standard deviation of the \(k\) fold scores and \(k\) is the number of folds. The \(\sqrt{k}\) in the denominator is the whole point: averaging more folds shrinks the noise in your estimate. Step through the folds below and watch the per-fold scores collapse into one steadier CV mean.
Make the folds with rsample
rsample turns "split the data" into tidy objects you can hold and reuse. First carve off a final test set with initial_split and never touch it until the very end. Then make the folds out of the training part only. Passing strata = defaulted keeps the yes/no balance steady in every fold, so no fold is accidentally all non-defaulters.
The folds tibble has one row per fold, and each <split [142/37]> says "142 rows to train on, 37 held out to score." Those two halves of any single fold have names: analysis() is the part you train on, assessment() is the part you score on. (rsample avoids "train/test" here so you do not confuse a fold's inner split with the final test set you locked away.)
Ask for ten folds
Five folds gave five scores. Ten-fold cross-validation, the common default, gives ten. Change the fold count so vfold_cv builds ten folds from the training set.
Show answer
set.seed(1)
folds10 <- vfold_cv(train, v = 10, strata = defaulted)
nrow(folds10) # one row per fold
#> [1] 10Run the workflow across every fold
Now the moment it pays off. fit_resamples takes your whole workflow and your folds, and for each fold it preps the recipe on that fold's analysis rows, fits the model, scores on the assessment rows, and hands back every fold's metrics. You write one line; it does the loop.
There it is: cross-validated accuracy 0.648, with a standard error of 0.0275 across the five folds. That single, steady number sits right in the middle of the wild 0.567-to-0.750 range a single split was handing you, and now it comes with an honest sense of its own precision. (The table carries a second row, roc_auc, another way to score a classifier; read the accuracy row for now and we will unpack roc_auc in Lesson 5.)
fit_resamples re-preps the recipe inside each fold, learning the normalization and dummy coding from that fold's analysis rows only. The assessment rows never touch preprocessing, so the leakage you closed in Lesson 3 stays closed, on every fold. Resampling baked data instead would let each fold's holdout leak into its own scaling, and quietly inflate the score.What does the standard error mean?
collect_metrics reports accuracy mean = 0.648 with std_err = 0.0275 over five folds. What does that 0.0275 tell you?
The bootstrap: sample with replacement
Cross-validation is one way to resample. The bootstrap is the other, and it makes its splits differently. Instead of cutting the data into folds, it draws a sample the same size as the training set with replacement: some rows get picked two or three times, and some are not picked at all. The rows left out, about 37% of them on average, become that resample's held-out set, the out-of-bag rows.
In rsample, bootstraps() builds them just like vfold_cv built folds. Look at the analysis and assessment sizes: every analysis set is the full 179 (padded with duplicates), and each out-of-bag set is a different size near a third.
You hand boots to fit_resamples exactly as you handed it folds. So when do you reach for which? Use k-fold cross-validation as your default for an honest estimate of test performance, since every row is scored exactly once. Reach for the bootstrap when you want many resamples to study how stable an estimate is, or to build confidence intervals, which is why it powers the out-of-bag error inside a random forest.
How is the bootstrap's holdout formed?
In 5-fold cross-validation, every row is held out exactly once, in its own fold. In a bootstrap resample of the same 179 training rows, how is the held-out (assessment) set formed instead?
References
A few authoritative places to take this further:
- rsample package documentation (tidymodels) - the official reference for
initial_split(),vfold_cv(),bootstraps(),analysis(), andassessment(). - Tidy Modeling with R, ch. 10: Resampling for evaluating performance - Kuhn and Silge on why one split is not enough and how
fit_resamplesworks, end to end. - Get Started: Evaluate your model with resampling - the official walk-through that runs a workflow across folds and collects the metrics.
- An Introduction to Statistical Learning, ch. 5: Resampling Methods (free PDF) - the textbook treatment of cross-validation and the bootstrap, with the math behind the estimates.
Lesson 4 complete
You no longer report a model's accuracy from one lucky split. You build folds or bootstrap resamples with rsample, run the whole workflow across them with fit_resamples, and read a cross-validated mean that comes with an honest standard error. That single change is the difference between a number you hope is right and one you can defend.
Next, Lesson 5: Measure with yardstick. You averaged accuracy here, but accuracy is rarely the metric that matters. You will choose the right metrics for the job, compute them as a set, and read them across all your resamples at once.