Πρόβλεψη Καρκίνου του Μαστού με Random Forests & Gradient Boosting

Στο συγκεκριμένο μέρος της ανάλυσης χρησιμοποιείται το dataset Breast Cancer Wisconsin, το οποίο περιλαμβάνει κυτταρολογικά χαρακτηριστικά από βιοψίες όγκων. Στόχος είναι η εκπαίδευση ενός Random Forest μοντέλου που προβλέπει αν ένας όγκος είναι καλοήθης ή κακοήθης. Η αξιολόγηση του μοντέλου γίνεται με βάση το accuracy, το sensitivity και το AUC, με ιδιαίτερη έμφαση στην αναγνώριση των κακοήθων περιπτώσεων.

TODO 1: Stratified train/test split (70/30)

# --- Φόρτωση πακέτων ---
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.2.0     ✔ 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)
## Warning: package 'mlbench' was built under R version 4.5.3
library(randomForest)
## Warning: package 'randomForest' was built under R version 4.5.3
## 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)
## Warning: package 'xgboost' was built under R version 4.5.3
library(caret)
## Warning: package 'caret' was built under R version 4.5.3
## Loading required package: lattice
## 
## Attaching package: 'caret'
## 
## The following object is masked from 'package:purrr':
## 
##     lift
library(pROC)
## Warning: package 'pROC' was built under R version 4.5.3
## 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
bc$Id <- NULL

# Μετατροπή των 9 features σε numeric
bc[, 1:9] <- lapply(bc[, 1:9], function(x) as.numeric(as.character(x)))

# Αφαίρεση missing values ΜΕΤΑ τη μετατροπή
bc <- na.omit(bc)

# Έλεγχος ότι δεν υπάρχουν NA
colSums(is.na(bc))
##    Cl.thickness       Cell.size      Cell.shape   Marg.adhesion    Epith.c.size 
##               0               0               0               0               0 
##     Bare.nuclei     Bl.cromatin Normal.nucleoli         Mitoses           Class 
##               0               0               0               0               0
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
train_index <- createDataPartition(
  bc$Class,
  p = 0.7,
  list = FALSE
)

train_bc <- bc[train_index, ]
test_bc  <- bc[-train_index, ]


# Έλεγχος διαστάσεων
dim(train_bc)
## [1] 479  10
dim(test_bc)
## [1] 204  10
# Έλεγχος αναλογιών των κλάσεων
prop.table(table(train_bc$Class))
## 
##    benign malignant 
## 0.6492693 0.3507307
prop.table(table(test_bc$Class))
## 
##    benign malignant 
## 0.6519608 0.3480392

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

rf_model <- randomForest(
  Class ~ .,            # Όλα τα features
  data = train_bc,
  ntree = 500,             # Αριθμός δέντρων
  importance = TRUE        # Για να πάρουμε variable importance
)

rf_model
## 
## Call:
##  randomForest(formula = Class ~ ., data = train_bc, 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       301        10  0.03215434
## malignant      6       162  0.03571429

TODO 3: Accuracy, Sensitivity, AUC στο test set

# Προβλέψεις κατηγορίας
rf_pred <- predict(
  rf_model,
  newdata = test_bc,
  type = "class"
)

# Πιθανότητες για την κλάση malignant
rf_prob <- predict(
  rf_model,
  newdata = test_bc,
  type = "prob"
)[, "malignant"]



# Confusion Matrix
cm_rf <- confusionMatrix(
  rf_pred,
  test_bc$Class,
  positive = "malignant"
)

cm_rf
## 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       
## 

Το Random Forest μοντέλο παρουσίασε εξαιρετική απόδοση, καθώς ταξινόμησε σωστά 200 από τις 204 παρατηρήσεις του test set, με accuracy περίπου 98.04%. Επιπλέον, μόνο 2 κακοήθεις περιπτώσεις ταξινομήθηκαν λανθασμένα ως καλοήθεις, κάτι που δείχνει υψηλή ευαισθησία στην ανίχνευση κακοήθειας. Αυτό είναι σημαντικό, επειδή σε ένα ιατρικό decision support tool η αποφυγή false negatives είναι κρίσιμη.

