Overview

This document works through three applied exercise:

The Auto and OJ data sets come with the ISLR2 package and contain 392 observations with 9 variables and 1,070 observations with 18 variables, respectively.

Exercise 5

We have already seen that SVMs with nonlinear kernels can produce nonlinear decision boundaries. Here, we’ll show that applying nonlinear transformations to predictors and then fitting logistic regression can likewise yield a nonlinear decision boundary.

(a) Quadratic decision boundary dataset

set.seed(1)
x1 <- runif(500) - 0.5
x2 <- runif(500) - 0.5
y  <- 1 * (x1^2 - x2^2 > 0)

dat5 <- data.frame(x1 = x1, x2 = x2, y = as.factor(y))
head(dat5)
##            x1          x2 y
## 1 -0.23449134  0.05417706 1
## 2 -0.12787610  0.18827524 0
## 3  0.07285336  0.15805755 0
## 4  0.40820779  0.16334273 1
## 5 -0.29831807 -0.02776580 1
## 6  0.39838968  0.46952817 0
table(dat5$y)
## 
##   0   1 
## 261 239

(b) Scatter the data points and use color to indicate each point’s true class

library(ggplot2)
## Warning: package 'ggplot2' was built under R version 4.6.1
ggplot(dat5, aes(x1, x2, color = y)) +
  geom_point(size = 1.6) +
  scale_color_manual(values = c("0" = "#2E5EAA", "1" = "#C0392B")) +
  labs(title = "True class labels", subtitle = "Quadratic boundary: x1^2 - x2^2 > 0",
       color = "Class") +
  theme_minimal(base_size = 12)

Exercise 7

This exercise applies support vector techniques to predict whether a car achieves high or low MPG using the Auto data set.

library(e1071)

load_islr2_data <- function(dataset_name) {
  if (requireNamespace("ISLR2", quietly = TRUE)) {
    library(ISLR2)
    return(get(dataset_name, envir = asNamespace("ISLR2")))
  }
  # Fallback: download the .rda directly from the ISLR2 CRAN source mirror
  rda_file <- paste0(dataset_name, ".rda")
  if (!file.exists(rda_file)) {
    url <- paste0("[https://raw.githubusercontent.com/cran/ISLR2/master/data/](https://raw.githubusercontent.com/cran/ISLR2/master/data/)",
                  dataset_name, ".rda")
    download.file(url, rda_file, mode = "wb", quiet = TRUE)
  }
  e <- new.env()
  load(rda_file, envir = e)
  get(dataset_name, envir = e)
}

Auto <- load_islr2_data("Auto")     # ISLR2::Auto — 392 obs. of 9 variables
str(Auto)
## 'data.frame':    392 obs. of  9 variables:
##  $ mpg         : num  18 15 18 16 17 15 14 14 14 15 ...
##  $ cylinders   : int  8 8 8 8 8 8 8 8 8 8 ...
##  $ displacement: num  307 350 318 304 302 429 454 440 455 390 ...
##  $ horsepower  : int  130 165 150 150 140 198 220 215 225 190 ...
##  $ weight      : int  3504 3693 3436 3433 3449 4341 4354 4312 4425 3850 ...
##  $ acceleration: num  12 11.5 11 12 10.5 10 9 8.5 10 8.5 ...
##  $ year        : int  70 70 70 70 70 70 70 70 70 70 ...
##  $ origin      : int  1 1 1 1 1 1 1 1 1 1 ...
##  $ name        : Factor w/ 304 levels "amc ambassador brougham",..: 49 36 231 14 161 141 54 223 241 2 ...
##  - attr(*, "na.action")= 'omit' Named int [1:5] 33 127 331 337 355
##   ..- attr(*, "names")= chr [1:5] "33" "127" "331" "337" ...

(a) Construct a high/low MPG indicator variable

