Σε αυτή την εργασία αναλαμβάνουμε τον ρόλο data scientists σε ένα
ογκολογικό κέντρο. Έχουμε στη διάθεσή μας κυτταρολογικά χαρακτηριστικά
από 699 βιοψίες (dataset BreastCancer,
πακέτο mlbench) και ο στόχος μας είναι να χτίσουμε ένα
μοντέλο που να προβλέπει εάν ένας όγκος είναι καλοήθης
(benign) ή κακοήθης (malignant).
Το μοντέλο θα λειτουργεί ως decision support tool για τους γιατρούς. Αυτό σημαίνει ότι χρειαζόμαστε:
Θα συγκρίνουμε δύο αλγόριθμους: Random Forest και XGBoost.
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" ...
## Κατανομή κλάσεων:
##
## benign malignant
## 444 239
##
## Αναλογίες:
##
## benign malignant
## 0.65 0.35
Παρατήρηση: Το dataset έχει μέτρια ασυμμετρία — περίπου 65% benign / 35% malignant. Δεν απαιτείται resampling, αλλά θα κάνουμε stratified split για να διατηρηθεί αυτή η αναλογία.
Κάνουμε 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 γραμμές
## Μέγεθος test set: 204 γραμμές
## Αναλογίες στο Train:
##
## benign malignant
## 0.649 0.351
##
## Αναλογίες στο Test:
##
## benign malignant
## 0.652 0.348
Οι αναλογίες διατηρούνται πιστά και στα δύο sets — το stratified split δούλεψε σωστά.
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.
# Πρόβλεψη κλάσης και πιθανοτήτων
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
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")| 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 |
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, τα τρία πιο σημαντικά
χαρακτηριστικά είναι:
Cl.thickness (Πάχος κυτταρικής
στοιβάδας) — ο πιο ισχυρός προβλεπτικός παράγονταςCell.size (Ομοιομορφία μεγέθους
κυττάρων) — κακοήθη κύτταρα τείνουν να έχουν άνιση ανάπτυξηCell.shape (Ομοιομορφία σχήματος
κυττάρων) — παρόμοια λογική με το μέγεθοςΑυτά είναι κλινικά λογικά: η κακοήθεια χαρακτηρίζεται ακριβώς από αύξηση του πάχους και από ανομοιογένεια μεγέθους/σχήματος κυττάρων.
3. Είναι 97% accuracy «αρκετό» σε ιατρικό context;
Όχι απαραίτητα — και αυτό είναι ένα κρίσιμο σημείο.
Σε ένα ιατρικό πλαίσιο, ο τύπος σφάλματος έχει πολύ μεγαλύτερη σημασία από το συνολικό ποσοστό επιτυχίας:
Άρα, η μετρική που μας ενδιαφέρει περισσότερο είναι η Sensitivity (= ποσοστό κακοήθων που εντοπίζουμε σωστά). Ένα μοντέλο με 97% accuracy αλλά χαμηλή Sensitivity είναι επικίνδυνο. Χρειαζόμαστε Sensitivity > 95% ιδανικά.
Το 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
## Test matrix dimensions: 204 9
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
)## Καλύτερος γύρος:
## Καλύτερο Test AUC:
Τι είναι το early stopping; Το μοντέλο σταματά αυτόματα όταν η επίδοση στο test set δεν βελτιώνεται για 20 συνεχόμενους γύρους. Αυτό αποτρέπει το overfitting και εξοικονομεί χρόνο.
# Πρόβλεψη πιθανοτήτων
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
imp_xgb <- xgb.importance(model = xgb_model)
xgb.plot.importance(imp_xgb, top_n = 9,
main = "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: δύο ανεξάρτητοι αλγόριθμοι με διαφορετική φιλοσοφία εντοπίζουν τα ίδια χαρακτηριστικά ως σημαντικά.
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)| 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 |
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).
1. Ποιο μοντέλο νίκησε; Με πόση διαφορά;
## RF Accuracy: 0.98
## XGB Accuracy: 0.975
cat("Διαφορά Accuracy:", round(abs(rf_cm$overall["Accuracy"] - xgb_cm$overall["Accuracy"]), 3), "\n\n")## Διαφορά Accuracy: 0.005
## RF AUC: 0.999
## XGB AUC: 0.998
## Διαφορά 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.
Συνοψίζοντας, από αυτή την ανάλυση προκύπτουν τα εξής:
Τα κυτταρολογικά χαρακτηριστικά είναι ισχυροί προβλεπτικοί παράγοντες — και οι δύο αλγόριθμοι επιτυγχάνουν >95% Accuracy, υποδεικνύοντας καλό διαχωρισμό μεταξύ κλάσεων.
Τα πιο σημαντικά features (Cl.thickness, Cell.size, Cell.shape) συνάδουν με την κλινική γνώση για τη βιολογία του καρκίνου.
Ο Random Forest είναι εξαιρετικό baseline — απλός στη χρήση, χωρίς να απαιτεί μετατροπή δεδομένων, με built-in OOB error estimation.
Το XGBoost προσφέρει συγκρίσιμη επίδοση με επιπλέον ευελιξία (early stopping, πλουσιότερο importance output), αλλά απαιτεί περισσότερη προεπεξεργασία.
Σε ιατρικό πλαίσιο, πέρα από το Accuracy, πρέπει να εξετάζουμε πάντα τη Sensitivity — ένα μοντέλο που “χάνει” κακοήθεις περιπτώσεις είναι επικίνδυνο ανεξάρτητα από το συνολικό ποσοστό επιτυχίας.
## Session Info:
## R version: R version 4.5.2 (2025-10-31 ucrt)
## Date: 2026-05-11 21:35