---
title: "Supervised Learning with tidylearn"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Supervised Learning with tidylearn}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.width = 7,
  fig.height = 5
)
```

```{r setup}
library(tidylearn)
library(dplyr)
```

## Introduction

Classification and regression with tidylearn's thirteen supervised methods,
and the one piece of bookkeeping that catches people out — replaying
training preprocessing on the test set.

**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:

```{r}
# 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

```{r}
# Train logistic regression
model_logistic <- tl_model(split$train, Species ~ ., method = "logistic")
print(model_logistic)
```

The `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.

```{r}
# Predicted labels
preds_logistic <- predict(model_logistic, new_data = split$test, type = "class")
head(preds_logistic)
```

```{r}
# Class probabilities
head(predict(model_logistic, new_data = split$test, type = "prob"))
```

`tl_evaluate()` resolves this for you — it always scores against class
labels, and computes whichever metrics you ask for:

```{r}
tl_evaluate(
  model_logistic,
  new_data = split$test,
  metrics = c("accuracy", "precision", "recall", "f1", "auc")
)
```

#### Decision Trees

```{r}
# Train decision tree
model_tree <- tl_model(split$train, Species ~ ., method = "tree")
print(model_tree)

# Predictions
preds_tree <- predict(model_tree, new_data = split$test)
```

### Multi-class Classification

```{r}
# Split full iris dataset
split_multi <- tl_split(iris, prop = 0.7, stratify = "Species", seed = 123)
```

#### Random Forest

```{r}
# Train random forest
model_forest <- tl_model(split_multi$train, Species ~ ., method = "forest")
print(model_forest)
```

```{r}
# Predictions
preds_forest <- predict(model_forest, new_data = split_multi$test)
head(preds_forest)
```

```{r}
# Accuracy on test set
mean(preds_forest$.pred == split_multi$test$Species)
```

#### Support Vector Machines

```{r}
# Train SVM
model_svm <- tl_model(split_multi$train, Species ~ ., method = "svm")
print(model_svm)

# Predictions
preds_svm <- predict(model_svm, new_data = split_multi$test)
```

## Regression

### Linear Regression

```{r}
# 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)
```

```{r}
# Predictions
preds_lm <- predict(model_lm, new_data = split_reg$test)
head(preds_lm)
```

```{r}
# Calculate RMSE
rmse <- sqrt(mean((preds_lm$.pred - split_reg$test$mpg)^2))
cat("RMSE:", round(rmse, 2), "\n")
```

### Polynomial Regression

```{r}
# Polynomial regression for non-linear relationships
model_poly <- tl_model(
  split_reg$train, mpg ~ wt,
  method = "polynomial", degree = 2
)
print(model_poly)
```

```{r}
# Predictions
preds_poly <- predict(model_poly, new_data = split_reg$test)

# RMSE
rmse_poly <- sqrt(mean((preds_poly$.pred - split_reg$test$mpg)^2))
cat("Polynomial RMSE:", round(rmse_poly, 2), "\n")
```

### Random Forest Regression

```{r}
# Train random forest for regression
model_rf_reg <- tl_model(split_reg$train, mpg ~ ., method = "forest")
print(model_rf_reg)
```

```{r}
# Predictions
preds_rf <- predict(model_rf_reg, new_data = split_reg$test)

# RMSE
rmse_rf <- sqrt(mean((preds_rf$.pred - split_reg$test$mpg)^2))
cat("Random Forest RMSE:", round(rmse_rf, 2), "\n")
```

## Regularized Regression

Regularization helps prevent overfitting by adding penalties to model complexity.

### Ridge Regression

```{r}
# Ridge regression (L2 regularization)
model_ridge <- tl_model(split_reg$train, mpg ~ ., method = "ridge")
print(model_ridge)

# Predictions
preds_ridge <- predict(model_ridge, new_data = split_reg$test)
```

### LASSO

```{r}
# LASSO (L1 regularization) - performs feature selection
model_lasso <- tl_model(split_reg$train, mpg ~ ., method = "lasso")
print(model_lasso)

# Predictions
preds_lasso <- predict(model_lasso, new_data = split_reg$test)
```

### Elastic Net

```{r}
# Elastic Net - combines L1 and L2 regularization
model_enet <- tl_model(
  split_reg$train, mpg ~ .,
  method = "elastic_net", alpha = 0.5
)
print(model_enet)

# Predictions
preds_enet <- predict(model_enet, new_data = split_reg$test)
```

## Model Comparison

```{r}
# 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")
)
```

```{r}
# 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)
```

## Advanced Features

### Using Preprocessed Data

```{r}
# Preprocess data
processed <- tl_prepare_data(
  split_reg$train,
  mpg ~ .,
  scale_method = "standardize",
  remove_correlated = TRUE,
  correlation_cutoff = 0.9
)
```

```{r}
# Train on preprocessed data
model_processed <- tl_model(processed$data, mpg ~ ., method = "linear")
print(model_processed)
```

### Formula Variations

```{r}
# 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

```{r}
# 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)
```

### Missing Values

```{r}
# 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"
)

# Train model
model_imputed <- tl_model(processed_missing$data, mpg ~ ., method = "linear")
```

## Best Practices

1. **Split before training.** A metric computed on the rows the model was
   fitted to tells you how well it memorised them.
2. **Stratify classification splits** so both sets carry the same class
   proportions as the source data.
3. **Scale inputs for `"nn"`** and replay the training transformation on
   the test set, as shown below. nnet fits on the values it is given. The
   other methods do not need it: glmnet and e1071 standardise internally by
   default for the regularised methods and SVM, `"deep"` normalises its own
   inputs, and linear models and trees are scale-invariant.
4. **Compare several models** on the same split before committing to one.
5. **Reach for regularisation** when predictors outnumber observations,
   or when they are strongly correlated.
6. **Match the metric to the task** -- accuracy, F1 or AUC for
   classification; RMSE or MAE for regression.

## Replaying Preprocessing on the Test Set

```{r}
# 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"
)
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:

```{r}
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")
```

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.
