library(tidyverse)
library(tidymodels)
library(ISLR2)Chapter 5 Lab: Resampling Methods
This is a modified version of the ISLR tidymodels lab for Chapter 5: Resampling Methods. This lab explores resampling methods for estimating model performance and the variability of statistics. We will use the validation-set approach, \(k\)-fold cross-validation, and the bootstrap. The code patterns in the worked examples should be used as templates for the questions. In most questions, you will need to modify a model formula, a resampling choice, or another small part of code that has already appeared.
Libraries and data
We will use two data sets. The Auto data contain automobile characteristics, including miles per gallon (mpg), horsepower, weight, acceleration, and other variables. The Portfolio data contain returns for two investments, X and Y.
Auto <- Auto %>%
as_tibble()
Auto %>% glimpse()Rows: 392
Columns: 9
$ mpg <dbl> 18, 15, 18, 16, 17, 15, 14, 14, 14, 15, 15, 14, 15, 14, 2…
$ cylinders <int> 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 4, 6, 6, 6, 4, …
$ displacement <dbl> 307, 350, 318, 304, 302, 429, 454, 440, 455, 390, 383, 34…
$ horsepower <int> 130, 165, 150, 150, 140, 198, 220, 215, 225, 190, 170, 16…
$ weight <int> 3504, 3693, 3436, 3433, 3449, 4341, 4354, 4312, 4425, 385…
$ acceleration <dbl> 12.0, 11.5, 11.0, 12.0, 10.5, 10.0, 9.0, 8.5, 10.0, 8.5, …
$ year <int> 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 7…
$ origin <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 3, …
$ name <fct> chevrolet chevelle malibu, buick skylark 320, plymouth sa…
Portfolio <- Portfolio %>%
as_tibble()
Portfolio %>% glimpse()Rows: 100
Columns: 2
$ X <dbl> -0.89525089, -1.56245433, -0.41708988, 1.04435573, -0.31556841, -1.7…
$ Y <dbl> -0.2349235, -0.8851760, 0.2718880, -0.7341975, 0.8419834, -2.0371910…
Validation Set Approach
A model usually looks better when evaluated on the same observations that were used to fit it. To obtain a more realistic estimate of future prediction error, we can divide the data into a training set and a testing set. The model is fit using only the training set and evaluated using the testing set.
We use initial_split() from rsample. Setting strata = mpg helps the two parts of the split have similar distributions of mpg.
set.seed(8675309)
Auto_split <- initial_split(Auto, strata = mpg, prop = 0.5)
Auto_split<Training/Testing/Total>
<194/198/392>
Auto_train <- Auto_split %>% training()
Auto_test <- Auto_split %>% testing()We will first predict mpg using horsepower with simple linear regression.
lm_spec <- linear_reg() %>%
set_mode("regression") %>%
set_engine("lm")
lm_fit <- lm_spec %>%
fit(mpg ~ horsepower, data = Auto_train)
lm_fit %>%
pluck("fit") %>%
summary()
Call:
stats::lm(formula = mpg ~ horsepower, data = data)
Residuals:
Min 1Q Median 3Q Max
-13.2765 -3.4931 -0.2491 2.7274 13.8267
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 39.133215 0.971363 40.29 <2e-16 ***
horsepower -0.150788 0.008673 -17.39 <2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 4.728 on 192 degrees of freedom
Multiple R-squared: 0.6116, Adjusted R-squared: 0.6095
F-statistic: 302.3 on 1 and 192 DF, p-value: < 2.2e-16
The testing RMSE measures prediction error on observations that were not used to fit the model.
lm_fit %>%
augment(new_data = Auto_test) %>%
rmse(truth = mpg, estimate = .pred)# A tibble: 1 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 rmse standard 5.09
We can compare this with the training RMSE.
lm_fit %>%
augment(new_data = Auto_train) %>%
rmse(truth = mpg, estimate = .pred)# A tibble: 1 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 rmse standard 4.70
The training RMSE will often be smaller because the model was fitted to those observations. A very large difference between training and testing performance can be a warning that the model does not generalize well.
Question 1: A different simple regression model
Use the same training and testing sets, but fit a simple linear regression model that predicts mpg from weight instead of horsepower.
Use the fitted-model summary, the training RMSE, and the testing RMSE to answer the following questions:
- Is
weighta statistically significant predictor ofmpg? - How does the training RMSE compare with the testing RMSE?
- Compared with the
horsepowermodel above, does theweightmodel appear to predictmpgbetter or worse on the testing data?
Helpful output: Modify the model formula in the preceding code. The summary() output and the two rmse() calculations used above provide the information needed.
Polynomial regression with the validation set
A straight line may be too restrictive for the relationship between horsepower and mpg. We can use a recipe to add a polynomial transformation.
poly_rec <- recipe(mpg ~ horsepower, data = Auto_train) %>%
step_poly(horsepower, degree = 2)
poly_wf <- workflow() %>%
add_recipe(poly_rec) %>%
add_model(lm_spec)
poly_fit <- poly_wf %>%
fit(data = Auto_train)
poly_fit %>%
extract_fit_engine() %>%
summary()
Call:
stats::lm(formula = ..y ~ ., data = data)
Residuals:
Min 1Q Median 3Q Max
-14.4714 -2.3251 0.0872 2.2062 13.1385
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 23.3098 0.2986 78.066 < 2e-16 ***
horsepower_poly_1 -82.1997 4.1589 -19.765 < 2e-16 ***
horsepower_poly_2 31.4360 4.1589 7.559 1.65e-12 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 4.159 on 191 degrees of freedom
Multiple R-squared: 0.701, Adjusted R-squared: 0.6979
F-statistic: 223.9 on 2 and 191 DF, p-value: < 2.2e-16
Now calculate both the testing and training RMSE for the quadratic model.
poly_fit %>%
augment(new_data = Auto_test) %>%
rmse(truth = mpg, estimate = .pred)# A tibble: 1 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 rmse standard 4.58
poly_fit %>%
augment(new_data = Auto_train) %>%
rmse(truth = mpg, estimate = .pred)# A tibble: 1 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 rmse standard 4.13
Question 2: Does a more flexible polynomial help?
Modify the preceding recipe so that it fits a degree-3 polynomial in horsepower rather than a degree-2 polynomial. Fit the new workflow using Auto_train.
Compare the degree-3 model with the degree-2 model using both training and testing RMSE. Based on those results:
- Which model fits the training data better?
- Which model predicts the testing data better?
- Does moving from degree 2 to degree 3 appear to provide a meaningful improvement, or does the extra flexibility mainly improve the training fit?
Helpful output: Change only the degree argument in step_poly(). Calculate the same two RMSE values shown immediately above for the new model.
The validation-set estimate can change when a different random split is used. For example, changing the seed produces a different collection of training and testing observations.
set.seed(2)
Auto_split_2 <- initial_split(Auto, strata = mpg, prop = 0.5)
Auto_train_2 <- training(Auto_split_2)
Auto_test_2 <- testing(Auto_split_2)
poly_rec_2 <- recipe(mpg ~ horsepower, data = Auto_train_2) %>%
step_poly(horsepower, degree = 2)
poly_wf_2 <- workflow() %>%
add_recipe(poly_rec_2) %>%
add_model(lm_spec)
poly_fit_2 <- poly_wf_2 %>%
fit(data = Auto_train_2)
poly_fit_2 %>%
augment(new_data = Auto_test_2) %>%
rmse(truth = mpg, estimate = .pred)# A tibble: 1 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 rmse standard 4.54
Question 3: Sensitivity to the random split
Repeat the degree-2 polynomial validation analysis using two additional seeds of your choice. For each seed, create a 50/50 stratified split, fit the degree-2 polynomial model, and calculate the testing RMSE.
Compare the two new testing RMSE values with the testing RMSE from the original split using seed 8675309.
- Are the three testing RMSE values identical?
- Is the conclusion that the quadratic model is useful sensitive to which observations happened to be placed in the testing set?
- What does this illustrate about a limitation of using one validation-set split?
Helpful output: Reuse the code immediately above and change only the seed and object names. A short list of the three testing RMSE values is sufficient; no new plotting functions are needed.
\(k\)-Fold Cross-Validation
Instead of relying on one train/test split, \(k\)-fold cross-validation repeatedly uses different portions of the training data for validation. We will use it to choose the polynomial degree.
First, mark the polynomial degree as a tuning parameter.
poly_tuned_rec <- recipe(mpg ~ horsepower, data = Auto_train) %>%
step_poly(horsepower, degree = tune())
poly_tuned_wf <- workflow() %>%
add_recipe(poly_tuned_rec) %>%
add_model(lm_spec)Create 10 folds.
Auto_folds <- Auto_train %>%
vfold_cv(v = 10)
Auto_folds# 10-fold cross-validation
# A tibble: 10 × 2
splits id
<list> <chr>
1 <split [174/20]> Fold01
2 <split [174/20]> Fold02
3 <split [174/20]> Fold03
4 <split [174/20]> Fold04
5 <split [175/19]> Fold05
6 <split [175/19]> Fold06
7 <split [175/19]> Fold07
8 <split [175/19]> Fold08
9 <split [175/19]> Fold09
10 <split [175/19]> Fold10
Create a grid containing polynomial degrees 1 through 10.
degree_grid <- grid_regular(degree(range = c(1, 10)), levels = 10)
degree_grid# A tibble: 10 × 1
degree
<dbl>
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9
10 10
Fit each degree across the folds.
tune_res <- poly_tuned_wf %>%
tune_grid(
resamples = Auto_folds,
grid = degree_grid
)The tuning results can be viewed graphically and as a table.
tune_res %>%
autoplot() +
theme_minimal()
tune_res %>%
collect_metrics()# A tibble: 20 × 7
degree .metric .estimator mean n std_err .config
<dbl> <chr> <chr> <dbl> <int> <dbl> <chr>
1 1 rmse standard 4.72 10 0.211 pre01_mod0_post0
2 1 rsq standard 0.645 10 0.0315 pre01_mod0_post0
3 2 rmse standard 4.13 10 0.249 pre02_mod0_post0
4 2 rsq standard 0.719 10 0.0404 pre02_mod0_post0
5 3 rmse standard 4.15 10 0.251 pre03_mod0_post0
6 3 rsq standard 0.716 10 0.0405 pre03_mod0_post0
7 4 rmse standard 4.17 10 0.239 pre04_mod0_post0
8 4 rsq standard 0.712 10 0.0389 pre04_mod0_post0
9 5 rmse standard 4.17 10 0.230 pre05_mod0_post0
10 5 rsq standard 0.711 10 0.0378 pre05_mod0_post0
11 6 rmse standard 4.22 10 0.225 pre06_mod0_post0
12 6 rsq standard 0.706 10 0.0379 pre06_mod0_post0
13 7 rmse standard 4.21 10 0.222 pre07_mod0_post0
14 7 rsq standard 0.708 10 0.0365 pre07_mod0_post0
15 8 rmse standard 4.24 10 0.225 pre08_mod0_post0
16 8 rsq standard 0.703 10 0.0373 pre08_mod0_post0
17 9 rmse standard 4.33 10 0.252 pre09_mod0_post0
18 9 rsq standard 0.694 10 0.0404 pre09_mod0_post0
19 10 rmse standard 4.49 10 0.286 pre10_mod0_post0
20 10 rsq standard 0.676 10 0.0439 pre10_mod0_post0
We can also display the best-performing models according to RMSE.
tune_res %>%
show_best(metric = "rmse")# A tibble: 5 × 7
degree .metric .estimator mean n std_err .config
<dbl> <chr> <chr> <dbl> <int> <dbl> <chr>
1 2 rmse standard 4.13 10 0.249 pre02_mod0_post0
2 3 rmse standard 4.15 10 0.251 pre03_mod0_post0
3 5 rmse standard 4.17 10 0.230 pre05_mod0_post0
4 4 rmse standard 4.17 10 0.239 pre04_mod0_post0
5 7 rmse standard 4.21 10 0.222 pre07_mod0_post0
Question 4: Repeat cross-validation with fewer folds
Repeat the polynomial tuning analysis using 5-fold cross-validation rather than 10-fold cross-validation. Keep the candidate polynomial degrees from 1 through 10.
Use both the tuning plot and the RMSE results to answer:
- Which degree has the lowest estimated cross-validation RMSE with 5 folds?
- Is the best degree the same as in the 10-fold analysis?
- Looking beyond only the single smallest RMSE, does the overall pattern of model performance across degrees look similar for 5-fold and 10-fold cross-validation?
Helpful output: Modify vfold_cv(v = 10) to use 5 folds, then rerun tune_grid(). autoplot(), collect_metrics(), and show_best(metric = "rmse") are the same tools used above.
The numerically smallest cross-validation error is not always the only reasonable criterion for model selection. A simpler model with nearly the same estimated performance may be preferable.
select_by_one_std_err() chooses the simplest model whose estimated performance is within one standard error of the numerical optimum. Since smaller polynomial degrees are simpler, we specify degree directly.
tune_res %>%
select_by_one_std_err(degree, metric = "rmse")# A tibble: 1 × 2
degree .config
<dbl> <chr>
1 2 pre02_mod0_post0
best_degree <- tune_res %>%
select_by_one_std_err(degree, metric = "rmse")The selected degree can be inserted into the workflow.
final_wf <- poly_wf %>%
finalize_workflow(best_degree)
final_fit <- final_wf %>%
fit(data = Auto_train)Question 5: Numerical best versus the one-standard-error choice
For the 10-fold tuning results, compare the degree identified by show_best(metric = "rmse") with the degree selected by select_by_one_std_err().
Use the tuning plot and the reported RMSE estimates/standard errors to answer:
- Are the numerically best degree and the one-standard-error degree the same?
- If they differ, how much predictive performance appears to be sacrificed by choosing the simpler model?
- Why might the one-standard-error choice be attractive even when its mean RMSE is not the absolute minimum?
Helpful output: The needed code has already been shown: autoplot(), show_best(), and select_by_one_std_err(). Focus your explanation on the balance between estimated prediction error and model simplicity.
The Bootstrap
Cross-validation is useful for estimating prediction error. The bootstrap can instead be used to estimate the variability of a statistic.
Portfolio example
Create 1000 bootstrap samples from the Portfolio data.
Portfolio_boots <- Portfolio %>%
bootstraps(times = 1000)
Portfolio_boots# Bootstrap sampling
# A tibble: 1,000 × 2
splits id
<list> <chr>
1 <split [100/37]> Bootstrap0001
2 <split [100/36]> Bootstrap0002
3 <split [100/34]> Bootstrap0003
4 <split [100/39]> Bootstrap0004
5 <split [100/41]> Bootstrap0005
6 <split [100/34]> Bootstrap0006
7 <split [100/36]> Bootstrap0007
8 <split [100/34]> Bootstrap0008
9 <split [100/33]> Bootstrap0009
10 <split [100/44]> Bootstrap0010
# ℹ 990 more rows
The following function computes the value of \(\alpha\) described in the textbook. It represents a weighting of two investments that minimizes total risk.
alpha.fn <- function(split){
data <- analysis(split)
X <- data$X
Y <- data$Y
(var(Y) - cov(X, Y)) /
(var(X) + var(Y) - 2 * cov(X, Y))
}Apply the function to every bootstrap sample.
alpha_res <- Portfolio_boots %>%
mutate(alpha = map_dbl(splits, alpha.fn))
alpha_res# Bootstrap sampling
# A tibble: 1,000 × 3
splits id alpha
<list> <chr> <dbl>
1 <split [100/37]> Bootstrap0001 0.581
2 <split [100/36]> Bootstrap0002 0.694
3 <split [100/34]> Bootstrap0003 0.571
4 <split [100/39]> Bootstrap0004 0.420
5 <split [100/41]> Bootstrap0005 0.493
6 <split [100/34]> Bootstrap0006 0.407
7 <split [100/36]> Bootstrap0007 0.570
8 <split [100/34]> Bootstrap0008 0.536
9 <split [100/33]> Bootstrap0009 0.547
10 <split [100/44]> Bootstrap0010 0.456
# ℹ 990 more rows
Summarize the bootstrap distribution.
alpha_res %>%
summarize(
alpha_mean = mean(alpha),
alpha_sd = sd(alpha)
)# A tibble: 1 × 2
alpha_mean alpha_sd
<dbl> <dbl>
1 0.575 0.0911
Question 6: How stable is the bootstrap estimate of \(\alpha\)?
Repeat the Portfolio bootstrap analysis using 2000 bootstrap samples instead of 1000. Calculate the mean and standard deviation of the 2000 bootstrap estimates of \(\alpha\).
Compare these values with the results from 1000 bootstrap samples.
- Does the estimated mean of \(\alpha\) change substantially?
- Does the estimated bootstrap standard deviation change substantially?
- Based on this comparison, does 1000 bootstrap samples appear to give a reasonably stable summary for this example?
Helpful output: Change only times = 1000 in bootstraps() and repeat the same mutate() and summarize() code. You do not need to write a new bootstrap function.
Bootstrapping regression coefficients
We can also use the bootstrap to estimate the variability of regression coefficients. First create bootstrap samples of the Auto data.
Auto_boots <- Auto %>%
bootstraps(times = 1000)The function below fits the simple regression model inside one bootstrap sample and returns the coefficient estimates.
boot.fn <- function(split){
lm_fit <- lm_spec %>%
fit(mpg ~ horsepower, data = split %>% analysis())
tidy(lm_fit)
}Apply the function to every bootstrap sample and summarize the resulting coefficients.
boot_res <- Auto_boots %>%
mutate(models = map(splits, boot.fn))
boot_res %>%
unnest(cols = c(models)) %>%
group_by(term) %>%
summarize(
mean = mean(estimate),
sd = sd(estimate)
)# A tibble: 2 × 3
term mean sd
<chr> <dbl> <dbl>
1 (Intercept) 40.0 0.867
2 horsepower -0.159 0.00750
For comparison, fit the model once using the complete data set.
lm(mpg ~ horsepower, data = Auto) %>%
tidy()# A tibble: 2 × 5
term estimate std.error statistic p.value
<chr> <dbl> <dbl> <dbl> <dbl>
1 (Intercept) 39.9 0.717 55.7 1.22e-187
2 horsepower -0.158 0.00645 -24.5 7.03e- 81
Question 7: Bootstrap a different simple regression
Modify the bootstrap function so that each bootstrap sample fits the model
\[ mpg \sim weight. \]
Using 1000 bootstrap samples:
- Find the bootstrap mean and bootstrap standard deviation for the intercept and the
weightcoefficient. - Fit
lm(mpg ~ weight, data = Auto)to the full data and display its coefficient estimates withtidy(). - Compare the full-data coefficient estimates with the bootstrap means. Are they reasonably close?
- Which coefficient shows greater bootstrap variability, based on the reported standard deviations?
Helpful output: Copy the boot.fn pattern above and change only the model formula. The unnest(), group_by(), and summarize() code can then be reused without modification.
Question 8: Bootstrap a multiple regression model
Make one further modification to the bootstrap function so that each bootstrap sample fits
\[ mpg \sim horsepower + weight. \]
Using 1000 bootstrap samples, produce the bootstrap mean and standard deviation for all three coefficients. Also fit the same multiple regression once to the complete Auto data and display its tidy() output.
Use these two outputs to answer:
- Are the bootstrap mean coefficient estimates close to the estimates from the full data?
- Which of the two predictor coefficients has the larger bootstrap standard deviation?
- Based on the signs of the coefficients in the multiple regression, describe how
mpgchanges withhorsepowerwhenweightis held fixed, and howmpgchanges withweightwhenhorsepoweris held fixed.
Helpful output: This requires only another change to the formula inside the same boot.fn pattern. The bootstrap summary code is exactly the same as in the preceding example and Question 7.