Setup & Προετοιμασία

Περιγραφή του Dataset

Το σύνολο δεδομένων που χρησιμοποιείται είναι το BreastCancer, το οποίο προέρχεται από το πακέτο mlbench και περιλαμβάνει δεδομένα από το Πανεπιστήμιο του Wisconsin.

  • Μέγεθος: Αποτελείται από 699 παρατηρήσεις (βιοψίες).

  • Χαρακτηριστικά: Περιλαμβάνει 9 κυτταρολογικά χαρακτηριστικά (όπως το πάχος της μάζας, η ομοιομορφία του μεγέθους των κυττάρων, η προσκόλληση οριακών κυττάρων κ.α.).

  • Κλίμακα: Κάθε χαρακτηριστικό βαθμολογείται σε μια κλίμακα από 1 έως 10.

  • Target Variable (Class): Είναι μια δυαδική μεταβλητή που χωρίζει τα δείγματα σε:

    • Benign (Καλοήθης όγκος)

    • Malignant (Κακοήθης όγκος)

Στόχος

Ως Data Scientists σε ένα ογκολογικό κέντρο, ο στόχος μας είναι να αναπτύξουμε ένα Decision Support Tool για τους παθολόγους.

Οι κύριες προτεραιότητες του μοντέλου είναι:

  1. Υψηλή Ακρίβεια (Accuracy): Η σωστή κατηγοριοποίηση των όγκων.

  2. Υψηλή Ευαισθησία (Sensitivity): Είναι κρίσιμο να μη χάσουμε καμία κακοήθη περίπτωση (false negative), καθώς αυτό θα σήμαινε καθυστέρηση στη θεραπεία ενός ασθενή.

  3. Ερμηνευσιμότητα: Να κατανοήσουμε ποια κυτταρολογικά χαρακτηριστικά είναι οι σημαντικότεροι δείκτες κακοήθειας.

# --- Φόρτωση πακέτων ---
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.2     ✔ tibble    3.3.1
## ✔ lubridate 1.9.5     ✔ tidyr     1.3.2
## ✔ purrr     1.2.1     
## ── 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(42)

# --- Φόρτωση δεδομένων ---
data("BreastCancer", package = "mlbench")
bc <- BreastCancer

# Καθαρισμός: αφαίρεση ID, χειρισμός missing, μετατροπή σε numeric
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

ΜΕΡΟΣ Α

1. Stratified train/test split (70/30)

Χρησιμοποιούμε τη συνάρτηση createDataPartition για να διασφαλίσουμε ότι το ποσοστό των κακοήθων όγκων παραμένει σταθερό και στα δύο δείγματα.

set.seed(42) # Για αναπαραγωγιμότητα του διαχωρισμού

# Δημιουργία δεικτών για το split
train_idx <- createDataPartition(bc$Class, p = 0.7, list = FALSE)

# Δημιουργία των συνόλων εκπαίδευσης και ελέγχου
train_set <- bc[train_idx, ]
test_set  <- bc[-train_idx, ]

# Έλεγχος αναλογίας κλάσεων (επαλήθευση stratified split)
cat("Αναλογία στο Train Set:\n")
## Αναλογία στο Train Set:
print(prop.table(table(train_set$Class)))
## 
##    benign malignant 
## 0.6492693 0.3507307
cat("\nΑναλογία στο Test Set:\n")
## 
## Αναλογία στο Test Set:
print(prop.table(table(test_set$Class)))
## 
##    benign malignant 
## 0.6519608 0.3480392

2. Εκπαίδευση του Random Forest

Εκπαιδεύουμε το μοντέλο Random Forest με 500 δέντρα. Η παράμετρος importance = TRUE επιτρέπει την καταγραφή της συνεισφοράς κάθε μεταβλητής στην τελική πρόβλεψη.

set.seed(42)
rf_model <- randomForest(
  Class ~ ., 
  data = train_set, 
  ntree = 500, 
  importance = TRUE
)

print(rf_model)
## 
## Call:
##  randomForest(formula = Class ~ ., data = train_set, 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.76%
## Confusion matrix:
##           benign malignant class.error
## benign       301        10  0.03215434
## malignant      8       160  0.04761905

