Στόχος αυτού του εργαστηρίου είναι η πρόβλεψη κακοήθειας όγκων χρησιμοποιώντας το 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" ...
Πραγματοποιούμε ένα 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
Εκπαιδεύουμε το μοντέλο χρησιμοποιώντας 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
Υπολογίζουμε τις προβλέψεις στο 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
Πήραμε Accuracy 98.04%
Αναλύουμε ποια χαρακτηριστικά των κυττάρων παίζουν τον σημαντικότερο ρόλο στην πρόβλεψη της κακοήθειας.
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
Αυτές οι τρεις μεταβλητές προσφέρουν τη μεγαλύτερη πληροφορία στο μοντέλο για τον διαχωρισμό μεταξύ καλοήθων και κακοήθων όγκων.
Σε ένα νοσοκομείο, το Accuracy μπορεί να είναι παραπλανητικό. Επειδή αν είχαμε 100 ασθενείς και οι 98 ήταν υγιείς, ένα μοντέλο που θα έλεγε «όλοι είναι υγιείς» θα είχε 98% Accuracy, αλλά θα πέθαιναν οι 2 ασθενείς που είχαν καρκίνο.
Μας ενδιαφέρει το Sensitivity, που μετράει πόσους από τους πραγματικούς κακοήθεις όγκους εντόπισε το μοντέλο. Στο δικό μας αποτέλεσμα (97.18%), σημαίνει ότι το μοντέλο «έχασε» το 2.82% των καρκίνων (False Negatives). Το False Negatives μας που είναι 2 (ασθενείς που έχουν καρκίνο αλλά το μοντέλο τους είπε «είσαι καλά»).
Στην ιατρική, το False Negative είναι καταστροφικό (μήνυση, κίνδυνος ζωής).
Το False Positive (να πεις σε κάποιον υγιή ότι ίσως έχει κάτι) είναι λιγότερο σοβαρό, γιατί θα φανεί στον επόμενο έλεγχο (π.χ. βιοψία).
# Μετατροπή των 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)
# --- 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):
# Προβλέψεις 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")
| 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) ή περισσότερα δεδομένα.