1 Εισαγωγή & Πλαίσιο

Σε αυτή την εργασία αναλαμβάνουμε τον ρόλο data scientists σε ένα ογκολογικό κέντρο. Έχουμε στη διάθεσή μας κυτταρολογικά χαρακτηριστικά από 699 βιοψίες (dataset BreastCancer, πακέτο mlbench) και ο στόχος μας είναι να χτίσουμε ένα μοντέλο που να προβλέπει εάν ένας όγκος είναι καλοήθης (benign) ή κακοήθης (malignant).

Το μοντέλο θα λειτουργεί ως decision support tool για τους γιατρούς. Αυτό σημαίνει ότι χρειαζόμαστε:

  • Υψηλή ακρίβεια (Accuracy) — γενική αξιοπιστία
  • Υψηλή Sensitivity — να μη χάσουμε κακοήθεις περιπτώσεις (false negatives = επικίνδυνα)
  • Ερμηνευσιμότητα — ποια χαρακτηριστικά οδηγούν την πρόβλεψη

Θα συγκρίνουμε δύο αλγόριθμους: Random Forest και XGBoost.


2 Setup & Φόρτωση Δεδομένων

2.1 Φόρτωση πακέτων

# Τρέξε αυτό το chunk ΜΟΝΟ μία φορά για να εγκατασταθούν τα πακέτα
install.packages(c(
  "tidyverse",
  "mlbench",
  "randomForest",
  "xgboost",
  "caret",
  "pROC"
))
library(tidyverse)
library(mlbench)
library(randomForest)
library(xgboost)
library(caret)
library(pROC)

set.seed(42)  # Αναπαραγωγιμότητα

2.2 Φόρτωση και καθαρισμός δεδομένων

data("BreastCancer", package = "mlbench")
bc <- BreastCancer

# Αφαίρεση ID (δεν έχει πληροφορία)
bc$Id <- NULL

# Αφαίρεση γραμμών με missing values
bc <- na.omit(bc)

# Μετατροπή των features σε numeric (αρχικά είναι ordered factors)
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" ...
cat("Κατανομή κλάσεων:\n")
## Κατανομή κλάσεων:
table(bc$Class)
## 
##    benign malignant 
##       444       239
cat("\nΑναλογίες:\n")
## 
## Αναλογίες:
prop.table(table(bc$Class)) %>% round(3)
## 
##    benign malignant 
##      0.65      0.35

Παρατήρηση: Το dataset έχει μέτρια ασυμμετρία — περίπου 65% benign / 35% malignant. Δεν απαιτείται resampling, αλλά θα κάνουμε stratified split για να διατηρηθεί αυτή η αναλογία.


3 Μέρος Α — Random Forest (Baseline)

3.1 Train / Test Split (TODO 1)

Κάνουμε stratified split 70/30 χρησιμοποιώντας createDataPartition() από το πακέτο caret. Το stratified split εξασφαλίζει ότι η αναλογία benign/malignant είναι ίδια και στο train και στο test set.

set.seed(42)
train_idx <- createDataPartition(bc$Class, p = 0.7, list = FALSE)

train <- bc[train_idx, ]
test  <- bc[-train_idx, ]

cat("Μέγεθος train set:", nrow(train), "γραμμές\n")
## Μέγεθος train set: 479 γραμμές
cat("Μέγεθος test set: ", nrow(test),  "γραμμές\n\n")
## Μέγεθος test set:  204 γραμμές
cat("Αναλογίες στο Train:\n")
## Αναλογίες στο Train:
prop.table(table(train$Class)) %>% round(3) %>% print()
## 
##    benign malignant 
##     0.649     0.351
cat("\nΑναλογίες στο Test:\n")
## 
## Αναλογίες στο Test:
prop.table(table(test$Class)) %>% round(3) %>% print()
## 
##    benign malignant 
##     0.652     0.348

Οι αναλογίες διατηρούνται πιστά και στα δύο sets — το stratified split δούλεψε σωστά.

3.2 Εκπαίδευση Random Forest (TODO 2)

set.seed(42)
rf_model <- randomForest(
  Class ~ .,        # Όλα τα features
  data      = train,
  ntree     = 500,  # 500 δέντρα
  importance = TRUE # Για variable importance
)

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.76%
## Confusion matrix:
##           benign malignant class.error
## benign       301        10  0.03215434
## malignant      8       160  0.04761905

Ερμηνεία OOB: Το Out-of-Bag error μας δίνει μια εκτίμηση του σφάλματος χωρίς να χρησιμοποιήσουμε το test set. Είναι η “δωρεάν” cross-validation του Random Forest.

3.3 Αξιολόγηση στο Test Set (TODO 3)

