bears <- read.table("/Users/robertpapshev/Downloads/bears.txt", header = TRUE) # Read the bears data from Downloads.
bears_indep <- bears[bears$Obs.No == 1, ] # Keep the first observation for each bear.
bears_indep$d <- as.integer(bears_indep$Sex == 2) # Code female as 1 and male as 0.
bears_indep$y <- sqrt(bears_indep$Weight) # Define the response.
predictors <- c("Head.L", "Head.W", "Neck.G", "Length", "Chest.G", "d") # Specify the six predictors.
full_bears <- lm(reformulate(predictors, response = "y"), data = bears_indep) # Fit the full model without interactions.
nrow(bears_indep) # Report the number of bears.
## [1] 99
I used the 99 first observations. There were no missing values in the response or six predictors.
forward <- lm(y ~ 1, data = bears_indep) # Start with the intercept-only model.
forward_steps <- data.frame() # Store the accepted additions.
repeat { # Continue until no remaining predictor meets the entry threshold.
candidates <- add1(forward, scope = formula(full_bears), test = "F") # Test each remaining addition.
candidates <- candidates[rownames(candidates) != "<none>", , drop = FALSE] # Keep only additions.
if (nrow(candidates) == 0) break # Stop if all predictors have entered.
j <- which.min(candidates[["Pr(>F)"]]) # Find the smallest partial F-test p-value.
if (candidates[["Pr(>F)"]][j] >= 0.10) break # Stop if the entry threshold is not met.
added <- rownames(candidates)[j] # Record the selected predictor.
forward_steps <- rbind(forward_steps, data.frame(Step = nrow(forward_steps) + 1, Added = added, F = candidates[["F value"]][j], p_value = candidates[["Pr(>F)"]][j])) # Save the step's test results.
forward <- update(forward, paste(". ~ . +", added)) # Add the selected predictor.
} # End forward selection.
forward_display <- forward_steps # Make a display copy of the step results.
forward_display$p_value <- format.pval(forward_display$p_value, digits = 4, eps = 1e-99) # Preserve small p-values.
knitr::kable(forward_display, digits = 4) # Print the selection sequence.
| Step | Added | F | p_value |
|---|---|---|---|
| 1 | Chest.G | 1970.9364 | 2.968e-66 |
| 2 | Neck.G | 37.8245 | 1.773e-08 |
| 3 | Length | 29.0018 | 5.222e-07 |
summary(forward) # Print the final model estimates and tests.
##
## Call:
## lm(formula = y ~ Chest.G + Neck.G + Length, data = bears_indep)
##
## Residuals:
## Min 1Q Median 3Q Max
## -1.4439 -0.3976 -0.0565 0.3903 1.8841
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -5.3737 0.4447 -12.08 < 2e-16 ***
## Chest.G 0.2550 0.0248 10.30 < 2e-16 ***
## Neck.G 0.2122 0.0384 5.53 2.8e-07 ***
## Length 0.0800 0.0149 5.39 5.2e-07 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.663 on 95 degrees of freedom
## Multiple R-squared: 0.974, Adjusted R-squared: 0.973
## F-statistic: 1.2e+03 on 3 and 95 DF, p-value: <2e-16
add1(forward, scope = formula(full_bears), test = "F") # Show the remaining entry tests.
## Single term additions
##
## Model:
## y ~ Chest.G + Neck.G + Length
## Df Sum of Sq RSS AIC F value Pr(>F)
## <none> 41.82 -77.31
## Head.L 1 0.8615 40.96 -77.38 1.977 0.163
## Head.W 1 0.0683 41.75 -75.48 0.154 0.696
## d 1 0.0192 41.80 -75.36 0.043 0.836
I added Chest.G first (\(F=1970.9364\), \(p=2.9684\times10^{-66}\)), then
Neck.G (\(F=37.8245\),
\(p=1.7732\times10^{-8}\)), then
Length (\(F=29.0018\),
\(p=5.2224\times10^{-7}\)). The
smallest remaining entry p-value was 0.1630 for Head.L, so
I stopped.
The final fitted model is
\[ \widehat{\sqrt{\mathrm{Weight}}} = -5.37368 + 0.25499\,\mathrm{Chest.G} + 0.21225\,\mathrm{Neck.G} + 0.08005\,\mathrm{Length}. \]
backward <- full_bears # Start backward selection with all six predictors.
backward_steps <- data.frame() # Store the removed predictors.
repeat { # Continue until all remaining predictors meet the retention threshold.
candidates <- drop1(backward, test = "F") # Test each possible deletion.
candidates <- candidates[rownames(candidates) != "<none>", , drop = FALSE] # Keep only deletions.
if (nrow(candidates) == 0) break # Stop if no predictors remain.
j <- which.max(candidates[["Pr(>F)"]]) # Find the largest partial F-test p-value.
if (candidates[["Pr(>F)"]][j] <= 0.10) break # Retain predictors significant at 10%.
removed <- rownames(candidates)[j] # Record the predictor to remove.
backward_steps <- rbind(backward_steps, data.frame(Step = nrow(backward_steps) + 1, Removed = removed, p_value = candidates[["Pr(>F)"]][j])) # Save the deletion.
backward <- update(backward, paste(". ~ . -", removed)) # Remove the selected predictor.
} # End backward selection.
knitr::kable(backward_steps, digits = 4) # Print the deletion sequence.
| Step | Removed | p_value |
|---|---|---|
| 1 | d | 0.8342 |
| 2 | Head.W | 0.4955 |
| 3 | Head.L | 0.1630 |
formula(backward) # Print the backward-selected model.
## y ~ Neck.G + Length + Chest.G
Using a 10% removal threshold for the backward comparison, I removed
d, Head.W, and Head.L, in that
order. Forward and backward selection gave the same final model.
best_subsets <- regsubsets(y ~ Head.L + Head.W + Neck.G + Length + Chest.G + d, data = bears_indep, nvmax = 6, nbest = 1, method = "exhaustive") # Find the lowest-RSS model at each size.
subset_summary <- summary(best_subsets) # Extract the subset results.
best_terms <- lapply(1:6, function(k) names(coef(best_subsets, id = k))[-1]) # Extract each model's predictors.
best_models <- lapply(best_terms, function(v) lm(reformulate(v, response = "y"), data = bears_indep)) # Refit each best subset.
subset_table <- data.frame(Predictors = 1:6, Model = vapply(best_terms, paste, character(1), collapse = " + "), RSS = subset_summary$rss) # Summarize the best models.
knitr::kable(subset_table, digits = 4) # Display models and RSS values.
| Predictors | Model | RSS |
|---|---|---|
| 1 | Chest.G | 76.0932 |
| 2 | Neck.G + Chest.G | 54.5860 |
| 3 | Neck.G + Length + Chest.G | 41.8193 |
| 4 | Head.L + Neck.G + Length + Chest.G | 40.9578 |
| 5 | Head.L + Head.W + Neck.G + Length + Chest.G | 40.7526 |
| 6 | Head.L + Head.W + Neck.G + Length + Chest.G + d | 40.7331 |
The forward-selected model is also the lowest-RSS three-predictor model, with RSS = 41.8193.
I counted the intercept in the number of betas, so \(p=k+1\) for a model with \(k\) predictors. I used the lab formulas \(\mathrm{AIC}=n\log(\mathrm{RSS}/n)+2p\) and \(\mathrm{BIC}=n\log(\mathrm{RSS}/n)+p\log(n)\).
n <- nrow(bears_indep) # Count the observations used in all bear models.
p <- 2:7 # Count regression coefficients, including the intercept.
full_mse <- deviance(full_bears) / df.residual(full_bears) # Estimate error variance from the full model.
cp <- subset_summary$rss / full_mse - n + 2 * p # Compute Mallows' Cp.
aic <- n * log(subset_summary$rss / n) + 2 * p # Compute AIC using the lab formula.
bic <- n * log(subset_summary$rss / n) + log(n) * p # Compute BIC using the lab's stated log(n) penalty.
criteria <- data.frame(Predictors = 1:6, Betas = p, Adjusted_R2 = vapply(best_models, function(m) summary(m)$adj.r.squared, numeric(1)), Cp = cp, AIC = aic, BIC = bic) # Combine the selection criteria.
knitr::kable(criteria, digits = 6) # Display the criteria for all six models.
| Predictors | Betas | Adjusted_R2 | Cp | AIC | BIC |
|---|---|---|---|---|---|
| 1 | 2 | 0.952610 | 76.86454 | -22.0529 | -16.8627 |
| 2 | 3 | 0.965650 | 30.28827 | -52.9389 | -45.1535 |
| 3 | 4 | 0.973407 | 3.45338 | -77.3143 | -66.9338 |
| 4 | 5 | 0.973678 | 3.50749 | -77.3752 | -64.3996 |
| 5 | 6 | 0.973528 | 5.04408 | -75.8724 | -60.3017 |
| 6 | 7 | 0.973253 | 7.00000 | -73.9198 | -55.7539 |
plot(p, criteria$Adjusted_R2, type = "b", pch = 19, xaxt = "n", xlab = "Number of betas (including intercept)", ylab = "Adjusted R-squared") # Plot adjusted R-squared against model size.
axis(1, at = p) # Label each model size.
Adjusted \(R^2\) is largest for
Head.L + Neck.G + Length + Chest.G (\(p=5\), adjusted \(R^2=0.973678\)).
plot(p, criteria$Cp, type = "b", pch = 19, xaxt = "n", xlab = "Number of betas (including intercept)", ylab = "Mallows' Cp") # Plot Cp against model size.
axis(1, at = p) # Label each model size.
abline(a = 0, b = 1, col = "red", lty = 2) # Add the reference line y = p.
legend("topright", legend = "Cp = p", col = "red", lty = 2, bty = "n") # Label the reference line.
The minimum \(C_p\) is 3.453379 for
Neck.G + Length + Chest.G (\(p=4\)), which is also close to the \(C_p=p\) line. I selected this model by
\(C_p\).
plot(p, criteria$AIC, type = "b", pch = 19, xaxt = "n", xlab = "Number of betas (including intercept)", ylab = "AIC") # Plot AIC against model size.
axis(1, at = p) # Label each model size.
AIC is smallest for Head.L + Neck.G + Length + Chest.G
(\(p=5\), AIC = -77.375182). The
three-predictor model is nearly tied (AIC = -77.314327).
plot(p, criteria$BIC, type = "b", pch = 19, xaxt = "n", xlab = "Number of betas (including intercept)", ylab = "BIC") # Plot BIC against model size.
axis(1, at = p) # Label each model size.
BIC is smallest for Neck.G + Length + Chest.G (\(p=4\), BIC = -66.933848).
data("prostate", package = "faraway") # Load all 97 observations.
full_prostate <- lm(lpsa ~ ., data = prostate) # Fit lpsa on all eight other variables.
summary(full_prostate) # Print coefficients, t-tests, and model fit statistics.
##
## Call:
## lm(formula = lpsa ~ ., data = prostate)
##
## Residuals:
## Min 1Q Median 3Q Max
## -1.733 -0.371 -0.017 0.414 1.638
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 0.66934 1.29639 0.52 0.6069
## lcavol 0.58702 0.08792 6.68 2.1e-09 ***
## lweight 0.45447 0.17001 2.67 0.0090 **
## age -0.01964 0.01117 -1.76 0.0823 .
## lbph 0.10705 0.05845 1.83 0.0704 .
## svi 0.76616 0.24431 3.14 0.0023 **
## lcp -0.10547 0.09101 -1.16 0.2496
## gleason 0.04514 0.15746 0.29 0.7750
## pgg45 0.00453 0.00442 1.02 0.3089
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.708 on 88 degrees of freedom
## Multiple R-squared: 0.655, Adjusted R-squared: 0.623
## F-statistic: 20.9 on 8 and 88 DF, p-value: <2e-16
The full model has \(R^2=0.6548\),
adjusted \(R^2=0.6234\), and residual
standard error 0.7084. Its overall test is \(F(8,88)=20.86\), \(p<2.2\times10^{-16}\). At the 5% level,
lcavol, lweight, and svi are
significant (\(p=2.11\times10^{-9}\),
0.00896, and 0.00233, respectively).
ridge_grid <- seq(0, 10, length.out = 100) # Define 100 equally spaced penalties.
ridge_fit <- lm.ridge(lpsa ~ ., data = prostate, lambda = ridge_grid) # Fit ridge across the requested grid.
ridge_index <- which.min(ridge_fit$GCV) # Find the smallest GCV score.
ridge_lambda <- ridge_fit$lambda[ridge_index] # Extract the selected penalty.
ridge_coef <- coef(ridge_fit)[ridge_index, ] # Extract coefficients on the original predictor scales.
names(ridge_coef)[1] <- "(Intercept)" # Match the OLS intercept name.
knitr::kable(data.frame(Lambda = ridge_lambda, GCV = ridge_fit$GCV[ridge_index]), digits = 9) # Report the selected penalty and score.
| Lambda | GCV | |
|---|---|---|
| 6.464646 | 6.46465 | 0.00551973 |
knitr::kable(cbind(OLS = coef(full_prostate), Ridge = ridge_coef), digits = 6) # Compare ridge and OLS estimates.
| OLS | Ridge | |
|---|---|---|
| (Intercept) | 0.669337 | 0.489814 |
| lcavol | 0.587022 | 0.517343 |
| lweight | 0.454467 | 0.443616 |
| age | -0.019637 | -0.015545 |
| lbph | 0.107054 | 0.096111 |
| svi | 0.766157 | 0.696395 |
| lcp | -0.105474 | -0.042807 |
| gleason | 0.045142 | 0.060521 |
| pgg45 | 0.004525 | 0.003520 |
The minimum GCV is 0.005519732 at \(\lambda=6.464646\). Ridge retains all eight
predictors. Most slope magnitudes decrease; for example,
lcavol decreases from 0.587022 to 0.517343 and
lcp changes from -0.105474 to -0.042807. The
gleason coefficient increases from 0.045142 to
0.060521.
x <- model.matrix(lpsa ~ ., data = prostate)[, -1] # Build the predictor matrix without an intercept column.
y <- prostate$lpsa # Extract the response.
lasso_fit <- glmnet(x, y, family = "gaussian", alpha = 1, standardize = TRUE) # Fit the LASSO coefficient path.
plot(lasso_fit, xvar = "lambda", label = TRUE) # Plot the coefficient paths as a function of the penalty.
As the penalty increases, the coefficients generally shrink toward
zero and predictors drop out. At sufficiently large penalties, every
slope is zero; lcavol remains nonzero at the largest
penalties before that point.
set.seed(462) # Fix the random fold assignment.
cv_lasso <- cv.glmnet(x, y, family = "gaussian", alpha = 1, standardize = TRUE, nfolds = 10, type.measure = "mse") # Select penalties by ten-fold cross-validation.
lasso_at_min <- glmnet(x, y, family = "gaussian", alpha = 1, standardize = TRUE, lambda = cv_lasso$lambda.min) # Fit at lambda.min using the lecture's separate-fit procedure.
lasso_at_1se <- glmnet(x, y, family = "gaussian", alpha = 1, standardize = TRUE, lambda = cv_lasso$lambda.1se) # Fit at lambda.1se using the same procedure.
lasso_min <- as.matrix(coef(lasso_at_min))[, 1] # Extract estimates from the lambda.min fit.
lasso_1se <- as.matrix(coef(lasso_at_1se))[, 1] # Extract estimates from the lambda.1se fit.
chosen_lambdas <- c(cv_lasso$lambda.min, cv_lasso$lambda.1se) # Collect the two selected penalties.
cv_indices <- match(chosen_lambdas, cv_lasso$lambda) # Locate their cross-validation results.
cv_table <- data.frame(Rule = c("lambda.min", "lambda.1se"), Lambda = chosen_lambdas, CV_MSE = cv_lasso$cvm[cv_indices], CV_SE = cv_lasso$cvsd[cv_indices]) # Summarize the selected penalties.
knitr::kable(cv_table, digits = 6) # Display penalties and cross-validation errors.
| Rule | Lambda | CV_MSE | CV_SE |
|---|---|---|---|
| lambda.min | 0.035671 | 0.581824 | 0.095724 |
| lambda.1se | 0.251649 | 0.675031 | 0.133129 |
coefficient_table <- cbind(OLS = coef(full_prostate), Ridge = ridge_coef, LASSO_min = lasso_min, LASSO_1se = lasso_1se) # Combine all coefficient estimates.
knitr::kable(coefficient_table, digits = 6) # Display the coefficient comparison.
| OLS | Ridge | LASSO_min | LASSO_1se | |
|---|---|---|---|---|
| (Intercept) | 0.669337 | 0.489814 | 0.601327 | 1.512110 |
| lcavol | 0.587022 | 0.517343 | 0.527788 | 0.446411 |
| lweight | 0.454467 | 0.443616 | 0.386505 | 0.083424 |
| age | -0.019637 | -0.015545 | -0.006887 | 0.000000 |
| lbph | 0.107054 | 0.096111 | 0.072558 | 0.000000 |
| svi | 0.766157 | 0.696395 | 0.596599 | 0.272045 |
| lcp | -0.105474 | -0.042807 | 0.000000 | 0.000000 |
| gleason | 0.045142 | 0.060521 | 0.000000 | 0.000000 |
| pgg45 | 0.004525 | 0.003520 | 0.002304 | 0.000000 |
With these folds, \(\lambda_{\min}=0.035671\) and \(\lambda_{1\mathrm{se}}=0.251649\). Their cross-validation MSEs are 0.581824 and 0.675031, respectively.
At lambda.min, LASSO retains lcavol,
lweight, age, lbph,
svi, and pgg45; it sets lcp and
gleason to zero. At lambda.1se, it retains
only lcavol, lweight, and svi,
with coefficients 0.446411, 0.083424, and 0.272045. These three slopes
are smaller than their OLS, ridge, and lambda.min
estimates. OLS and ridge retain all eight predictors.
selected_terms <- setdiff(names(lasso_1se)[lasso_1se != 0], "(Intercept)") # Extract the nonzero LASSO predictors.
refit <- lm(reformulate(selected_terms, response = "lpsa"), data = prostate) # Refit the selected predictors by ordinary least squares.
summary(refit) # Print the refitted estimates and model statistics.
##
## Call:
## lm(formula = reformulate(selected_terms, response = "lpsa"),
## data = prostate)
##
## Residuals:
## Min 1Q Median 3Q Max
## -1.7296 -0.4576 0.0281 0.4640 1.5701
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -0.2681 0.5435 -0.49 0.623
## lcavol 0.5516 0.0747 7.39 6.3e-11 ***
## lweight 0.5085 0.1502 3.39 0.001 **
## svi 0.6662 0.2098 3.18 0.002 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.717 on 93 degrees of freedom
## Multiple R-squared: 0.626, Adjusted R-squared: 0.614
## F-statistic: 52 on 3 and 93 DF, p-value: <2e-16
refit_comparison <- data.frame(Term = names(coef(refit)), Full_OLS = coef(full_prostate)[names(coef(refit))], LASSO_1se = lasso_1se[names(coef(refit))], Refit_OLS = coef(refit)) # Compare coefficients retained in the refit.
knitr::kable(refit_comparison, digits = 6, row.names = FALSE) # Display the retained coefficient estimates.
| Term | Full_OLS | LASSO_1se | Refit_OLS |
|---|---|---|---|
| (Intercept) | 0.669337 | 1.512110 | -0.268093 |
| lcavol | 0.587022 | 0.446411 | 0.551638 |
| lweight | 0.454467 | 0.083424 | 0.508541 |
| svi | 0.766157 | 0.272045 | 0.666158 |
The refitted model is
\[ \widehat{\mathrm{lpsa}} = -0.26809 + 0.55164\,\mathrm{lcavol} + 0.50854\,\mathrm{lweight} + 0.66616\,\mathrm{svi}. \]
Compared with the full model, the lcavol and
svi slopes decrease, while the lweight slope
increases. The refitted slopes are larger than the
lambda.1se estimates. The refit uses three predictors
instead of eight; its \(R^2\) is 0.6264
versus 0.6548, adjusted \(R^2\) is
0.6144 versus 0.6234, and residual standard error is 0.7168 versus
0.7084.