library(ISLR2)
data(Weekly)
head(Weekly)
## Year Lag1 Lag2 Lag3 Lag4 Lag5 Volume Today Direction
## 1 1990 0.816 1.572 -3.936 -0.229 -3.484 0.1549760 -0.270 Down
## 2 1990 -0.270 0.816 1.572 -3.936 -0.229 0.1485740 -2.576 Down
## 3 1990 -2.576 -0.270 0.816 1.572 -3.936 0.1598375 3.514 Up
## 4 1990 3.514 -2.576 -0.270 0.816 1.572 0.1616300 0.712 Up
## 5 1990 0.712 3.514 -2.576 -0.270 0.816 0.1537280 1.178 Up
## 6 1990 1.178 0.712 3.514 -2.576 -0.270 0.1544440 -1.372 Down
summary(Weekly)
## Year Lag1 Lag2 Lag3
## Min. :1990 Min. :-18.1950 Min. :-18.1950 Min. :-18.1950
## 1st Qu.:1995 1st Qu.: -1.1540 1st Qu.: -1.1540 1st Qu.: -1.1580
## Median :2000 Median : 0.2410 Median : 0.2410 Median : 0.2410
## Mean :2000 Mean : 0.1506 Mean : 0.1511 Mean : 0.1472
## 3rd Qu.:2005 3rd Qu.: 1.4050 3rd Qu.: 1.4090 3rd Qu.: 1.4090
## Max. :2010 Max. : 12.0260 Max. : 12.0260 Max. : 12.0260
## Lag4 Lag5 Volume Today
## Min. :-18.1950 Min. :-18.1950 Min. :0.08747 Min. :-18.1950
## 1st Qu.: -1.1580 1st Qu.: -1.1660 1st Qu.:0.33202 1st Qu.: -1.1540
## Median : 0.2380 Median : 0.2340 Median :1.00268 Median : 0.2410
## Mean : 0.1458 Mean : 0.1399 Mean :1.57462 Mean : 0.1499
## 3rd Qu.: 1.4090 3rd Qu.: 1.4050 3rd Qu.:2.05373 3rd Qu.: 1.4050
## Max. : 12.0260 Max. : 12.0260 Max. :9.32821 Max. : 12.0260
## Direction
## Down:484
## Up :605
##
##
##
##
cor(Weekly[, -9])
## Year Lag1 Lag2 Lag3 Lag4
## Year 1.00000000 -0.032289274 -0.03339001 -0.03000649 -0.031127923
## Lag1 -0.03228927 1.000000000 -0.07485305 0.05863568 -0.071273876
## Lag2 -0.03339001 -0.074853051 1.00000000 -0.07572091 0.058381535
## Lag3 -0.03000649 0.058635682 -0.07572091 1.00000000 -0.075395865
## Lag4 -0.03112792 -0.071273876 0.05838153 -0.07539587 1.000000000
## Lag5 -0.03051910 -0.008183096 -0.07249948 0.06065717 -0.075675027
## Volume 0.84194162 -0.064951313 -0.08551314 -0.06928771 -0.061074617
## Today -0.03245989 -0.075031842 0.05916672 -0.07124364 -0.007825873
## Lag5 Volume Today
## Year -0.030519101 0.84194162 -0.032459894
## Lag1 -0.008183096 -0.06495131 -0.075031842
## Lag2 -0.072499482 -0.08551314 0.059166717
## Lag3 0.060657175 -0.06928771 -0.071243639
## Lag4 -0.075675027 -0.06107462 -0.007825873
## Lag5 1.000000000 -0.05851741 0.011012698
## Volume -0.058517414 1.00000000 -0.033077783
## Today 0.011012698 -0.03307778 1.000000000
plot(Weekly$Volume, type = "l", xlab = "Index (time order)", ylab = "Volume",
main = "Weekly Trading Volume Over Time")
glm.fit <- glm(Direction ~ Lag1 + Lag2 + Lag3 + Lag4 + Lag5 + Volume,
data = Weekly, family = binomial)
summary(glm.fit)
##
## Call:
## glm(formula = Direction ~ Lag1 + Lag2 + Lag3 + Lag4 + Lag5 +
## Volume, family = binomial, data = Weekly)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 0.26686 0.08593 3.106 0.0019 **
## Lag1 -0.04127 0.02641 -1.563 0.1181
## Lag2 0.05844 0.02686 2.175 0.0296 *
## Lag3 -0.01606 0.02666 -0.602 0.5469
## Lag4 -0.02779 0.02646 -1.050 0.2937
## Lag5 -0.01447 0.02638 -0.549 0.5833
## Volume -0.02274 0.03690 -0.616 0.5377
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 1496.2 on 1088 degrees of freedom
## Residual deviance: 1486.4 on 1082 degrees of freedom
## AIC: 1500.4
##
## Number of Fisher Scoring iterations: 4
#Among the six predictors, only Lag2 is statistically significant (p = 0.0296), with a positive coefficient (0.0584) suggesting that a positive return two weeks prior is associated with a higher probability of the market going up this week. All other predictors (Lag1, Lag3, Lag4, Lag5, Volume) are not statistically significant at the 0.05 level
glm.probs <- predict(glm.fit, type = "response")
glm.pred <- rep("Down", length(glm.probs))
glm.pred[glm.probs > 0.5] <- "Up"
table(Predicted = glm.pred, Actual = Weekly$Direction)
## Actual
## Predicted Down Up
## Down 54 48
## Up 430 557
mean(glm.pred == Weekly$Direction)
## [1] 0.5610652
train <- (Weekly$Year <= 2008)
Weekly.test <- Weekly[!train, ]
Direction.test <- Weekly$Direction[!train]
glm.fit.d <- glm(Direction ~ Lag2, data = Weekly, family = binomial, subset = train)
summary(glm.fit.d)
##
## Call:
## glm(formula = Direction ~ Lag2, family = binomial, data = Weekly,
## subset = train)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 0.20326 0.06428 3.162 0.00157 **
## Lag2 0.05810 0.02870 2.024 0.04298 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 1354.7 on 984 degrees of freedom
## Residual deviance: 1350.5 on 983 degrees of freedom
## AIC: 1354.5
##
## Number of Fisher Scoring iterations: 4
glm.probs.d <- predict(glm.fit.d, Weekly.test, type = "response")
glm.pred.d <- rep("Down", length(glm.probs.d))
glm.pred.d[glm.probs.d > 0.5] <- "Up"
table(Predicted = glm.pred.d, Actual = Direction.test)
## Actual
## Predicted Down Up
## Down 9 5
## Up 34 56
mean(glm.pred.d == Direction.test)
## [1] 0.625
#62.5% test accuracy, with the model still favoring “Up” predictions but performing much better than the full model.
library(MASS)
##
## Attaching package: 'MASS'
## The following object is masked from 'package:ISLR2':
##
## Boston
lda.fit <- lda(Direction ~ Lag2, data = Weekly, subset = train)
lda.pred <- predict(lda.fit, Weekly.test)
table(Predicted = lda.pred$class, Actual = Direction.test)
## Actual
## Predicted Down Up
## Down 9 5
## Up 34 56
mean(lda.pred$class == Direction.test)
## [1] 0.625
qda.fit <- qda(Direction ~ Lag2, data = Weekly, subset = train)
qda.pred <- predict(qda.fit, Weekly.test)
table(Predicted = qda.pred$class, Actual = Direction.test)
## Actual
## Predicted Down Up
## Down 0 0
## Up 43 61
mean(qda.pred$class == Direction.test)
## [1] 0.5865385
#QDA predicts “Up” for every test observation, giving 58.65% accuracy (equal to the proportion of actual Up weeks
library(class)
train.X <- as.matrix(Weekly$Lag2[train])
test.X <- as.matrix(Weekly$Lag2[!train])
train.Direction <- Weekly$Direction[train]
set.seed(1)
knn.pred <- knn(train.X, test.X, train.Direction, k = 1)
table(Predicted = knn.pred, Actual = Direction.test)
## Actual
## Predicted Down Up
## Down 21 30
## Up 22 31
mean(knn.pred == Direction.test)
## [1] 0.5
#Accuracy = 0.50 (50%), very close to but not identical to the Python result (49.04%) — this small difference comes from how R and Python each break ties randomly when multiple training points are equally close to a test point. Like before, KNN with K=1 performs poorly here, close to a coin flip, since it’s highly sensitive to noise in the weak Lag2-Direction relationship
library(e1071)
nb.fit <- naiveBayes(Direction ~ Lag2, data = Weekly, subset = train)
nb.pred <- predict(nb.fit, Weekly.test)
table(Predicted = nb.pred, Actual = Direction.test)
## Actual
## Predicted Down Up
## Down 0 0
## Up 43 61
mean(nb.pred == Direction.test)
## [1] 0.5865385
#Accuracy = 0.5865 (58.65%), matching the Python result exactly — Naive Bayes predicts “Up” for every test observation, just like QDA, effectively defaulting to the majority class
#part (i): Which method performs best?Logistic regression and LDA tie for best performance (62.5%), followed by QDA and Naive Bayes (58.65%, both defaulting to majority-class predictions), with KNN (K=1) performing worst (50%) due to its sensitivity to noise. This matches the pattern found in the Python analysis
for (k in c(1, 3, 5, 10, 15, 20, 30, 50)) {
set.seed(1)
knn.pred.k <- knn(train.X, test.X, train.Direction, k = k)
acc <- mean(knn.pred.k == Direction.test)
cat("K =", k, ": Accuracy =", round(acc, 4), "\n")
}
## K = 1 : Accuracy = 0.5
## K = 3 : Accuracy = 0.5481
## K = 5 : Accuracy = 0.5385
## K = 10 : Accuracy = 0.5481
## K = 15 : Accuracy = 0.5865
## K = 20 : Accuracy = 0.5865
## K = 30 : Accuracy = 0.5481
## K = 50 : Accuracy = 0.5577
#Accuracy generally improves as K increases from 1 (50%) up to a peak of 58.65% at K=15 and K=20, similar to the pattern seen in Python (where K=20 peaked at 59.6%). Even at its best, KNN doesn’t beat logistic regression/LDA (62.5%), confirming that a simple linear boundary using Lag2 works better than KNN’s flexible, local approach on this weak-signal data
data(Auto)
median.mpg <- median(Auto$mpg)
Auto$mpg01 <- as.numeric(Auto$mpg > median.mpg)
head(Auto)
## mpg cylinders displacement horsepower weight acceleration year origin
## 1 18 8 307 130 3504 12.0 70 1
## 2 15 8 350 165 3693 11.5 70 1
## 3 18 8 318 150 3436 11.0 70 1
## 4 16 8 304 150 3433 12.0 70 1
## 5 17 8 302 140 3449 10.5 70 1
## 6 15 8 429 198 4341 10.0 70 1
## name mpg01
## 1 chevrolet chevelle malibu 0
## 2 buick skylark 320 0
## 3 plymouth satellite 0
## 4 amc rebel sst 0
## 5 ford torino 0
## 6 ford galaxie 500 0
table(Auto$mpg01)
##
## 0 1
## 196 196
par(mfrow = c(2, 3))
boxplot(cylinders ~ mpg01, data = Auto, main = "Cylinders")
boxplot(displacement ~ mpg01, data = Auto, main = "Displacement")
boxplot(horsepower ~ mpg01, data = Auto, main = "Horsepower")
boxplot(weight ~ mpg01, data = Auto, main = "Weight")
boxplot(acceleration ~ mpg01, data = Auto, main = "Acceleration")
boxplot(year ~ mpg01, data = Auto, main = "Year")
#These match the Python results exactly — cylinders, displacement,
horsepower, and weight all show strong, clear separation between mpg01
groups (mpg01=1 cars have fewer cylinders, lower displacement, less
horsepower, lower weight), while acceleration and year show weaker
separation. We’ll use cylinders, displacement, horsepower, and weight as
our predictors going forward
set.seed(1)
train.idx <- sample(1:nrow(Auto), size = 0.7 * nrow(Auto))
Auto.train <- Auto[train.idx, ]
Auto.test <- Auto[-train.idx, ]
mpg01.test <- Auto$mpg01[-train.idx]
dim(Auto.train)
## [1] 274 10
dim(Auto.test)
## [1] 118 10
#274 training rows, 118 test rows, matching the 70/30 split from Python
lda.fit.auto <- lda(mpg01 ~ cylinders + displacement + horsepower + weight,
data = Auto.train)
lda.pred.auto <- predict(lda.fit.auto, Auto.test)
table(Predicted = lda.pred.auto$class, Actual = mpg01.test)
## Actual
## Predicted 0 1
## 0 50 3
## 1 11 54
mean(lda.pred.auto$class == mpg01.test)
## [1] 0.8813559
qda.fit.auto <- qda(mpg01 ~ cylinders + displacement + horsepower + weight,
data = Auto.train)
qda.pred.auto <- predict(qda.fit.auto, Auto.test)
table(Predicted = qda.pred.auto$class, Actual = mpg01.test)
## Actual
## Predicted 0 1
## 0 52 5
## 1 9 52
mean(qda.pred.auto$class == mpg01.test)
## [1] 0.8813559
#Accuracy = 0.8814 → Test Error = 0.1186 (11.9%), identical accuracy to LDA but with a slightly different error pattern (9 false positives, 5 false negatives vs. LDA’s 11/3). Unlike the Python run (where QDA outperformed LDA), here they tie exactly — again likely due to the different train/test split
glm.fit.auto <- glm(mpg01 ~ cylinders + displacement + horsepower + weight,
data = Auto.train, family = binomial)
glm.probs.auto <- predict(glm.fit.auto, Auto.test, type = "response")
glm.pred.auto <- rep(0, length(glm.probs.auto))
glm.pred.auto[glm.probs.auto > 0.5] <- 1
table(Predicted = glm.pred.auto, Actual = mpg01.test)
## Actual
## Predicted 0 1
## 0 53 3
## 1 8 54
mean(glm.pred.auto == mpg01.test)
## [1] 0.9067797
#Accuracy = 0.9068 → Test Error = 0.0932 (9.3%). This is the best result so far among LDA/QDA/logistic regression on this R split, with a fairly balanced error pattern (8 false positives, 3 false negatives)
nb.fit.auto <- naiveBayes(mpg01 ~ cylinders + displacement + horsepower + weight,
data = Auto.train)
nb.pred.auto <- predict(nb.fit.auto, Auto.test)
table(Predicted = nb.pred.auto, Actual = mpg01.test)
## Actual
## Predicted 0 1
## 0 52 4
## 1 9 53
mean(nb.pred.auto == mpg01.test)
## [1] 0.8898305
#Accuracy = 0.8898 → Test Error = 0.1102 (11.0%), landing between LDA/QDA (11.9%) and logistic regression (9.3%). A reasonably balanced error pattern (9 false positives, 4 false negatives
train.X.auto <- scale(Auto.train[, c("cylinders", "displacement", "horsepower", "weight")])
test.X.auto <- scale(Auto.test[, c("cylinders", "displacement", "horsepower", "weight")],
center = attr(train.X.auto, "scaled:center"),
scale = attr(train.X.auto, "scaled:scale"))
train.mpg01 <- Auto.train$mpg01
for (k in c(1, 3, 5, 10, 15, 20, 30)) {
set.seed(1)
knn.pred.auto <- knn(train.X.auto, test.X.auto, train.mpg01, k = k)
acc <- mean(knn.pred.auto == mpg01.test)
cat("K =", k, ": Accuracy =", round(acc, 4), "\n")
}
## K = 1 : Accuracy = 0.8644
## K = 3 : Accuracy = 0.8729
## K = 5 : Accuracy = 0.8644
## K = 10 : Accuracy = 0.8644
## K = 15 : Accuracy = 0.8898
## K = 20 : Accuracy = 0.8898
## K = 30 : Accuracy = 0.8898
#Best KNN accuracy is 0.8898 (11.0% test error), achieved at K=15, 20, and 30 — tying with Naive Bayes but still slightly worse than logistic regression’s 9.3% error. Unlike K=1 sensitivity issues seen in Question 13, here K=1 already performs reasonably well (86.4%), and accuracy stabilizes/improves slightly with larger K
##Logistic regression performs best on this train/test split, though all methods perform reasonably well (all above 88% accuracy), confirming that cylinders, displacement, horsepower, and weight are strong predictors of mpg01 regardless of method. This differs slightly from the Python run (where QDA won), which is expected given the different random train/test split — a good reminder that these small differences in performance between methods aren’t hugely meaningful with a single train/test split; results can shift somewhat with different splits
##question 16 :
Boston <- ISLR2::Boston
median.crim <- median(Boston$crim)
Boston$crim01 <- as.numeric(Boston$crim > median.crim)
table(Boston$crim01)
##
## 0 1
## 253 253
par(mfrow = c(3, 4), mar = c(4, 4, 2, 1))
boxplot(zn ~ crim01, data = Boston, main = "zn")
boxplot(indus ~ crim01, data = Boston, main = "indus")
boxplot(chas ~ crim01, data = Boston, main = "chas")
boxplot(nox ~ crim01, data = Boston, main = "nox")
boxplot(rm ~ crim01, data = Boston, main = "rm")
boxplot(age ~ crim01, data = Boston, main = "age")
boxplot(dis ~ crim01, data = Boston, main = "dis")
boxplot(rad ~ crim01, data = Boston, main = "rad")
boxplot(tax ~ crim01, data = Boston, main = "tax")
boxplot(ptratio ~ crim01, data = Boston, main = "ptratio")
boxplot(lstat ~ crim01, data = Boston, main = "lstat")
boxplot(medv ~ crim01, data = Boston, main = "medv")
#Great, that worked. These match the Python results — nox, age, dis, rad, tax, and indus show the strongest separation between low-crime and high-crime suburbs (higher crime associated with more industrial land, more pollution, older housing, closer proximity to employment centers, greater highway access, and higher taxes), while chas, rm, ptratio, lstat, and medv show weaker separation. We’ll use nox, age, dis, rad, tax, indus as our predictors going forward.
set.seed(1)
train.idx.b <- sample(1:nrow(Boston), size = 0.7 * nrow(Boston))
Boston.train <- Boston[train.idx.b, ]
Boston.test <- Boston[-train.idx.b, ]
crim01.test <- Boston$crim01[-train.idx.b]
dim(Boston.train)
## [1] 354 14
dim(Boston.test)
## [1] 152 14
#354 training rows, 152 test rows, matching the split size from Python
glm.fit.b <- glm(crim01 ~ nox + age + dis + rad + tax + indus,
data = Boston.train, family = binomial)
glm.probs.b <- predict(glm.fit.b, Boston.test, type = "response")
glm.pred.b <- rep(0, length(glm.probs.b))
glm.pred.b[glm.probs.b > 0.5] <- 1
table(Predicted = glm.pred.b, Actual = crim01.test)
## Actual
## Predicted 0 1
## 0 58 6
## 1 15 73
mean(glm.pred.b == crim01.test)
## [1] 0.8618421
lda.fit.b <- lda(crim01 ~ nox + age + dis + rad + tax + indus,
data = Boston.train)
lda.pred.b <- predict(lda.fit.b, Boston.test)
table(Predicted = lda.pred.b$class, Actual = crim01.test)
## Actual
## Predicted 0 1
## 0 71 20
## 1 2 59
mean(lda.pred.b$class == crim01.test)
## [1] 0.8552632
#Accuracy = 0.8553 → Test Error = 0.1447 (14.5%), close to logistic regression but slightly worse, and much better than what we saw in the Python run (where LDA had 17.8% error). Same pattern though — more false negatives (20) than false positives (2), suggesting LDA still tends to miss actual high-crime suburbs
nb.fit.b <- naiveBayes(crim01 ~ nox + age + dis + rad + tax + indus,
data = Boston.train)
nb.pred.b <- predict(nb.fit.b, Boston.test)
table(Predicted = nb.pred.b, Actual = crim01.test)
## Actual
## Predicted 0 1
## 0 65 19
## 1 8 60
mean(nb.pred.b == crim01.test)
## [1] 0.8223684
#Accuracy = 0.8224 → Test Error = 0.1776 (17.8%), worse than both logistic regression (13.8%) and LDA (14.5%), consistent with the pattern from Python where Naive Bayes underperformed due to violated independence/normality assumptions among correlated predictors like tax, rad, and indus
train.X.b <- scale(Boston.train[, c("nox", "age", "dis", "rad", "tax", "indus")])
test.X.b <- scale(Boston.test[, c("nox", "age", "dis", "rad", "tax", "indus")],
center = attr(train.X.b, "scaled:center"),
scale = attr(train.X.b, "scaled:scale"))
train.crim01 <- Boston.train$crim01
for (k in c(1, 3, 5, 10, 15, 20)) {
set.seed(1)
knn.pred.b <- knn(train.X.b, test.X.b, train.crim01, k = k)
acc <- mean(knn.pred.b == crim01.test)
cat("K =", k, ": Accuracy =", round(acc, 4), "\n")
}
## K = 1 : Accuracy = 0.9079
## K = 3 : Accuracy = 0.9276
## K = 5 : Accuracy = 0.9276
## K = 10 : Accuracy = 0.9079
## K = 15 : Accuracy = 0.9145
## K = 20 : Accuracy = 0.9013
#Best KNN accuracy is 0.9276 (7.24% test error), achieved at K=3 and K=5 — the best result of any method in this R analysis of Question 16, consistent with the Python findings where KNN also performed best. This confirms that the relationship between these predictors and crime classification is highly local/non-linear, which KNN captures better than the linear or normality-based methods. ##. Overall conclusion: KNN clearly outperforms the parametric methods (logistic regression, LDA, Naive Bayes) on this dataset, suggesting the boundary between high-crime and low-crime suburbs is non-linear and doesn’t fit well with methods assuming linear boundaries or normal/independent predictor distributions. Naive Bayes performs worst, likely due to violated independence assumptions among correlated predictors like tax, rad, and indus.