Το μοντέλο πέτυχε OOB error 3.76%.

Από το Confusion Matrix προκύπτει ότι η ακρίβεια είναι υψηλή και στις δύο κλάσεις, με ελαφρώς μεγαλύτερο σφάλμα στην αναγνώριση των κακοήθων όγκων (~4.8%).

3. Αξιολόγηση στο Test Set (Accuracy, Sensitivity, AUC)

Ελέγχουμε την απόδοση του μοντέλου σε άγνωστα δεδομένα (Test Set) για να υπολογίσουμε την τελική ακρίβεια και την ευαισθησία του.

# Προβλέψεις
rf_pred <- predict(rf_model, test_set)
rf_prob <- predict(rf_model, test_set, type = "prob")[, "malignant"]

# Μετρικές
rf_cm <- confusionMatrix(rf_pred, test_set$Class, positive = "malignant")
print(rf_cm)
## Confusion Matrix and Statistics
## 
##            Reference
## Prediction  benign malignant
##   benign       131         2
##   malignant      2        69
##                                           
##                Accuracy : 0.9804          
##                  95% CI : (0.9506, 0.9946)
##     No Information Rate : 0.652           
##     P-Value [Acc > NIR] : <2e-16          
##                                           
##                   Kappa : 0.9568          
##                                           
##  Mcnemar's Test P-Value : 1               
##                                           
##             Sensitivity : 0.9718          
##             Specificity : 0.9850          
##          Pos Pred Value : 0.9718          
##          Neg Pred Value : 0.9850          
##              Prevalence : 0.3480          
##          Detection Rate : 0.3382          
##    Detection Prevalence : 0.3480          
##       Balanced Accuracy : 0.9784          
##                                           
##        'Positive' Class : malignant       
## 
# AUC
rf_roc <- roc(test_set$Class, rf_prob, levels = c("benign", "malignant"))
## Setting direction: controls < cases
cat("\nRandom Forest AUC:", round(auc(rf_roc), 4), "\n")
## 
## Random Forest AUC: 0.9985

Το μοντέλο πέτυχε Accuracy 98.04% στο test set.

Ιδιαίτερα σημαντική είναι η Sensitivity (97.18%), καθώς δείχνει ότι το μοντέλο εντοπίζει σχεδόν όλες τις κακοήθεις περιπτώσεις, έχοντας μόλις 2 “αστοχίες” (False Negatives).

Το AUC (0.9985) επιβεβαιώνει την κορυφαία διαχωριστική ικανότητα του αλγορίθμου.

4. Variable Importance Plot & Top 3 Features

Σε αυτό το βήμα, εξετάζουμε ποια κυτταρολογικά χαρακτηριστικά έχουν τη μεγαλύτερη βαρύτητα για την πρόβλεψη της κακοήθειας.

# Γράφημα Σημαντικότητας
varImpPlot(rf_model, main = "Variable Importance - Random Forest")

# Εμφάνιση των τιμών για ακριβή κατάταξη
importance(rf_model)
##                    benign malignant MeanDecreaseAccuracy MeanDecreaseGini
## Cl.thickness    12.702656 19.830096            19.363837        11.845927
## Cell.size       11.605774 16.330563            19.826188        60.344346
## Cell.shape      10.417048 20.502944            21.800283        53.561147
## Marg.adhesion    6.228395 12.425305            13.009741         5.977215
## Epith.c.size    10.510480  3.579421            11.128863        11.709247
## Bare.nuclei     20.271034 19.036320            26.035566        34.154820
## Bl.cromatin      6.086543 14.621620            14.691182        14.587449
## Normal.nucleoli 11.849778 11.676750            15.229482        23.815034
## Mitoses          4.984157  0.839361             4.608274         1.550928

Βάσει του δείκτη MeanDecreaseGini, τα τρία σημαντικότερα χαρακτηριστικά για την πρόβλεψη της κακοήθειας είναι τα:

  1. Cell.size
  2. Cell.shape
  3. Bare.nuclei

