library(tidyverse)
library(rpart)
library(rpart.plot)
library(caret)
library(knitr)
library(kableExtra)

1 Εισαγωγή & Παρουσίαση Dataset

Το Mushroom Dataset (UCI Machine Learning Repository) περιέχει 8.124 μανιτάρια με 22 κατηγορικά χαρακτηριστικά. Στόχος είναι να προβλέψουμε αν ένα μανιτάρι είναι βρώσιμο (e) ή τοξικό (p).

Η ανάλυση ακολουθεί τα 4 βήματα του μαθήματος και συγκρίνει το CART με τη Λογιστική Παλινδρόμηση.


2 Βήμα 1 — Εξερεύνηση Δεδομένων (EDA)

mush <- read.csv("mushrooms.csv", stringsAsFactors = TRUE)

# Καθαρά ονόματα στηλών
colnames(mush) <- c(
  "class", "cap_shape", "cap_surface", "cap_color", "bruises", "odor",
  "gill_attachment", "gill_spacing", "gill_size", "gill_color",
  "stalk_shape", "stalk_root", "stalk_surface_above_ring",
  "stalk_surface_below_ring", "stalk_color_above_ring",
  "stalk_color_below_ring", "veil_type", "veil_color",
  "ring_number", "ring_type", "spore_print_color", "population", "habitat"
)

# Μετατροπή target σε περιγραφικές τιμές
mush$class <- fct_recode(mush$class, edible = "e", poisonous = "p")

cat("Διαστάσεις:", nrow(mush), "x", ncol(mush), "\n")
## Διαστάσεις: 8124 x 23
print(table(mush$class))
## 
##    edible poisonous 
##      4208      3916
# Ποσοστό τοξικών ανά μυρωδιά — η πιο διαχωριστική μεταβλητή
mush %>%
  count(odor, class) %>%
  group_by(odor) %>%
  mutate(pct = n / sum(n)) %>%
  filter(class == "poisonous") %>%
  ggplot(aes(x = reorder(odor, pct), y = pct, fill = pct)) +
  geom_col(alpha = 0.85) +
  scale_fill_gradient(low = "#A5D6A7", high = "#B71C1C") +
  scale_y_continuous(labels = scales::percent) +
  coord_flip() +
  labs(title = "Ποσοστό Τοξικών ανά Μυρωδιά (odor)",
       x = "Κωδικός Μυρωδιάς", y = "% Τοξικά") +
  theme_minimal() +
  theme(legend.position = "none")

Μανιτάρια με μυρωδιά f (foul) ή p (pungent) είναι σχεδόν πάντα τοξικά. Η μεταβλητή odor αναμένεται να είναι η πιο σημαντική στο δέντρο.


3 Βήμα 2 — Προεπεξεργασία

# Αφαίρεση μεταβλητής με μία μόνο τιμή (veil_type — χωρίς πληροφορία)
mush <- mush %>% select(-veil_type)

# Διαχωρισμός Train / Test (75% / 25%, stratified)
set.seed(42)
train_idx <- createDataPartition(mush$class, p = 0.75, list = FALSE)
train_set <- mush[ train_idx, ]
test_set  <- mush[-train_idx, ]

cat("Train:", nrow(train_set), "| Test:", nrow(test_set), "\n")
## Train: 6093 | Test: 2031
print(round(prop.table(table(train_set$class)), 3))
## 
##    edible poisonous 
##     0.518     0.482

4 Βήμα 3 — Εκπαίδευση Μοντέλων

4.1 Δέντρο Απόφασης (CART)

tree_model <- rpart(class ~ ., data = train_set, method = "class",
                    control = rpart.control(cp = 0.001, minbucket = 7))

# Κλάδεμα με βέλτιστο cp
best_cp    <- tree_model$cptable[which.min(tree_model$cptable[,"xerror"]), "CP"]
tree_pruned <- prune(tree_model, cp = best_cp)

cat("Βέλτιστο cp:", round(best_cp, 6), "\n")
## Βέλτιστο cp: 0.001
cat("Φύλλα δέντρου:", sum(tree_pruned$frame$var == "<leaf>"), "\n")
## Φύλλα δέντρου: 6
rpart.plot(tree_pruned, type = 4, extra = 104,
           box.palette   = c("#A5D6A7", "#EF9A9A"),
           fallen.leaves = TRUE,
           main          = "CART Decision Tree — Mushroom Dataset")

