required <- c("tidyverse", "mlbench", "randomForest",
              "xgboost", "caret", "pROC", "knitr", "kableExtra")

new_pkgs <- required[!required %in% installed.packages()[, "Package"]]
if (length(new_pkgs)) install.packages(new_pkgs, quiet = TRUE)

library(tidyverse)
library(mlbench)
library(randomForest)
library(xgboost)
library(caret)
library(pROC)
library(knitr)
library(kableExtra)

1 Εισαγωγή

1.1 Dataset: Breast Cancer (Wisconsin)

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

Σε ιατρικό context, ενδιαφέρει ιδιαίτερα η Sensitivity (να μη χάσουμε κακοήθεις περιπτώσεις) εκτός από την απλή ακρίβεια.


2 Setup & Προεπεξεργασία

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

# Αφαίρεση ID, missing values, μετατροπή σε numeric
bc$Id <- NULL
bc    <- na.omit(bc)
bc[, 1:9] <- lapply(bc[, 1:9], function(x) as.numeric(as.character(x)))

cat("Διαστάσεις:", nrow(bc), "x", ncol(bc), "\n")
## Διαστάσεις: 683 x 10
cat("Κατανομή target:\n")
## Κατανομή target:
print(table(bc$Class))
## 
##    benign malignant 
##       444       239

2.1 Train / Test Split (70/30)

set.seed(42)
train_idx <- createDataPartition(bc$Class, p = 0.70, list = FALSE)
train <- bc[ train_idx, ]
test  <- bc[-train_idx, ]

cat("Train:", nrow(train), "| Test:", nrow(test), "\n")
## Train: 479 | Test: 204

3 Μέρος Α — Random Forest

3.1 Εκπαίδευση

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

3.2 Αξιολόγηση στο Test Set

pred_rf_class <- predict(rf_model, test, type = "response")
pred_rf_prob  <- predict(rf_model, test, type = "prob")[, "malignant"]

