Εισαγωγή

Στόχος αυτού του εργαστηρίου είναι η πρόβλεψη κακοήθειας όγκων χρησιμοποιώντας το dataset Breast Cancer (Wisconsin). Θα εφαρμόσουμε τον αλγόριθμο Random Forest για την ταξινόμηση και θα αξιολογήσουμε την απόδοσή του.

1.Αρχικά, φορτώνουμε τις απαραίτητες βιβλιοθήκες και το dataset. Προχωράμε σε καθαρισμό των δεδομένων, αφαίρεση περιττών στηλών (Id) και διαχείριση ελλιπών τιμών (nulls).

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

# Καθαρισμός
bc$Id <- NULL
bc <- na.omit(bc)

# Μετατροπή των χαρακτηριστικών σε αριθμητικά (numeric)
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" ...

2. Διαχωρισμός Δεδομένων (TODO 1)

Πραγματοποιούμε ένα stratified split (70/30) για να διατηρήσουμε τις αναλογίες των κλάσεων (benign / malignant) τόσο στο training όσο και στο test set.

### 2. Διαχωρισμός Δεδομένων (TODO 1)
set.seed(42)
train_idx <- createDataPartition(bc$Class, p = 0.7, list = FALSE)

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

# ΔΙΟΡΘΩΣΗ: Χρησιμοποιούμε Class αντί για diabetes
cat("Αναλογίες στο Train set:\n")
## Αναλογίες στο Train set:
print(prop.table(table(train$Class)) %>% round(3))
## 
##    benign malignant 
##     0.649     0.351
cat("\nΑναλογίες στο Test set:\n")
## 
## Αναλογίες στο Test set:
print(prop.table(table(test$Class)) %>% round(3))
## 
##    benign malignant 
##     0.652     0.348

3. Εκπαίδευση Μοντέλου Random Forest (TODO 2)

Εκπαιδεύουμε το μοντέλο χρησιμοποιώντας 500 δέντρα και ενεργοποιούμε τον υπολογισμό της σημασίας των μεταβλητών.

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

4. Αξιολόγηση Μοντέλου (TODO 3)

Υπολογίζουμε τις προβλέψεις στο test set και εξάγουμε τις μετρήσεις ακρίβειας (Accuracy), ευαισθησίας (Sensitivity) και την περιοχή κάτω από την καμπύλη (AUC).

# Προβλέψεις
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 & ROC Curve
rf_roc <- roc(test$Class, rf_prob, levels = c("benign", "malignant"))
## Setting direction: controls < cases
rf_auc <- rf_roc$auc
plot(rf_roc, col = "royalblue", lwd = 2, main = "ROC Curve - Random Forest")

cat("\n--- Τελικές Μετρήσεις ---\n")
## 
## --- Τελικές Μετρήσεις ---
cat("Accuracy:   ", round(rf_cm$overall["Accuracy"], 4), "\n")
## Accuracy:    0.9804
cat("Sensitivity:", round(rf_cm$byClass["Sensitivity"], 4), "\n")
## Sensitivity: 0.9718
cat("RF AUC:     ", round(rf_roc$auc, 3), "\n")
## RF AUC:      0.999

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

Πήραμε Accuracy 98.04%

5. Σημασία Μεταβλητών (TODO 4)

Αναλύουμε ποια χαρακτηριστικά των κυττάρων παίζουν τον σημαντικότερο ρόλο στην πρόβλεψη της κακοήθειας.

varImpPlot(rf_model, pch = 19, col = "darkred", main = "Variable Importance — Random Forest")

# Πίνακας σημασίας
importance_table <- as.data.frame(importance(rf_model))
importance_table %>% arrange(desc(MeanDecreaseGini))
##                    benign malignant MeanDecreaseAccuracy MeanDecreaseGini
## Cell.size       11.605774 16.330563            19.826188        60.344346
## Cell.shape      10.417048 20.502944            21.800283        53.561147
## Bare.nuclei     20.271034 19.036320            26.035566        34.154820
## Normal.nucleoli 11.849778 11.676750            15.229482        23.815034
## Bl.cromatin      6.086543 14.621620            14.691182        14.587449
## Cl.thickness    12.702656 19.830096            19.363837        11.845927
## Epith.c.size    10.510480  3.579421            11.128863        11.709247
## Marg.adhesion    6.228395 12.425305            13.009741         5.977215
## Mitoses          4.984157  0.839361             4.608274         1.550928

Συμπέρασμα

Με βάση το παραπάνω γράφημα, τα top 3 χαρακτηριστικά είναι:

Bare.nuclei

Cell.shape

Cell.size

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

3. 🎯 Είναι αρκετό το accuracy που βρήκαμε;

Σε ένα νοσοκομείο, το Accuracy μπορεί να είναι παραπλανητικό. Επειδή αν είχαμε 100 ασθενείς και οι 98 ήταν υγιείς, ένα μοντέλο που θα έλεγε «όλοι είναι υγιείς» θα είχε 98% Accuracy, αλλά θα πέθαιναν οι 2 ασθενείς που είχαν καρκίνο.