TODO 4: Variable Importance Plot

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

Το Variable Importance plot δείχνει ότι τα σημαντικότερα χαρακτηριστικά για την πρόβλεψη της κακοήθειας είναι κυρίως τα Cell.size, Cell.shape και Bare.nuclei. Με βάση το μέτρο MeanDecreaseGini, οι μεταβλητές αυτές συμβάλλουν περισσότερο στη διάκριση μεταξύ καλοήθων και κακοήθων όγκων. Αυτό σημαίνει ότι το μέγεθος των κυττάρων, το σχήμα των κυττάρων και οι γυμνοί πυρήνες αποτελούν βασικούς παράγοντες που χρησιμοποιεί το Random Forest μοντέλο για την ταξινόμηση των περιπτώσεων.

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

# Features για train και test set
x_train <- as.matrix(train_bc[, -which(names(train_bc) == "Class")])
x_test  <- as.matrix(test_bc[, -which(names(test_bc) == "Class")])

# Target variable: benign = 0, malignant = 1
y_train <- ifelse(train_bc$Class == "malignant", 1, 0)
y_test  <- ifelse(test_bc$Class == "malignant", 1, 0)

# Έλεγχος διαστάσεων
dim(x_train)
## [1] 479   9
dim(x_test)
## [1] 204   9
# Έλεγχος target
table(y_train)
## y_train
##   0   1 
## 311 168
table(y_test)
## y_test
##   0   1 
## 133  71

Τα δεδομένα προετοιμάστηκαν σωστά για το XGBoost, καθώς τα features μετατράπηκαν σε matrix και η μεταβλητή στόχος κωδικοποιήθηκε αριθμητικά, με 0 για τις καλοήθεις και 1 για τις κακοήθεις περιπτώσεις.Το training set περιλαμβάνει 479 παρατηρήσεις και 9 χαρακτηριστικά, ενώ το test set περιλαμβάνει 204 παρατηρήσεις και 9 χαρακτηριστικά. Η κατανομή των κλάσεων παρέμεινε παρόμοια στα δύο σύνολα δεδομένων, γεγονός που δείχνει ότι ο stratified διαχωρισμός πραγματοποιήθηκε σωστά.

TODO 6: Εκπαίδευση XGBoost μοντέλο με early stopping

# Μετατροπή των δεδομένων σε DMatrix μορφή για XGBoost
dtrain <- xgb.DMatrix(
  data = x_train,
  label = y_train
)

dtest <- xgb.DMatrix(
  data = x_test,
  label = y_test
)

# Παράμετροι του XGBoost μοντέλου
params <- list(
  objective = "binary:logistic",
  eval_metric = "auc",
  max_depth = 4,
  eta = 0.1
)

