workflowsets workflow_set() in R: Compare Models at Once
The workflowsets workflow_set() function in R bundles a list of preprocessors and a list of parsnip model specs into a single tibble, where each row is one fully formed workflow. Pass that tibble to workflow_map() to fit, resample, or tune every combination in one call and compare them with rank_results() or autoplot().
workflow_set(preproc = list(rec = rec), models = list(lm = lm_spec)) # one preproc, one model workflow_set(list(rec1 = rec1, rec2 = rec2), list(lm = lm_spec)) # 2 preprocs x 1 model workflow_set(list(rec = rec), list(lm = lm_spec, rf = rf_spec)) # 1 preproc x 2 models workflow_set(list(rec1, rec2), list(lm, rf), cross = TRUE) # full 2x2 grid workflow_set(list(rec1, rec2), list(lm, rf), cross = FALSE) # paired element-wise extract_workflow(ws, id = "rec_lm") # pull one workflow out
Need explanation? Read on for examples and pitfalls.
What workflow_set() does
workflow_set() turns a Cartesian product of preprocessors and model specs into a tibble of workflows. It does not fit anything. It records every (preprocessor, model) pair as one row, generates a stable wflow_id for each row from the names you supplied, and returns a tibble that subsequent functions in the workflowsets package know how to map over. The actual call to each engine happens later when you pipe the set into workflow_map().
This shape is what makes model comparison painless. Instead of writing one workflow() per candidate and one fit_resamples() per workflow, you list the preprocessors once, list the models once, and let the set carry the bookkeeping. Resampling, tuning, ranking, and plotting all read from the same tibble.
info column holds each workflow object as a list-column, and the result column starts empty. workflow_map() writes resampling or tuning results into result row by row, so the set grows in place rather than producing a separate fit object per candidate.workflow_set() syntax and arguments
workflow_set() takes a named list of preprocessors and a named list of model specs. Four arguments, and most calls use only the first two.
The preproc argument accepts recipes, formulas, or workflow_variables() calls; each entry must be named because the names become the left half of every wflow_id. The models argument accepts parsnip specs (unfit), one per entry, also named. The cross argument decides whether you get every preprocessor crossed with every model (the default and most common case) or whether the two lists are zipped pairwise (rare, but useful when each preprocessor only makes sense with one model). The case_weights argument names a column from your training data that should be passed to each fit as importance weights.
workflow_set() returns a tibble with class workflow_set. Columns are wflow_id (character), info (list of one-row tibbles holding the workflow and metadata), option (list of saved options for workflow_map), and result (list, empty until workflow_map populates it).
<preproc_name>_<model_name>, so the same call always produces the same ids. That stability matters when you later filter the set by id, join it to a grid of hyperparameters, or compare runs across sessions.Four examples of workflow_set() in action
workflow_set() shines when you want a small grid of candidates compared on the same resamples. The examples below all use mtcars so they run quickly in the browser.
The first example crosses both recipes with both specs into a 2 by 2 grid. Use this when you want to ask, in one shot, which preprocessor pairs best with which model.
The second example zips the two lists pairwise with cross = FALSE. Two preprocessors, two models, two workflows. Use this when normalisation only matters for the linear model and the basic recipe only makes sense for the random forest.
The third example fits every workflow in the cross set on 5-fold cross-validation in one workflow_map() call. The verb string "fit_resamples" tells workflowsets which tune function to apply per row.
The fourth example pulls one workflow out of the set by id, in case you want to refit it on the full data after picking the winner. extract_workflow() is the inverse of add_model() plus add_recipe().
workflow_set() compared with single workflows and caret
workflow_set() is the multi-candidate sibling of workflow(). Reach for it the moment you have more than one candidate; stick with plain workflow() while you only have one.
| Tool | Returns | Best for |
|---|---|---|
workflow() |
one workflow object | a single preprocessor + single model |
workflow_set() |
a tibble of workflows | many candidates compared on the same resamples |
caret::train() |
a single train object |
legacy code; one model at a time, no shared resamples |
The contrast with caret::train() is the headline: caret fits one model per call, so comparing five models means five train() calls and manual bookkeeping. workflow_set() plus workflow_map() does the same job in two lines, with shared folds, shared metric collection, and a single tibble of results.
Common pitfalls
Unnamed lists fail loudly. Both preproc and models must be named lists. workflow_set(list(rec_basic), list(lm_spec)) errors with "names attribute must be the same length as the vector." Always pass list(basic = rec_basic, ...) even for one element.
Mixed problem types silently rank poorly. If one model spec is linear_reg() and another is logistic_reg(), every row still builds, but the workflows expect different outcomes. workflow_map() will only error at fit time, not at construction. Keep one outcome type per set.
Forgetting cross = FALSE produces n times m workflows. Two preprocessors and three models gives six workflows by default. If you only intended pairs, the set quietly explodes to six and resampling time grows with it. Always state cross explicitly when the list lengths match.
data = mtcars instead of resamples = folds. The function will error with "argument resamples is missing." Build the rset first with vfold_cv(), bootstraps(), or validation_split() and pass that.Try it yourself
Try it: Build a workflow set that crosses a single recipe (mpg ~ . with dummy encoding) against three model specs: a linear regression, a random forest with 100 trees, and a boosted tree with 50 trees. Save the result to ex_ws. Confirm it contains three rows.
Click to reveal solution
Explanation: One preprocessor crossed with three models gives three rows. The ids combine the preprocessor name with each model name, so the order in the models list determines the order of rec_lm, rec_rf, rec_bt.
Related workflowsets and tidymodels functions
workflow_map(): applyfit_resamples(),tune_grid(), ortune_bayes()to every row of a workflow set in one call.rank_results(): sort theresultcolumn by a chosen metric, returning a long tibble ready for comparison.autoplot.workflow_set(): visualise metric distributions across all workflows on the same resamples.extract_workflow(): pull one workflow back out of the set by itswflow_id.as_workflow_set(): cast a list of already-built workflows into a workflow_set tibble.
See the workflowsets reference at workflowsets.tidymodels.org for the full argument table and edge cases.
FAQ
What is the difference between workflow_set() and workflow()?
workflow() produces a single workflow object holding one preprocessor and one model. workflow_set() produces a tibble of workflows, one row per (preprocessor, model) pair. Use workflow() when you have a single candidate and workflow_set() when you want to compare many candidates on the same resamples without writing the resampling and metric-collection code multiple times.
Can workflow_set() handle hyperparameter tuning?
Yes. Build the set with workflow_set(), then call workflow_map("tune_grid", resamples = folds, grid = 10). Each row that contains a tunable parsnip spec gets its own tuning grid, and collect_metrics() or rank_results() aggregates across all rows. Pass a list of grids through the option_add() helper if different workflows need different grids.
How do I extract the best workflow after fitting a set?
Run rank_results(ws_fit, rank_metric = "rmse") to find the top wflow_id, then call extract_workflow(ws_fit, id = "the_winner") to pull that workflow back out as a normal workflow object. Refit it on the full training data with last_fit() or fit() before producing final predictions.
Does cross = FALSE require equal-length preproc and models lists?
Yes. When cross = FALSE, workflow_set zips the two lists element by element, so they must be the same length. Different lengths raise an error about mismatched dimensions. Set cross = TRUE (the default) whenever the lists differ in length, or pad the shorter list with repeated entries.
Is workflow_set() faster than fitting models one at a time?
The fit time per workflow is the same. The savings come from sharing one set of resamples across all candidates, eliminating duplicate metric-collection code, and getting parallel evaluation for free when you register a parallel backend with doParallel::registerDoParallel(). The bookkeeping savings are larger than the raw compute savings.