mpg01 <- as.factor(ifelse(Auto$mpg > median(Auto$mpg), 1, 0))
table(mpg01)
## mpg01
##   0   1 
## 196 196
# Build the modeling data set: drop mpg (used to build the label) and name 
# (a near-unique car-model identifier, not a usable predictor)
Auto2 <- Auto
Auto2$mpg01 <- mpg01
Auto2$mpg  <- NULL
Auto2$name <- NULL
str(Auto2)
## 'data.frame':    392 obs. of  8 variables:
##  $ cylinders   : int  8 8 8 8 8 8 8 8 8 8 ...
##  $ displacement: num  307 350 318 304 302 429 454 440 455 390 ...
##  $ horsepower  : int  130 165 150 150 140 198 220 215 225 190 ...
##  $ weight      : int  3504 3693 3436 3433 3449 4341 4354 4312 4425 3850 ...
##  $ acceleration: num  12 11.5 11 12 10.5 10 9 8.5 10 8.5 ...
##  $ year        : int  70 70 70 70 70 70 70 70 70 70 ...
##  $ origin      : int  1 1 1 1 1 1 1 1 1 1 ...
##  $ mpg01       : Factor w/ 2 levels "0","1": 1 1 1 1 1 1 1 1 1 1 ...
##  - attr(*, "na.action")= 'omit' Named int [1:5] 33 127 331 337 355
##   ..- attr(*, "names")= chr [1:5] "33" "127" "331" "337" ...

(b) Fit a linear‑kernel support vector classifier across a range of cost values

set.seed(1)
tune.linear <- tune(svm, mpg01 ~ ., data = Auto2, kernel = "linear",
                     ranges = list(cost = c(0.01, 0.1, 1, 5, 10, 100)))
summary(tune.linear)
## 
## Parameter tuning of 'svm':
## 
## - sampling method: 10-fold cross validation 
## 
## - best parameters:
##  cost
##     1
## 
## - best performance: 0.08435897 
## 
## - Detailed performance results:
##    cost      error dispersion
## 1 1e-02 0.08923077 0.04698309
## 2 1e-01 0.09185897 0.04393409
## 3 1e+00 0.08435897 0.03662670
## 4 5e+00 0.08948718 0.03898410
## 5 1e+01 0.08948718 0.03898410
## 6 1e+02 0.08692308 0.03887151

Cross‑validation error bottoms out near a cost value of 1 (around 8.4%) and remains in a narrow range (8.4%–9.2%) across most tested cost values. As cost increases to 10 or 100, the error edges upward slightly, reflecting the model’s shift toward a stiffer margin with lower bias and higher variance — a setup that begins to overfit individual training points. ### (c) Evaluate radial and polynomial SVMs across a grid of gamma, degree, and cost values to compare performance

set.seed(1)
tune.radial <- tune(svm, mpg01 ~ ., data = Auto2, kernel = "radial",
                     ranges = list(cost = c(0.1, 1, 5, 10, 100),
                                   gamma = c(0.01, 0.1, 1, 5)))
summary(tune.radial)
## 
## Parameter tuning of 'svm':
## 
## - sampling method: 10-fold cross validation 
## 
## - best parameters:
##  cost gamma
##     1     1
## 
## - best performance: 0.06634615 
## 
## - Detailed performance results:
##     cost gamma      error dispersion
## 1    0.1  0.01 0.11224359 0.03836937
## 2    1.0  0.01 0.08673077 0.04551036
## 3    5.0  0.01 0.08416667 0.04010502
## 4   10.0  0.01 0.08673077 0.04040897
## 5  100.0  0.01 0.08685897 0.03483004
## 6    0.1  0.10 0.08923077 0.04698309
## 7    1.0  0.10 0.08923077 0.04376306
## 8    5.0  0.10 0.07910256 0.03292568
## 9   10.0  0.10 0.08166667 0.04149504
## 10 100.0  0.10 0.08410256 0.03390616
## 11   0.1  1.00 0.08673077 0.04535158
## 12   1.0  1.00 0.06634615 0.03244101
## 13   5.0  1.00 0.08916667 0.02708952
## 14  10.0  1.00 0.08923077 0.02732003
## 15 100.0  1.00 0.10448718 0.04560852
## 16   0.1  5.00 0.54602564 0.05105434
## 17   1.0  5.00 0.09173077 0.03417795
## 18   5.0  5.00 0.09423077 0.04945093
## 19  10.0  5.00 0.09173077 0.04983028
## 20 100.0  5.00 0.09429487 0.05109336
set.seed(1)
tune.poly <- tune(svm, mpg01 ~ ., data = Auto2, kernel = "polynomial",
                   ranges = list(cost = c(0.1, 1, 5, 10, 100),
                                 degree = c(2, 3, 4)))
