1 Σκοπός της εργασίας

Σκοπός της εργασίας είναι να δημιουργηθεί ένα case study με Δέντρο Απόφασης CART για την πρόβλεψη του αν οι εβδομαδιαίες πωλήσεις ενός καταστήματος Walmart είναι υψηλές ή χαμηλές.

Η εξαρτημένη μεταβλητή της εργασίας είναι η Success. Παίρνει την τιμή High όταν οι συνολικές εβδομαδιαίες πωλήσεις είναι πάνω από τη διάμεσο και την τιμή Low όταν είναι ίσες ή κάτω από τη διάμεσο.

2 Διερεύνηση και προετοιμασία του dataset

required_packages <- c(
  "dplyr", "caTools", "rpart", "rpart.plot", 
  "ROCR", "caret", "e1071", "knitr"
)

new_packages <- required_packages[!(required_packages %in% installed.packages()[, "Package"])]
if (length(new_packages) > 0) {
  install.packages(new_packages, dependencies = TRUE)
}

invisible(lapply(required_packages, library, character.only = TRUE))
csv_file <- "walmart_cleaned - walmart_cleaned.csv"

if (!file.exists(csv_file)) {
  csv_file <- file.choose()
}

WalmartRaw <- read.csv(csv_file, stringsAsFactors = FALSE)

names(WalmartRaw)
##  [1] "X"            "Store"        "Date"         "IsHoliday"    "Dept"        
##  [6] "Weekly_Sales" "Temperature"  "Fuel_Price"   "MarkDown1"    "MarkDown2"   
## [11] "MarkDown3"    "MarkDown4"    "MarkDown5"    "CPI"          "Unemployment"
## [16] "Type"         "Size"
dim(WalmartRaw)
## [1] 421570     17

Το dataset περιέχει στοιχεία εβδομαδιαίων πωλήσεων Walmart. Στην αρχική του μορφή οι γραμμές αφορούν πωλήσεις ανά κατάστημα, ημερομηνία και τμήμα. Για την παρούσα εργασία τα δεδομένα ομαδοποιούνται σε επίπεδο καταστήματος ανά εβδομάδα, ώστε το μοντέλο να προβλέπει τη συνολική εβδομαδιαία επίδοση κάθε καταστήματος.

# Καθαρισμός της μεταβλητής CPI, επειδή στο csv εμφανίζεται με πολλές τελείες.
clean_cpi <- function(x) {
  x_chr <- as.character(x)
  digits <- gsub("[^0-9]", "", x_chr)
  out <- suppressWarnings(as.numeric(digits) / (10^(nchar(digits) - 3)))
  out[nchar(digits) <= 3] <- suppressWarnings(as.numeric(digits[nchar(digits) <= 3]))
  out[digits == "" | is.na(digits)] <- NA
  return(out)
}

WalmartRaw$CPI <- clean_cpi(WalmartRaw$CPI)
WalmartRaw$Date <- as.Date(WalmartRaw$Date, format = "%d/%m/%Y")

Walmart <- WalmartRaw %>%
  group_by(Store, Date) %>%
  summarise(
    Weekly_Sales = sum(Weekly_Sales, na.rm = TRUE),
    IsHoliday = first(IsHoliday),
    Temperature = mean(Temperature, na.rm = TRUE),
    Fuel_Price = mean(Fuel_Price, na.rm = TRUE),
    CPI = mean(CPI, na.rm = TRUE),
    Unemployment = mean(Unemployment, na.rm = TRUE),
    Type = first(Type),
    Size = first(Size),
    .groups = "drop"
  ) %>%
  mutate(
    Type = factor(paste0("Type_", Type)),
    IsHoliday = factor(IsHoliday, levels = c(0, 1), labels = c("No", "Yes")),
    Success = factor(
      ifelse(Weekly_Sales > median(Weekly_Sales, na.rm = TRUE), "High", "Low"),
      levels = c("Low", "High")
    ),
    Success01 = ifelse(Success == "High", 1, 0)
  )
