Dataset: Mall Customer Segmentation (Kaggle) — 200 πελάτες, μεταβλητές:
CustomerID, Gender, Age, Annual Income (k$), Spending Score (1-100).
library(cluster)
library(scatterplot3d)
mall = read.csv("Mall_Customers.csv")
colnames(mall) = c("customer","Gender","Age","Income","Spending")
str(mall)
## 'data.frame': 200 obs. of 5 variables:
## $ customer: int 1 2 3 4 5 6 7 8 9 10 ...
## $ Gender : chr "Male" "Male" "Female" "Female" ...
## $ Age : int 19 21 20 23 31 22 35 23 64 30 ...
## $ Income : int 15 15 16 16 17 17 18 18 19 19 ...
## $ Spending: int 39 81 6 77 40 76 6 94 3 72 ...
summary(mall[,c("Age","Income","Spending")])
## Age Income Spending
## Min. :18.00 Min. : 15.00 Min. : 1.00
## 1st Qu.:28.75 1st Qu.: 41.50 1st Qu.:34.75
## Median :36.00 Median : 61.50 Median :50.00
## Mean :38.85 Mean : 60.56 Mean :50.20
## 3rd Qu.:49.00 3rd Qu.: 78.00 3rd Qu.:73.00
## Max. :70.00 Max. :137.00 Max. :99.00
Γιατί K-means. Το διάλεξα διότι δεν θέλω κατι supervised:
Δεν υπάρχει μεταβλητή-στόχος => δεν κάνει sense classification/regression. Το πρόβλημα είναι καθαρά unsupervised: πρέπει να ανακαλύψουμε δομή, όχι να προβλέψουμε κάτι γνωστό.
Οι μεταβλητές (Income, Spending, Age) είναι συνεχείς αριθμητικές => K-means δουλεύει φυσικά πάνω σε ευκλείδεια απόσταση.
Το dataset είναι μικρό όπου n=200 και θέλουμε ξεκάθαρα, ερμηνεύσιμα clusters για να τα παρουσιάσουμε στο marketing team => K-means με λίγα, spherical clusters είναι ιδανικό, σε αντίθεση με πιο πολύπλοκες μεθόδους ’οπως DBSCAN και GMM που προσφέρουν ευελιξία κάτι που εδώ δεν χρειαζόμαστε.
Εναλλακτικά θα μπορούσε να χρησιμοποιηθεί hierarchical clustering . Δείχνουμε dendrogram παρακάτω σαν επιβεβαίωση αλλά το K-means προτιμάται εδώ γιατί πρότων θέλουμε συγκεκριμένο, σταθερό αριθμό ομάδων για καμπάνιες και δεύτερον θέλουμε επίσης scale καλύτερα αν αυξηθεί ο αριθμός πελατών στο μέλλον.
standardization. Το Income είναι σε κλίμακα 15-137
(χιλιάδες $) ενώ το Spending Score σε 1-100 — χωρίς scale, το Income θα
κυριαρχούσε τεχνητά στην απόσταση. Άρα χρησιμοποιούμε
scale().
X = scale(mall[, c("Income","Spending")])
set.seed(123)
wcss = sapply(1:10, function(k) kmeans(X, centers = k, nstart = 25)$tot.withinss)
plot(1:10, wcss, type = "b", pch = 19, col = "steelblue",
xlab = "Αριθμός clusters (k)", ylab = "WCSS (Within-Cluster SS)",
main = "Elbow Method - Income & Spending")
sil = sapply(2:10, function(k) {
km = kmeans(X, centers = k, nstart = 25)
mean(silhouette(km$cluster, dist(X))[, 3])
})
plot(2:10, sil, type = "b", pch = 19, col = "darkorange",
xlab = "Αριθμός clusters (k)", ylab = "Μέσο Silhouette Width",
main = "Silhouette Method - Income & Spending")
data.frame(k = 2:10, silhouette = round(sil, 3))
## k silhouette
## 1 2 0.286
## 2 3 0.467
## 3 4 0.494
## 4 5 0.555
## 5 6 0.540
## 6 7 0.528
## 7 8 0.457
## 8 9 0.459
## 9 10 0.441
Απάντηση: Το elbow plot δείχνει καθαρή “κάμψη” (kink) στο k=5 (η WCSS πέφτει απότομα ως το 5, μετά η βελτίωση επιπεδώνεται). Το silhouette το επιβεβαιώνει ποσοτικά: η μεγαλύτερη μέση τιμή silhouette (0.555) είναι ακριβώς στο k=5.
Τεκμηρίωση: 5 διακριτές ομάδες πελατών.
km5 = kmeans(X, centers = 5, nstart = 25)
mall$Cluster = km5$cluster
table(mall$Cluster)
##
## 1 2 3 4 5
## 22 23 81 39 35
profile = aggregate(cbind(Income, Spending, Age) ~ Cluster, data = mall, FUN = mean)
profile$n = as.vector(table(mall$Cluster))
profile[order(profile$Cluster), ]
## Cluster Income Spending Age n
## 1 1 25.72727 79.36364 25.27273 22
## 2 2 26.30435 20.91304 45.21739 23
## 3 3 55.29630 49.51852 42.71605 81
## 4 4 86.53846 82.12821 32.69231 39
## 5 5 88.20000 17.11429 41.11429 35
plot(mall$Income, mall$Spending, col = mall$Cluster, pch = 19,
xlab = "Annual Income (k$)", ylab = "Spending Score (1-100)",
main = "Πελάτες κατά Cluster")
points(km5$centers[,1]*attr(X,"scaled:scale")[1] + attr(X,"scaled:center")[1],
km5$centers[,2]*attr(X,"scaled:scale")[2] + attr(X,"scaled:center")[2],
col = 1:5, pch = 8, cex = 2.5, lwd = 3)
legend("topright", legend = paste("Cluster", 1:5), col = 1:5, pch = 19, cex=0.8)
Προφίλ (με βάση τα πραγματικά μέσα):
| Cluster | Income | Spending | Age (μέση) | n | Προφίλ |
|---|---|---|---|---|---|
| 1 | ~26k$ | ~79 | ~25 | 22 | Χαμηλό εισόδημα, υψηλή δαπάνη — “impulsive/young spenders” |
| 2 | ~26k$ | ~21 | ~45 | 23 | Χαμηλό εισόδημα, χαμηλή δαπάνη — “careful/budget” |
| 3 | ~55k$ | ~50 | ~43 | 81 | Μέσο εισόδημα, μέση δαπάνη — “mainstream/average” (η μεγαλύτερη ομάδα) |
| 4 | ~87k$ | ~82 | ~33 | 39 | Υψηλό εισόδημα, υψηλή δαπάνη — “premium loyal” |
| 5 | ~88k$ | ~17 | ~41 | 35 | Υψηλό εισόδημα, χαμηλή δαπάνη — “untapped high-value” |
Απάντηση: Ναι — το Cluster 5 (Income ≈ 88k$, Spending ≈ 17). Είναι 35 πελάτες (17.5% της βάσης) που έχουν την οικονομική δυνατότητα αλλά δεν ξοδεύουν στο mall. Αυτή είναι κλασική «ευκαιρία» ομάδα: η αγοραστική δύναμη υπάρχει, κάτι άλλο (εμπειρία, σχετικότητα προϊόντων, απόσταση, brand fit) τους αποτρέπει. Αξίζει ειδική έρευνα/στόχευση δεν είναι απλά “δεν έχουν λεφτά”, το πρόβλημα είναι αλλού.
X3 = scale(mall[, c("Age","Income","Spending")])
set.seed(123)
wcss3 = sapply(1:10, function(k) kmeans(X3, centers = k, nstart = 25)$tot.withinss)
sil3 = sapply(2:10, function(k) {
km = kmeans(X3, centers = k, nstart = 25)
mean(silhouette(km$cluster, dist(X3))[, 3])
})
par(mfrow = c(1,2))
plot(1:10, wcss3, type="b", pch=19, col="steelblue", main="Elbow (3D)",
xlab="k", ylab="WCSS")
plot(2:10, sil3, type="b", pch=19, col="darkorange", main="Silhouette (3D)",
xlab="k", ylab="avg width")
par(mfrow = c(1,1))
data.frame(k = 2:10, silhouette_3D = round(sil3,3))
## k silhouette_3D
## 1 2 0.335
## 2 3 0.358
## 3 4 0.404
## 4 5 0.417
## 5 6 0.427
## 6 7 0.417
## 7 8 0.407
## 8 9 0.421
## 9 10 0.401
km3D = kmeans(X3, centers = 5, nstart = 25)
mall$Cluster3D = km3D$cluster
profile3D = aggregate(cbind(Age, Income, Spending) ~ Cluster3D, data = mall, FUN = mean)
profile3D$n = as.vector(table(mall$Cluster3D))
profile3D[order(profile3D$Cluster3D), ]
## Cluster3D Age Income Spending n
## 1 1 39.87179 86.10256 19.35897 39
## 2 2 32.87500 86.10000 81.52500 40
## 3 3 25.18519 41.09259 62.24074 54
## 4 4 46.25000 26.75000 18.35000 20
## 5 5 55.63830 54.38298 48.85106 47
colors5 = c("#E41A1C","#377EB8","#4DAF4A","#984EA3","#FF7F00")
scatterplot3d(mall$Age, mall$Income, mall$Spending,
color = colors5[mall$Cluster3D], pch = 19,
xlab = "Age", ylab = "Income (k$)", zlab = "Spending Score",
main = "3D Clustering: Age + Income + Spending")
Απάντηση: Η εικόνα αλλάζει σημαντικά:
Το silhouette προτείνει πλέον k=6 (0.427) αντί για k=5, αν και το k=5 (0.417) είναι πολύ κοντά, η ηλικία εισάγει επιπλέον διαχωριστική πληροφορία.
Κρατώντας k=5 για συγκρισιμότητα, το μεγάλο “mainstream” cluster (81 άτομα) της 2D ανάλυσης σπάει σε ηλικιακά διαφοροποιημένα υπο-group, δεν είναι πια μία ενιαία μάζα, βλέπουμε νεότερους vs μεγαλύτερους με παρόμοιο εισόδημα/δαπάνη.
Το πρώην “premium” cluster (υψηλό income, υψηλό spending) αποκαλύπτεται ότι είναι σχετικά νεαρό (μέση ηλικία ~33), ενώ το “untapped high-value” cluster (υψηλό income, χαμηλό spending) είναι μεγαλύτερης ηλικίας (~41-46), πληροφορία που το 2D μοντέλο έκρυβε εντελώς.
Business insight: η ηλικία δείχνει ότι το «high-income low-spending» πρόβλημα πιθανόν σχετίζεται με γενιά/lifestyle και όχι απλά τυχαία — άρα η καμπάνια στόχευσης πρέπει να είναι διαφορετική (π.χ. πιο “premium/experience” προσανατολισμένη παρά “νεανικό branding”).