summary(tune.poly)
## 
## Parameter tuning of 'svm':
## 
## - sampling method: 10-fold cross validation 
## 
## - best parameters:
##  cost degree
##   100      3
## 
## - best performance: 0.08423077 
## 
## - Detailed performance results:
##     cost degree      error dispersion
## 1    0.1      2 0.27846154 0.09486227
## 2    1.0      2 0.25307692 0.13751948
## 3    5.0      2 0.17621795 0.04945319
## 4   10.0      2 0.18647436 0.05598001
## 5  100.0      2 0.18128205 0.06251437
## 6    0.1      3 0.20192308 0.11347783
## 7    1.0      3 0.09448718 0.04180527
## 8    5.0      3 0.08429487 0.04016554
## 9   10.0      3 0.08435897 0.04544023
## 10 100.0      3 0.08423077 0.03636273
## 11   0.1      4 0.26564103 0.09977887
## 12   1.0      4 0.21205128 0.09560470
## 13   5.0      4 0.18371795 0.06175709
## 14  10.0      4 0.16589744 0.06962914
## 15 100.0      4 0.12756410 0.05208506
data.frame(
  Kernel = c("Linear", "Radial", "Polynomial"),
  Best_Params = c(paste("cost =", tune.linear$best.parameters$cost),
                   paste0("cost = ", tune.radial$best.parameters$cost,
                          ", gamma = ", tune.radial$best.parameters$gamma),
                   paste0("cost = ", tune.poly$best.parameters$cost,
                          ", degree = ", tune.poly$best.parameters$degree)),
  Best_CV_Error = c(tune.linear$best.performance,
                     tune.radial$best.performance,
                     tune.poly$best.performance)
)
##       Kernel            Best_Params Best_CV_Error
## 1     Linear               cost = 1    0.08435897
## 2     Radial    cost = 1, gamma = 1    0.06634615
## 3 Polynomial cost = 100, degree = 3    0.08423077

The radial‑kernel SVM delivers the best cross‑validation performance of the three models, reaching about 6.6% error at cost = 1 and gamma = 1, noticeably better than the linear classifier’s roughly 8.4% error. The polynomial kernel requires a much higher cost (100) to approach similar accuracy (≈8.4%) and is far more sensitive to the cost–degree combination overall — low‑cost polynomial models perform poorly, indicating that this kernel needs substantial flexibility (i.e., many allowed margin violations) before it can learn a useful boundary in this feature space.

(d) Plots supporting parts (b) and (c)

The hint recommends using plot.svm(), which—when the model includes more than two predictors—produces a 2‑D slice based on a selected pair of variables while holding all others at their median values. Here, we illustrate this using weight and horsepower, two of the strongest predictors of gas mileage.

svm.linear.best <- svm(mpg01 ~ ., data = Auto2, kernel = "linear",
                        cost = tune.linear$best.parameters$cost)
plot(svm.linear.best, Auto2, horsepower ~ weight)

svm.radial.best <- svm(mpg01 ~ ., data = Auto2, kernel = "radial",
                        cost = tune.radial$best.parameters$cost,
                        gamma = tune.radial$best.parameters$gamma)
plot(svm.radial.best, Auto2, horsepower ~ weight)

svm.poly.best <- svm(mpg01 ~ ., data = Auto2, kernel = "polynomial",
                      cost = tune.poly$best.parameters$cost,
                      degree = tune.poly$best.parameters$degree)
plot(svm.poly.best, Auto2, horsepower ~ weight)

The linear‑kernel SVM produces a single straight decision boundary in the weight–horsepower plane, while the radial and polynomial kernels generate curved boundaries that wrap around the data—reflecting their ability to capture nonlinear relationships between engine characteristics and gas‑mileage class. The radial kernel’s boundary is smoother and aligns more naturally with the data than the polynomial kernel’s, consistent with its lower cross‑validation error reported above.


Exercise 8

This exercise uses the OJ data set from the ISLR2 package.