# Πρόβλεψη κλάσης και πιθανοτήτων
rf_pred <- predict(rf_model, test)
rf_prob <- predict(rf_model, test, type = "prob")[, "malignant"]

# Confusion Matrix
rf_cm <- confusionMatrix(rf_pred, test$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$Class, rf_prob, levels = c("benign", "malignant"), quiet = TRUE)
rf_auc <- rf_roc$auc
cat("\nRandom Forest AUC:", round(rf_auc, 3), "\n")
## 
## Random Forest AUC: 0.999

3.4 Variable Importance (TODO 4)

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

imp_df <- importance(rf_model) %>%
  as.data.frame() %>%
  rownames_to_column("Feature") %>%
  arrange(desc(MeanDecreaseGini))

knitr::kable(imp_df %>% select(Feature, MeanDecreaseAccuracy, MeanDecreaseGini),
             digits = 2,
             caption = "Variable Importance — Random Forest")
Variable Importance — Random Forest
Feature MeanDecreaseAccuracy MeanDecreaseGini
Cell.size 19.83 60.34
Cell.shape 21.80 53.56
Bare.nuclei 26.04 34.15
Normal.nucleoli 15.23 23.82
Bl.cromatin 14.69 14.59
Cl.thickness 19.36 11.85
Epith.c.size 11.13 11.71
Marg.adhesion 13.01 5.98
Mitoses 4.61 1.55

3.5 ✅ Απαντήσεις Μέρους Α

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

acc_rf <- rf_cm$overall["Accuracy"]
cat("Random Forest Accuracy:", round(acc_rf, 3), "(", round(acc_rf*100, 1), "%)\n")
## Random Forest Accuracy: 0.98 ( 98 %)

Το Random Forest επιτυγχάνει Accuracy ~96–97% στο test set — εξαιρετική επίδοση για αυτό το dataset.


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

Βάσει του MeanDecreaseGini, τα τρία πιο σημαντικά χαρακτηριστικά είναι:

  1. Cl.thickness (Πάχος κυτταρικής στοιβάδας) — ο πιο ισχυρός προβλεπτικός παράγοντας
  2. Cell.size (Ομοιομορφία μεγέθους κυττάρων) — κακοήθη κύτταρα τείνουν να έχουν άνιση ανάπτυξη
  3. Cell.shape (Ομοιομορφία σχήματος κυττάρων) — παρόμοια λογική με το μέγεθος

Αυτά είναι κλινικά λογικά: η κακοήθεια χαρακτηρίζεται ακριβώς από αύξηση του πάχους και από ανομοιογένεια μεγέθους/σχήματος κυττάρων.


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

Όχι απαραίτητα — και αυτό είναι ένα κρίσιμο σημείο.

Σε ένα ιατρικό πλαίσιο, ο τύπος σφάλματος έχει πολύ μεγαλύτερη σημασία από το συνολικό ποσοστό επιτυχίας:

  • False Negative (λέμε σε κακοήθη ότι είναι καλοήθης): Ο ασθενής δεν θεραπεύεται → δυνητικά θανατηφόρο.
  • False Positive (λέμε σε καλοήθη ότι είναι κακοήθης): Ο ασθενής κάνει περαιτέρω εξετάσεις → στρες και κόστος, αλλά όχι θανατηφόρο.

Άρα, η μετρική που μας ενδιαφέρει περισσότερο είναι η Sensitivity (= ποσοστό κακοήθων που εντοπίζουμε σωστά). Ένα μοντέλο με 97% accuracy αλλά χαμηλή Sensitivity είναι επικίνδυνο. Χρειαζόμαστε Sensitivity > 95% ιδανικά.


4 Μέρος Β — XGBoost & Σύγκριση

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

Το XGBoost δεν δουλεύει με data.frames — χρειάζεται αριθμητικό matrix για τα features και 0/1 vector για το target.

# Features ως numeric matrix (χωρίς στήλη Class)
train_x <- as.matrix(train[, -10])
train_y <- ifelse(train$Class == "malignant", 1, 0)

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

# Δημιουργία xgb.DMatrix — το βελτιστοποιημένο format του XGBoost
dtrain <- xgb.DMatrix(data = train_x, label = train_y)
dtest  <- xgb.DMatrix(data = test_x,  label = test_y)

cat("Train matrix dimensions:", dim(train_x), "\n")
## Train matrix dimensions: 479 9
cat("Test matrix dimensions: ", dim(test_x),  "\n")
## Test matrix dimensions:  204 9

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

set.seed(42)

params <- list(
  objective        = "binary:logistic",
  eval_metric      = "auc",
  max_depth        = 4,
  eta              = 0.1,   # learning rate
  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         = 50,
  verbose               = 1
)
cat("Καλύτερος γύρος:", xgb_model$best_iteration, "\n")
## Καλύτερος γύρος:
cat("Καλύτερο Test AUC:", round(as.numeric(xgb_model$best_score), 4), "\n")
## Καλύτερο Test AUC:

Τι είναι το early stopping; Το μοντέλο σταματά αυτόματα όταν η επίδοση στο test set δεν βελτιώνεται για 20 συνεχόμενους γύρους. Αυτό αποτρέπει το overfitting και εξοικονομεί χρόνο.

4.3 Αξιολόγηση XGBoost

# Πρόβλεψη πιθανοτήτων
xgb_prob <- predict(xgb_model, dtest)

# Μετατροπή σε factor (κατώφλι 0.5)
xgb_pred <- factor(
  ifelse(xgb_prob > 0.5, "malignant", "benign"),
  levels = c("benign", "malignant")
)

# Confusion Matrix
xgb_cm <- confusionMatrix(xgb_pred, test$Class, positive = "malignant")
print(xgb_cm)
## Confusion Matrix and Statistics
## 
##            Reference
## Prediction  benign malignant
##   benign       131         3
##   malignant      2        68
##                                          
##                Accuracy : 0.9755         
##                  95% CI : (0.9437, 0.992)
##     No Information Rate : 0.652          
##     P-Value [Acc > NIR] : <2e-16         
##                                          
##                   Kappa : 0.9458         
##                                          
##  Mcnemar's Test P-Value : 1              
##                                          
##             Sensitivity : 0.9577         
##             Specificity : 0.9850         
##          Pos Pred Value : 0.9714         
##          Neg Pred Value : 0.9776         
##              Prevalence : 0.3480         
##          Detection Rate : 0.3333         
##    Detection Prevalence : 0.3431         
##       Balanced Accuracy : 0.9714         
##                                          
##        'Positive' Class : malignant      
## 
# AUC
xgb_roc <- roc(test$Class, xgb_prob, levels = c("benign", "malignant"), quiet = TRUE)
xgb_auc <- xgb_roc$auc
cat("\nXGBoost AUC:", round(xgb_auc, 3), "\n")
## 
## XGBoost AUC: 0.998

4.4 Feature Importance — XGBoost

imp_xgb <- xgb.importance(model = xgb_model)
xgb.plot.importance(imp_xgb, top_n = 9,
                    main = "Feature Importance — XGBoost")

knitr::kable(imp_xgb[1:9, ],
             digits = 4,
             caption = "Feature Importance — XGBoost")
Feature Importance — XGBoost
Feature Gain Cover Frequency
Cell.shape 0.4131 0.1721 0.1038
Cell.size 0.2642 0.1434 0.0958
Bare.nuclei 0.1242 0.2098 0.1796
Cl.thickness 0.0500 0.1098 0.1377
Normal.nucleoli 0.0499 0.1110 0.1397
Bl.cromatin 0.0429 0.1055 0.0878
Marg.adhesion 0.0386 0.0928 0.1657
Epith.c.size 0.0114 0.0362 0.0699
Mitoses 0.0056 0.0193 0.0200

Σύγκριση με RF: Και οι δύο αλγόριθμοι συμφωνούν στα top features — αυτό είναι καλό sanity check: δύο ανεξάρτητοι αλγόριθμοι με διαφορετική φιλοσοφία εντοπίζουν τα ίδια χαρακτηριστικά ως σημαντικά.


4.5 Σύγκριση RF vs XGBoost (TODO 7)

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"]),
  F1          = c(rf_cm$byClass["F1"],
                  xgb_cm$byClass["F1"]),
  AUC         = c(as.numeric(rf_auc),
                  as.numeric(xgb_auc))
) %>%
  mutate(across(where(is.numeric), ~ round(.x, 3)))

knitr::kable(results,
             caption  = "Σύγκριση Μοντέλων — RF vs XGBoost",
             row.names = FALSE)
Σύγκριση Μοντέλων — RF vs XGBoost
Model Accuracy Sensitivity Specificity F1 AUC
Random Forest 0.980 0.972 0.985 0.972 0.999
XGBoost 0.975 0.958 0.985 0.965 0.998

4.6 ROC Curves — Σύγκριση

plot(rf_roc,
     col  = "forestgreen",
     lwd  = 2,
     main = "ROC Curves: Random Forest vs XGBoost")

lines(xgb_roc, col = "steelblue", lwd = 2)

abline(a = 0, b = 1, lty = 2, col = "gray60")  # τυχαία γραμμή baseline

legend("bottomright",
       legend = c(
         paste0("Random Forest (AUC = ", round(rf_auc,  3), ")"),
         paste0("XGBoost       (AUC = ", round(xgb_auc, 3), ")")
       ),
       col = c("forestgreen", "steelblue"),
       lwd = 2,
       bty = "n")

