Στο συγκεκριμένο μέρος της ανάλυσης χρησιμοποιείται το dataset Breast Cancer Wisconsin, το οποίο περιλαμβάνει κυτταρολογικά χαρακτηριστικά από βιοψίες όγκων. Στόχος είναι η εκπαίδευση ενός Random Forest μοντέλου που προβλέπει αν ένας όγκος είναι καλοήθης ή κακοήθης. Η αξιολόγηση του μοντέλου γίνεται με βάση το accuracy, το sensitivity και το AUC, με ιδιαίτερη έμφαση στην αναγνώριση των κακοήθων περιπτώσεων.
## ── 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
## Warning: package 'mlbench' was built under R version 4.5.3
## 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
## Warning: package 'xgboost' was built under R version 4.5.3
## 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
## 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
## '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
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
## [1] 204 10
##
## benign malignant
## 0.6492693 0.3507307
##
## benign malignant
## 0.6519608 0.3480392
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
# Προβλέψεις κατηγορίας
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 είναι κρίσιμη.
Το Variable Importance plot δείχνει ότι τα σημαντικότερα χαρακτηριστικά για την πρόβλεψη της κακοήθειας είναι κυρίως τα Cell.size, Cell.shape και Bare.nuclei. Με βάση το μέτρο MeanDecreaseGini, οι μεταβλητές αυτές συμβάλλουν περισσότερο στη διάκριση μεταξύ καλοήθων και κακοήθων όγκων. Αυτό σημαίνει ότι το μέγεθος των κυττάρων, το σχήμα των κυττάρων και οι γυμνοί πυρήνες αποτελούν βασικούς παράγοντες που χρησιμοποιεί το Random Forest μοντέλο για την ταξινόμηση των περιπτώσεων.
# 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
## [1] 204 9
## y_train
## 0 1
## 311 168
## y_test
## 0 1
## 133 71
Τα δεδομένα προετοιμάστηκαν σωστά για το XGBoost, καθώς τα features μετατράπηκαν σε matrix και η μεταβλητή στόχος κωδικοποιήθηκε αριθμητικά, με 0 για τις καλοήθεις και 1 για τις κακοήθεις περιπτώσεις.Το training set περιλαμβάνει 479 παρατηρήσεις και 9 χαρακτηριστικά, ενώ το test set περιλαμβάνει 204 παρατηρήσεις και 9 χαρακτηριστικά. Η κατανομή των κλάσεων παρέμεινε παρόμοια στα δύο σύνολα δεδομένων, γεγονός που δείχνει ότι ο stratified διαχωρισμός πραγματοποιήθηκε σωστά.
# Μετατροπή των δεδομένων σε 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
## $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
## [1] 0.9966112
Το XGBoost μοντέλο πέτυχε την καλύτερη απόδοσή του στην 24η επανάληψη, με AUC ίσο με 0.9966. Η τιμή αυτή δείχνει εξαιρετική διαχωριστική ικανότητα μεταξύ καλοήθων και κακοήθων όγκων. Επιπλέον, η χρήση του early stopping βοήθησε ώστε η εκπαίδευση να σταματήσει στο κατάλληλο σημείο και να μειωθεί ο κίνδυνος overfitting.
# =========================
# 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 μπόρεσε να αποδώσει πολύ καλά χωρίς έντονη ανάγκη για πιο σύνθετη βελτιστοποίηση.
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 οδήγησαν σε πολύ καλή διαχωριστική ικανότητα.
# 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, δηλαδή στην ικανότητα του μοντέλου να εντοπίζει σωστά τις κακοήθεις περιπτώσεις.