Σκοπός της εργασίας είναι να δημιουργηθεί ένα case study με Δέντρο Απόφασης CART για την πρόβλεψη του αν οι εβδομαδιαίες πωλήσεις ενός καταστήματος Walmart είναι υψηλές ή χαμηλές.
Η εξαρτημένη μεταβλητή της εργασίας είναι η Success. Παίρνει την τιμή High όταν οι συνολικές εβδομαδιαίες πωλήσεις είναι πάνω από τη διάμεσο και την τιμή Low όταν είναι ίσες ή κάτω από τη διάμεσο.
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")
| 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 | Observations | Percentage |
|---|---|---|
| Low | 3218 | 50.01 |
| High | 3217 | 49.99 |
Η μεταβλητή Success χωρίζει τις παρατηρήσεις σε δύο
κατηγορίες: Low και High. Με αυτόν τον τρόπο
το πρόβλημα μετατρέπεται σε πρόβλημα ταξινόμησης, ώστε
να μπορούν να εφαρμοστούν τόσο η λογιστική παλινδρόμηση όσο και το
CART.
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 | Observations | Percentage |
|---|---|---|
| Training set | 4183 | 65% |
| Testing set | 2252 | 35% |
Το dataset χωρίστηκε σε 65% training set και 35% testing set. Το training set χρησιμοποιείται για την εκπαίδευση των μοντέλων, ενώ το testing set χρησιμοποιείται μόνο για την αξιολόγηση των προβλέψεων. Αυτό είναι απαραίτητο ώστε να ελεγχθεί αν τα μοντέλα λειτουργούν καλά σε δεδομένα που δεν έχουν ξαναδεί.
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.
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.
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.
# 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,
τόσο καλύτερη είναι η διακριτική ικανότητα του μοντέλου.
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"
)
| 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.
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")
| 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 δίνει επιπλέον και ένα ερμηνεύσιμο δέντρο αποφάσεων.
Στην παρούσα εργασία δημιουργήθηκε ένα 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.