OJ <- load_islr2_data("OJ")     # ISLR2::OJ — 1,070 obs. of 18 variables
str(OJ)
## 'data.frame':    1070 obs. of  18 variables:
##  $ Purchase      : Factor w/ 2 levels "CH","MM": 1 1 1 2 1 1 1 1 1 1 ...
##  $ WeekofPurchase: num  237 239 245 227 228 230 232 234 235 238 ...
##  $ StoreID       : num  1 1 1 1 7 7 7 7 7 7 ...
##  $ PriceCH       : num  1.75 1.75 1.86 1.69 1.69 1.69 1.69 1.75 1.75 1.75 ...
##  $ PriceMM       : num  1.99 1.99 2.09 1.69 1.69 1.99 1.99 1.99 1.99 1.99 ...
##  $ DiscCH        : num  0 0 0.17 0 0 0 0 0 0 0 ...
##  $ DiscMM        : num  0 0.3 0 0 0 0 0.4 0.4 0.4 0.4 ...
##  $ SpecialCH     : num  0 0 0 0 0 0 1 1 0 0 ...
##  $ SpecialMM     : num  0 1 0 0 0 1 1 0 0 0 ...
##  $ LoyalCH       : num  0.5 0.6 0.68 0.4 0.957 ...
##  $ SalePriceMM   : num  1.99 1.69 2.09 1.69 1.69 1.99 1.59 1.59 1.59 1.59 ...
##  $ SalePriceCH   : num  1.75 1.75 1.69 1.69 1.69 1.69 1.69 1.75 1.75 1.75 ...
##  $ PriceDiff     : num  0.24 -0.06 0.4 0 0 0.3 -0.1 -0.16 -0.16 -0.16 ...
##  $ Store7        : Factor w/ 2 levels "No","Yes": 1 1 1 1 2 2 2 2 2 2 ...
##  $ PctDiscMM     : num  0 0.151 0 0 0 ...
##  $ PctDiscCH     : num  0 0 0.0914 0 0 ...
##  $ ListPriceDiff : num  0.24 0.24 0.23 0 0 0.3 0.3 0.24 0.24 0.24 ...
##  $ STORE         : num  1 1 1 1 0 0 0 0 0 0 ...

(a) Split the data into 800 training and 270 test samples

set.seed(1)
train.idx <- sample(seq_len(nrow(OJ)), 800)
OJ.train <- OJ[train.idx, ]
OJ.test  <- OJ[-train.idx, ]

dim(OJ.train); dim(OJ.test)
## [1] 800  18
## [1] 270  18

(b) Evaluate a linear support vector classifier at C = 0.01

svm.linear <- svm(Purchase ~ ., data = OJ.train, kernel = "linear", cost = 0.01)
summary(svm.linear)
## 
## Call:
## svm(formula = Purchase ~ ., data = OJ.train, kernel = "linear", cost = 0.01)
## 
## 
## Parameters:
##    SVM-Type:  C-classification 
##  SVM-Kernel:  linear 
##        cost:  0.01 
## 
## Number of Support Vectors:  435
## 
##  ( 219 216 )
## 
## 
## Number of Classes:  2 
## 
## Levels: 
##  CH MM

With cost = 0.01—a very small penalty that produces an extremely wide margin—the classifier ends up using 435 of the 800 training observations as support vectors. That’s more than half the training set, indicating a loose margin with many points either on or inside it, which is exactly what we expect when the cost parameter is set this low.

(c) Training and test error rates

train.pred.lin <- predict(svm.linear, OJ.train)
test.pred.lin  <- predict(svm.linear, OJ.test)

train.err.lin <- mean(train.pred.lin != OJ.train$Purchase)
test.err.lin  <- mean(test.pred.lin  != OJ.test$Purchase)

cat("Linear SVC (cost = 0.01)\n")
## Linear SVC (cost = 0.01)
cat("  Training error:", round(train.err.lin, 4), "\n")
##   Training error: 0.175
cat("  Test error:    ", round(test.err.lin, 4), "\n")
##   Test error:     0.1778

(d) Use cross‑validation to select the optimal cost value

set.seed(1)
tune.linear.oj <- tune(svm, Purchase ~ ., data = OJ.train, kernel = "linear",
                        ranges = list(cost = 10^seq(-2, 1, length = 10)))
summary(tune.linear.oj)
## 
## Parameter tuning of 'svm':
## 
## - sampling method: 10-fold cross validation 
## 
## - best parameters:
##       cost
##  0.4641589
## 
## - best performance: 0.16875 
## 
## - Detailed performance results:
##           cost   error dispersion
## 1   0.01000000 0.17625 0.02853482
## 2   0.02154435 0.17625 0.02972676
## 3   0.04641589 0.17500 0.02568506
## 4   0.10000000 0.17250 0.03162278
## 5   0.21544347 0.17250 0.02751262
## 6   0.46415888 0.16875 0.02651650
## 7   1.00000000 0.17500 0.02946278
## 8   2.15443469 0.17125 0.03064696
## 9   4.64158883 0.17125 0.03175973
## 10 10.00000000 0.17375 0.03197764
best.cost.linear <- tune.linear.oj$best.parameters$cost
best.cost.linear
## [1] 0.4641589

(e) Training and test error rates using the tuned cost

svm.linear.best <- svm(Purchase ~ ., data = OJ.train, kernel = "linear",
                        cost = best.cost.linear)

