rsample validation_split() in R: Train Validation Split
The rsample validation_split() function in R creates a single train/validation partition wrapped as a resampling object, so you can tune a model on the validation set without burning your held-out test set.
validation_split(df) # default 75/25 train/validation validation_split(df, prop = 0.8) # custom 80/20 validation_split(df, strata = y) # stratified by outcome analysis(vs$splits[[1]]) # extract training rows assessment(vs$splits[[1]]) # extract validation rows set.seed(123); validation_split(df) # reproducible split initial_validation_split(df, prop = c(.6,.2)) # modern 3-way split
Need explanation? Read on for examples and pitfalls.
What validation_split() does
validation_split() partitions a data frame once into a training portion and a validation portion, and returns that pair as an rset. It lives in the rsample package, the resampling engine of the tidymodels ecosystem. The split itself is the same as initial_split(), but the return value is shaped differently. You get a one-row tibble with a splits list-column, the same shape that vfold_cv() and bootstraps() produce. That shape is what tune::tune_grid() expects in its resamples argument.
The point of a held-out validation set is to tune hyperparameters without touching the final test set. You fit candidate models on the training rows, score them on the validation rows, pick the best, and only then evaluate on a fresh test split.
Syntax and arguments
The signature mirrors initial_split() so the same defaults apply.
The arguments you reach for in practice:
- data: the data frame or tibble to partition.
- prop: the fraction of rows kept for training. The default
3/4sends 75 percent to training, 25 percent to the validation slice. - strata: a column name. Splitting is performed within each level of this variable so the class mix is preserved on both sides.
- breaks: when
stratais numeric, the number of quantile bins used to stratify the values. - pool: strata levels smaller than this fraction of the data are merged before splitting, which prevents empty cells.
initial_validation_split() plus validation_set() pair covered below.validation_split() examples
Basic 80/20 train/validation split
Call validation_split() on a data frame and print the result to see the rset shape. The return value is a one-row tibble, not a single split, because rsample treats it as a resampling object with exactly one resample.
The <split [25/7]> notation reports 25 training rows and 7 validation rows. The id column tags this resample as validation, which is how tune later labels the metric output.
Extract the training and validation data
Use analysis() for training rows and assessment() for validation rows. The pair are the rsample names for what most people call the train and validation slices.
The training() and testing() helpers also work on the underlying rsplit, but analysis() and assessment() read more clearly when the second slice is a validation set rather than a test set.
Stratified split on a classification target
Pass a class column to strata to keep the class balance equal in both slices. This matters most for imbalanced outcomes where a random split can leave the validation set short on a minority class.
Every Species level lands 37 rows in training. Compare that to an unstratified split, where the counts will drift by a few rows each run.
Hand the rset to tune_grid()
The shape of validation_split() is built for tune::tune_grid(), which expects an rset in its resamples argument. You define a tunable model spec, pass the validation rset, and tune fits each grid candidate on the training rows and scores it on the validation rows.
Cross-validation rsets like vfold_cv() plug into the exact same slot. That interchangeability is the reason validation_split() returns a one-row tibble instead of a bare split.
Compare with alternatives
Use validation_split() when you want a single held-out slice with tune compatibility, but prefer initial_validation_split() for new code. The table below shows when each rsample helper fits.
| Function | Output shape | Use case |
|---|---|---|
validation_split() |
rset, 1 row | Single train/validation pair, tune-compatible (soft-deprecated) |
initial_validation_split() |
3-way split object | Train, validation, AND test in one call, modern replacement |
initial_split() |
rsplit, 1 split | Plain train/test, no tuning loop |
vfold_cv() |
rset, k rows | K-fold cross-validation in tune_grid() |
bootstraps() |
rset, t rows | Resampled performance estimates with replacement |
The modern path is initial_validation_split() followed by validation_set():
initial_validation_split() makes the three-way separation explicit so you never reach for the wrong slice.Common pitfalls
Three mistakes show up over and over when readers first reach for validation_split().
- Treating the rset as a split.
validation_split(df)returns a tibble. Callingtraining()on it errors. You must index into$splits[[1]]first, or useanalysis()andassessment()on the inner rsplit.
- Forgetting set.seed() before the call. rsample uses R's RNG to draw the partition. Without a seed, every script run produces a different split, and your reported validation metric will jitter.
- Using the validation set as the test set. Tuning on the validation slice and reporting that score as final performance leaks information. Hold out a separate test slice with
initial_validation_split()or a secondinitial_split()step.
validation_split(df, prop = 0.8) puts 80 percent in training, NOT 80 percent in validation. prop always names the training fraction, even though the function name highlights the validation slice.Try it yourself
Try it: Split iris into a 70/30 train/validation pair stratified by Species, then count how many setosa rows land in the validation slice. Save the count to ex_setosa_val.
Click to reveal solution
Explanation: Stratified splitting on Species keeps each class proportional, so a 70/30 split of 50 setosa rows puts 35 in training and 15 in the validation slice. assessment() pulls the validation rows from the inner rsplit object.
Related rsample functions
- [
initial_split()](rsample-initial_split-in-R.html) for a plain train/test pair with no tuning loop - [
vfold_cv()](rsample-vfold_cv-in-R.html) for k-fold cross-validation in tune_grid - [
bootstraps()](rsample-bootstraps-in-R.html) for resampled performance estimates - [
group_initial_split()](rsample-group_initial_split-in-R.html) when rows belong to groups that must not split across slices
External reference: the official rsample::validation_split() documentation.
FAQ
What is the difference between validation_split() and initial_split()?
initial_split() returns a single rsplit object, the shape you pass to training() and testing() for a plain holdout. validation_split() makes the same partition but wraps it as a one-row rset, the shape tune::tune_grid() expects in its resamples argument. Use initial_split() when you only need a train/test pair. Use validation_split() when the held-out rows are a validation set you will score tuning candidates against.
Is validation_split() deprecated?
It is soft-deprecated as of rsample 1.2.0, which means the function still works and will not error, but new code should prefer initial_validation_split() paired with validation_set(). The new pair produces an explicit three-way train, validation, and test split, which makes the separation between tuning and final scoring impossible to confuse.
How do I set the validation set proportion?
Pass the prop argument. prop names the training fraction, not the validation fraction, so prop = 0.8 gives 80 percent training and 20 percent validation. For a three-way initial_validation_split(), pass a two-element vector like prop = c(0.6, 0.2) for 60 percent training, 20 percent validation, and the remaining 20 percent test.
Can I use validation_split() with tune_grid()?
Yes. The one-row rset slots into tune_grid(resamples = vs) exactly like a vfold_cv() object. tune fits each candidate on the training rows, scores it on the validation rows, and reports the validation metric. For k-fold instead, swap validation_split() for vfold_cv(v = 10); everything else stays the same.
Why do I get two rows when I call assessment() on the result?
You probably called assessment() on the rset tibble rather than the inner rsplit. Index in first: assessment(vs$splits[[1]]). The $splits[[1]] unwraps the list-column to get the rsplit object the resample helpers operate on.