dataset_overview <- data.frame(
  Metric = c(
    "Αρχικές γραμμές dataset",
    "Γραμμές μετά την ομαδοποίηση",
    "Αριθμός καταστημάτων",
    "Αριθμός εβδομάδων",
    "Διάμεσος εβδομαδιαίων πωλήσεων"
  ),
  Value = c(
    nrow(WalmartRaw),
    nrow(Walmart),
    length(unique(Walmart$Store)),
    length(unique(Walmart$Date)),
    round(median(Walmart$Weekly_Sales, na.rm = TRUE), 2)
  )
)

knitr::kable(dataset_overview, caption = "Βασική περιγραφή του dataset")
Βασική περιγραφή του dataset
Metric Value
Αρχικές γραμμές dataset 421570
Γραμμές μετά την ομαδοποίηση 6435
Αριθμός καταστημάτων 45
Αριθμός εβδομάδων 143
Διάμεσος εβδομαδιαίων πωλήσεων 960746

Η παραπάνω σύνοψη δείχνει το μέγεθος του dataset μετά την προετοιμασία. Η διάμεσος των εβδομαδιαίων πωλήσεων χρησιμοποιείται ως όριο για τη δημιουργία της κατηγορικής μεταβλητής Success.

success_table <- as.data.frame(table(Walmart$Success))
colnames(success_table) <- c("Success", "Observations")
success_table$Percentage <- round(100 * success_table$Observations / sum(success_table$Observations), 2)

knitr::kable(success_table, caption = "Κατανομή της μεταβλητής Success")
Κατανομή της μεταβλητής Success
Success Observations Percentage
Low 3218 50.01
High 3217 49.99

Η μεταβλητή Success χωρίζει τις παρατηρήσεις σε δύο κατηγορίες: Low και High. Με αυτόν τον τρόπο το πρόβλημα μετατρέπεται σε πρόβλημα ταξινόμησης, ώστε να μπορούν να εφαρμοστούν τόσο η λογιστική παλινδρόμηση όσο και το CART.

3 Διαχωρισμός σε training και testing set

set.seed(946)

split <- caTools::sample.split(Walmart$Success, SplitRatio = 0.65)
Train <- subset(Walmart, split == TRUE)
Test <- subset(Walmart, split == FALSE)

split_table <- data.frame(
  Dataset = c("Training set", "Testing set"),
  Observations = c(nrow(Train), nrow(Test)),
  Percentage = c("65%", "35%")
)

knitr::kable(split_table, caption = "Διαχωρισμός dataset")
Διαχωρισμός dataset
Dataset Observations Percentage
Training set 4183 65%
Testing set 2252 35%

Το dataset χωρίστηκε σε 65% training set και 35% testing set. Το training set χρησιμοποιείται για την εκπαίδευση των μοντέλων, ενώ το testing set χρησιμοποιείται μόνο για την αξιολόγηση των προβλέψεων. Αυτό είναι απαραίτητο ώστε να ελεγχθεί αν τα μοντέλα λειτουργούν καλά σε δεδομένα που δεν έχουν ξαναδεί.

4 Δημιουργία μοντέλου λογιστικής παλινδρόμησης

LogModel <- glm(
  Success01 ~ Type + Size + IsHoliday + Temperature + Fuel_Price + CPI + Unemployment,
  data = Train,
  family = binomial
)

log_coef <- as.data.frame(summary(LogModel)$coefficients)
log_coef <- round(log_coef, 4)

knitr::kable(log_coef, caption = "Συντελεστές μοντέλου λογιστικής παλινδρόμησης")
Συντελεστές μοντέλου λογιστικής παλινδρόμησης
Estimate Std. Error z value Pr(>|z|)
(Intercept) -17.0637 261.3110 -0.0653 0.9479
TypeType_2 12.1750 261.3105 0.0466 0.9628
TypeType_3 10.6582 261.3107 0.0408 0.9675
Size 0.0001 0.0000 20.8619 0.0000
IsHolidayYes 0.5936 0.2184 2.7182 0.0066
Temperature 0.0081 0.0031 2.5985 0.0094
Fuel_Price -0.1275 0.1186 -1.0751 0.2823
CPI -0.0265 0.0018 -15.0870 0.0000
Unemployment -0.0702 0.0295 -2.3805 0.0173