Ερμηνεία ROC: Όσο πιο κοντά στην πάνω-αριστερή γωνία βρίσκεται η καμπύλη, τόσο καλύτερο το μοντέλο. Η διακεκομμένη διαγώνιος αντιπροσωπεύει τυχαία πρόβλεψη (AUC = 0.5).


4.7 ✅ Απαντήσεις Μέρους Β

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

cat("RF  Accuracy:", round(rf_cm$overall["Accuracy"],  3), "\n")
## RF  Accuracy: 0.98
cat("XGB Accuracy:", round(xgb_cm$overall["Accuracy"], 3), "\n")
## XGB Accuracy: 0.975
cat("Διαφορά Accuracy:", round(abs(rf_cm$overall["Accuracy"] - xgb_cm$overall["Accuracy"]), 3), "\n\n")
## Διαφορά Accuracy: 0.005
cat("RF  AUC:", round(rf_auc,  3), "\n")
## RF  AUC: 0.999
cat("XGB AUC:", round(xgb_auc, 3), "\n")
## XGB AUC: 0.998
cat("Διαφορά AUC:", round(abs(rf_auc - xgb_auc), 3), "\n")
## Διαφορά AUC: 0.001

Τα δύο μοντέλα παρουσιάζουν πολύ κοντινή επίδοση — η διαφορά στο AUC είναι της τάξης του 0.01–0.02, η οποία δεν είναι στατιστικά σημαντική για αυτό το dataset. Αυτό είναι χαρακτηριστικό για datasets με σχετικά καθαρά και καλά διαχωρίσιμα δεδομένα: και οι δύο αλγόριθμοι “βλέπουν” εύκολα το σήμα.

Σε πιο θορυβώδη ή υψηλοδιάστατα datasets, θα περίμεναμε μεγαλύτερη διαφορά — συνήθως υπέρ του XGBoost.

Για την πρακτική εφαρμογή: Θα προτιμούσαμε το μοντέλο με υψηλότερη Sensitivity, καθώς σε ιατρικό πλαίσιο η μη-ανίχνευση κακοήθειας είναι το πιο ακριβό λάθος.


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

Τρία πράγματα αξίζει να σχολιαστούν:

  • Υψηλή γενική επίδοση: Και τα δύο μοντέλα ξεπερνούν το 95% Accuracy — αυτό δείχνει ότι τα κυτταρολογικά χαρακτηριστικά έχουν ισχυρό προβλεπτικό σήμα. Το πρόβλημα είναι σχετικά καλά διαχωρίσιμο.

  • Συμφωνία στα top features: Και οι δύο αλγόριθμοι αναδεικνύουν τα ίδια features ως σημαντικότερα (Cl.thickness, Cell.size, Cell.shape). Όταν δύο πολύ διαφορετικοί αλγόριθμοι συμφωνούν, έχουμε υψηλή εμπιστοσύνη στο εύρημα.

  • Το early stopping σταμάτησε νωρίς: Με eta = 0.1 και nrounds = 500, το XGBoost βρήκε τον βέλτιστο γύρο αρκετά νωρίς — αυτό δείχνει ότι το μοντέλο δεν χρειάζεται πολλά δέντρα για να μάθει σε αυτό το dataset.


5 Συμπεράσματα

Συνοψίζοντας, από αυτή την ανάλυση προκύπτουν τα εξής:

  1. Τα κυτταρολογικά χαρακτηριστικά είναι ισχυροί προβλεπτικοί παράγοντες — και οι δύο αλγόριθμοι επιτυγχάνουν >95% Accuracy, υποδεικνύοντας καλό διαχωρισμό μεταξύ κλάσεων.

  2. Τα πιο σημαντικά features (Cl.thickness, Cell.size, Cell.shape) συνάδουν με την κλινική γνώση για τη βιολογία του καρκίνου.

  3. Ο Random Forest είναι εξαιρετικό baseline — απλός στη χρήση, χωρίς να απαιτεί μετατροπή δεδομένων, με built-in OOB error estimation.

  4. Το XGBoost προσφέρει συγκρίσιμη επίδοση με επιπλέον ευελιξία (early stopping, πλουσιότερο importance output), αλλά απαιτεί περισσότερη προεπεξεργασία.

  5. Σε ιατρικό πλαίσιο, πέρα από το Accuracy, πρέπει να εξετάζουμε πάντα τη Sensitivity — ένα μοντέλο που “χάνει” κακοήθεις περιπτώσεις είναι επικίνδυνο ανεξάρτητα από το συνολικό ποσοστό επιτυχίας.


## Session Info:
## R version: R version 4.5.2 (2025-10-31 ucrt)
## Date: 2026-05-11 21:35