
Supervised Learning with tidylearn
Source:vignettes/supervised-learning.Rmd
supervised-learning.Rmd
library(tidylearn)
library(dplyr)
#>
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#>
#> filter, lag
#> The following objects are masked from 'package:base':
#>
#> intersect, setdiff, setequal, unionIntroduction
This vignette demonstrates supervised learning capabilities in tidylearn. All methods shown here wrap established R packages - the algorithms are unchanged, tidylearn simply provides a consistent interface and tidy output.
Wrapped packages include:
- stats (
lm(),glm()) for linear and logistic regression - rpart for decision trees
- randomForest for random forests
- gbm and xgboost for gradient boosting
- glmnet for regularization (ridge, lasso, elastic net)
- e1071 for support vector machines
- nnet for neural networks
Access raw model objects via model$fit for
package-specific functionality.
Classification
Binary Classification
Let’s create a binary classification problem from the iris dataset:
# Create binary classification dataset. setosa is linearly separable from
# the other two species, and logistic regression has no finite maximum
# likelihood estimate on separable data -- glm() fits, warns that the
# algorithm did not converge, and returns coefficients that diverged.
# versicolor and virginica overlap, so this is a real classification
# problem. Even here, a 70% split of 100 rows is separable at some seeds;
# this one is not.
iris_binary <- iris %>%
filter(Species %in% c("versicolor", "virginica")) %>%
mutate(Species = droplevels(Species))
# Split data
split <- tl_split(iris_binary, prop = 0.7, stratify = "Species", seed = 42)Logistic Regression
# Train logistic regression
model_logistic <- tl_model(split$train, Species ~ ., method = "logistic")
print(model_logistic)
#> tidylearn Model
#> ===============
#> Paradigm: supervised
#> Method: logistic
#> Task: Classification
#> Formula: Species ~ .
#>
#> Training observations: 70The type argument controls what .pred
holds. This matters: the default "response" returns
probabilities for logistic regression but class
labels for trees, forests and SVMs. Ask for
type = "class" when you want labels regardless of
method.
# Predicted labels
preds_logistic <- predict(model_logistic, new_data = split$test, type = "class")
head(preds_logistic)
#> # A tibble: 6 × 1
#> .pred
#> <fct>
#> 1 versicolor
#> 2 versicolor
#> 3 versicolor
#> 4 versicolor
#> 5 versicolor
#> 6 versicolor
# Class probabilities
head(predict(model_logistic, new_data = split$test, type = "prob"))
#> # A tibble: 6 × 2
#> versicolor virginica
#> <dbl> <dbl>
#> 1 1.000 0.0000690
#> 2 1.000 0.0000917
#> 3 0.999 0.00101
#> 4 1.000 0.00000874
#> 5 1.000 0.000208
#> 6 0.952 0.0480tl_evaluate() resolves this for you — it always scores
against class labels, and computes whichever metrics you ask for:
tl_evaluate(
model_logistic,
new_data = split$test,
metrics = c("accuracy", "precision", "recall", "f1", "auc")
)
#> # A tibble: 5 × 2
#> metric value
#> <chr> <dbl>
#> 1 accuracy 0.867
#> 2 precision 1
#> 3 recall 0.733
#> 4 f1 0.846
#> 5 auc 0.991Decision Trees
# Train decision tree
model_tree <- tl_model(split$train, Species ~ ., method = "tree")
print(model_tree)
#> tidylearn Model
#> ===============
#> Paradigm: supervised
#> Method: tree
#> Task: Classification
#> Formula: Species ~ .
#>
#> Training observations: 70
# Predictions
preds_tree <- predict(model_tree, new_data = split$test)Multi-class Classification
# Split full iris dataset
split_multi <- tl_split(iris, prop = 0.7, stratify = "Species", seed = 123)Random Forest
# Train random forest
model_forest <- tl_model(split_multi$train, Species ~ ., method = "forest")
print(model_forest)
#> tidylearn Model
#> ===============
#> Paradigm: supervised
#> Method: forest
#> Task: Classification
#> Formula: Species ~ .
#>
#> Training observations: 105
# Predictions
preds_forest <- predict(model_forest, new_data = split_multi$test)
head(preds_forest)
#> # A tibble: 6 × 1
#> .pred
#> <fct>
#> 1 setosa
#> 2 setosa
#> 3 setosa
#> 4 setosa
#> 5 setosa
#> 6 setosa
# Accuracy on test set
mean(preds_forest$.pred == split_multi$test$Species)
#> [1] 0.9333333Support Vector Machines
# Train SVM
model_svm <- tl_model(split_multi$train, Species ~ ., method = "svm")
print(model_svm)
#> tidylearn Model
#> ===============
#> Paradigm: supervised
#> Method: svm
#> Task: Classification
#> Formula: Species ~ .
#>
#> Training observations: 105
# Predictions
preds_svm <- predict(model_svm, new_data = split_multi$test)Regression
Linear Regression
# Split mtcars data
split_reg <- tl_split(mtcars, prop = 0.7, seed = 123)
# Train linear model
model_lm <- tl_model(split_reg$train, mpg ~ wt + hp + disp, method = "linear")
print(model_lm)
#> tidylearn Model
#> ===============
#> Paradigm: supervised
#> Method: linear
#> Task: Regression
#> Formula: mpg ~ wt + hp + disp
#>
#> Training observations: 22Polynomial Regression
# Polynomial regression for non-linear relationships
model_poly <- tl_model(
split_reg$train, mpg ~ wt,
method = "polynomial", degree = 2
)
print(model_poly)
#> tidylearn Model
#> ===============
#> Paradigm: supervised
#> Method: polynomial
#> Task: Regression
#> Formula: mpg ~ wt
#>
#> Training observations: 22Random Forest Regression
Regularized Regression
Regularization helps prevent overfitting by adding penalties to model complexity.
Ridge Regression
# Ridge regression (L2 regularization)
model_ridge <- tl_model(split_reg$train, mpg ~ ., method = "ridge")
print(model_ridge)
#> tidylearn Model
#> ===============
#> Paradigm: supervised
#> Method: ridge
#> Task: Regression
#> Formula: mpg ~ .
#>
#> Training observations: 22
# Predictions
preds_ridge <- predict(model_ridge, new_data = split_reg$test)LASSO
# LASSO (L1 regularization) - performs feature selection
model_lasso <- tl_model(split_reg$train, mpg ~ ., method = "lasso")
print(model_lasso)
#> tidylearn Model
#> ===============
#> Paradigm: supervised
#> Method: lasso
#> Task: Regression
#> Formula: mpg ~ .
#>
#> Training observations: 22
# Predictions
preds_lasso <- predict(model_lasso, new_data = split_reg$test)Elastic Net
# Elastic Net - combines L1 and L2 regularization
model_enet <- tl_model(
split_reg$train, mpg ~ .,
method = "elastic_net", alpha = 0.5
)
print(model_enet)
#> tidylearn Model
#> ===============
#> Paradigm: supervised
#> Method: elastic_net
#> Task: Regression
#> Formula: mpg ~ .
#>
#> Training observations: 22
# Predictions
preds_enet <- predict(model_enet, new_data = split_reg$test)Model Comparison
# Compare multiple models
models <- list(
linear = tl_model(split_reg$train, mpg ~ ., method = "linear"),
tree = tl_model(split_reg$train, mpg ~ ., method = "tree"),
forest = tl_model(split_reg$train, mpg ~ ., method = "forest")
)
# Calculate RMSE for each model
results <- data.frame(
Model = character(),
RMSE = numeric(),
stringsAsFactors = FALSE
)
for (model_name in names(models)) {
preds <- predict(models[[model_name]], new_data = split_reg$test)
rmse <- sqrt(mean((preds$.pred - split_reg$test$mpg)^2))
results <- rbind(results, data.frame(
Model = model_name,
RMSE = rmse
))
}
results <- results %>% arrange(RMSE)
print(results)
#> Model RMSE
#> 1 forest 2.021532
#> 2 linear 2.281450
#> 3 tree 4.095888Advanced Features
Using Preprocessed Data
# Preprocess data
processed <- tl_prepare_data(
split_reg$train,
mpg ~ .,
scale_method = "standardize",
remove_correlated = TRUE,
correlation_cutoff = 0.9
)
#> Removing 1 highly correlated features
#> Scaling numeric features using method: standardizeFormula Variations
# Interaction terms
model_interact <- tl_model(split_reg$train, mpg ~ wt * hp, method = "linear")
# Polynomial terms using I()
model_poly_manual <- tl_model(
split_reg$train, mpg ~ wt + I(wt^2), method = "linear"
)
# Subset of predictors
model_subset <- tl_model(
split_reg$train, mpg ~ wt + hp + disp, method = "linear"
)Handling Different Data Types
Categorical Predictors
# Create dataset with categorical variables
mtcars_cat <- mtcars %>%
mutate(
cyl = as.factor(cyl),
gear = as.factor(gear),
am = as.factor(am)
)
split_cat <- tl_split(mtcars_cat, prop = 0.7, seed = 123)
# Model with categorical predictors
model_cat <- tl_model(split_cat$train, mpg ~ ., method = "forest")
print(model_cat)
#> tidylearn Model
#> ===============
#> Paradigm: supervised
#> Method: forest
#> Task: Regression
#> Formula: mpg ~ .
#>
#> Training observations: 22Missing Values
# Create data with missing values. Seeded so the vignette renders the
# same output on every build.
set.seed(123)
mtcars_missing <- mtcars
mtcars_missing[sample(seq_len(nrow(mtcars_missing)), 5), "hp"] <- NA
mtcars_missing[sample(seq_len(nrow(mtcars_missing)), 3), "wt"] <- NA
# Preprocess to handle missing values
processed_missing <- tl_prepare_data(
mtcars_missing,
mpg ~ .,
impute_method = "mean",
scale_method = "standardize"
)
#> Imputing missing values using method: mean
#> Scaling numeric features using method: standardize
# Train model
model_imputed <- tl_model(processed_missing$data, mpg ~ ., method = "linear")Best Practices
- Split before training. A metric computed on the rows the model was fitted to measures memorisation, not performance.
- Stratify classification splits so both sets carry the same class proportions as the source data.
- Scale inputs for methods that need it – regularised regression, SVM and neural networks – and replay the training transformation on the test set, as shown below. Trees and forests are scale-invariant.
- Compare several models on the same split before committing to one.
- Reach for regularisation when predictors outnumber observations, or when they are strongly correlated.
- Match the metric to the task – accuracy, F1 or AUC for classification; RMSE or MAE for regression.
Summary
tidylearn provides a unified interface for supervised learning:
- Classification: Logistic regression, decision trees, random forests, SVM, etc.
- Regression: Linear, polynomial, random forests, regularized methods
- Preprocessing: Integrated data preparation tools
-
Consistent API: Same function
(
tl_model()) for all methods - Tidy Output: Easy-to-use predictions and model objects
# Complete workflow example
final_split <- tl_split(iris, prop = 0.7, stratify = "Species", seed = 42)
final_prep <- tl_prepare_data(
final_split$train, Species ~ .,
scale_method = "standardize"
)
#> Scaling numeric features using method: standardize
final_model <- tl_model(final_prep$data, Species ~ ., method = "forest")A model trained on scaled features expects scaled inputs at
prediction time. tl_prepare_data() keeps the parameters it
learned, so the same transformation can be replayed on the test set —
applying the training centre and scale, never recomputing them
from the test data:
scaling <- final_prep$preprocessing_steps$scaling$scaling_params
final_test <- final_split$test
for (col in names(scaling)) {
final_test[[col]] <-
(final_test[[col]] - scaling[[col]]$mean) / scaling[[col]]$sd
}
final_preds <- predict(final_model, new_data = final_test)
accuracy <- mean(final_preds$.pred == final_split$test$Species)
cat("Test Accuracy:", round(accuracy * 100, 1), "%\n")
#> Test Accuracy: 95.6 %Skipping that step and predicting on raw test data is a common and
quiet mistake — the model still returns predictions, they are just
wrong. For multi-step preprocessing, tl_pipeline() handles
this bookkeeping for you: tl_predict_pipeline() replays the
training preprocessing automatically.