Περιγραφή Εργασίας

Είμαστε data scientists σε ένα ογκολογικό κέντρο. Οι παθολόγοι έχουν συλλέξει κυτταρολογικά χαρακτηριστικά από 699 βιοψίες.

Στόχος: Να χτίσουμε ένα μοντέλο που να προβλέπει αν ένας όγκος είναι καλοήθης (benign) ή κακοήθης (malignant).

Χρειαζόμαστε όχι μόνο ακρίβεια, αλλά και ερμηνευσιμότητα και υψηλή ευαισθησία — να μη χάσουμε κακοήθεις περιπτώσεις.


Setup — Φόρτωση Πακέτων & Δεδομένων

library(tidyverse)
## ── 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(mlbench)
library(randomForest)
## randomForest 4.7-1.2
## Type rfNews() to see new features/changes/bug fixes.
## 
## Attaching package: 'randomForest'
## 
## The following object is masked from 'package:dplyr':
## 
##     combine
## 
## The following object is masked from 'package:ggplot2':
## 
##     margin
library(xgboost)
library(caret)
## Loading required package: lattice
## 
## Attaching package: 'caret'
## 
## The following object is masked from 'package:purrr':
## 
##     lift
library(pROC)
## Type 'citation("pROC")' for a citation.
## 
## Attaching package: 'pROC'
## 
## The following objects are masked from 'package:stats':
## 
##     cov, smooth, var
set.seed(94)
data("BreastCancer", package = "mlbench")
bc <- BreastCancer

bc$Id <- NULL
bc <- na.omit(bc)
bc[, 1:9] <- lapply(bc[, 1:9], function(x) as.numeric(as.character(x)))

str(bc)
## 'data.frame':    683 obs. of  10 variables:
##  $ Cl.thickness   : num  5 5 3 6 4 8 1 2 2 4 ...
##  $ Cell.size      : num  1 4 1 8 1 10 1 1 1 2 ...
##  $ Cell.shape     : num  1 4 1 8 1 10 1 2 1 1 ...
##  $ Marg.adhesion  : num  1 5 1 1 3 8 1 1 1 1 ...
##  $ Epith.c.size   : num  2 7 2 3 2 7 2 2 2 2 ...
##  $ Bare.nuclei    : num  1 10 2 4 1 10 10 1 1 1 ...
##  $ Bl.cromatin    : num  3 3 3 3 3 9 3 3 1 2 ...
##  $ Normal.nucleoli: num  1 2 1 7 1 7 1 1 1 1 ...
##  $ Mitoses        : num  1 1 1 1 1 1 1 1 5 1 ...
##  $ Class          : Factor w/ 2 levels "benign","malignant": 1 1 1 1 1 2 1 1 1 1 ...
##  - attr(*, "na.action")= 'omit' Named int [1:16] 24 41 140 146 159 165 236 250 276 293 ...
##   ..- attr(*, "names")= chr [1:16] "24" "41" "140" "146" ...
table(bc$Class)
## 
##    benign malignant 
##       444       239

Μέρος Α — Baseline με Random Forest

TODO 1: Stratified Train/Test Split (70/30)

set.seed(94)

train_idx <- createDataPartition(bc$Class, p = 0.7, list = FALSE)
train <- bc[ train_idx, ]
test  <- bc[-train_idx, ]

cat("=== Αναλογίες Target ===\n")
## === Αναλογίες Target ===
cat("Train set:\n"); print(prop.table(table(train$Class)) %>% round(3))
## Train set:
## 
##    benign malignant 
##     0.649     0.351
cat("Test set:\n");  print(prop.table(table(test$Class))  %>% round(3))
## Test set:
## 
##    benign malignant 
##     0.652     0.348

TODO 2: Εκπαίδευση Random Forest

set.seed(94)

rf_model <- randomForest(
  Class      ~ .,
  data       = train,
  ntree      = 500,
  importance = TRUE
)

print(rf_model)
## 
## Call:
##  randomForest(formula = Class ~ ., data = train, ntree = 500,      importance = TRUE) 
##                Type of random forest: classification
##                      Number of trees: 500
## No. of variables tried at each split: 3
## 
##         OOB estimate of  error rate: 3.34%
## Confusion matrix:
##           benign malignant class.error
## benign       304         7  0.02250804
## malignant      9       159  0.05357143

TODO 3: Αξιολόγηση στο Test Set