Η λογιστική παλινδρόμηση χρησιμοποιείται ως μοντέλο σύγκρισης με το CART. Οι ανεξάρτητες μεταβλητές είναι οι ίδιες με αυτές που θα χρησιμοποιηθούν στο δέντρο, ώστε η σύγκριση να είναι δίκαιη. Οι μεταβλητές με μικρότερη τιμή Pr(>|z|) θεωρούνται πιο σημαντικές στατιστικά για την πρόβλεψη της πιθανότητας High.

Η μεταβλητή Store δεν χρησιμοποιείται, επειδή λειτουργεί κυρίως ως αναγνωριστικό καταστήματος. Αν την χρησιμοποιούσαμε, το μοντέλο θα μάθαινε κυρίως ποια συγκεκριμένα καταστήματα έχουν υψηλές ή χαμηλές πωλήσεις, αντί να αξιολογήσει γενικότερους επιχειρηματικούς παράγοντες.

LogProb <- predict(LogModel, newdata = Test, type = "response")
LogPred <- factor(ifelse(LogProb >= 0.5, "High", "Low"), levels = c("Low", "High"))

LogConfusion <- table(Actual = Test$Success, Predicted = LogPred)
LogAccuracy <- sum(diag(LogConfusion)) / sum(LogConfusion)

LogConfusion
##       Predicted
## Actual Low High
##   Low  987  139
##   High 154  972
LogAccuracy
## [1] 0.8698934

Με κατώφλι πρόβλεψης το 0.5, η λογιστική παλινδρόμηση ταξινομεί κάθε παρατήρηση ως High ή Low. Η ακρίβεια του μοντέλου στο testing set είναι 0.87. Αυτή η τιμή χρησιμοποιείται ως σημείο σύγκρισης με το CART.

5 Δημιουργία μοντέλου CART

CartTree <- rpart(
  Success ~ Type + Size + IsHoliday + Temperature + Fuel_Price + CPI + Unemployment,
  data = Train,
  method = "class",
  minbucket = 25
)

prp(CartTree)

Το CART δημιουργείται με method = "class", επειδή η εξαρτημένη μεταβλητή είναι κατηγορική, δηλαδή Low ή High. Η παράμετρος minbucket = 25 ορίζει τον ελάχιστο αριθμό παρατηρήσεων που πρέπει να υπάρχουν σε κάθε τελικό υποσύνολο του δέντρου. Έτσι αποφεύγεται ένα υπερβολικά πολύπλοκο δέντρο που μπορεί να κάνει overfitting.

Το δέντρο δείχνει τη διαδικασία απόφασης με απλούς κανόνες. Σε αντίθεση με τη λογιστική παλινδρόμηση, το CART είναι πιο εύκολο να παρουσιαστεί, γιατί φαίνεται καθαρά ποιοι διαχωρισμοί οδηγούν σε πρόβλεψη Low ή High.

6 Εφαρμογή πρόβλεψης και αξιολόγηση CART

PredictCART <- predict(CartTree, newdata = Test, type = "class")

CartConfusion <- table(Actual = Test$Success, Predicted = PredictCART)
CartAccuracy <- sum(diag(CartConfusion)) / sum(CartConfusion)

CartConfusion
##       Predicted
## Actual  Low High
##   Low   992  134
##   High   39 1087
CartAccuracy
## [1] 0.9231794

Το CART αξιολογείται στο testing set, δηλαδή σε δεδομένα που δεν χρησιμοποιήθηκαν για την εκπαίδευση του μοντέλου. Η ακρίβεια του αρχικού CART είναι 0.923. Ο πίνακας σύγχυσης δείχνει πόσες παρατηρήσεις ταξινομήθηκαν σωστά και πόσες λανθασμένα στις κατηγορίες Low και High.

7 ROC curve και AUC

# ROC / AUC λογιστικής παλινδρόμησης
pred_log <- prediction(LogProb, Test$Success01)
perf_log <- performance(pred_log, "tpr", "fpr")
AUC_Log <- as.numeric(performance(pred_log, "auc")@y.values)

# ROC / AUC CART
CartProb <- predict(CartTree, newdata = Test, type = "prob")[, "High"]
pred_cart <- prediction(CartProb, Test$Success01)
perf_cart <- performance(pred_cart, "tpr", "fpr")
AUC_CART <- as.numeric(performance(pred_cart, "auc")@y.values)

