# install.packages("tidymodels")
# install.packages("ranger")
# pak::pak("bgreenwell/vip@main") # latest stable release
library(tidymodels)
library(vip)
tidymodels_prefer()Random Forests for Classification
Random Forest Classification with tidymodels — hotel_rates Dataset
NOTE: modeldata::hotel_rates ships as a regression dataset (predicting avg_price_per_room). To demonstrate classification, we derive a binary outcome: has_children = TRUE if the booking included one or more children, else FALSE. The raw children and babies counts are dropped from the predictors afterward, since keeping them would leak the answer directly.
Packages needed: tidymodels (includes ranger via the “ranger” engine), modeldata (for hotel_rates), vip (for variable importance plots)
1. Load and prepare the data
Rows: 15,402
Columns: 26
$ lead_time <dbl> 241, 273, 248, 236, 243, 267, 94, 10, 1…
$ stays_in_weekend_nights <dbl> 0, 2, 2, 2, 4, 2, 4, 0, 0, 0, 0, 0, 0, …
$ stays_in_week_nights <dbl> 1, 5, 5, 5, 10, 5, 7, 1, 1, 1, 1, 1, 1,…
$ adults <dbl> 2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, …
$ meal <fct> bed_and_breakfast, bed_and_breakfast, b…
$ country <fct> prt, aus, gbr, prt, gbr, null, prt, esp…
$ market_segment <fct> online_travel_agent, offline_travel_age…
$ distribution_channel <fct> ta_to, ta_to, ta_to, ta_to, ta_to, ta_t…
$ is_repeated_guest <dbl> 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, …
$ previous_cancellations <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ previous_bookings_not_canceled <dbl> 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, …
$ reserved_room_type <fct> a, a, a, a, a, a, f, e, h, a, a, g, a, …
$ assigned_room_type <fct> c, a, c, a, a, a, f, f, h, e, e, g, e, …
$ booking_changes <dbl> 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, …
$ agent <fct> devin_rivera_borrego, lia_nauth, jawhar…
$ company <fct> not_applicable, not_applicable, not_app…
$ days_in_waiting_list <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ customer_type <fct> transient, transient_party, transient, …
$ required_car_parking_spaces <dbl> 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, …
$ total_of_special_requests <dbl> 1, 0, 0, 2, 0, 0, 1, 1, 0, 2, 2, 0, 2, …
$ arrival_date <date> 2016-07-02, 2016-07-02, 2016-07-02, 20…
$ arrival_date_num <dbl> 2016.5, 2016.5, 2016.5, 2016.5, 2016.5,…
$ near_christmas <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ near_new_years <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ historical_adr <dbl> 104.9811, 104.9811, 104.9811, 104.9811,…
$ has_children <fct> yes, no, no, no, no, no, no, no, no, no…
count(hotels, has_children) # check class balance2. Split the data: train / test
set.seed(123)
hotels_split <- initial_split(hotels, prop = 0.80, strata = has_children)
hotels_train <- training(hotels_split)
hotels_test <- testing(hotels_split)3. Preprocessing recipe
Random forests don’t need dummy variables, centering, or scaling — the ranger engine handles factors and raw numeric scales natively. The main preprocessing worth doing here is collapsing rare factor levels (e.g. in agent, company, country) so the forest isn’t swamped with near-unique categories, and removing any zero-variance columns.
hotels_rec <-
recipe(has_children ~ ., data = hotels_train) |>
step_other(agent, company, country, threshold = 0.01) |>
step_zv(all_predictors())4. Model specification (with tunable hyperparameters)
rf_spec <-
rand_forest(
mtry = tune(),
trees = 500,
min_n = tune()
) |>
set_engine("ranger", importance = "impurity") |>
set_mode("classification")5. Workflow: bundle recipe + model
hotels_wflow <-
workflow() |>
add_recipe(hotels_rec) |>
add_model(rf_spec)6. Tune mtry and min_n via cross-validation
set.seed(456)
hotels_folds <- vfold_cv(hotels_train, v = 5, strata = has_children)
class_metrics <- metric_set(roc_auc, accuracy, sensitivity, specificity)
set.seed(789)
rf_grid <- grid_regular(
mtry(range = c(2, 8)),
min_n(range = c(2, 20)),
levels = 4
)
rf_tune_results <-
hotels_wflow |>
tune_grid(
resamples = hotels_folds,
grid = rf_grid,
metrics = class_metrics
)
# Inspect the top-performing hyperparameter combinations by ROC AUC
show_best(rf_tune_results, metric = "roc_auc", n = 5)autoplot(rf_tune_results)7. Select the best hyperparameters and finalize the workflow
best_rf <- select_best(rf_tune_results, metric = "roc_auc")
final_wflow <- finalize_workflow(hotels_wflow, best_rf)8. Final fit on full training set + evaluation on the held-out test set
final_fit <- last_fit(
final_wflow,
split = hotels_split,
metrics = class_metrics
)
# Test-set performance
collect_metrics(final_fit)# Predictions (class + probabilities) on the test set
test_predictions <- collect_predictions(final_fit)
head(test_predictions)9. Confusion matrix and ROC curve
10. Variable importance
final_fit |>
extract_fit_parsnip() |>
vip(num_features = 15) +
labs(title = "Random Forest Variable Importance")NULL
11. (Optional) Extract the final fitted workflow for reuse / deployment
final_wflow_fit <- extract_workflow(final_fit)
# saveRDS(final_wflow_fit, "hotels_rf_workflow.rds")
# Example: predict on new/unseen data using the finalized workflow
# predict(final_wflow_fit, new_data = some_new_data, type = "prob")