Αυτό υποδεικνύει ότι το μέγεθος, το σχήμα των κυττάρων και η κατάσταση των πυρήνων αποτελούν τους κρισιμότερους παράγοντες για τη διάγνωση.

Απαντήσεις στις ερωτήσεις

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

98.04%

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

  1. Cell.size
  2. Cell.shape
  3. Bare.nuclei

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

Όχι απαραίτητα. Σε ιατρικά θέματα, η Ευαισθησία (Sensitivity) είναι συχνά πιο σημαντική από το Accuracy, καθώς ένα False Negative (παράβλεψη κακοήθειας) μπορεί να είναι μοιραίο. Στη δική μας περίπτωση, το Sensitivity είναι 97.18%, που είναι εξαιρετικό αλλά πάντα επιδιώκουμε το 100%.

ΜΕΡΟΣ Β

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

Προετοιμάζουμε τα δεδομένα μετατρέποντας τα features σε αριθμητικό matrix και την κλάση στόχο σε τιμές 0 (benign) και 1 (malignant).

# Μετατροπή των features σε matrix
train_x <- as.matrix(train_set[, 1:9])
test_x  <- as.matrix(test_set[, 1:9])

# Μετατροπή του target σε numeric (0 για benign, 1 για malignant)
train_y <- ifelse(train_set$Class == "malignant", 1, 0)
test_y  <- ifelse(test_set$Class == "malignant", 1, 0)

# Δημιουργία των DMatrix αντικειμένων (optimized για XGBoost)
dtrain <- xgb.DMatrix(data = train_x, label = train_y)
dtest  <- xgb.DMatrix(data = test_x, label = test_y)

6. Εκπαίδευση XGBoost με early stopping

Εκπαιδεύουμε το μοντέλο XGBoost. Χρησιμοποιούμε eta = 0.1 (ρυθμός μάθησης) και early_stopping_rounds = 20, ώστε η εκπαίδευση να σταματήσει αυτόματα αν το μοντέλο πάψει να βελτιώνεται στο test set, αποφεύγοντας το overfitting.