plot(perf_log, main = "ROC Curve: Logistic Regression vs CART", lwd = 2)
plot(perf_cart, add = TRUE, lwd = 2, lty = 2)
abline(a = 0, b = 1, lty = 3)
legend(
  "bottomright",
  legend = c(
    paste0("Logistic Regression AUC = ", round(AUC_Log, 3)),
    paste0("CART AUC = ", round(AUC_CART, 3))
  ),
  lwd = 2,
  lty = c(1, 2)
)

Η ROC curve δείχνει πόσο καλά ξεχωρίζει κάθε μοντέλο τις παρατηρήσεις High από τις παρατηρήσεις Low. Το AUC της λογιστικής παλινδρόμησης είναι 0.957, ενώ το AUC του CART είναι 0.974. Όσο πιο κοντά είναι το AUC στο 1, τόσο καλύτερη είναι η διακριτική ικανότητα του μοντέλου.

8 Cross-validation για επιλογή της παραμέτρου cp

set.seed(946)

numFolds <- trainControl(method = "cv", number = 7)
cpGrid <- expand.grid(.cp = seq(0.001, 0.05, by = 0.001))

CartCV <- train(
  Success ~ Type + Size + IsHoliday + Temperature + Fuel_Price + CPI + Unemployment,
  data = Train,
  method = "rpart",
  trControl = numFolds,
  tuneGrid = cpGrid,
  metric = "Accuracy"
)
knitr::kable(
  data.frame(`Best cp` = CartCV$bestTune$cp),
  caption = "Βέλτιστη τιμή cp από το cross-validation"
)
Βέλτιστη τιμή cp από το cross-validation
Best.cp
0.001

Στο σημείο αυτό εφαρμόστηκε cross-validation για το μοντέλο CART. Ο σκοπός του cross-validation είναι να επιλεγεί η καταλληλότερη τιμή της παραμέτρου πολυπλοκότητας cp, χωρίς να χρησιμοποιηθεί το testing set για τη δημιουργία του μοντέλου. Η παράμετρος cp επηρεάζει το μέγεθος και την πολυπλοκότητα του δέντρου: μικρότερη τιμή οδηγεί σε πιο αναλυτικό δέντρο, ενώ μεγαλύτερη τιμή οδηγεί σε πιο απλό δέντρο. Με τη μέθοδο αυτή, το μοντέλο δοκιμάζει διαφορετικές τιμές cp μέσα από το training set και επιλέγει εκείνη που δίνει την καλύτερη ακρίβεια.

best_cp <- if ("cp" %in% names(CartCV$bestTune)) {
  CartCV$bestTune$cp
} else {
  CartCV$bestTune$.cp
}

CartTreeCV <- rpart(
  Success ~ Type + Size + IsHoliday + Temperature + Fuel_Price + CPI + Unemployment,
  data = Train,
  method = "class",
  cp = best_cp
)

prp(CartTreeCV)

Το νέο CART μοντέλο δημιουργείται με την τιμή cp που επέλεξε το cross-validation. Με αυτόν τον τρόπο το τελικό δέντρο δεν βασίζεται σε αυθαίρετη επιλογή παραμέτρου, αλλά σε συστηματικό έλεγχο μέσα στο training set.

PredictCV <- predict(CartTreeCV, newdata = Test, type = "class")

CVConfusion <- table(Actual = Test$Success, Predicted = PredictCV)
CVAccuracy <- sum(diag(CVConfusion)) / sum(CVConfusion)

CVProb <- predict(CartTreeCV, newdata = Test, type = "prob")[, "High"]
pred_cv <- prediction(CVProb, Test$Success01)
AUC_CV <- as.numeric(performance(pred_cv, "auc")@y.values)

CVConfusion
##       Predicted
## Actual  Low High
##   Low  1059   67
##   High   57 1069
CVAccuracy
## [1] 0.9449378
AUC_CV
## [1] 0.9841578

Μετά το cross-validation, η ακρίβεια του CART στο testing set είναι 0.945 και το AUC είναι 0.984. Αυτό δείχνει αν η επιλογή του cp βελτίωσε ή όχι την απόδοση σε σχέση με το αρχικό CART.

