Define models with parsnip
In Lesson 1 you cleaned the lender's data with a recipe. Now you need the model that actually predicts who defaults. Here is the snag: R has dozens of modeling packages, and each one speaks its own dialect. The argument names differ, the way you hand over data differs, even the shape of the answer differs. parsnip gives you one calm way to describe any model, so you can fit a logistic regression today and a random forest tomorrow without rewriting a line of the code around it.
By the end of this lesson you will be able to:
- Tell a model (the mathematical form) apart from the engine (the package that fits it)
- Build a parsnip spec from three plain choices: model type, engine, and mode
- Fit it, read the tidy predictions, and swap in a completely different model without touching the rest of your code
Prerequisites: you can run R and use the |> pipe, and you have built a recipe to prep and bake your data.
Every model speaks a different dialect
Each modeling package in base R was written by different people at different times, so each has its own way of being called. Suppose our lender wants to try three models on the same question, who defaults. Watch how little the three calls have in common.
# Logistic regression
glm(defaulted ~ ., data = train, family = binomial)
# Random forest
randomForest(defaulted ~ ., data = train, ntree = 500)
# Penalized logistic (lasso)
glmnet(x = model.matrix(defaulted ~ ., train)[, -1],
y = train$defaulted, family = "binomial")
Three models, three different stories. The differences are not cosmetic; they reach all the way to how you read the result.
| Model | Function | How the data goes in | What predict() returns |
|---|---|---|---|
| Logistic regression | glm() |
a formula plus a data frame | log-odds numbers you convert yourself |
| Random forest | randomForest() |
a formula plus a data frame | a factor of classes, or a votes matrix |
| Lasso | glmnet() |
a numeric x matrix plus a y vector |
a matrix, one column per penalty value |
To move from one model to the next you rewrite the call, reshape the data, and re-learn how to read the output. parsnip removes all three frictions at once.
A model, an engine, and a mode
parsnip asks you for three things, each in plain words.
- The model type: the mathematical form of the model, such as a logistic regression or a random forest.
logistic_reg()andrand_forest()are model types. - The engine: which package actually does the fitting. The same logistic regression can be estimated by
glm, byglmnet, even by Stan. You pick one withset_engine(). - The mode: are you predicting a category (classification) or a number (regression)? You choose with
set_mode().
The model-versus-engine split is the heart of parsnip, so let us make it precise. A logistic regression is a mathematical statement: the log-odds of the outcome are a straight-line combination of the predictors,
\[ \log\frac{p}{1-p} = \beta_0 + \beta_1 x_1 + \cdots + \beta_k x_k \]
where \(p\) is the probability the applicant defaults, \(x_1, \dots, x_k\) are the predictors (income, age, and so on), and \(\beta_0, \dots, \beta_k\) are the numbers the model learns. That equation is the model. How you actually find the best \(\beta\) values, by maximum likelihood in glm, by penalized likelihood in glmnet, by sampling in Stan, is the engine. Same model, different engine.
In R, the spec reads like a sentence:
One honest catch lives in the word engine. The engine package has to be installed. glm rides along with R itself, but to use the randomForest engine in a moment, the randomForest package must be present, and parsnip will tell you plainly if it is missing.
A spec is a plan until you fit it
Notice what just happened, or rather what did not. We described a model, yet no data has been touched and nothing has been learned. A spec is a blueprint, exactly as the recipe in Lesson 1 was a plan until prep() ran it. Learning happens only when you call fit() with training data.
First, the lender's loan book, built right here so this page stands on its own:
Now fit the spec to the training set. parsnip hands your formula and data to the engine, runs it, and keeps the real fitted model in a tidy wrapper. extract_fit_engine() hands that real model back, and here it is an ordinary glm object you could pass to any glm method:
Model or engine?
You have a spec that reads logistic_reg() |> set_engine("glm"). You change it to logistic_reg() |> set_engine("glmnet"). What did you change?
Swap the model, keep your code
Here is what the unified interface buys you. To try a random forest instead of a logistic regression, you change the spec and nothing else. The fit() call and the predict() call stay exactly the same.
A random forest is a completely different algorithm (hundreds of decision trees voting), yet the code around it does not flinch. We load the engine package so parsnip can reach it, then fit on the same training data:
Same fit(), same predict(). Only the first line, the spec, changed. That is the whole promise: your modeling code stops caring which algorithm is underneath it.
Predictions you can always read
Look again at what predict() returned: a tibble with one tidy column, .pred_class. That is not a glm quirk or a random forest quirk. parsnip guarantees the same shape for every model. Ask for class probabilities instead and you always get one column per class, each named for its class:
Compare that to base R, where every model answers predict() in its own format:
| Model | What base-R predict() hands back |
|---|---|
glm() |
a numeric vector on the log-odds scale, unless you remember type = "response" |
randomForest() |
a factor of classes, or a votes matrix with type = "prob" |
glmnet() |
a matrix, one column per penalty value |
With parsnip the answer is always a tibble, with the same number of rows as your input and predictably named columns: .pred_class for the label, and .pred_<level> for each probability. You can pipe it straight into the next step without reshaping anything.
.pred_ columns straight to yardstick to score the model, with no glue code in between.What does predict() give you?
You fit two parsnip models, a logistic regression and a random forest, and call predict(fit, new_data = test) on each with no type argument. What comes back?
Specify a decision tree
Your lender wants to try one more model: a single decision tree, fit by the rpart engine, for classification. The model type is decision_tree(), and the mode is already set for you. Add the one verb that names the fitting package, then check it.
Show answer
library(rpart)
tree_spec <- decision_tree(tree_depth = 5) |>
set_engine("rpart") |>
set_mode("classification")
tree_spec
#> Decision Tree Model Specification (classification)
#>
#> Main Arguments:
#> tree_depth = 5
#>
#> Computational engine: rpartOne interface, many more engines
parsnip ships with engines for the common models, and you can always ask what is on offer for a given model type:
When you need a model parsnip does not cover out of the box, an add-on package registers new engines that plug into the same interface. The most useful one for tabular data is bonsai, which adds the modern boosted-tree engines, LightGBM and CatBoost, behind the familiar boost_tree() model type:
library(bonsai)
boost_spec <- boost_tree(trees = 500, learn_rate = 0.05) |>
set_engine("lightgbm") |>
set_mode("classification")
fit_boost <- fit(boost_spec, defaulted ~ ., data = train)
That spec reads exactly like the others: a model type, an engine, a mode. (LightGBM relies on a compiled library, so run that block in a local R session rather than here in the browser.) The point holds either way: once you know the three-part pattern, every model in the tidymodels universe looks the same.
References
- parsnip package documentation (tidymodels) - the official reference for model types, set_engine(), set_mode(), and fit().
- Tidy Modeling with R, ch. 6: Fitting models with parsnip - Kuhn and Silge's walk-through of the specify, fit, predict pattern.
- Search parsnip models and engines - a browsable list of every model type and the engines that fit it.
- bonsai package documentation - the add-on that registers LightGBM and CatBoost as parsnip engines.
Lesson 2 complete
You can now describe any model the tidymodels way: a model type, an engine, and a mode, fit with one call and read through predictably tidy predictions. That single interface is what lets you swap a logistic regression for a random forest, or a boosted tree, without rewriting the code around it.
Next, Lesson 3: Bundle steps with workflows. You will tie the recipe from Lesson 1 and the model spec from this lesson into one object that preprocesses, fits, and predicts as a single unit, so your training and test data always flow through the exact same pipeline with no chance of a mismatch.