train.pred.lin2 <- predict(svm.linear.best, OJ.train)
test.pred.lin2  <- predict(svm.linear.best, OJ.test)

train.err.lin2 <- mean(train.pred.lin2 != OJ.train$Purchase)
test.err.lin2  <- mean(test.pred.lin2  != OJ.test$Purchase)

cat("Linear SVC (tuned cost =", round(best.cost.linear, 4), ")\n")
## Linear SVC (tuned cost = 0.4642 )
cat("  Training error:", round(train.err.lin2, 4), "\n")
##   Training error: 0.165
cat("  Test error:    ", round(test.err.lin2, 4), "\n")
##   Test error:     0.1556

Tuning the cost parameter improves both training and test performance compared to the initial cost = 0.01 fit — the test error decreases from 0.1778 to 0.1556, reflecting a better‑balanced margin.

(f) Repeat (b)–(e) with a radial kernel (default gamma)

# (b) baseline cost = 0.01
svm.radial <- svm(Purchase ~ ., data = OJ.train, kernel = "radial", cost = 0.01)

# (c) baseline train/test error
train.pred.rad <- predict(svm.radial, OJ.train)
test.pred.rad  <- predict(svm.radial, OJ.test)
train.err.rad <- mean(train.pred.rad != OJ.train$Purchase)
test.err.rad  <- mean(test.pred.rad  != OJ.test$Purchase)

cat("Radial SVM (cost = 0.01)\n")
## Radial SVM (cost = 0.01)
cat("  Training error:", round(train.err.rad, 4), "\n")
##   Training error: 0.3938
cat("  Test error:    ", round(test.err.rad, 4), "\n\n")
##   Test error:     0.3778
# (d) tune cost (default gamma = 1/ncol(x))
set.seed(1)
tune.radial.oj <- tune(svm, Purchase ~ ., data = OJ.train, kernel = "radial",
                        ranges = list(cost = 10^seq(-2, 1, length = 10)))
best.cost.radial <- tune.radial.oj$best.parameters$cost

# (e) train/test error at tuned cost
svm.radial.best <- svm(Purchase ~ ., data = OJ.train, kernel = "radial",
                        cost = best.cost.radial)
train.pred.rad2 <- predict(svm.radial.best, OJ.train)
test.pred.rad2  <- predict(svm.radial.best, OJ.test)
train.err.rad2 <- mean(train.pred.rad2 != OJ.train$Purchase)
test.err.rad2  <- mean(test.pred.rad2  != OJ.test$Purchase)

cat("Radial SVM (tuned cost =", round(best.cost.radial, 4), ")\n")
## Radial SVM (tuned cost = 0.4642 )
cat("  Training error:", round(train.err.rad2, 4), "\n")
##   Training error: 0.1475
cat("  Test error:    ", round(test.err.rad2, 4), "\n")
##   Test error:     0.1778

(g) Repeat (b)–(e) with a polynomial kernel (degree = 2)

# (b) baseline cost = 0.01
svm.poly <- svm(Purchase ~ ., data = OJ.train, kernel = "polynomial",
                 degree = 2, cost = 0.01)

# (c) baseline train/test error
train.pred.poly <- predict(svm.poly, OJ.train)
test.pred.poly  <- predict(svm.poly, OJ.test)
train.err.poly <- mean(train.pred.poly != OJ.train$Purchase)
test.err.poly  <- mean(test.pred.poly  != OJ.test$Purchase)

cat("Polynomial SVM, degree=2 (cost = 0.01)\n")
## Polynomial SVM, degree=2 (cost = 0.01)
cat("  Training error:", round(train.err.poly, 4), "\n")
##   Training error: 0.3725
cat("  Test error:    ", round(test.err.poly, 4), "\n\n")
##   Test error:     0.3667
# (d) tune cost
set.seed(1)
tune.poly.oj <- tune(svm, Purchase ~ ., data = OJ.train, kernel = "polynomial",
                      degree = 2, ranges = list(cost = 10^seq(-2, 1, length = 10)))
best.cost.poly <- tune.poly.oj$best.parameters$cost

# (e) train/test error at tuned cost
svm.poly.best <- svm(Purchase ~ ., data = OJ.train, kernel = "polynomial",
                      degree = 2, cost = best.cost.poly)
train.pred.poly2 <- predict(svm.poly.best, OJ.train)
test.pred.poly2  <- predict(svm.poly.best, OJ.test)
train.err.poly2 <- mean(train.pred.poly2 != OJ.train$Purchase)
test.err.poly2  <- mean(test.pred.poly2  != OJ.test$Purchase)