9 Σύγκριση μοντέλων

BaselineClass <- names(which.max(table(Train$Success)))
BaselinePred <- factor(rep(BaselineClass, nrow(Test)), levels = c("Low", "High"))
BaselineAccuracy <- sum(BaselinePred == Test$Success) / length(BaselinePred)

comparison <- data.frame(
  Model = c("Baseline", "Logistic Regression", "CART", "CART with Cross-validation"),
  Accuracy = c(BaselineAccuracy, LogAccuracy, CartAccuracy, CVAccuracy),
  AUC = c(NA, AUC_Log, AUC_CART, AUC_CV)
)

comparison$Accuracy <- round(comparison$Accuracy, 3)
comparison$AUC <- round(comparison$AUC, 3)

knitr::kable(comparison, caption = "Σύγκριση μοντέλων στο testing set")
Σύγκριση μοντέλων στο testing set
Model Accuracy AUC
Baseline 0.500 NA
Logistic Regression 0.870 0.957
CART 0.923 0.974
CART with Cross-validation 0.945 0.984

Ο πίνακας συγκρίνει τα μοντέλα με βάση την ακρίβεια και το AUC. Το baseline μοντέλο προβλέπει πάντα την πιο συχνή κατηγορία και χρησιμοποιείται ως απλό σημείο αναφοράς. Η λογιστική παλινδρόμηση δίνει ένα στατιστικό μοντέλο πρόβλεψης, ενώ το CART δίνει επιπλέον και ένα ερμηνεύσιμο δέντρο αποφάσεων.

10 Συμπέρασμα

Στην παρούσα εργασία δημιουργήθηκε ένα case study πρόβλεψης υψηλών και χαμηλών εβδομαδιαίων πωλήσεων Walmart με χρήση λογιστικής παλινδρόμησης και CART. Αρχικά, το dataset καθαρίστηκε και ομαδοποιήθηκε σε επίπεδο καταστήματος ανά εβδομάδα, ώστε η ανάλυση να έχει πιο ξεκάθαρη επιχειρηματική ερμηνεία. Στη συνέχεια δημιουργήθηκε η μεταβλητή Success, η οποία χωρίζει τις εβδομάδες σε High και Low με βάση τη διάμεσο των πωλήσεων.

Η λογιστική παλινδρόμηση χρησιμοποιήθηκε ως μοντέλο σύγκρισης. Παρότι μπορεί να δώσει προβλέψεις και να δείξει στατιστικά σημαντικές μεταβλητές, η ερμηνεία της βασίζεται σε συντελεστές. Αντίθετα, το CART παρουσιάζει τη λογική της πρόβλεψης με τη μορφή δέντρου, κάτι που κάνει το μοντέλο πιο εύκολο να εξηγηθεί σε επιχειρηματικό περιβάλλον.

Στο τελικό μοντέλο δεν χρησιμοποιήθηκε η μεταβλητή Store, επειδή λειτουργεί κυρίως ως αναγνωριστικό καταστήματος. Αν χρησιμοποιούνταν, το δέντρο θα μάθαινε απλώς ποια συγκεκριμένα καταστήματα έχουν συνήθως υψηλές ή χαμηλές πωλήσεις, χωρίς να δίνει γενικότερα συμπεράσματα για τους παράγοντες που επηρεάζουν τις πωλήσεις. Για αυτόν τον λόγο χρησιμοποιήθηκαν μεταβλητές με επιχειρηματική σημασία, όπως ο τύπος καταστήματος, το μέγεθος, οι αργίες, η θερμοκρασία, η τιμή καυσίμων, ο CPI και η ανεργία.

Με βάση τα αποτελέσματα στο testing set, το καλύτερο μοντέλο ως προς την ακρίβεια είναι το CART with Cross-validation, με accuracy 0.945. Συνολικά, το CART είναι χρήσιμο όχι μόνο επειδή κάνει προβλέψεις, αλλά κυρίως επειδή παράγει κανόνες απόφασης που μπορούν να παρουσιαστούν και να εξηγηθούν εύκολα. Το cross-validation βελτιώνει τη μεθοδολογία, γιατί η επιλογή της παραμέτρου cp γίνεται μέσα στο training set και όχι με βάση το testing set.