This document works through three exercises from the Applied section of ISLR Chapter 9 (Support Vector Machines):
Auto data set.Purchase (Citrus Hill
vs. Minute Maid) in the OJ data set, including cost tuning
via cross-validation.The Auto and OJ data sets are the standard
data sets that ship with the ISLR2 package (392
observations / 9 variables, and 1,070 observations / 18 variables,
respectively).
We have seen that we can fit an SVM with a non-linear kernel in order to perform classification using a non-linear decision boundary. We will now see that we can also obtain a non-linear decision boundary by performing logistic regression using non-linear transformations of the features.
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
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)
The two classes are clearly separated, but by a curve (a hyperbola-like boundary), not by a straight line — this is the whole point of the exercise.
glm.fit <- glm(y ~ x1 + x2, data = dat5, family = binomial)
summary(glm.fit)
##
## Call:
## glm(formula = y ~ x1 + x2, family = binomial, data = dat5)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -0.087260 0.089579 -0.974 0.330
## x1 0.196199 0.316864 0.619 0.536
## x2 -0.002854 0.305712 -0.009 0.993
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 692.18 on 499 degrees of freedom
## Residual deviance: 691.79 on 497 degrees of freedom
## AIC: 697.79
##
## Number of Fisher Scoring iterations: 3
Neither x1 nor x2 is statistically
significant on its own — unsurprising, since the true boundary is
quadratic and a linear-in-x1,x2 model has no
way to represent it.
glm.probs <- predict(glm.fit, type = "response")
glm.pred <- as.factor(ifelse(glm.probs > 0.5, 1, 0))
dat5$pred_linear <- glm.pred
table(Predicted = glm.pred, Actual = dat5$y)
## Actual
## Predicted 0 1
## 0 258 212
## 1 3 27
ggplot(dat5, aes(x1, x2, color = pred_linear)) +
geom_point(size = 1.6) +
scale_color_manual(values = c("0" = "#2E5EAA", "1" = "#C0392B")) +
labs(title = "Logistic regression (linear terms) — predicted labels",
color = "Predicted class") +
theme_minimal(base_size = 12)
As expected, the decision boundary produced by the linear logistic model is a straight line, and it does a poor job separating the true classes — most points are predicted into a single dominant class near the boundary.
glm.fit2 <- glm(y ~ poly(x1, 2) + poly(x2, 2) + I(x1 * x2),
data = dat5, family = binomial)
summary(glm.fit2)
##
## Call:
## glm(formula = y ~ poly(x1, 2) + poly(x2, 2) + I(x1 * x2), family = binomial,
## data = dat5)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -102.2 4302.0 -0.024 0.981
## poly(x1, 2)1 2715.3 141109.5 0.019 0.985
## poly(x1, 2)2 27218.5 842987.2 0.032 0.974
## poly(x2, 2)1 -279.7 97160.4 -0.003 0.998
## poly(x2, 2)2 -28693.0 875451.3 -0.033 0.974
## I(x1 * x2) -206.4 41802.8 -0.005 0.996
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 6.9218e+02 on 499 degrees of freedom
## Residual deviance: 3.5810e-06 on 494 degrees of freedom
## AIC: 12
##
## Number of Fisher Scoring iterations: 25
glm.probs2 <- predict(glm.fit2, type = "response")
glm.pred2 <- as.factor(ifelse(glm.probs2 > 0.5, 1, 0))
dat5$pred_nonlinear <- glm.pred2
table(Predicted = glm.pred2, Actual = dat5$y)
## Actual
## Predicted 0 1
## 0 261 0
## 1 0 239
ggplot(dat5, aes(x1, x2, color = pred_nonlinear)) +
geom_point(size = 1.6) +
scale_color_manual(values = c("0" = "#2E5EAA", "1" = "#C0392B")) +
labs(title = "Logistic regression (quadratic terms) — predicted labels",
color = "Predicted class") +
theme_minimal(base_size = 12)
With quadratic terms included, the fitted decision boundary is
obviously non-linear and essentially reproduces the true quadratic
boundary — the model perfectly separates the two classes on the training
data in this case, because the transformed features (x1^2,
x2^2) exactly match the functional form of the true
boundary.
svm.fit <- svm(y ~ x1 + x2, data = dat5, kernel = "linear", cost = 0.1)
svm.pred <- predict(svm.fit, dat5)
dat5$pred_svc <- svm.pred
table(Predicted = svm.pred, Actual = dat5$y)
## Actual
## Predicted 0 1
## 0 261 239
## 1 0 0
ggplot(dat5, aes(x1, x2, color = pred_svc)) +
geom_point(size = 1.6) +
scale_color_manual(values = c("0" = "#2E5EAA", "1" = "#C0392B")) +
labs(title = "Support vector classifier (linear kernel) — predicted labels",
color = "Predicted class") +
theme_minimal(base_size = 12)
The linear support vector classifier, like linear logistic regression, cannot represent a curved boundary — with this cost it predicts every observation into a single class, since no straight line comes close to separating the true quadratic pattern.
svm.fit2 <- svm(y ~ x1 + x2, data = dat5, kernel = "radial", gamma = 1, cost = 1)
svm.pred2 <- predict(svm.fit2, dat5)
dat5$pred_svm_radial <- svm.pred2
table(Predicted = svm.pred2, Actual = dat5$y)
## Actual
## Predicted 0 1
## 0 258 11
## 1 3 228
ggplot(dat5, aes(x1, x2, color = pred_svm_radial)) +
geom_point(size = 1.6) +
scale_color_manual(values = c("0" = "#2E5EAA", "1" = "#C0392B")) +
labs(title = "SVM (radial kernel) — predicted labels",
color = "Predicted class") +
theme_minimal(base_size = 12)
err <- function(pred, truth) mean(pred != truth)
data.frame(
Model = c("Logistic (linear terms)", "Logistic (quadratic terms)",
"SVC (linear kernel)", "SVM (radial kernel)"),
Training_Error = c(err(dat5$pred_linear, dat5$y),
err(dat5$pred_nonlinear, dat5$y),
err(dat5$pred_svc, dat5$y),
err(dat5$pred_svm_radial, dat5$y))
)
## Model Training_Error
## 1 Logistic (linear terms) 0.430
## 2 Logistic (quadratic terms) 0.000
## 3 SVC (linear kernel) 0.478
## 4 SVM (radial kernel) 0.028
Two linear methods — logistic regression with only linear terms, and a linear-kernel support vector classifier — both fail to capture the true quadratic decision boundary, and their training error rates are correspondingly poor (the linear SVC in particular collapses to predicting a single class). Once non-linearity is introduced — either by adding quadratic/interaction terms to logistic regression, or by using a radial-kernel SVM — both approaches recover the true boundary shape and achieve low training error. This demonstrates the exercise’s core point: a non-linear decision boundary can be obtained either by using an inherently non-linear method (SVM with a non-linear kernel) or by manually engineering non-linear features into an otherwise linear method (logistic regression) — the two approaches are more similar in spirit than they might first appear, though SVMs with kernels avoid having to guess the right functional form in advance.
In this problem, you will use support vector approaches in order
to predict whether a given car gets high or low gas mileage based on the
Auto data set.
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/",
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" ...
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" ...
costset.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 is lowest around cost = 1
(roughly 8.4%), and stays in a fairly narrow band (8.4%–9.2%) across
most of the cost range tested — larger costs (10, 100) push the error
slightly higher, consistent with a more rigid,
lower-bias/higher-variance margin starting to overfit individual
training points.
gamma/degree/costset.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 achieves the lowest cross-validation error of
the three (≈6.6%, at cost = 1, gamma = 1),
modestly outperforming the linear support vector classifier (≈8.4%). The
polynomial kernel needs a much larger cost (100) to reach a comparable
error rate (≈8.4%), and is generally more sensitive to the cost/degree
combination — low-cost polynomial fits perform poorly, suggesting the
polynomial kernel needs a lot of margin violations tolerated before it
fits any usable boundary for this feature space.
The hint suggests using plot.svm(), which for
p > 2 predictors plots a 2-D slice using a chosen pair
of variables (all others held at their medians). We illustrate this
using weight and horsepower, two of the more
informative 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 plot shows a single straight decision boundary through the weight/horsepower plane, while the radial and polynomial kernel plots both bend around the data, consistent with their ability to model non-linear relationships between engine characteristics and gas-mileage class. The radial kernel’s boundary is visibly smoother and hugs the data more naturally than the polynomial kernel’s, which tracks with the radial kernel’s lower cross-validation error above.
This problem involves the OJ data set which is part
of 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 ...
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
cost = 0.01svm.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 heavily penalized, very wide
margin), the classifier uses 435 of the 800 training observations as
support vectors — more than half the training set — which indicates a
wide margin with many points violating it, consistent with such a small
cost value.
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
cost via cross-validationset.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
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 cost improves both training and test error
relative to the arbitrary cost = 0.01 starting point — test
error drops from 0.1778 to 0.1556.
gamma)# (b) baseline cost = 0.01
svm.radial <- svm(Purchase ~ ., data = OJ.train, kernel = "radial", cost = 0.01)
summary(svm.radial)
##
## Call:
## svm(formula = Purchase ~ ., data = OJ.train, kernel = "radial", cost = 0.01)
##
##
## Parameters:
## SVM-Type: C-classification
## SVM-Kernel: radial
## cost: 0.01
##
## Number of Support Vectors: 634
##
## ( 319 315 )
##
##
## Number of Classes: 2
##
## Levels:
## CH MM
# (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")
## 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)))
summary(tune.radial.oj)
##
## Parameter tuning of 'svm':
##
## - sampling method: 10-fold cross validation
##
## - best parameters:
## cost
## 0.4641589
##
## - best performance: 0.17125
##
## - Detailed performance results:
## cost error dispersion
## 1 0.01000000 0.39375 0.04007372
## 2 0.02154435 0.39375 0.04007372
## 3 0.04641589 0.20875 0.04041881
## 4 0.10000000 0.18625 0.02853482
## 5 0.21544347 0.18250 0.03016160
## 6 0.46415888 0.17125 0.02045490
## 7 1.00000000 0.17125 0.02128673
## 8 2.15443469 0.17875 0.02128673
## 9 4.64158883 0.18125 0.02144923
## 10 10.00000000 0.18625 0.02853482
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
degree = 2)# (b) baseline cost = 0.01
svm.poly <- svm(Purchase ~ ., data = OJ.train, kernel = "polynomial",
degree = 2, cost = 0.01)
summary(svm.poly)
##
## Call:
## svm(formula = Purchase ~ ., data = OJ.train, kernel = "polynomial",
## degree = 2, cost = 0.01)
##
##
## Parameters:
## SVM-Type: C-classification
## SVM-Kernel: polynomial
## cost: 0.01
## degree: 2
## coef.0: 0
##
## Number of Support Vectors: 636
##
## ( 321 315 )
##
##
## Number of Classes: 2
##
## Levels:
## CH MM
# (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")
## 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)))
summary(tune.poly.oj)
##
## Parameter tuning of 'svm':
##
## - sampling method: 10-fold cross validation
##
## - best parameters:
## cost
## 2.154435
##
## - best performance: 0.1775
##
## - Detailed performance results:
## cost error dispersion
## 1 0.01000000 0.39125 0.04210189
## 2 0.02154435 0.36625 0.03537988
## 3 0.04641589 0.35125 0.03839216
## 4 0.10000000 0.32125 0.05001736
## 5 0.21544347 0.21875 0.03830162
## 6 0.46415888 0.20625 0.04497299
## 7 1.00000000 0.20250 0.04116363
## 8 2.15443469 0.17750 0.04158325
## 9 4.64158883 0.18375 0.03387579
## 10 10.00000000 0.18125 0.02779513
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
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)
All three tuned models land in a similar ballpark on test error
(0.1556–0.2111), which is typical for this data set — OJ
has a reasonably strong, mostly linear signal (driven largely by
LoyalCH, the customer’s brand-loyalty measure), so the
extra flexibility of radial or polynomial kernels buys little beyond
what a well-tuned linear support vector classifier already captures.
Based on the results above, the tuned linear support vector
classifier (or the tuned radial SVM, which performs comparably)
gives the best overall result on this data set — noting that the
polynomial kernel’s advantage over its own untuned baseline is the
largest of the three, but it still does not clearly surpass the linear
and radial models once all three are properly tuned.
sessionInfo()
## R version 4.5.1 (2025-06-13)
## Platform: aarch64-apple-darwin20
## Running under: macOS Sequoia 15.0.1
##
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib; LAPACK version 3.12.1
##
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
##
## 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 ggplot2_4.0.0 e1071_1.7-16
##
## loaded via a namespace (and not attached):
## [1] vctrs_0.6.5 cli_3.6.5 knitr_1.50 rlang_1.1.6
## [5] xfun_0.53 generics_0.1.4 S7_0.2.0 jsonlite_2.0.0
## [9] labeling_0.4.3 glue_1.8.0 htmltools_0.5.8.1 sass_0.4.10
## [13] scales_1.4.0 rmarkdown_2.30 grid_4.5.1 tibble_3.3.0
## [17] evaluate_1.0.5 jquerylib_0.1.4 fastmap_1.2.0 yaml_2.3.10
## [21] lifecycle_1.0.4 compiler_4.5.1 dplyr_1.1.4 RColorBrewer_1.1-3
## [25] pkgconfig_2.0.3 rstudioapi_0.19.0 farver_2.1.2 digest_0.6.37
## [29] R6_2.6.1 tidyselect_1.2.1 class_7.3-23 dichromat_2.0-0.1
## [33] pillar_1.11.1 magrittr_2.0.4 bslib_0.9.0 withr_3.0.2
## [37] tools_4.5.1 proxy_0.4-27 gtable_0.3.6 cachem_1.1.0