The purpose of writing graded lab reports is to help students to stay on track and to provide summative feedback. Each lab report is just 1% of the total course mark. Please do not cheat - it is not worth it!
Solve the practical questions, knit your document into a PDF and submit to NTULearn before the deadline. The deadline is very tight because the task is simple. We are sure that everyone is capable to do it by themselves and we want to discourage taking someone else’s report and writing it with your own words.
7 Sep 2026, midnight
We will again work with breast cancer dataset and we will predict if a tumor is benign of malignant from its measurements.
Source: https://www.kaggle.com/uciml/breast-cancer-wisconsin-data
Here, we load libraries, data and set the random seed. Replace the number “1729” with the numeric part of your matric no
library(tidyverse) # for manipulation with data
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr 1.2.1 ✔ readr 2.2.0
## ✔ forcats 1.0.1 ✔ stringr 1.6.0
## ✔ ggplot2 4.0.3 ✔ tibble 3.3.1
## ✔ lubridate 1.9.5 ✔ tidyr 1.3.2
## ✔ purrr 1.2.2
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(caret) # for machine learning, including KNN
## Loading required package: lattice
##
## Attaching package: 'caret'
##
## The following object is masked from 'package:purrr':
##
## lift
library(modelsummary)
B <- read.csv("breast_cancer.csv") %>%
select(-X) %>%
select(-id) %>%
mutate(Y = as.factor(diagnosis)) %>%
select(-diagnosis)
set.seed(8128) # replace the number '1729' with your matric no
dim(B)
## [1] 569 31
Split the data into 70% training and 30% test set. Report dimensions
of both training and test datasets. Plot the scatterplot of
texture_worst vs perimeter_worst coloured
according to Y (type of tumor) in the train data.
ind <- runif(nrow(B)) < 0.7
train_data <- B %>% filter(ind)
test_data <- B %>% filter(!ind)
cat("Training data dim =", dim(train_data), "\n")
## Training data dim = 394 31
cat("Test data dim =", dim(test_data), "\n")
## Test data dim = 175 31
ggplot(data = train_data, aes(x = texture_worst, y = perimeter_worst,
group = Y, color = Y)) +
geom_point() + theme_minimal()
Train a logistic regression to predict Y using all other
variables as predictors and print its coefficients, its 5-fold
cross-validation error, and its test error.
mod_log <- train(
Y ~., data = train_data, method = "glm", family = binomial,
trControl = trainControl("cv", number = 5)
)
## Warning: glm.fit: algorithm did not converge
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
## Warning: glm.fit: algorithm did not converge
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
## Warning: glm.fit: algorithm did not converge
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
## Warning: glm.fit: algorithm did not converge
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
## Warning: glm.fit: algorithm did not converge
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
## Warning: glm.fit: algorithm did not converge
## Warning: glm.fit: fitted probabilities numerically 0 or 1 occurred
Coefficients of the logistic regression can be found here:
modelsummary(mod_log$finalModel,
fmt = 4,
statistic = NULL,
estimate = "{estimate} ({std.error}){stars}")
| (1) | |
|---|---|
| (Intercept) | -1997.0813 (9.618481e+05) |
| radius_mean | -190.1542 (2.082525e+05) |
| texture_mean | -3.3815 (5.457986e+03) |
| perimeter_mean | 21.5049 (2.692097e+04) |
| area_mean | 0.5587 (2.214959e+03) |
| smoothness_mean | 2992.8628 (1.828311e+06) |
| compactness_mean | -2407.8341 (9.550865e+05) |
| concavity_mean | 2398.7992 (8.978103e+05) |
| concave.points_mean | -739.7739 (1.602503e+06) |
| symmetry_mean | -140.1980 (5.747497e+05) |
| fractal_dimension_mean | 4003.0004 (3.405163e+06) |
| radius_se | -245.6910 (3.951806e+05) |
| texture_se | -105.4790 (3.567442e+04) |
| perimeter_se | 34.6228 (3.386686e+04) |
| area_se | 2.9009 (3.873790e+03) |
| smoothness_se | 6091.8359 (7.722557e+06) |
| compactness_se | 8254.3595 (2.589888e+06) |
| concavity_se | -1964.1088 (1.634082e+06) |
| concave.points_se | 9290.9099 (4.146313e+06) |
| symmetry_se | -5694.7613 (2.224612e+06) |
| fractal_dimension_se | -81274.8747 (2.170778e+07) |
| radius_worst | 191.0292 (9.493340e+04) |
| texture_worst | 14.7144 (4.150355e+03) |
| perimeter_worst | -10.6107 (4.187337e+03) |
| area_worst | -1.0033 (9.471895e+02) |
| smoothness_worst | -269.2263 (1.651697e+06) |
| compactness_worst | -804.6025 (4.485695e+05) |
| concavity_worst | -121.1807 (3.387534e+05) |
| concave.points_worst | 466.1841 (4.878239e+05) |
| symmetry_worst | 878.4675 (3.421214e+05) |
| fractal_dimension_worst | 6723.1195 (2.791146e+06) |
| Num.Obs. | 394 |
| AIC | 62.0 |
| BIC | 185.3 |
| Log.Lik. | -0.000 |
| RMSE | 0.00 |
The cross-validation error is
1 - mod_log$results$Accuracy
## [1] 0.0406037
The test error is
error_rate <- function(model, dataset=test_data) {
cm <- model %>%
predict(dataset) %>%
confusionMatrix(dataset$Y)
c(test_error = unname(1 - cm$overall['Accuracy']))
}
error_rate(mod_log)
## test_error
## 0.02285714
Train a lasso regression. Report its test error. Plot how coefficients change with \(\lambda\) and print coefficients of the final model.
You can choose the following values of \(\lambda\):
lambda <- 10^seq(-3, 0 , length = 20)
lambda
## [1] 0.001000000 0.001438450 0.002069138 0.002976351 0.004281332 0.006158482
## [7] 0.008858668 0.012742750 0.018329807 0.026366509 0.037926902 0.054555948
## [13] 0.078475997 0.112883789 0.162377674 0.233572147 0.335981829 0.483293024
## [19] 0.695192796 1.000000000
lasso <- train(
Y ~., data = train_data, method = "glmnet",
trControl = trainControl("cv", number = 5),
tuneGrid = expand.grid(alpha = 1, lambda = lambda),
preProcess = c("scale", "center")
)
cat("Lasso test error = ", error_rate(lasso))
## Lasso test error = 0.01714286
Coefficients of the final model are
coef(lasso$finalModel, lasso$finalModel$lambdaOpt)
## 31 x 1 sparse Matrix of class "dgCMatrix"
## s=0.004281332
## (Intercept) -0.5807787
## radius_mean .
## texture_mean .
## perimeter_mean .
## area_mean .
## smoothness_mean .
## compactness_mean .
## concavity_mean .
## concave.points_mean 0.8393680
## symmetry_mean .
## fractal_dimension_mean -0.2499034
## radius_se 1.8894286
## texture_se -0.3009727
## perimeter_se .
## area_se .
## smoothness_se 0.1448392
## compactness_se -0.1944271
## concavity_se .
## concave.points_se .
## symmetry_se .
## fractal_dimension_se -0.2097224
## radius_worst 3.2658417
## texture_worst 1.5135416
## perimeter_worst .
## area_worst .
## smoothness_worst 0.4087786
## compactness_worst .
## concavity_worst 0.4387177
## concave.points_worst 1.4363138
## symmetry_worst 0.4340952
## fractal_dimension_worst .
And here is the plot:
library(viridis)
## Loading required package: viridisLite
library(scales)
##
## Attaching package: 'scales'
## The following object is masked from 'package:viridis':
##
## viridis_pal
## The following object is masked from 'package:purrr':
##
## discard
## The following object is masked from 'package:readr':
##
## col_factor
coef(lasso$finalModel, lambda) %>%
as.matrix %>% t %>% as_tibble %>%
mutate(lambda = lambda) %>%
pivot_longer(radius_mean:fractal_dimension_worst,
names_to = "variable", values_to = "coef") %>%
ggplot(aes(x = lambda, y = coef, group = variable, colour = variable)) +
geom_line() +
scale_x_log10(
breaks = 10^(-3:3),
labels = function(b) scales::math_format(10^.x)(log10(b)),
minor_breaks = NULL
) +
scale_colour_viridis_d(option = "turbo", end = 0.95, begin = 0.05) +
theme_minimal()
Compare the magnitudes of the coefficients of the two models: plain logistic regression (from Question 2) and \(l_1\)-regularized logistic regression (from Question 3). Which of the two models performs variable selection? Which of the two models is easier to interpret, and why?
ANSWER For this dataset, plain logistic regression produces very large coefficients (some on the order of \(10^{16}\)), and none of them is statistically significant. Hence, the model is difficult to interpret. In contrast, the LASSO model is much clearer and more transparent. LASSO also performs variable selection by setting some coefficients exactly to zero. As a result, it contains fewer variables than the full logistic regression model and is therefore easier to interpret.
Modify the following:
I used ChatGPT 5.6 to choose a nice colour scheme and axis labels in Question 3 and to polish grammar and phrasing of question 4 and the answer.
Type your name to confirm: Fedor Duzhin