Ο πρώτος διαχωρισμός γίνεται βάσει της μυρωδιάς (odor) — επιβεβαιώνει το EDA. Με λίγους μόνο κόμβους το δέντρο φτάνει σχεδόν τέλεια ακρίβεια.

4.2 Λογιστική Παλινδρόμηση

Χρησιμοποιώ υποσύνολο μεταβλητών για να αποφύγω προβλήματα perfect separation που προκύπτουν από τις πολλές κατηγορικές μεταβλητές.

logit_model <- glm(
  class ~ odor + spore_print_color + gill_color + ring_type,
  data   = train_set,
  family = binomial(link = "logit")
)

summary(logit_model)
## 
## Call:
## glm(formula = class ~ odor + spore_print_color + gill_color + 
##     ring_type, family = binomial(link = "logit"), data = train_set)
## 
## Coefficients: (1 not defined because of singularities)
##                      Estimate Std. Error z value Pr(>|z|)   
## (Intercept)         1.090e+11  3.334e+13   0.003  0.99739   
## odorc               8.656e+01  3.919e+04   0.002  0.99824   
## odorf              -1.090e+11  3.334e+13  -0.003  0.99739   
## odorl               3.888e-02  2.452e+04   0.000  1.00000   
## odorm               2.459e+01  6.873e+04   0.000  0.99971   
## odorn              -2.445e+00  2.544e+04   0.000  0.99992   
## odorp               8.566e+01  3.695e+04   0.002  0.99815   
## odors              -1.090e+11  3.334e+13  -0.003  0.99739   
## odory              -1.090e+11  3.334e+13  -0.003  0.99739   
## spore_print_colorh  1.090e+11  3.334e+13   0.003  0.99739   
## spore_print_colork  1.770e+01  7.026e+04   0.000  0.99980   
## spore_print_colorn  3.884e+00  5.772e+04   0.000  0.99995   
## spore_print_coloro  1.173e-01  8.261e+04   0.000  1.00000   
## spore_print_colorr  9.398e+01  7.791e+04   0.001  0.99904   
## spore_print_coloru  1.687e+01  8.819e+04   0.000  0.99985   
## spore_print_colorw  4.381e+01  5.773e+04   0.001  0.99939   
## spore_print_colory -9.752e-02  7.713e+04   0.000  1.00000   
## gill_colore        -1.090e+11  3.334e+13  -0.003  0.99739   
## gill_colorg        -1.090e+11  3.334e+13  -0.003  0.99739   
## gill_colorh        -1.090e+11  3.334e+13  -0.003  0.99739   
## gill_colork        -1.090e+11  3.334e+13  -0.003  0.99739   
## gill_colorn        -1.090e+11  3.334e+13  -0.003  0.99739   
## gill_coloro        -1.090e+11  3.334e+13  -0.003  0.99739   
## gill_colorp        -1.090e+11  3.334e+13  -0.003  0.99739   
## gill_colorr        -1.090e+11  3.334e+13  -0.003  0.99739   
## gill_coloru        -1.090e+11  3.334e+13  -0.003  0.99739   
## gill_colorw        -1.090e+11  3.334e+13  -0.003  0.99739   
## gill_colory        -1.090e+11  3.334e+13  -0.003  0.99739   
## ring_typef         -1.090e+11  3.334e+13  -0.003  0.99739   
## ring_typel         -5.782e-01  2.868e+04   0.000  0.99998   
## ring_typen                 NA         NA      NA       NA   
## ring_typep         -1.469e+00  4.671e-01  -3.145  0.00166 **
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 8438.82  on 6092  degrees of freedom
## Residual deviance:  188.88  on 6062  degrees of freedom
## AIC: 250.88
## 
## Number of Fisher Scoring iterations: 25

5 Βήμα 4 — Αξιολόγηση & Σύγκριση

# Προβλέψεις CART
pred_tree <- predict(tree_pruned, test_set, type = "class")