cat("Polynomial SVM, degree=2 (tuned cost =", round(best.cost.poly, 4), ")\n")
## Polynomial SVM, degree=2 (tuned cost = 2.1544 )
cat("  Training error:", round(train.err.poly2, 4), "\n")
##   Training error: 0.1588
cat("  Test error:    ", round(test.err.poly2, 4), "\n")
##   Test error:     0.2111

(h) Compare all approaches to determine which model achieves the best performance

results <- data.frame(
  Kernel = rep(c("Linear", "Radial", "Polynomial (deg=2)"), each = 2),
  Cost = c("0.01 (baseline)", round(best.cost.linear, 4),
           "0.01 (baseline)", round(best.cost.radial, 4),
           "0.01 (baseline)", round(best.cost.poly, 4)),
  Training_Error = round(c(train.err.lin, train.err.lin2,
                             train.err.rad, train.err.rad2,
                             train.err.poly, train.err.poly2), 4),
  Test_Error = round(c(test.err.lin, test.err.lin2,
                         test.err.rad, test.err.rad2,
                         test.err.poly, test.err.poly2), 4)
)
results
##               Kernel            Cost Training_Error Test_Error
## 1             Linear 0.01 (baseline)         0.1750     0.1778
## 2             Linear          0.4642         0.1650     0.1556
## 3             Radial 0.01 (baseline)         0.3938     0.3778
## 4             Radial          0.4642         0.1475     0.1778
## 5 Polynomial (deg=2) 0.01 (baseline)         0.3725     0.3667
## 6 Polynomial (deg=2)          2.1544         0.1588     0.2111
results_tuned <- results[c(2, 4, 6), ]
results_tuned$Kernel <- factor(results_tuned$Kernel, levels = results_tuned$Kernel)

ggplot(results_tuned, aes(x = Kernel, y = Test_Error, fill = Kernel)) +
  geom_col(width = 0.55, show.legend = FALSE) +
  geom_text(aes(label = Test_Error), vjust = -0.4) +
  scale_fill_manual(values = c("#2E5EAA", "#C0392B", "#27AE60")) +
  labs(title = "OJ Test Error by Kernel (Tuned Cost)",
       y = "Test error rate", x = NULL) +
  theme_minimal(base_size = 12)

The tuned models all end up with comparable test errors (0.1556–0.2111), reflecting the largely linear structure of the OJ data driven by LoyalCH. As a result, nonlinear kernels add little value. The tuned linear SVM — or the tuned radial SVM, which performs about the same — offers the best overall performance. The polynomial kernel improves significantly with tuning but still doesn’t outperform the linear or radial models.

Session Info

sessionInfo()
## R version 4.6.0 (2026-04-24 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
## 
## Matrix products: default
##   LAPACK version 3.12.1
## 
## locale:
## [1] LC_COLLATE=English_United States.utf8 
## [2] LC_CTYPE=English_United States.utf8   
## [3] LC_MONETARY=English_United States.utf8
## [4] LC_NUMERIC=C                          
## [5] LC_TIME=English_United States.utf8    
## 
## time zone: America/Chicago
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] ISLR2_1.3-2   e1071_1.7-17  ggplot2_4.0.3
## 
## loaded via a namespace (and not attached):
##  [1] vctrs_0.7.3        cli_3.6.6          knitr_1.51         rlang_1.2.0       
##  [5] xfun_0.59          otel_0.2.0         generics_0.1.4     S7_0.2.2          
##  [9] jsonlite_2.0.0     labeling_0.4.3     glue_1.8.1         htmltools_0.5.9   
## [13] sass_0.4.10        scales_1.4.0       rmarkdown_2.31     grid_4.6.0        
## [17] tibble_3.3.1       evaluate_1.0.5     jquerylib_0.1.4    fastmap_1.2.0     
## [21] yaml_2.3.12        lifecycle_1.0.5    compiler_4.6.0     dplyr_1.2.1       
## [25] RColorBrewer_1.1-3 pkgconfig_2.0.3    rstudioapi_0.19.0  farver_2.1.2      
## [29] digest_0.6.39      R6_2.6.1           class_7.3-23       tidyselect_1.2.1  
## [33] pillar_1.11.1      magrittr_2.0.5     bslib_0.11.0       proxy_0.4-29      
## [37] withr_3.0.3        tools_4.6.0        gtable_0.3.6       cachem_1.1.0