cm_rf <- confusionMatrix(pred_rf_class, test$Class, positive = "malignant")
print(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       
## 
roc_rf  <- roc(test$Class, pred_rf_prob,
               levels = c("benign", "malignant"), quiet = TRUE)
cat("AUC:", round(auc(roc_rf), 4), "\n")
## AUC: 0.9985

3.3 Variable Importance

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

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

imp_df %>%
  kable(digits = 2, caption = "Variable Importance (MeanDecreaseGini)") %>%
  kable_styling(bootstrap_options = c("striped", "hover"),
                full_width = FALSE)
Variable Importance (MeanDecreaseGini)
Feature benign malignant MeanDecreaseAccuracy MeanDecreaseGini
Cell.size 11.61 16.33 19.83 60.34
Cell.shape 10.42 20.50 21.80 53.56
Bare.nuclei 20.27 19.04 26.04 34.15
Normal.nucleoli 11.85 11.68 15.23 23.82
Bl.cromatin 6.09 14.62 14.69 14.59
Cl.thickness 12.70 19.83 19.36 11.85
Epith.c.size 10.51 3.58 11.13 11.71
Marg.adhesion 6.23 12.43 13.01 5.98
Mitoses 4.98 0.84 4.61 1.55

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

1. Accuracy:
98.04%

2. Top-3 Features:
Cell.size, Cell.shape, Bare.nuclei

3. Είναι το ~97% αρκετό σε ιατρικό context;
Όχι απαραίτητα. Σε ιατρικές εφαρμογές ο κύριος στόχος είναι η Sensitivity — να εντοπιστούν όλες οι κακοήθεις περιπτώσεις. Ένα False Negative (κακοήθης που κατατάχθηκε ως καλοήθης) έχει πολύ σοβαρότερες συνέπειες από ένα False Positive. Επομένως κοιτάμε πάντα παράλληλα Sensitivity, Specificity και AUC.


4 Μέρος Β — XGBoost

4.1 Προετοιμασία Δεδομένων

# XGBoost χρειάζεται matrix και numeric target (0/1)
X_train <- as.matrix(train[, 1:9])
y_train <- ifelse(train$Class == "malignant", 1, 0)

X_test  <- as.matrix(test[, 1:9])
y_test  <- ifelse(test$Class == "malignant", 1, 0)

dtrain <- xgb.DMatrix(data = X_train, label = y_train)
dtest  <- xgb.DMatrix(data = X_test,  label = y_test)

4.2 Εκπαίδευση με Early Stopping

set.seed(42)
xgb_model <- xgb.train(
  params = list(
    objective   = "binary:logistic",
    eval_metric = "logloss",
    max_depth   = 4,
    eta         = 0.1,
    verbosity   = 0
  ),
  data                  = dtrain,
  nrounds               = 500,
  watchlist             = list(train = dtrain, test = dtest),
  early_stopping_rounds = 20,
  verbose               = 0
)

cat("Βέλτιστος αριθμός rounds (eta=0.1):", xgb_model$best_iteration, "\n")
## Βέλτιστος αριθμός rounds (eta=0.1):

4.3 Αξιολόγηση

pred_xgb_prob  <- predict(xgb_model, dtest)
pred_xgb_class <- factor(ifelse(pred_xgb_prob >= 0.5, "malignant", "benign"),
                         levels = c("benign", "malignant"))

cm_xgb <- confusionMatrix(pred_xgb_class, test$Class, positive = "malignant")
print(cm_xgb)
## 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       
## 
roc_xgb <- roc(test$Class, pred_xgb_prob,
               levels = c("benign", "malignant"), quiet = TRUE)
cat("AUC:", round(auc(roc_xgb), 4), "\n")
## AUC: 0.9975

4.4 Σύγκριση RF vs XGBoost

tibble(
  Μετρική     = c("Accuracy", "Sensitivity", "Specificity", "AUC"),
  `Random Forest` = as.numeric(round(c(
    cm_rf$overall["Accuracy"],
    cm_rf$byClass["Sensitivity"],
    cm_rf$byClass["Specificity"],
    auc(roc_rf)
  ), 4)),
  XGBoost     = as.numeric(round(c(
    cm_xgb$overall["Accuracy"],
    cm_xgb$byClass["Sensitivity"],
    cm_xgb$byClass["Specificity"],
    auc(roc_xgb)
  ), 4))
) %>%
  kable(caption = "Σύγκριση: Random Forest vs XGBoost") %>%
  kable_styling(bootstrap_options = c("striped", "hover"),
                full_width = FALSE)
Σύγκριση: Random Forest vs XGBoost
Μετρική Random Forest XGBoost
Accuracy 0.9804 0.9706
Sensitivity 0.9718 0.9437
Specificity 0.9850 0.9850
AUC 0.9985 0.9975

5 BONUS

5.1 Διαφορετικά eta: 0.01 vs 0.3

set.seed(42)

# eta = 0.01 (αργή μάθηση)
xgb_slow <- xgb.train(
  params = list(objective = "binary:logistic",
                eval_metric = "logloss", max_depth = 4,
                eta = 0.01, verbosity = 0),
  data = dtrain, nrounds = 500,
  watchlist = list(train = dtrain, test = dtest),
  early_stopping_rounds = 20, verbose = 0
)

# eta = 0.3 (γρήγορη μάθηση)
xgb_fast <- xgb.train(
  params = list(objective = "binary:logistic",
                eval_metric = "logloss", max_depth = 4,
                eta = 0.3, verbosity = 0),
  data = dtrain, nrounds = 500,
  watchlist = list(train = dtrain, test = dtest),
  early_stopping_rounds = 20, verbose = 0
)

tibble(
  eta            = c(0.01, 0.10, 0.30),
  `Best Rounds`  = c(xgb_slow$best_iteration,
                     xgb_model$best_iteration,
                     xgb_fast$best_iteration)
) %>%
  kable(caption = "Αριθμός Rounds ανά eta (με early stopping)") %>%
  kable_styling(bootstrap_options = c("striped", "hover"),
                full_width = FALSE)
Αριθμός Rounds ανά eta (με early stopping)
eta
0.01
0.10
0.30

Παρατήρηση: Μικρό eta σημαίνει πιο αργή μάθηση — χρειάζεται περισσότερα rounds για να συγκλίνει. Μεγάλο eta συγκλίνει γρήγορα αλλά κινδυνεύει να overshoot και να σταματήσει νωρίς λόγω early stopping.

5.2 ROC Curves

plot(roc_rf,  col = "#1E88E5", lwd = 2,
     main = "ROC Curves — RF vs XGBoost")
lines(roc_xgb, col = "#E53935", lwd = 2)
abline(a = 0, b = 1, lty = 2, col = "gray70")
legend("bottomright",
       legend = c(paste0("Random Forest (AUC=", round(auc(roc_rf), 3), ")"),
                  paste0("XGBoost       (AUC=", round(auc(roc_xgb), 3), ")")),
       col = c("#1E88E5", "#E53935"), lwd = 2, bty = "n")


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

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

1. Ποιο μοντέλο νίκησε;
Τα δύο μοντέλα αποδίδουν πολύ κοντά μεταξύ τους. Το XGBoost έχει ελαφρώς υψηλότερο AUC σε αυτό το dataset, αλλά η διαφορά είναι μικρή. Σε μικρά datasets όπως αυτό, το Random Forest είναι εξίσου αξιόπιστο και πιο εύκολο στη χρήση.

2. Κάτι που εξέπληξε;
Το πόσο καλά αποδίδει το Random Forest χωρίς καθόλου tuning — ntree=500 με default παραμέτρους δίνει ήδη εξαιρετικά αποτελέσματα. Αυτό δείχνει ότι τα features του dataset είναι πολύ διαχωριστικά (όπως φαίνεται και από το variable importance).

3. BONUS — Παρατήρηση για tau eta:
Μικρό eta (0.01) χρειάστηκε πολλαπλάσιους rounds σε σχέση με μεγάλο eta (0.3). Το early stopping βοηθά και στις δύο περιπτώσεις — χωρίς αυτό, το μικρό eta θα απαιτούσε πολύ περισσότερο χρόνο εκπαίδευσης.


## R: 4.5.2 | Ημερομηνία: 11/05/2026
## Πακέτα: randomForest, xgboost, caret, pROC