library(readxl)
library(dplyr)
library(tidyr)
library(ggplot2)
library(MASS) # lda, qda, isoMDS
library(car) # leveneTest, vif
library(lmtest) # bptest, dwtest, coeftest
library(caret) # confusionMatrix
library(pROC) # ROC
library(sandwich) # HC3 robustné štandardné chyby
library(knitr)
library(kableExtra)Keďže prvé zadanie nebolo vypracované v plnej miere a druhé zadanie je riešené v samostatnom prostredí, predspracovanie dát vykonávame na začiatku znovu, aby bol report úplný a samostatne reprodukovateľný.
Potravinárska spoločnosť plánuje ďalšiu marketingovú kampaň. K dispozícii máme dáta o 1 956 zákazníkoch, ktorí absolvovali pilotnú kampaň. Cieľom druhého zadania je hlbšie preskúmať vzťahy v dátach pomocou regresnej analýzy, MANOVA, diskriminačnej analýzy, viacrozmerného škálovania a logistickej regresie. Záverečné odporúčania majú odpovedať na otázku, na koho a ako sa zacieliť, aby kampaň zasiahla nielen už aktívnych zákazníkov, ale aj segmenty s nevyužitým potenciálom.
## [1] 1956 25
V dátach je 161 riadkov (8,2 %) s aspoň jednou chýbajúcou hodnotou, hoci v žiadnej premennej nepresahuje miera chýbania 1,4 %. Listwise odstránenie by teda znamenalo stratu vyše 160 pozorovaní — to už nie je zanedbateľné a hrozí, že odstránime systematicky určitý typ zákazníkov (najmä pri premennej Income, kde sa NA často viažu k vyšším príjmovým skupinám, ktoré niekedy odmietajú odpovedať).
Vzhľadom na to, že:
zvolili sme mediánovú imputáciu pre číselné premenné a modálnu imputáciu pre kategoriálne premenné. Nepoužívame priemer, lebo by deformoval pravé chvosty distribúcií. Ide o jednoduchý, transparentný postup s minimálnou pridanou neurčitosťou pri nízkej miere chýbania.
## Age Education Marital_Status Income
## 27 9 12 26
## Kidhome Teenhome Recency MntWines
## 5 1 15 20
## MntFruits MntMeatProducts MntFishProducts MntSweetProducts
## 13 18 15 18
## MntGoldProds NumDealsPurchases NumWebPurchases NumCatalogPurchases
## 21 15 14 17
## NumStorePurchases NumWebVisitsMonth
## 23 3
## [1] 161
Pri premennej Income je jedna hodnota 666 666 €, čo je viac než štvornásobok ďalšej najvyššej (162 397 €). Z business pohľadu pôsobí ako chyba zápisu (poškodené pero hodnoty „666 666”), preto ju nahrádzame NA a následne imputujeme medianom.
Pri premennej Marital_Status sú 3 záznamy s hodnotou „Alone”, ktorú zlučujeme s kategóriou „Single” (sémanticky totožné, počet záznamov by jeden samostatne stojaci level neopodstatnil).
Food <- Food %>%
mutate(
Income = ifelse(Income > 600000, NA, Income),
Marital_Status = ifelse(Marital_Status == "Alone", "Single", Marital_Status)
)
# Imputácia
num_cols <- names(Food)[sapply(Food, is.numeric)]
for (c in num_cols) {
Food[[c]][is.na(Food[[c]])] <- median(Food[[c]], na.rm = TRUE)
}
Food$Education[is.na(Food$Education)] <-
names(sort(table(Food$Education), decreasing = TRUE))[1]
Food$Marital_Status[is.na(Food$Marital_Status)] <-
names(sort(table(Food$Marital_Status), decreasing = TRUE))[1]
# Faktorizácia
Food$Education <- factor(Food$Education,
levels = c("Basic","Graduation","Master","PhD"))
Food$Marital_Status <- factor(Food$Marital_Status)
sum(is.na(Food)) # kontrola## [1] 0
Pre potreby ďalších analýz zostavujeme:
Food <- Food %>%
mutate(
Deti = ifelse(Kidhome + Teenhome > 0, 1, 0),
Activity = ifelse(AcceptedCmp1 + AcceptedCmp2 + AcceptedCmp3 +
AcceptedCmp4 + AcceptedCmp5 + Response > 0, 1, 0),
TotalSpend = MntWines + MntFruits + MntMeatProducts +
MntFishProducts + MntSweetProducts + MntGoldProds,
Children = Kidhome + Teenhome
)
Food$Deti <- factor(Food$Deti, levels = c(0,1), labels = c("Bez detí","S deťmi"))
Food$Activity_f <- factor(Food$Activity, levels = c(0,1), labels = c("Nereagoval","Reagoval"))
cat("Deti:\n"); print(table(Food$Deti))## Deti:
##
## Bez detí S deťmi
## 511 1445
##
## Activity:
##
## Nereagoval Reagoval
## 1424 532
##
## Podiel aktívnych: 27.2 %
Skupiny majú obe dostatočnú veľkosť pre MANOVA aj diskriminačnú analýzu (511 zákazníkov bez detí, 1 445 so deťmi). Pri Activity máme 27,2 % „pozitívnych” prípadov — rozdelenie nie je extrémne nevyvážené, takže pre logistickú regresiu nepotrebujeme špeciálne techniky (over-sampling, váženie tried).
Predpokladáme, že celkové výdavky zákazníka na potravinárske kategórie nie sú náhodné, ale dajú sa významne vysvetliť kombináciou ekonomických, demografických a behaviorálnych charakteristík. Konkrétne očakávame:
Závislú premennú TotalSpend logaritmicky transformujeme — distribúcia je výrazne pravostranne zošikmená (šikmosť ≈ 0,9; po log1p ≈ −0,4), čo vyhovuje predpokladu normality reziduí lineárneho modelu a zároveň interpretuje koeficienty ako približne percentuálne zmeny.
par(mfrow = c(1,2))
hist(Food$TotalSpend, breaks = 40, col = "steelblue",
main = "Pôvodná distribúcia TotalSpend",
xlab = "TotalSpend (€)")
hist(log1p(Food$TotalSpend), breaks = 40, col = "steelblue",
main = "Po log1p transformácii",
xlab = "log(1 + TotalSpend)")Food$logSpend <- log1p(Food$TotalSpend)
model_reg <- lm(logSpend ~ Income + Age + Children + Recency +
NumWebVisitsMonth + NumDealsPurchases, data = Food)
summary(model_reg)##
## Call:
## lm(formula = logSpend ~ Income + Age + Children + Recency + NumWebVisitsMonth +
## NumDealsPurchases, data = Food)
##
## Residuals:
## Min 1Q Median 3Q Max
## -8.673 -0.368 0.056 0.453 3.964
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2.6566711 0.1278439 20.78 < 0.0000000000000002 ***
## Income 0.0000488 0.0000011 44.20 < 0.0000000000000002 ***
## Age 0.0066084 0.0016481 4.01 0.000063 ***
## Children -0.6936621 0.0281065 -24.68 < 0.0000000000000002 ***
## Recency 0.0007382 0.0006002 1.23 0.219
## NumWebVisitsMonth 0.0256180 0.0100117 2.56 0.011 *
## NumDealsPurchases 0.2417218 0.0102904 23.49 < 0.0000000000000002 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.763 on 1949 degrees of freedom
## Multiple R-squared: 0.729, Adjusted R-squared: 0.729
## F-statistic: 876 on 6 and 1949 DF, p-value: <0.0000000000000002
Celková významnosť modelu je výrazná (F ≈ 875, p < 0,001), R² = 0,73. To znamená, že 73 % variability logaritmu výdavkov vysvetlíme len šiestimi prediktormi — pre marketingovú aplikáciu veľmi silný výsledok.
Konkrétne koeficienty (každý výrazne významný okrem Recency):
##
## Shapiro-Wilk normality test
##
## data: model_reg$residuals[1:1500]
## W = 0.85, p-value <0.0000000000000002
##
## studentized Breusch-Pagan test
##
## data: model_reg
## BP = 112, df = 6, p-value <0.0000000000000002
##
## Durbin-Watson test
##
## data: model_reg
## DW = 2, p-value = 0.6
## alternative hypothesis: true autocorrelation is greater than 0
## Income Age Children Recency
## 1.854 1.094 1.466 1.002
## NumWebVisitsMonth NumDealsPurchases
## 1.995 1.331
Slovné zhrnutie testov predpokladov:
##
## t test of coefficients:
##
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2.65667108 0.20709004 12.83 <0.0000000000000002 ***
## Income 0.00004878 0.00000296 16.49 <0.0000000000000002 ***
## Age 0.00660841 0.00201527 3.28 0.0011 **
## Children -0.69366209 0.04723620 -14.68 <0.0000000000000002 ***
## Recency 0.00073823 0.00062731 1.18 0.2394
## NumWebVisitsMonth 0.02561795 0.01727061 1.48 0.1381
## NumDealsPurchases 0.24172177 0.02830170 8.54 <0.0000000000000002 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Aj s robustnými štandardnými chybami zostávajú všetky pôvodne významné koeficienty štatisticky významné. Záver z modelu je robustný voči porušeniu homoskedasticity.
Príjem a počet detí spolu vysvetľujú väčšinu rozdielov v útratách — vekové a behaviorálne premenné dolaďujú zostatok. Z marketingovej perspektívy to znamená, že segmentácia podľa kombinácie príjem × deti by mala byť základom akejkoľvek personalizácie ponuky: ide o dve premenné s najsilnejším a opačne smerujúcim efektom. V ďalších analýzach túto hypotézu ďalej rozpracujeme.
Líšia sa zákazníci s deťmi od zákazníkov bez detí v rozložení útrat naprieč produktovými kategóriami? Univariátne testy by ukázali rozdiely v každej kategórii zvlášť, ale MANOVA odpovedá komplexnejšie: či sa celkový profil spotreby (víno + ovocie + mäso + ryby + sladkosti) systémovo líši.
Faktor: Deti (Bez detí / S deťmi). Závislé premenné: MntWines, MntFruits, MntMeatProducts, MntFishProducts, MntSweetProducts (podľa zadania).
Všetkých 5 premenných je silne pravostranne zošikmených (šikmosť 1,2 – 2,2). Aplikujeme log1p transformáciu, ktorá výrazne približuje distribúcie k symetrii (šikmosť po transformácii v intervale −0,55 až 0,15). Bez transformácie by predpoklady viacrozmernej normality boli porušené tak silno, že by výsledky MANOVA boli neinterpretovateľné.
manova_data <- Food %>%
transmute(
Deti,
L_Wines = log1p(MntWines),
L_Fruits = log1p(MntFruits),
L_Meat = log1p(MntMeatProducts),
L_Fish = log1p(MntFishProducts),
L_Sweet = log1p(MntSweetProducts)
)
# Šikmosť pred a po
skew <- function(x) mean((x - mean(x))^3)/sd(x)^3
sapply(Food[, c("MntWines","MntFruits","MntMeatProducts",
"MntFishProducts","MntSweetProducts")], skew)## MntWines MntFruits MntMeatProducts MntFishProducts
## 1.187 2.157 2.120 2.028
## MntSweetProducts
## 2.227
## L_Wines L_Fruits L_Meat L_Fish L_Sweet
## -0.55365 0.14489 -0.06694 -0.01202 0.11769
Box’s M-test pre homogenitu kovariančných matíc (implementácia podľa formulácie Coopera 1971):
box_m_test <- function(data, group) {
data <- as.matrix(data)
group <- factor(group)
k <- nlevels(group)
p <- ncol(data)
ns <- table(group)
covs <- lapply(levels(group), function(g) cov(data[group == g, , drop = FALSE]))
pooled <- Reduce("+", Map(function(s, n) (n - 1) * s, covs, ns)) / (sum(ns) - k)
M <- sum((ns - 1) * sapply(covs, function(s) log(det(s)))) -
(sum(ns) - k) * log(det(pooled))
M <- -M # convention
c1 <- (2*p^2 + 3*p - 1) / (6 * (p + 1) * (k - 1)) *
(sum(1/(ns - 1)) - 1/(sum(ns) - k))
df <- p * (p + 1) * (k - 1) / 2
X2 <- M * (1 - c1)
pval <- 1 - pchisq(X2, df)
list(M = M, ChiSq = X2, df = df, p.value = pval)
}
bm <- box_m_test(manova_data[, -1], manova_data$Deti)
cat(sprintf("Box's M = %.2f, χ²(%d) = %.2f, p = %.4e\n",
bm$M, bm$df, bm$ChiSq, bm$p.value))## Box's M = 111.06, χ²(15) = 110.64, p = 1.1102e-16
Box’s M zamieta H₀ o rovnosti kovariančných matíc (p < 0,001). To naznačuje, že kovariančné štruktúry medzi skupinami nie sú rovnaké. Box’s M je však veľmi citlivý pri veľkých vzorkách (n = 1 956) — aj malá odchýlka sa stane „štatisticky významnou”. Pre samotnú MANOVA preto použijeme Pillaiho stopu, ktorá je voči porušeniu predpokladu o homogenite kovariančných matíc najrobustnejšia.
manova_fit <- manova(cbind(L_Wines, L_Fruits, L_Meat, L_Fish, L_Sweet) ~ Deti,
data = manova_data)
summary(manova_fit, test = "Pillai")## Df Pillai approx F num Df den Df Pr(>F)
## Deti 1 0.29 159 5 1950 <0.0000000000000002 ***
## Residuals 1954
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## Df Wilks approx F num Df den Df Pr(>F)
## Deti 1 0.71 159 5 1950 <0.0000000000000002 ***
## Residuals 1954
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## Df Hotelling-Lawley approx F num Df den Df Pr(>F)
## Deti 1 0.408 159 5 1950 <0.0000000000000002 ***
## Residuals 1954
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## Df Roy approx F num Df den Df Pr(>F)
## Deti 1 0.408 159 5 1950 <0.0000000000000002 ***
## Residuals 1954
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Všetky štyri testy zhodne ukazujú vysoko významný efekt premennej Deti (p < 0,001). Pillai’s trace = 0,29 znamená, že 29 % rozptylu v lineárnej kombinácii piatich kategórií spotreby je spojených s príslušnosťou ku skupine. Hypotézu o rovnakých priemerných profiloch spotreby medzi rodinami s deťmi a bez detí zamietame.
Aby sme videli, v ktorých kategóriách je rozdiel najvýraznejší, postupne otestujeme každú DV osobitne. Pri 5 testoch upravíme hladinu významnosti Bonferroniho korekciou (α/5 = 0,01).
## Response L_Wines :
## Df Sum Sq Mean Sq F value Pr(>F)
## Deti 1 472 472 163 <0.0000000000000002 ***
## Residuals 1954 5651 3
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Response L_Fruits :
## Df Sum Sq Mean Sq F value Pr(>F)
## Deti 1 947 947 472 <0.0000000000000002 ***
## Residuals 1954 3917 2
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Response L_Meat :
## Df Sum Sq Mean Sq F value Pr(>F)
## Deti 1 1056 1056 569 <0.0000000000000002 ***
## Residuals 1954 3627 2
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Response L_Fish :
## Df Sum Sq Mean Sq F value Pr(>F)
## Deti 1 1138 1138 535 <0.0000000000000002 ***
## Residuals 1954 4159 2
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Response L_Sweet :
## Df Sum Sq Mean Sq F value Pr(>F)
## Deti 1 922 922 452 <0.0000000000000002 ***
## Residuals 1954 3989 2
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
manova_data %>%
group_by(Deti) %>%
summarise(across(everything(), mean)) %>%
knitr::kable(digits = 2, caption = "Priemerné hodnoty log(1+útrata) podľa skupín")| Deti | L_Wines | L_Fruits | L_Meat | L_Fish | L_Sweet |
|---|---|---|---|---|---|
| Bez detí | 5.53 | 3.33 | 5.35 | 3.74 | 3.33 |
| S deťmi | 4.41 | 1.75 | 3.68 | 2.01 | 1.77 |
Všetkých 5 kategórií vykazuje vysoko významný rozdiel (p < 0,001 v každej, t. j. aj po Bonferroniho korekcii). V každej kategórii míňajú zákazníci bez detí výrazne viac. Najväčšie rozdiely (na log-škále) sú:
Najmenší (no stále veľký) rozdiel je pri víne — pravdepodobne preto, že víno nakupujú aj rodičia, len menej často.
manova_data %>%
pivot_longer(-Deti, names_to = "Kategoria", values_to = "log_utrata") %>%
mutate(Kategoria = factor(Kategoria,
levels = c("L_Wines","L_Fruits","L_Meat","L_Fish","L_Sweet"),
labels = c("Víno","Ovocie","Mäso","Ryby","Sladkosti"))) %>%
ggplot(aes(x = Kategoria, y = log_utrata, fill = Deti)) +
geom_boxplot(alpha = 0.85) +
scale_fill_manual(values = c("Bez detí" = "#2C7FB8", "S deťmi" = "#F03B20")) +
labs(title = "Profil útrat podľa rodinného statusu",
y = "log(1 + útrata €)", x = NULL) +
theme_minimal(base_size = 12)Business interpretácia. Rodinný status (deti vs. bez detí) systematicky určuje výdavkový profil zákazníka vo všetkých sledovaných kategóriách. Nie je to len o celkovej výške výdavkov — rozdiel sa prejavuje špecificky najmä pri kategórii ryby/mäso/sladkosti, čo sú produkty často viazané na osobnú indulgenciu. Rodiny s deťmi redukujú spotrebu týchto kategórií najsilnejšie. Pre kampaň to znamená, že obe skupiny si zaslúžia odlišnú ponuku: rodinám s deťmi má zmysel ponúkať balíčky, multi-pack úspory, akcie pre celú rodinu, zatiaľ čo bezdetní zákazníci reagujú lepšie na ponuky prémiovej kvality.
MANOVA potvrdila, že profily spotreby sa medzi skupinami líšia. Diskriminačná analýza ide o krok ďalej: dokážeme len na základe profilu útrat klasifikovať zákazníka do skupiny „má deti” vs. „nemá deti”? Otázka je relevantná, lebo údaj o deťoch nemusíme mať v iných datasetoch, ale spotrebné údaje áno. Ak je klasifikácia úspešná, môžeme nepriamo identifikovať rodinný status nových zákazníkov len z ich nákupov.
Dataset rozdelíme stratifikovane 70/30 (zachováme podiel skupín) a porovnáme lineárny (LDA) a kvadratický (QDA) model. Keďže Box’s M zamietol rovnosť kovariančných matíc, QDA má teoretickú výhodu — neukladá rovnaké kovariancie. Na druhej strane LDA je menej náchylný na preučenie.
set.seed(42)
manova_data$Deti <- factor(manova_data$Deti)
n <- nrow(manova_data)
idx <- caret::createDataPartition(manova_data$Deti, p = 0.7, list = FALSE)
train <- manova_data[idx, ]
test <- manova_data[-idx, ]
cat("Tréning:", nrow(train), " test:", nrow(test), "\n")## Tréning: 1370 test: 586
## Call:
## lda(Deti ~ ., data = train)
##
## Prior probabilities of groups:
## Bez detí S deťmi
## 0.2613 0.7387
##
## Group means:
## L_Wines L_Fruits L_Meat L_Fish L_Sweet
## Bez detí 5.487 3.300 5.329 3.751 3.312
## S deťmi 4.450 1.771 3.693 2.014 1.758
##
## Coefficients of linear discriminants:
## LD1
## L_Wines 0.33576
## L_Fruits -0.08771
## L_Meat -0.65352
## L_Fish -0.22747
## L_Sweet -0.12050
pred_lda <- predict(lda_fit, test)
cm_lda <- caret::confusionMatrix(pred_lda$class, test$Deti, positive = "S deťmi")
cm_lda## Confusion Matrix and Statistics
##
## Reference
## Prediction Bez detí S deťmi
## Bez detí 97 43
## S deťmi 56 390
##
## Accuracy : 0.831
## 95% CI : (0.798, 0.861)
## No Information Rate : 0.739
## P-Value [Acc > NIR] : 0.0000000732
##
## Kappa : 0.55
##
## Mcnemar's Test P-Value : 0.228
##
## Sensitivity : 0.901
## Specificity : 0.634
## Pos Pred Value : 0.874
## Neg Pred Value : 0.693
## Prevalence : 0.739
## Detection Rate : 0.666
## Detection Prevalence : 0.761
## Balanced Accuracy : 0.767
##
## 'Positive' Class : S deťmi
##
qda_fit <- qda(Deti ~ ., data = train)
pred_qda <- predict(qda_fit, test)
cm_qda <- caret::confusionMatrix(pred_qda$class, test$Deti, positive = "S deťmi")
cm_qda## Confusion Matrix and Statistics
##
## Reference
## Prediction Bez detí S deťmi
## Bez detí 97 44
## S deťmi 56 389
##
## Accuracy : 0.829
## 95% CI : (0.796, 0.859)
## No Information Rate : 0.739
## P-Value [Acc > NIR] : 0.000000128
##
## Kappa : 0.546
##
## Mcnemar's Test P-Value : 0.271
##
## Sensitivity : 0.898
## Specificity : 0.634
## Pos Pred Value : 0.874
## Neg Pred Value : 0.688
## Prevalence : 0.739
## Detection Rate : 0.664
## Detection Prevalence : 0.759
## Balanced Accuracy : 0.766
##
## 'Positive' Class : S deťmi
##
porovnanie <- data.frame(
Model = c("LDA", "QDA"),
Accuracy = c(cm_lda$overall["Accuracy"], cm_qda$overall["Accuracy"]),
Sensitivity = c(cm_lda$byClass["Sensitivity"], cm_qda$byClass["Sensitivity"]),
Specificity = c(cm_lda$byClass["Specificity"], cm_qda$byClass["Specificity"]),
Kappa = c(cm_lda$overall["Kappa"], cm_qda$overall["Kappa"])
)
knitr::kable(porovnanie, digits = 4)| Model | Accuracy | Sensitivity | Specificity | Kappa |
|---|---|---|---|---|
| LDA | 0.8311 | 0.9007 | 0.634 | 0.5498 |
| QDA | 0.8294 | 0.8984 | 0.634 | 0.5462 |
LDA dosahuje presnosť ≈ 83 %, QDA ≈ 81 %. Hoci predpoklad rovnosti kovariančných matíc nebol formálne splnený, LDA v praxi prekonáva QDA — zrejme preto, že:
Pre praktické nasadenie zvolíme LDA.
Koeficienty diskriminačnej funkcie ukazujú, ktoré kategórie najviac „ťahajú” zákazníka smerom k bezdetnej skupine:
Z biznis hľadiska: ak vidíme nového zákazníka s vysokými útratami na mäso/ryby/víno, s presnosťou >80 % predpokladáme bezdetnú domácnosť. Pre adresnú kampaň (napr. s ponukou prémiových produktov) to dáva spoľahlivé pravidlo.
# Distribúcia LD skóre v test sete
plot_df <- data.frame(LD1 = pred_lda$x[,1],
Deti = test$Deti)
ggplot(plot_df, aes(x = LD1, fill = Deti)) +
geom_density(alpha = 0.55) +
scale_fill_manual(values = c("Bez detí" = "#2C7FB8", "S deťmi" = "#F03B20")) +
geom_vline(xintercept = 0, linetype = "dashed", colour = "grey30") +
labs(title = "Rozdelenie LD1 skóre v testovacej množine",
x = "Lineárna diskriminačná funkcia (LD1)", y = "Hustota") +
theme_minimal(base_size = 12)Hustoty oboch skupín sú dobre oddelené, ale s prekryvom — najmä bezdetní zákazníci s nižšími útratami (napr. mladí jednotlivci alebo páry) sa môžu mýliť s rodičmi tínedžerov, ktorí nakupujú samostatne.
Predchádzajúce analýzy sa pozerali na zákazníkov jednotlivo. MDS prinesie iný pohľad: ako sú jednotlivé vekové ročníky podobné/odlišné podľa svojho súhrnného nákupného správania? Toto je dôležité pre marketing, lebo z neho vyplýva, či má zmysel zacieliť kampaň na úzku vekovú skupinu (ak sú niektoré roky odlišné od ostatných) alebo na širší pás (ak sú susedné roky podobné).
Pre každý rok veku (48 hodnôt 28 – 75) sčítavame šesť kategórií spotreby a štyri kanály nákupu. Agregát teda predstavuje „nákupný profil generácie”.
age_df <- Food %>%
group_by(Age) %>%
summarise(across(c(MntWines, MntFruits, MntMeatProducts, MntFishProducts,
MntSweetProducts, MntGoldProds,
NumDealsPurchases, NumWebPurchases,
NumCatalogPurchases, NumStorePurchases), sum)) %>%
as.data.frame()
rownames(age_df) <- as.integer(age_df$Age)
age_df_num <- age_df %>% dplyr::select(-Age)
cat("Rozmery agregovaného datasetu:", dim(age_df_num), "\n")## Rozmery agregovaného datasetu: 48 10
Premenné sú v rôznych mierkach (€ útraty vs. počty nákupov), preto ich štandardizujeme pred výpočtom vzdialeností.
mds_m <- cmdscale(d_age, k = 2, eig = TRUE)
# Vysvetlený podiel "rozptylu" (sumár vlastných čísel)
varexp <- mds_m$eig / sum(abs(mds_m$eig))
round(varexp[1:5], 3)## [1] 0.913 0.031 0.016 0.012 0.009
stress_metric <- sqrt(sum((d_age - dist(mds_m$points))^2) / sum(d_age^2))
cat("Stress (metrické MDS):", round(stress_metric, 4), "\n")## Stress (metrické MDS): 0.0729
Prvé dve dimenzie vysvetľujú spolu vyše 70 % „rozptylu” (súčet prvých dvoch vlastných čísel) — pre 10-rozmerný priestor je to dobrá redukcia.
set.seed(42)
mds_nm <- MASS::isoMDS(d_age, k = 2, trace = FALSE)
cat("Stress (nemetrické MDS, Kruskalova škála):",
round(mds_nm$stress, 4), "%\n")## Stress (nemetrické MDS, Kruskalova škála): 3.131 %
Hodnota stresu < 5 % zodpovedá podľa Kruskalovej škály „výbornému” usporiadaniu — nemetrický model rekonštruuje poradie vzdialeností veľmi spoľahlivo.
df_m <- data.frame(Age = as.integer(rownames(age_df_num)),
D1 = mds_m$points[,1], D2 = mds_m$points[,2],
Typ = "Metrické")
df_nm <- data.frame(Age = as.integer(rownames(age_df_num)),
D1 = mds_nm$points[,1], D2 = mds_nm$points[,2],
Typ = "Nemetrické")
df_all <- rbind(df_m, df_nm)
ggplot(df_all, aes(D1, D2, label = Age, colour = Age)) +
geom_point(size = 3) +
geom_text(vjust = -1, size = 3) +
scale_colour_viridis_c(option = "C") +
facet_wrap(~ Typ, scales = "free") +
labs(title = "MDS: poloha vekových ročníkov v 2D priestore",
x = "Dimenzia 1", y = "Dimenzia 2") +
theme_minimal(base_size = 12)# Korelácie pôvodných a obnovených vzdialeností
d_m <- dist(mds_m$points)
d_nm <- dist(mds_nm$points)
cat("Korelácia pôvodné vs metrické:", round(cor(d_age, d_m), 4), "\n")## Korelácia pôvodné vs metrické: 0.9971
## Korelácia pôvodné vs nemetrické: 0.9979
Obe verzie MDS dávajú veľmi podobné konfigurácie — pozícia jednotlivých vekov v 2D priestore sa medzi metrickým a nemetrickým prístupom líši len detailami. To je dobrý znak: vzdialenosti medzi vekmi sú prevažne lineárne reprodukovateľné, transformácia poradia ich nezhoršuje. Korelácia rekonštruovaných vzdialeností s originálnymi je >0,99 pri oboch metódach.
V konfigurácii vidíme tri pomerne jasné zoskupenia:
Druhá dimenzia oddeľuje zákazníkov nakupujúcich prevažne v obchode vs. online/katalógovo — najviac negatívne hodnoty patria stredne starým rokom s vysokým objemom nákupov v katalógu, čo súvisí so silnou priamou marketingovou históriou tejto skupiny.
Marketingový záver z MDS. Vekové ročníky nie sú rovnocenné. Pre kampaň má zmysel pracovať so širším pásom 45 – 60 rokov ako jadrom cieľovej skupiny (susedné ročníky sú si vzájomne podobné, čo umožňuje jednotnú komunikáciu). Mladší (do 35) sú odlišný segment — vyžadujú vlastnú stratégiu, pretože sa správajú inak a sú vo vzorke poddimenzionovaní (čo môže naznačovať priestor na získanie nových zákazníkov mimo súčasnej základne).
Aká je pravdepodobnosť, že zákazník akceptuje aspoň jednu marketingovú ponuku? Závislou premennou je Activity (1 = aspoň jedna z piatich kampaní alebo posledná Response akceptovaná, 0 = žiadna).
Cieľom je nielen predikcia, ale aj identifikácia faktorov spojených s vyššou pravdepodobnosťou reakcie — to dáva marketingu argumentačnú výbavu pre cielenie a personalizáciu.
Prediktory volíme tak, aby pokryli tri typy charakteristík zákazníka: ekonomickú, behaviorálnu a demografickú. Súčasne sa snažíme vyhnúť redundancii (napr. nezahŕňame všetkých 6 premenných útrat, lebo sú silne korelované a Income s nimi tiež).
| Prediktor | Typ | Argument výberu |
|---|---|---|
| log(Income) | ekonomický | Najsilnejší prediktor v regresnej analýze, logaritmus znižuje vplyv chvostov |
| Recency | behaviorálny | Aktuálnosť vzťahu so zákazníkom — nie sumárny objem, ale „čerstvosť” |
| log(MntWines) | preferenčný | Víno je najtypickejší prémiový produkt; vínne útraty sú indikátor lifestylu |
| NumCatalogPurchases | kanálový | História priameho marketingu — kto reagoval na katalóg, môže reagovať aj na novú ponuku |
| NumWebVisitsMonth | digitálny engagement | Aktuálne online správanie |
| Children | demografický | MANOVA aj regresia ukázali, že je to silný diferenciátor |
| Education | demografický | Zaradené ako kontrola; teoreticky môže ovplyvniť ochotu reagovať |
Z analýzy vedome vylučujeme Age (slabý prediktor v regresnej analýze, naviac kolineárny s niektorými behaviorálnymi premennými cez životný cyklus), všetky 5 AcceptedCmp premenné (zakázané — sú súčasťou Activity) a redundantné kategórie *Mnt** (vínne útraty ich dostatočne reprezentujú).
set.seed(123)
idx <- caret::createDataPartition(log_data$Activity, p = 0.7, list = FALSE)
train_l <- log_data[idx, ]
test_l <- log_data[-idx, ]
# Plný model
logit_full <- glm(Activity ~ logIncome + Recency + logWines +
NumCatalogPurchases + NumWebVisitsMonth + Children + Education,
data = train_l, family = binomial)
summary(logit_full)##
## Call:
## glm(formula = Activity ~ logIncome + Recency + logWines + NumCatalogPurchases +
## NumWebVisitsMonth + Children + Education, family = binomial,
## data = train_l)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -9.11192 3.10422 -2.94 0.00333 **
## logIncome 0.59937 0.30143 1.99 0.04676 *
## Recency -0.01000 0.00231 -4.33 0.0000152184 ***
## logWines 0.27221 0.07619 3.57 0.00035 ***
## NumCatalogPurchases 0.13309 0.03011 4.42 0.0000098504 ***
## NumWebVisitsMonth 0.23838 0.04107 5.80 0.0000000065 ***
## Children -0.54436 0.11518 -4.73 0.0000022888 ***
## EducationGraduation -0.47582 0.50158 -0.95 0.34281
## EducationMaster -0.40064 0.51972 -0.77 0.44077
## EducationPhD -0.31836 0.51715 -0.62 0.53815
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 1610.1 on 1369 degrees of freedom
## Residual deviance: 1387.2 on 1360 degrees of freedom
## AIC: 1407
##
## Number of Fisher Scoring iterations: 4
Vidíme, že Education nie je významná. Otestujeme, či ju môžeme z modelu vypustiť.
logit_red <- glm(Activity ~ logIncome + Recency + logWines +
NumCatalogPurchases + NumWebVisitsMonth + Children,
data = train_l, family = binomial)
anova(logit_red, logit_full, test = "Chisq")## [1] 1407
## [1] 1403
Likelihood ratio test medzi modelmi nevypovedá o významnom zlepšení po pridaní Education (p > 0,05), AIC redukovaného modelu je nižšie. Ďalej teda pracujeme s redukovaným modelom — princíp parsimony.
OR <- exp(coef(logit_red))
CI <- exp(confint(logit_red))
or_table <- data.frame(
Predictor = names(coef(logit_red)),
Coef = round(coef(logit_red), 4),
OR = round(OR, 3),
CI_low = round(CI[,1], 3),
CI_high = round(CI[,2], 3),
p_value = round(summary(logit_red)$coefficients[,4], 4)
)
or_tableInterpretácia odds ratios:
prob_te <- predict(logit_red, test_l, type = "response")
pred_te <- ifelse(prob_te > 0.5, 1, 0)
cm_log <- caret::confusionMatrix(factor(pred_te), factor(test_l$Activity),
positive = "1")
cm_log## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 404 117
## 1 26 39
##
## Accuracy : 0.756
## 95% CI : (0.719, 0.79)
## No Information Rate : 0.734
## P-Value [Acc > NIR] : 0.121
##
## Kappa : 0.233
##
## Mcnemar's Test P-Value : 0.0000000000000522
##
## Sensitivity : 0.2500
## Specificity : 0.9395
## Pos Pred Value : 0.6000
## Neg Pred Value : 0.7754
## Prevalence : 0.2662
## Detection Rate : 0.0666
## Detection Prevalence : 0.1109
## Balanced Accuracy : 0.5948
##
## 'Positive' Class : 1
##
# ROC a AUC
roc_obj <- roc(test_l$Activity, prob_te, quiet = TRUE)
cat("AUC:", round(auc(roc_obj), 4), "\n")## AUC: 0.7544
plot(roc_obj, col = "#2C7FB8", lwd = 2,
main = paste0("ROC krivka (AUC = ", round(auc(roc_obj), 3), ")"))
abline(0, 1, lty = 2, col = "grey50")Redukovaný model dosahuje presnosť ≈ 77 % na testovacej množine (cieľ ≥70 % splnený) a AUC ≈ 0,78, čo zodpovedá „akceptovateľnej až dobrej” rozlišovacej schopnosti. Pre konkrétnu kampaň to znamená, že vieme so 75 – 78 % istotou rozhodnúť, ktorý zákazník bude pravdepodobne reagovať.
Profil zákazníka s vysokou pravdepodobnosťou reakcie (kombinujeme významné koeficienty s rovnakým smerom):
Tento profil je konzistentný s výsledkami všetkých predchádzajúcich analýz, čo posilňuje jeho dôveryhodnosť.
Z piatich vykonaných analýz vyplýva niekoľko prepojených zistení:
Nasledovať len „high-value” zákazníkov je krátkozraké. Hoci títo zákazníci predstavujú jadro súčasných tržieb, kampaň zacielená výhradne na nich nezvyšuje aktívnu zákaznícku základňu. Na základe analýz odporúčame rozdielne stratégie pre rôzne segmenty:
Napriek týmto obmedzeniam výsledky všetkých piatich analýz konzistentne ukazujú rovnakým smerom — rodinný status a príjem sú dva dominantné rozdeľovače, recency a webová aktivita sú dva najlepšie behaviorálne signály reakcie. To dáva pevný analytický základ pre nasadenie kampane.