# Προβλέψεις Logistic
pred_logit_prob <- predict(logit_model, test_set, type = "response")
pred_logit      <- factor(ifelse(pred_logit_prob >= 0.5, "poisonous", "edible"),
                          levels = levels(test_set$class))
cat("=== Confusion Matrix: CART ===\n")
## === Confusion Matrix: CART ===
cm_tree <- confusionMatrix(pred_tree, test_set$class, positive = "poisonous")
print(cm_tree)
## Confusion Matrix and Statistics
## 
##            Reference
## Prediction  edible poisonous
##   edible      1052         2
##   poisonous      0       977
##                                           
##                Accuracy : 0.999           
##                  95% CI : (0.9964, 0.9999)
##     No Information Rate : 0.518           
##     P-Value [Acc > NIR] : <2e-16          
##                                           
##                   Kappa : 0.998           
##                                           
##  Mcnemar's Test P-Value : 0.4795          
##                                           
##             Sensitivity : 0.9980          
##             Specificity : 1.0000          
##          Pos Pred Value : 1.0000          
##          Neg Pred Value : 0.9981          
##              Prevalence : 0.4820          
##          Detection Rate : 0.4810          
##    Detection Prevalence : 0.4810          
##       Balanced Accuracy : 0.9990          
##                                           
##        'Positive' Class : poisonous       
## 
cat("=== Confusion Matrix: Logistic Regression ===\n")
## === Confusion Matrix: Logistic Regression ===
cm_logit <- confusionMatrix(pred_logit, test_set$class, positive = "poisonous")
print(cm_logit)
## Confusion Matrix and Statistics
## 
##            Reference
## Prediction  edible poisonous
##   edible      1052        10
##   poisonous      0       969
##                                          
##                Accuracy : 0.9951         
##                  95% CI : (0.991, 0.9976)
##     No Information Rate : 0.518          
##     P-Value [Acc > NIR] : < 2.2e-16      
##                                          
##                   Kappa : 0.9901         
##                                          
##  Mcnemar's Test P-Value : 0.004427       
##                                          
##             Sensitivity : 0.9898         
##             Specificity : 1.0000         
##          Pos Pred Value : 1.0000         
##          Neg Pred Value : 0.9906         
##              Prevalence : 0.4820         
##          Detection Rate : 0.4771         
##    Detection Prevalence : 0.4771         
##       Balanced Accuracy : 0.9949         
##                                          
##        'Positive' Class : poisonous      
## 
tibble(
  Μετρική  = c("Accuracy", "Sensitivity", "Specificity", "Kappa"),
  CART     = round(c(cm_tree$overall["Accuracy"],
                     cm_tree$byClass["Sensitivity"],
                     cm_tree$byClass["Specificity"],
                     cm_tree$overall["Kappa"]), 4),
  Logistic = round(c(cm_logit$overall["Accuracy"],
                     cm_logit$byClass["Sensitivity"],
                     cm_logit$byClass["Specificity"],
                     cm_logit$overall["Kappa"]), 4)
) %>%
  kable(caption = "Σύγκριση: CART vs Λογιστική Παλινδρόμηση") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Σύγκριση: CART vs Λογιστική Παλινδρόμηση
Μετρική CART Logistic
Accuracy 0.999 0.9951
Sensitivity 0.998 0.9898
Specificity 1.000 1.0000
Kappa 0.998 0.9901

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

  • Και τα δύο μοντέλα αποδίδουν πολύ καλά, κυρίως λόγω της μεταβλητής odor που διαχωρίζει σχεδόν τέλεια τις δύο κλάσεις.
  • Το CART υπερτερεί γιατί χειρίζεται φυσικά τις κατηγορικές μεταβλητές, παράγει ευανάγνωστους κανόνες, και χρησιμοποιεί όλες τις μεταβλητές χωρίς προβλήματα.
  • Η Λογιστική Παλινδρόμηση με πολλές κατηγορικές μεταβλητές δημιουργεί εκατοντάδες dummies που προκαλούν perfect separation — γι’ αυτό χρησιμοποίησα μόνο υποσύνολο μεταβλητών.
  • Φυσική βελτίωση θα ήταν η χρήση Random Forest για πιο σταθερά αποτελέσματα.

EA-008 | Δέντρα Απόφασης & CART | Mushroom Dataset