Μας ενδιαφέρει το Sensitivity, που μετράει πόσους από τους πραγματικούς κακοήθεις όγκους εντόπισε το μοντέλο. Στο δικό μας αποτέλεσμα (97.18%), σημαίνει ότι το μοντέλο «έχασε» το 2.82% των καρκίνων (False Negatives). Το False Negatives μας που είναι 2 (ασθενείς που έχουν καρκίνο αλλά το μοντέλο τους είπε «είσαι καλά»).

Στην ιατρική, το False Negative είναι καταστροφικό (μήνυση, κίνδυνος ζωής).

Το False Positive (να πεις σε κάποιον υγιή ότι ίσως έχει κάτι) είναι λιγότερο σοβαρό, γιατί θα φανεί στον επόμενο έλεγχο (π.χ. βιοψία).

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

# Μετατροπή των features σε μήτρα (αφαιρούμε τη στήλη Class)
X_train <- as.matrix(train[, -10])
X_test  <- as.matrix(test[, -10])

# Μετατροπή του target σε 0 και 1
y_train <- ifelse(train$Class == "malignant", 1, 0)
y_test  <- ifelse(test$Class == "malignant", 1, 0)

# Δημιουργία αντικειμένων dtrain/dtest για βέλτιστη απόδοση
dtrain <- xgb.DMatrix(data = X_train, label = y_train)
dtest  <- xgb.DMatrix(data = X_test, label = y_test)

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

# --- TODO 6: Εκπαίδευση XGBoost ---
set.seed(42)

# 1. Ορίζουμε τις παραμέτρους σε λίστα
params <- list(
  max_depth = 4,
  eta = 0.1,
  objective = "binary:logistic"
)

# 2. Κρατάμε το όνομα της μεταβλητής 'watchlist' για να ταιριάζει με τον κώδικά σου
watchlist <- list(train = dtrain, test = dtest)

# 3. Στη συνάρτηση όμως, χρησιμοποιούμε το νέο όνομα παραμέτρου 'evals'
xgb_model <- xgb.train(
  params = params,
  data = dtrain,
  nrounds = 500,
  evals = watchlist,       # ΕΔΩ ΕΙΝΑΙ ΤΟ ΚΛΕΙΔΙ: evals = watchlist
  early_stopping_rounds = 20,
  verbose = 0
)

cat("Καλύτερος γύρος (Iteration):", xgb_model$best_iteration, "\n")
## Καλύτερος γύρος (Iteration):

TODO 7 Σύγκρινε XGBoost vs Random Forest σε ένα πίνακα

# Προβλέψεις XGBoost 
xgb_prob <- predict(xgb_model, dtest)
xgb_pred <- ifelse(xgb_prob > 0.5, "malignant", "benign")
xgb_pred <- factor(xgb_pred, levels = c("benign", "malignant"))

# Confusion Matrix για XGBoost
xgb_cm <- confusionMatrix(xgb_pred, test$Class, positive = "malignant")
print(xgb_cm)
## Confusion Matrix and Statistics
## 
##            Reference
## Prediction  benign malignant
##   benign       131         4
##   malignant      2        67
##                                           
##                Accuracy : 0.9706          
##                  95% CI : (0.9371, 0.9891)
##     No Information Rate : 0.652           
##     P-Value [Acc > NIR] : <2e-16          
##                                           
##                   Kappa : 0.9348          
##                                           
##  Mcnemar's Test P-Value : 0.6831          
##                                           
##             Sensitivity : 0.9437          
##             Specificity : 0.9850          
##          Pos Pred Value : 0.9710          
##          Neg Pred Value : 0.9704          
##              Prevalence : 0.3480          
##          Detection Rate : 0.3284          
##    Detection Prevalence : 0.3382          
##       Balanced Accuracy : 0.9643          
##                                           
##        'Positive' Class : malignant       
## 
# Υπολογισμός AUC για XGBoost
xgb_auc <- roc(test$Class, xgb_prob, levels = c("benign", "malignant"))$auc
## Setting direction: controls < cases
# Δημιουργία Συγκριτικού Πίνακα
comparison <- data.frame(
  Metric = c("Accuracy", "Sensitivity", "Specificity", "AUC"),
  RandomForest = c(
    rf_cm$overall["Accuracy"],
    rf_cm$byClass["Sensitivity"],
    rf_cm$byClass["Specificity"],
    rf_auc
  ),
  XGBoost = c(
    xgb_cm$overall["Accuracy"],
    xgb_cm$byClass["Sensitivity"],
    xgb_cm$byClass["Specificity"],
    xgb_auc
  )
)

knitr::kable(comparison, digits = 3, caption = "Σύγκριση Απόδοσης: RF vs XGBoost")
Σύγκριση Απόδοσης: RF vs XGBoost
Metric RandomForest XGBoost
Accuracy Accuracy 0.980 0.971
Sensitivity Sensitivity 0.972 0.944
Specificity Specificity 0.985 0.985
AUC 0.999 0.997

Συμπέρασμα

Το Random Forest λειτούργησε καλύτερα λόγω του μικρού μεγέθους του δείγματος (700 εγγραφές). Το XGBoost είναι πιο ευαίσθητο στις παραμέτρους και για να αποδώσει το 100% των δυνατοτήτων του, θα χρειαζόταν εκτεταμένο hyperparameter tuning (π.χ. μέσω του πακέτου caret) ή περισσότερα δεδομένα.