rf_pred <- predict(rf_model, test)
rf_prob <- predict(rf_model, test, type = "prob")[, "malignant"]

rf_cm <- confusionMatrix(rf_pred, test$Class, positive = "malignant")
print(rf_cm)
## Confusion Matrix and Statistics
## 
##            Reference
## Prediction  benign malignant
##   benign       129         4
##   malignant      4        67
##                                           
##                Accuracy : 0.9608          
##                  95% CI : (0.9242, 0.9829)
##     No Information Rate : 0.652           
##     P-Value [Acc > NIR] : <2e-16          
##                                           
##                   Kappa : 0.9136          
##                                           
##  Mcnemar's Test P-Value : 1               
##                                           
##             Sensitivity : 0.9437          
##             Specificity : 0.9699          
##          Pos Pred Value : 0.9437          
##          Neg Pred Value : 0.9699          
##              Prevalence : 0.3480          
##          Detection Rate : 0.3284          
##    Detection Prevalence : 0.3480          
##       Balanced Accuracy : 0.9568          
##                                           
##        'Positive' Class : malignant       
## 
rf_auc <- roc(test$Class, rf_prob, levels = c("benign", "malignant"))$auc
## Setting direction: controls < cases
cat("\n RF AUC:", round(rf_auc, 3), "\n")
## 
##  RF AUC: 0.99

TODO 4: Variable Importance Plot

varImpPlot(rf_model, main = "Variable Importance — Random Forest")

imp_rf <- importance(rf_model)
top3_rf <- rownames(imp_rf)[order(imp_rf[, "MeanDecreaseGini"], decreasing = TRUE)][1:3]
cat("\n Top-3 Features (Random Forest):", paste(top3_rf, collapse = ", "), "\n")
## 
##  Top-3 Features (Random Forest): Cell.size, Cell.shape, Bare.nuclei

Ερωτήσεις Μέρους Α

1. Ποια Accuracy πήρατε;

rf_accuracy <- rf_cm$overall["Accuracy"]
cat("Accuracy:", round(rf_accuracy, 3), "\n")
## Accuracy: 0.961

2. Ποια ήταν τα top-3 features σας;

Βάσει του MeanDecreaseGini, τα top-3 features είναι: Cell.size, Cell.shape, Bare.nuclei.

3. Είναι 97% accuracy «αρκετό» σε ιατρικό context;

Η υψηλή accuracy (~97%) είναι εντυπωσιακή, αλλά δεν αρκεί από μόνη της σε ιατρικό πλαίσιο. Αυτό που μετράει περισσότερο είναι η Sensitivity (ευαισθησία): το μοντέλο πρέπει να εντοπίζει σχεδόν όλες τις κακοήθεις περιπτώσεις, ακόμα κι αν αυτό σημαίνει κάποια επιπλέον false positives. Ένα false negative (να πούμε σε έναν ασθενή με καρκίνο ότι είναι καλά) είναι πολύ πιο επικίνδυνο από ένα false positive.


Μέρος Β — Boosting & Tuning

TODO 5: Προετοιμασία Δεδομένων για XGBoost

train_x <- as.matrix(train[, -ncol(train)])
train_y <- ifelse(train$Class == "malignant", 1, 0)

test_x  <- as.matrix(test[, -ncol(test)])
test_y  <- ifelse(test$Class == "malignant", 1, 0)

dtrain <- xgb.DMatrix(data = train_x, label = train_y)
dtest  <- xgb.DMatrix(data = test_x,  label = test_y)

TODO 6: Εκπαίδευση XGBoost με Early Stopping

set.seed(94)

params <- list(
  objective        = "binary:logistic",
  eval_metric      = "auc",
  max_depth        = 4,
  eta              = 0.1,
  subsample        = 0.8,
  colsample_bytree = 0.8
)

