Info

Objective

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!

Your task

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.

Deadline

7 Sep 2026, midnight

Libraries

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

Question 1

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()

Question 2

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

Question 3

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()

Question 4

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.

Declaration of Generative AI usage

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