set.seed(577)
n <- 800
p <- 20
X <- matrix(rnorm(n * p), n, p)
# X2-X10 correlated with X1 (rho ~ 0.7): creates high tree correlation for bagging
for (j in 2:10) X[, j] <- 0.7 * X[, 1] + sqrt(1 - 0.7^2) * X[, j]
colnames(X) <- paste0("X", 1:p)
# Signal distributed across correlated block + mild nonlinear term | noise sd = 2
y <- 3 * X[,1] + 2.5 * X[,2] + 2 * X[,3] + 1.5 * X[,4] + X[,5] + 0.5 * X[,11]^2 + rnorm(n, 0, 2)
train_idx <- 1:500
test_idx <- 501:800
train_df <- data.frame(y = y[train_idx], X[train_idx, ])
test_df <- data.frame(y = y[test_idx], X[test_idx, ])
# Bagging: fit randomForest with mtry = p (all predictors at each split = bagging)
B_vals <- c(1, 5, 10, 25, 50, 100, 200, 300, 500)
bag_test_errors <- sapply(B_vals, function(B) {
set.seed(577)
rf <- randomForest(y ~ ., data = train_df, ntree = B, mtry = p)
pred <- predict(rf, newdata = test_df)
mean((test_df$y - pred)^2)
})
# Single tree baseline
single_tree <- rpart(y ~ ., data = train_df, control = rpart.control(cp = 0.01))
single_pred <- predict(single_tree, newdata = test_df)
single_mse <- mean((test_df$y - single_pred)^2)
bag_df <- tibble(B = B_vals, MSE = bag_test_errors)
plt <- ggplot(bag_df, aes(x = B, y = MSE)) +
geom_line(linewidth = 1.2, color = clr("blue")) +
geom_point(size = 3, color = clr("blue")) +
geom_hline(yintercept = single_mse, linetype = "dashed", color = clr("red"), linewidth = 1) +
annotate("text", x = 400, y = single_mse + 0.3, label = "Single tree",
color = clr("red"), size = 5) +
labs(title = "Bagging: Test MSE vs Number of Trees",
subtitle = "Error converges as B increases | Single tree baseline in red",
x = "Number of Bagged Trees (B)", y = "Test MSE") +
theme_bw(base_size = 16)
ggsave(file.path(fig_dir, "05_bagging_convergence.png"), plt, width = 10, height = 5, dpi = 150)
plt