set.seed(42)
xgb_model <- xgb.train(
  params = list(
    objective = "binary:logistic", 
    eta = 0.1, 
    max_depth = 4, 
    eval_metric = "auc"
  ),
  data = dtrain,
  nrounds = 500,
  watchlist = list(train = dtrain, test = dtest),
  early_stopping_rounds = 20,
  print_every_n = 10,
  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.989148  test-auc:0.987557 
## [11] train-auc:0.995856  test-auc:0.994599 
## [21] train-auc:0.998096  test-auc:0.996505 
## [31] train-auc:0.999177  test-auc:0.996294 
## [41] train-auc:0.999541  test-auc:0.996399 
## Stopping. Best iteration:
## [45] train-auc:0.999675  test-auc:0.996611
## 
## [45] train-auc:0.999675  test-auc:0.996611

ο XGBoost σταμάτησε στην 45η επανάληψη (Best iteration), καθώς η διαδικασία του early stopping εντόπισε ότι η απόδοση στο test set σταμάτησε να βελτιώνεται.

Το test-auc (0.9966) είναι εξαιρετικά υψηλό, υποδηλώνοντας ότι το μοντέλο έχει πολύ καλή γενίκευση.

7.Σύγκριση XGBoost vs Random Forest

Συγκρίνουμε τις επιδόσεις των δύο αλγορίθμων στο test set χρησιμοποιώντας τις βασικές μετρικές αξιολόγησης.

# Προβλέψεις XGBoost (πιθανότητες)
xgb_prob <- predict(xgb_model, dtest)
# Μετατροπή σε κλάσεις (κατώφλι 0.5)
xgb_pred <- factor(ifelse(xgb_prob > 0.5, "malignant", "benign"), 
                   levels = c("benign", "malignant"))

# Confusion Matrix για XGBoost
xgb_cm <- confusionMatrix(xgb_pred, test_set$Class, positive = "malignant")

# Συγκεντρωτικός Πίνακας
comparison <- data.frame(
  Metric = c("Accuracy", "Sensitivity", "Specificity", "AUC"),
  RandomForest = c(rf_cm$overall["Accuracy"], 
                   rf_cm$byClass["Sensitivity"], 
                   rf_cm$byClass["Specificity"], 
                   auc(rf_roc)),
  XGBoost = c(xgb_cm$overall["Accuracy"], 
               xgb_cm$byClass["Sensitivity"], 
               xgb_cm$byClass["Specificity"], 
               auc(roc(test_set$Class, xgb_prob, quiet = TRUE)))
)

print(comparison)
##                  Metric RandomForest   XGBoost
## Accuracy       Accuracy    0.9803922 0.9754902
## Sensitivity Sensitivity    0.9718310 0.9577465
## Specificity Specificity    0.9849624 0.9849624
##                     AUC    0.9985174 0.9966112

Συγκρίνοντας τα δύο μοντέλα, το Random Forest παρουσιάζει οριακά καλύτερη απόδοση με Accuracy 98.04% έναντι 97.55% του XGBoost.

Πιο σημαντικό είναι ότι το Random Forest πέτυχε υψηλότερη Sensitivity (97.18% vs 95.77%), γεγονός που το καθιστά προτιμότερο στο ιατρικό αυτό πλαίσιο, καθώς εντοπίζει περισσότερες περιπτώσεις κακοήθειας.

8. Δοκιμή διαφορετικών Learning Rates (eta)

Σε αυτό το βήμα εξετάζουμε την επίδραση του ρυθμού μάθησης (learning rate) στην ταχύτητα σύγκλισης του μοντέλου. Δοκιμάζουμε μια πολύ χαμηλή τιμή (0.01) και μια υψηλότερη (0.3).

# 1. Εκπαίδευση με eta = 0.01 (Πολύ αργή μάθηση)
set.seed(42)
xgb_low <- xgb.train(
  params = list(objective = "binary:logistic", eta = 0.01, eval_metric = "auc"),
  data = dtrain, 
  nrounds = 500, 
  evals = list(test = dtest),
  early_stopping_rounds = 20, 
  verbose = 0
)

# 2. Εκπαίδευση με eta = 0.3 (Γρήγορη μάθηση)
set.seed(42)
xgb_high <- xgb.train(
  params = list(objective = "binary:logistic", eta = 0.3, eval_metric = "auc"),
  data = dtrain, 
  nrounds = 500, 
  evals = list(test = dtest),
  early_stopping_rounds = 20, 
  verbose = 0
)

# Εξαγωγή του αριθμού των γύρων που εκτελέστηκαν
rounds_low <- nrow(xgb_low$evaluation_log)
rounds_high <- nrow(xgb_high$evaluation_log)

# Εκτύπωση αποτελεσμάτων
cat("Αριθμός γύρων για eta=0.01:", rounds_low, "\n")
## Αριθμός γύρων για eta=0.01:
cat("Αριθμός γύρων για eta=0.3 :", rounds_high, "\n")
## Αριθμός γύρων για eta=0.3 :
  • Για eta = 0.01: Ο αλγόριθμος χρειάστηκε τον μέγιστο αριθμό επαναλήψεων (500 γύρους), καθώς ο χαμηλός ρυθμός μάθησης επιβάλλει πολύ μικρές και συντηρητικές διορθώσεις σε κάθε δέντρο. Αυτό προσφέρει σταθερότητα αλλά απαιτεί μεγάλο χρόνο εκπαίδευσης.

  • Για eta = 0.3: Ο αλγόριθμος συνέκλινε πολύ ταχύτερα (σε λιγότερους από 50 γύρους), καθώς οι μεγάλες διορθώσεις οδηγούν σε γρήγορη ελαχιστοποίηση του σφάλματος. Ωστόσο, ο κίνδυνος για overfitting είναι αυξημένος, γι’ αυτό και το early stopping σταμάτησε την εκπαίδευση μόλις η απόδοση στο test set σταμάτησε να βελτιώνεται.

9. ROC Comparison Plot

Στο τελευταίο αυτό βήμα, προχωράμε σε μια οπτική σύγκριση της απόδοσης των δύο μοντέλων. Σχεδιάζουμε τις καμπύλες ROC και υπολογίζουμε το AUC (Area Under the Curve) για το καθένα.

Η σύγκριση αυτή μας επιτρέπει να δούμε ποιος αλγόριθμος έχει την καλύτερη διαχωριστική ικανότητα μεταξύ καλοήθων και κακοήθων δειγμάτων σε όλο το εύρος των πιθανών κατωφλίων (thresholds).

# Δημιουργία ROC αντικειμένου για το XGBoost
xgb_roc <- roc(test_set$Class, xgb_prob, quiet = TRUE)

# Σχεδίαση της καμπύλης του Random Forest (Μπλε)
plot(rf_roc, col = "royalblue", lwd = 3, main = "ROC Comparison: RF vs XGBoost")

# Προσθήκη της καμπύλης του XGBoost (Πορτοκαλί)
plot(xgb_roc, col = "darkorange", lwd = 3, add = TRUE)

# Προσθήκη Legend
legend("bottomright", 
       legend = c(paste0("Random Forest (AUC = ", round(auc(rf_roc), 3), ")"),
                  paste0("XGBoost (AUC = ", round(auc(xgb_roc), 3), ")")),
       col = c("royalblue", "darkorange"), lwd = 3)

Η οπτική σύγκριση των καμπυλών ROC επιβεβαιώνει την κορυφαία απόδοση και των δύο αλγορίθμων, με το Random Forest (AUC = 0.999) να υπερτερεί οριακά του XGBoost (AUC = 0.997).

Η εγγύτητα των καμπυλών στην πάνω αριστερή γωνία υποδηλώνει ότι και τα δύο μοντέλα επιτυγχάνουν πολύ υψηλή ευαισθησία (Sensitivity) διατηρώντας ταυτόχρονα εξαιρετικά χαμηλά ποσοστά ψευδώς θετικών αποτελεσμάτων.

Απαντήσεις στις ερωτήσεις

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

Νικητής αναδείχθηκε το Random Forest, αν και η διαφορά ήταν οριακή.

Συγκεκριμένα, το Random Forest πέτυχε Accuracy 98.04% έναντι 97.55% του XGBoost (διαφορά 0.49%). Στο AUC, η διαφορά ήταν ακόμα μικρότερη (0.999 έναντι 0.997), επιβεβαιώνοντας ότι και τα δύο μοντέλα είναι εξαιρετικά αξιόπιστα για το συγκεκριμένο πρόβλημα.

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

Προκαλεί εντύπωση το γεγονός ότι το Random Forest, ένας πιο “απλός” αλγόριθμος σε σχέση με το XGBoost, κατάφερε να υπερτερήσει.

Συνήθως, το XGBoost θεωρείται πιο ισχυρό σε μεγάλα και περίπλοκα σύνολα δεδομένων. Στην προκειμένη περίπτωση, φαίνεται πως η φύση των κυτταρολογικών χαρακτηριστικών ευνοεί τη δομή του Random Forest, ενώ το μέγεθος του δείγματος ήταν επαρκές για να “μάθει” το μοντέλο χωρίς να χρειαστεί την επιπλέον πολυπλοκότητα του Gradient Boosting.

3. Τι παρατηρήσατε με τα 3 διαφορετικά eta;

Η δομή της εκπαίδευσης άλλαξε ριζικά ανάλογα με το learning rate:

  • Με το πολύ χαμηλό eta (0.01), το μοντέλο ήταν εξαιρετικά “συντηρητικό” και χρειάστηκε το μέγιστο πλήθος γύρων (500), καθώς η βελτίωση σε κάθε βήμα ήταν απειροελάχιστη.

  • Με το ενδιάμεσο eta (0.1), είχαμε μια ισορροπημένη σύγκλιση (σταμάτησε στις 45 επαναλήψεις).

  • Με το υψηλό eta (0.3), η εκπαίδευση ολοκληρώθηκε πολύ γρήγορα, καθώς ο αλγόριθμος έκανε μεγάλα βήματα βελτιστοποίησης. Ωστόσο, υπάρχει ο κίνδυνος το μοντέλο να “προσπεράσει” την ιδανική λύση (overshooting) αν το βήμα είναι υπερβολικά μεγάλο.