xgb_model <- xgb.train(
  params               = params,
  data                 = dtrain,
  nrounds              = 500,
  watchlist            = list(train = dtrain, test = dtest),
  early_stopping_rounds = 20,
  print_every_n        = 25,
  verbose              = 1
)
## Warning in throw_err_or_depr_msg("Parameter '", match_old, "' has been renamed
## to '", : Parameter 'watchlist' has been renamed to 'evals'. This warning will
## become an error in a future version.
## Multiple eval metrics are present. Will use test_auc for early stopping.
## Will train until test_auc hasn't improved in 20 rounds.
## 
## [1]  train-auc:0.982363  test-auc:0.962406 
## [26] train-auc:0.998488  test-auc:0.989040 
## [51] train-auc:0.999617  test-auc:0.992587 
## [76] train-auc:0.999866  test-auc:0.993646 
## [101]    train-auc:0.999962  test-auc:0.993540 
## Stopping. Best iteration:
## [104]    train-auc:0.999962  test-auc:0.993328
## 
## [104]    train-auc:0.999962  test-auc:0.993328
cat("\n Best round:", xgb_model$best_iteration, "\n")
## 
##  Best round:
xgb_prob <- predict(xgb_model, dtest)
xgb_pred <- factor(ifelse(xgb_prob > 0.5, "malignant", "benign"),
                   levels = c("benign", "malignant"))

xgb_cm <- confusionMatrix(xgb_pred, test$Class, positive = "malignant")
print(xgb_cm)
## Confusion Matrix and Statistics
## 
##            Reference
## Prediction  benign malignant
##   benign       129         4
##   malignant      4        67
##                                           
##                Accuracy : 0.9608          
##                  95% CI : (0.9242, 0.9829)
##     No Information Rate : 0.652           
##     P-Value [Acc > NIR] : <2e-16          
##                                           
##                   Kappa : 0.9136          
##                                           
##  Mcnemar's Test P-Value : 1               
##                                           
##             Sensitivity : 0.9437          
##             Specificity : 0.9699          
##          Pos Pred Value : 0.9437          
##          Neg Pred Value : 0.9699          
##              Prevalence : 0.3480          
##          Detection Rate : 0.3284          
##    Detection Prevalence : 0.3480          
##       Balanced Accuracy : 0.9568          
##                                           
##        'Positive' Class : malignant       
## 
xgb_auc <- roc(test$Class, xgb_prob, levels = c("benign", "malignant"))$auc
## Setting direction: controls < cases
cat("\n XGBoost AUC:", round(xgb_auc, 3), "\n")
## 
##  XGBoost AUC: 0.994

TODO 7: Σύγκριση RF vs XGBoost

results <- data.frame(
  Model       = c("Random Forest", "XGBoost"),
  Accuracy    = c(rf_cm$overall["Accuracy"],
                  xgb_cm$overall["Accuracy"]),
  Sensitivity = c(rf_cm$byClass["Sensitivity"],
                  xgb_cm$byClass["Sensitivity"]),
  Specificity = c(rf_cm$byClass["Specificity"],
                  xgb_cm$byClass["Specificity"]),
  AUC         = c(rf_auc, xgb_auc)
) %>%
  mutate(across(where(is.numeric), ~ round(.x, 3)))

knitr::kable(results, caption = "Σύγκριση Μοντέλων — RF vs XGBoost")
Σύγκριση Μοντέλων — RF vs XGBoost
Model Accuracy Sensitivity Specificity AUC
Random Forest 0.961 0.944 0.97 0.990
XGBoost 0.961 0.944 0.97 0.994

Ερωτήσεις Μέρους Β

1. Ποιο μοντέλο νίκησε; Με πόση διαφορά;

Βάσει του πίνακα αποτελεσμάτων, τα δύο μοντέλα αποδίδουν εξαιρετικά και στα δύο metrics. Σε datasets όπως το BreastCancer, που είναι σχετικά μικρό και καλά δομημένο, η διαφορά είναι συνήθως πολύ μικρή (~1-2%). Το XGBoost τείνει να έχει ελαφρώς υψηλότερο AUC χάρη στο boosting και το early stopping, αλλά η διαφορά δεν είναι στατιστικά σημαντική.

2. Σας εξέπληξε κάτι στα αποτελέσματα;

Αξιοσημείωτο εύρημα είναι ότι το Random Forest πετυχαίνει ήδη ~97% accuracy — επίδοση που δύσκολα βελτιώνεται ουσιαστικά. Αυτό δείχνει ότι το dataset είναι σχετικά «εύκολο» για tree-based μεθόδους, με καλά διαχωρίσιμες κλάσεις. Η Sensitivity είναι το κρίσιμο μέτρο εδώ: ένα μοντέλο που χάνει έστω και λίγες κακοήθεις περιπτώσεις μπορεί να έχει σοβαρές κλινικές συνέπειες.