# Εκπαίδευση XGBoost με early stopping
xgb_model <- xgb.train(
  params = params,
  data = dtrain,
  nrounds = 500,
  watchlist = list(
    train = dtrain,
    test = dtest
  ),
  early_stopping_rounds = 20,
  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 
## [2]  train-auc:0.989311  test-auc:0.988245 
## [3]  train-auc:0.991923  test-auc:0.988775 
## [4]  train-auc:0.992708  test-auc:0.988192 
## [5]  train-auc:0.993560  test-auc:0.993805 
## [6]  train-auc:0.994363  test-auc:0.994652 
## [7]  train-auc:0.994670  test-auc:0.994546 
## [8]  train-auc:0.995502  test-auc:0.994440 
## [9]  train-auc:0.995340  test-auc:0.994599 
## [10] train-auc:0.995588  test-auc:0.994599 
## [11] train-auc:0.995856  test-auc:0.994599 
## [12] train-auc:0.995933  test-auc:0.994599 
## [13] train-auc:0.996172  test-auc:0.994387 
## [14] train-auc:0.996507  test-auc:0.994281 
## [15] train-auc:0.996785  test-auc:0.994387 
## [16] train-auc:0.996928  test-auc:0.995235 
## [17] train-auc:0.997139  test-auc:0.994917 
## [18] train-auc:0.997407  test-auc:0.995764 
## [19] train-auc:0.997713  test-auc:0.995976 
## [20] train-auc:0.997809  test-auc:0.996399 
## [21] train-auc:0.998096  test-auc:0.996505 
## [22] train-auc:0.998373  test-auc:0.996294 
## [23] train-auc:0.998507  test-auc:0.996399 
## [24] train-auc:0.998832  test-auc:0.996294 
## [25] train-auc:0.998890  test-auc:0.996611 
## [26] train-auc:0.998871  test-auc:0.996611 
## [27] train-auc:0.998852  test-auc:0.996505 
## [28] train-auc:0.998947  test-auc:0.996399 
## [29] train-auc:0.999024  test-auc:0.996399 
## [30] train-auc:0.999120  test-auc:0.996399 
## [31] train-auc:0.999177  test-auc:0.996294 
## [32] train-auc:0.999177  test-auc:0.996399 
## [33] train-auc:0.999215  test-auc:0.996188 
## [34] train-auc:0.999311  test-auc:0.996082 
## [35] train-auc:0.999407  test-auc:0.996399 
## [36] train-auc:0.999388  test-auc:0.996082 
## [37] train-auc:0.999426  test-auc:0.996188 
## [38] train-auc:0.999502  test-auc:0.996294 
## [39] train-auc:0.999522  test-auc:0.996399 
## [40] train-auc:0.999522  test-auc:0.996294 
## [41] train-auc:0.999541  test-auc:0.996399 
## [42] train-auc:0.999598  test-auc:0.996611 
## [43] train-auc:0.999636  test-auc:0.996611 
## [44] train-auc:0.999675  test-auc:0.996611 
## Stopping. Best iteration:
## [45] train-auc:0.999675  test-auc:0.996611
## 
## [45] train-auc:0.999675  test-auc:0.996611
# Έλεγχος attributes του XGBoost μοντέλου
xgb.attributes(xgb_model)
## $best_iteration
## [1] 24
## 
## $best_score
## [1] 0.9966112
# Καλύτερος αριθμός επαναλήψεων
best_iteration <- as.numeric(xgb.attr(xgb_model, "best_iteration"))

# Καλύτερο AUC score
best_score <- as.numeric(xgb.attr(xgb_model, "best_score"))

best_iteration
## [1] 24
best_score
## [1] 0.9966112

Το XGBoost μοντέλο πέτυχε την καλύτερη απόδοσή του στην 24η επανάληψη, με AUC ίσο με 0.9966. Η τιμή αυτή δείχνει εξαιρετική διαχωριστική ικανότητα μεταξύ καλοήθων και κακοήθων όγκων. Επιπλέον, η χρήση του early stopping βοήθησε ώστε η εκπαίδευση να σταματήσει στο κατάλληλο σημείο και να μειωθεί ο κίνδυνος overfitting.

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

# =========================
# Random Forest metrics
# =========================

rf_pred <- predict(
  rf_model,
  newdata = test_bc,
  type = "class"
)

rf_prob <- predict(
  rf_model,
  newdata = test_bc,
  type = "prob"
)[, "malignant"]

cm_rf <- confusionMatrix(
  rf_pred,
  test_bc$Class,
  positive = "malignant"
)

roc_rf <- roc(
  response = test_bc$Class,
  predictor = rf_prob,
  levels = c("benign", "malignant")
)
## Setting direction: controls < cases
# =========================
# XGBoost metrics
# =========================

xgb_prob <- predict(
  xgb_model,
  newdata = dtest
)

xgb_pred <- ifelse(xgb_prob > 0.5, "malignant", "benign")

xgb_pred <- factor(
  xgb_pred,
  levels = levels(test_bc$Class)
)

cm_xgb <- confusionMatrix(
  xgb_pred,
  test_bc$Class,
  positive = "malignant"
)

roc_xgb <- roc(
  response = test_bc$Class,
  predictor = xgb_prob,
  levels = c("benign", "malignant")
)
## Setting direction: controls < cases
# =========================
# Comparison table
# =========================

comparison_table <- tibble(
  Model = c("Random Forest", "XGBoost"),
  
  Accuracy = c(
    cm_rf$overall["Accuracy"],
    cm_xgb$overall["Accuracy"]
  ),
  
  Sensitivity = c(
    cm_rf$byClass["Sensitivity"],
    cm_xgb$byClass["Sensitivity"]
  ),
  
  Specificity = c(
    cm_rf$byClass["Specificity"],
    cm_xgb$byClass["Specificity"]
  ),
  
  AUC = c(
    auc(roc_rf),
    auc(roc_xgb)
  )
)

comparison_table
## # A tibble: 2 × 5
##   Model         Accuracy Sensitivity Specificity   AUC
##   <chr>            <dbl>       <dbl>       <dbl> <dbl>
## 1 Random Forest    0.980       0.972       0.985 0.998
## 2 XGBoost          0.975       0.958       0.985 0.997

Από τη σύγκριση των δύο μοντέλων φαίνεται ότι τόσο το Random Forest όσο και το XGBoost πέτυχαν πολύ υψηλή απόδοση στην ταξινόμηση των όγκων σε καλοήθεις και κακοήθεις. Το Random Forest είχε accuracy περίπου 98.04%, sensitivity 97.18% και specificity 98.50%, δείχνοντας ότι κατάφερε να ταξινομήσει σωστά σχεδόν όλες τις περιπτώσεις του test set. Ιδιαίτερα σημαντικό είναι ότι έχασε μόνο 2 κακοήθεις περιπτώσεις, κάτι που δείχνει υψηλή ευαισθησία στην ανίχνευση κακοήθειας.

Το XGBoost παρουσίασε επίσης εξαιρετική διαχωριστική ικανότητα, με πολύ υψηλό AUC, το οποίο έφτασε περίπου το 0.9966 κατά την εκπαίδευση με early stopping. Αυτό δείχνει ότι το μοντέλο μπορεί να ξεχωρίζει με μεγάλη ακρίβεια τις καλοήθεις από τις κακοήθεις περιπτώσεις. Συνολικά, και τα δύο μοντέλα είναι κατάλληλα για το συγκεκριμένο πρόβλημα, όμως στο ιατρικό πλαίσιο μεγαλύτερη σημασία έχει το sensitivity, επειδή το πιο κρίσιμο λάθος είναι να προβλεφθεί ένας κακοήθης όγκος ως καλοήθης. Επομένως, το καλύτερο μοντέλο δεν επιλέγεται μόνο με βάση το accuracy, αλλά κυρίως με βάση την ικανότητά του να εντοπίζει σωστά τις κακοήθεις περιπτώσεις.

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

TODO 8: Δοκιμή διαφορετικών eta στο XGBoost

train_xgb_eta <- function(eta_value) {
  
  params_eta <- list(
    objective = "binary:logistic",
    eval_metric = "auc",
    max_depth = 4,
    eta = eta_value
  )
  
  model_eta <- xgb.train(
    params = params_eta,
    data = dtrain,
    nrounds = 500,
    evals = list(
      train = dtrain,
      test = dtest
    ),
    early_stopping_rounds = 20,
    maximize = TRUE,
    verbose = 0
  )
  
  # Παίρνουμε τα καλύτερα αποτελέσματα από τα attributes
  best_iteration <- as.numeric(xgb.attr(model_eta, "best_iteration")) + 1
  best_score <- as.numeric(xgb.attr(model_eta, "best_score"))
  
  # Πόσοι γύροι έτρεξαν συνολικά μέχρι να σταματήσει
  stopped_at <- nrow(model_eta$evaluation_log)
  
  tibble(
    eta = eta_value,
    Best_Iteration = best_iteration,
    Stopped_At = stopped_at,
    Best_Test_AUC = best_score
  )
}

eta_results <- bind_rows(
  train_xgb_eta(0.01),
  train_xgb_eta(0.1),
  train_xgb_eta(0.3)
)

eta_results <- eta_results %>%
  mutate(Best_Test_AUC = round(Best_Test_AUC, 4))

eta_results
## # A tibble: 3 × 3
##     eta Best_Iteration Best_Test_AUC
##   <dbl>          <dbl>         <dbl>
## 1  0.01             35         0.995
## 2  0.1              25         0.997
## 3  0.3              30         0.997

Από τη δοκιμή διαφορετικών τιμών του eta παρατηρήθηκε ότι όλα τα μοντέλα πέτυχαν πολύ υψηλό AUC. Το μικρότερο eta = 0.01 μαθαίνει πιο αργά και χρειάζεται περισσότερα iterations, ενώ το μεγαλύτερο eta = 0.30 συγκλίνει γρηγορότερα και στην παρούσα ανάλυση πέτυχε ελαφρώς καλύτερο AUC. Παρόλα αυτά, οι διαφορές ήταν πολύ μικρές, γεγονός που δείχνει ότι το dataset είναι σχετικά εύκολο για το XGBoost και ότι όλες οι τιμές eta οδήγησαν σε πολύ καλή διαχωριστική ικανότητα.

TODO 9: ROC plot με 2 καμπύλες — Random Forest και XGBoost

# Random Forest probabilities
rf_prob <- predict(
  rf_model,
  newdata = test_bc,
  type = "prob"
)[, "malignant"]

# XGBoost probabilities
xgb_prob <- predict(
  xgb_model,
  newdata = dtest
)

# ROC για Random Forest
roc_rf <- roc(
  response = test_bc$Class,
  predictor = rf_prob,
  levels = c("benign", "malignant")
)
## Setting direction: controls < cases
# ROC για XGBoost
roc_xgb <- roc(
  response = test_bc$Class,
  predictor = xgb_prob,
  levels = c("benign", "malignant")
)
## Setting direction: controls < cases
# ROC plot με δύο καμπύλες
plot(
  roc_rf,
  col = "blue",
  lwd = 2,
  main = "ROC Curves — Random Forest vs XGBoost",
  legacy.axes = TRUE
)

lines(
  roc_xgb,
  col = "red",
  lwd = 2
)

legend(
  "bottomright",
  legend = c(
    paste("Random Forest AUC =", round(auc(roc_rf), 4)),
    paste("XGBoost AUC =", round(auc(roc_xgb), 4))
  ),
  col = c("blue", "red"),
  lwd = 2
)

Το ROC plot δείχνει ότι και τα δύο μοντέλα έχουν εξαιρετική διαχωριστική ικανότητα μεταξύ καλοήθων και κακοήθων όγκων. Οι καμπύλες βρίσκονται πολύ κοντά στην επάνω αριστερή γωνία, κάτι που δείχνει υψηλή ευαισθησία και υψηλή ειδικότητα ταυτόχρονα.

Το Random Forest πέτυχε AUC 0.9982, ενώ το XGBoost πέτυχε AUC 0.9966. Και οι δύο τιμές είναι πολύ κοντά στο 1, άρα και τα δύο μοντέλα μπορούν να ξεχωρίσουν με μεγάλη ακρίβεια τις δύο κλάσεις. Το Random Forest έχει ελαφρώς υψηλότερο AUC, όμως η διαφορά είναι πολύ μικρή, επομένως και τα δύο μοντέλα παρουσιάζουν εξαιρετική απόδοση.

Συνολικά, το ROC plot επιβεβαιώνει ότι τόσο το Random Forest όσο και το XGBoost είναι κατάλληλα για το συγκεκριμένο πρόβλημα ταξινόμησης. Ωστόσο, επειδή το πρόβλημα αφορά ιατρική διάγνωση, η επιλογή του καλύτερου μοντέλου δεν πρέπει να βασίζεται μόνο στο AUC, αλλά και στο sensitivity, δηλαδή στην ικανότητα του μοντέλου να εντοπίζει σωστά τις κακοήθεις περιπτώσεις.