Celem projektu jest identyfikacja segmentów klientów na podstawie
cech demograficznych i zakupowych z wykorzystaniem metod klasteryzacji
niesuperwizowanej. W analizie porównuję rozwiązania uzyskane
metodami:
k-means (rozwiązanie bazowe),
PAM (odporniejsze na obserwacje odstające – medoidy),
klasteryzacja hierarchiczna (complete oraz Ward.D2),oraz wykonuję
elementy walidacji i praktycznego zastosowania: dobór liczby
klastrów,
porównanie metod, predykcja przynależności nowych obserwacji.
Wykorzystano zbiór danych Mall_Customers.csv, w którym jednostką
analizy jest #pojedynczy klient. Do klastrowania wybrano zmienne:
Age – wiek,
Annual Income (k$) – dochód roczny,
Spending Score (1–100) – wskaźnik intensywności wydatków.
Wykorzystane biblioteki
library(dplyr) library(ggplot2) library(factoextra) library(cluster)
library(flexclust) library(psych) library(dendextend) library(hopkins)
library(mclust)
ustawiam seed w celu powtarzalności wyników
set.seed(123)
Uporządkowanie nazw kolumn (bez spacji i znaków specjalnych)
mall <- read.csv(“Mall_Customers.csv”, stringsAsFactors =
FALSE)
names(mall) <- make.names(names(mall))
glimpse(mall)
summary(mall)
W klasteryzacji opartej o odległości i centroidy konieczne jest
ujednolicenie skali zmiennych. Dlatego dane są standaryzowane (scale()),
aby żadna zmienna (np. dochód) nie dominowała wyniku.
X <- mall %>% select(Age, Annual.Income..k..,
Spending.Score..1.100.)
colSums(is.na(X))
str(X)
X_s <- scale(X)
Zanim zastosuję algorytmy klasteryzacji, sprawdzam czy dane wykazują
tendencję do tworzenia skupisk. W tym celu obliczam statystykę Hopkinsa
(pakiet hopkins). Wartość bliska 1 wskazuje na silną strukturę
klastrową.
X_hopkins <- as.data.frame(X_s)
stopifnot(sum(!is.finite(as.matrix(X_hopkins))) == 0)
m <- min(50, max(10, floor(nrow(X_hopkins) / 10)))
H <- hopkins(X_hopkins, m = m)
H
Interpretacja: Uzyskano wartość Hopkinsa 0.967832, co wskazuje na
bardzo silną tendencję klastrową danych.
Dobór liczby klastrów
Liczbę klastrów dobieram na podstawie dwóch kryteriów:
silhouette – ocenia jakość rozdzielenia klastrów,
gap statistic – porównuje wynik do struktury losowej (bardziej
konserwatywne kryterium).
fviz_nbclust(X_s, FUNcluster = kmeans, method = “silhouette”) +
ggtitle(“Silhouette (k-means)”)
fviz_nbclust(X_s, FUNcluster = kmeans, method = “gap_stat”, nboot =
50) + ggtitle(“Gap statistic (k-means)”)
Wyniki: silhouette sugeruje k = 8, gap statistic sugeruje k = 6. W
dalszej analizie:k = 6 traktuję jako rozwiązanie główne (konserwatywne i
interpretowalne),k = 8 jako wariant porównawczy.
K-means: rozwiązanie główne (k = 6) i wariant porównawczy (k =
8)
K-means dla k = 6
km6 <- eclust(X_s, “kmeans”, k = 6, hc_metric = “euclidean”)
fviz_cluster( km6, geom = “point”, ggtheme = theme_minimal(base_size
= 12), main = “K-means clustering (k = 6)” )
fviz_silhouette(km6)
Profilowanie klastrów (średnie wartości zmiennych w
segmentach):
mall_km6 <- mall %>% mutate(cluster_k6 =
factor(km6$cluster))
mall_km6 %>% group_by(cluster_k6) %>% summarise( Age =
mean(Age), Income = mean(Annual.Income..k..), Spending =
mean(Spending.Score..1.100.), n = n() )
K-means dla k = 8 (porównanie)
km8 <- eclust(X_s, “kmeans”, k = 8, hc_metric = “euclidean”)
fviz_cluster( km8, geom = “point”, ggtheme = theme_minimal(base_size
= 12), main = “K-means clustering (k = 8)” )
fviz_silhouette(km8)
Profilowanie klastrów (średnie wartości zmiennych w
segmentach):
mall_km8 <- mall %>% mutate(cluster_k8 =
factor(km8$cluster))
mall_km8 %>% group_by(cluster_k8) %>% summarise( Age =
mean(Age), Income = mean(Annual.Income..k..), Spending =
mean(Spending.Score..1.100.), n = n() )
Porównanie jakości k=6 vs k=8 (średnia silhouette)
mean_sil_k6 <- mean(silhouette(km6$cluster, dist(X_s))[, 3])
mean_sil_k8 <- mean(silhouette(km8$cluster, dist(X_s))[, 3])
mean_sil_k6
mean_sil_k8
Wniosek: k=8 ma nieco wyższą średnią silhouette (0.4105837) w
stosunku do k=6 (0.3838699), jednak ze względu na kryterium gap oraz
interpretowalność segmentów, k = 6 pozostaje rozwiązaniem głównym, a k =
8 pełni rolę porównawczą
PAM (k = 6) i porównanie z k-means
pam6 <- eclust(X_s, “pam”, k = 6)
fviz_cluster( pam6, geom = “point”, ggtheme = theme_minimal(base_size
= 12), main = “PAM clustering (k = 6)” )
fviz_silhouette(pam6)
mall_pam6 <- mall %>% mutate(cluster_pam6 =
factor(pam6$cluster))
mall_pam6 %>% group_by(cluster_pam6) %>% summarise( Age =
mean(Age), Income = mean(Annual.Income..k..), Spending =
mean(Spending.Score..1.100.), n = n() )
Porównanie przypisań k-means vs PAM wykonuję przy pomocy Adjusted
Rand Index (ARI) – im bliżej 1, tym bardziej zgodne segmentacje.
comp_km_pam <- comPart(km6\(cluster,
pam6\)cluster)
comp_km_pam
Adjusted Rand Index wyniósł 0.74, a miary Jaccarda i
Fowlkesa–Mallowsa odpowiednio 0.65 i 0.79, co wskazuje, że obie metody
identyfikują bardzo podobne segmenty klientów.
Predykcja przynależności do klastrów (train/test)
Aby pokazać praktyczną użyteczność segmentacji, wykonuję podział na
zbiór treningowy i testowy. Następnie uczę k-means na danych
treningowych i przypisuję klaster dla nowych obserwacji.
n <- nrow(X_s)
idx_train <- sample.int(n, size = floor(0.8 * n))
X_train <- X_s[idx_train, , drop = FALSE]
X_test <- X_s[-idx_train, , drop = FALSE]
mall_train <- mall[idx_train, ]
mall_test <- mall[-idx_train, ]
k-means na zbiorze treningowym
km_train <- kmeans(X_train, centers = 6, nstart = 25)
predykcja: przypisanie nowych obserwacji do istniejących
klastrów
km_train_kcca <- as.kcca(km_train, X_train) cluster_pred <-
predict(km_train_kcca, X_test)
mall_test_pred <- mall_test %>% mutate(cluster_pred =
factor(cluster_pred))
table(cluster_pred) head(mall_test_pred)
mall_test_pred %>% group_by(cluster_pred) %>% summarise( Age =
mean(Age), Income = mean(Annual.Income..k..), Spending =
mean(Spending.Score..1.100.), n = n() )
dodajemy etykiety do train
mall_train\(cluster_train <-
factor(km_train\)cluster)
ggplot() + geom_point( data = mall_train, aes(x = Annual.Income..k..,
y = Spending.Score..1.100., color = cluster_train), alpha = 0.7 ) +
geom_point( data = mall_test_pred, aes(x = Annual.Income..k.., y =
Spending.Score..1.100.), shape = 1, size = 2 ) + theme_minimal(base_size
= 12) + labs( title = “Predykcja przynależności nowych klientów do
klastrów”, subtitle = “Wypełnione: dane treningowe, puste kółka: dane
testowe”, color = “Cluster” )
Klasteryzacja hierarchiczna i porównanie z k-means
Klasteryzacja hierarchiczna umożliwia analizę struktury danych w
postaci dendrogramu. Porównuję dwa standardowe warianty łączenia:
complete linkage, Ward.D2 (minimalizacja wariancji
wewnątrzklastrowej)
dm <- dist(X_s, method = “euclidean”)
hc_complete <- hclust(dm, method = “complete”) hc_ward <-
hclust(dm, method = “ward.D2”)
plot(hc_complete, main = “Klasteryzacja hierarchiczna: kompletne
połączenie”, cex = 0.7) plot(hc_ward, main = “Klasteryzacja
hierarchiczna: Ward.D2”, cex = 0.7)
plot(hc_ward, labels = FALSE, main = “Klasteryzacja hierarchiczna:
Ward.D2 (ukryte etykiety)”)
Cięcie drzewa do k = 6 i silhouette
k <- 6 cl_hc_complete <- cutree(hc_complete, k = k) cl_hc_ward
<- cutree(hc_ward, k = k)
sil_complete <- silhouette(cl_hc_complete, dm) sil_ward <-
silhouette(cl_hc_ward, dm)
mean_sil_complete <- mean(sil_complete[, 3]) mean_sil_ward <-
mean(sil_ward[, 3])
mean_sil_complete mean_sil_ward
Wynik: Ward.D2 (0.420117) ma wyższą silhouette niż complete
(0.3745611) oznacza to lepszą jakość segmentacji hierarchicznej
Wizualizacja klastrów (Ward.D2)
fviz_cluster(list(data = X_s, cluster = cl_hc_ward), ggtheme =
theme_minimal(base_size = 12), main = “HC Ward.D2 clusters (k = 6)
projected to 2D”)
Porównanie HC vs k-means (ARI)
ARI_hc_ward_vs_km6 <- mclust::adjustedRandIndex(cl_hc_ward,
km6\(cluster)
ARI_hc_complete_vs_km6 <- mclust::adjustedRandIndex(cl_hc_complete,
km6\)cluster)
ARI_hc_ward_vs_km6 ARI_hc_complete_vs_km6
Wynik: ARI (Ward.D2 vs k-means) = 0.69 oznacza to wysoką zgodność
pomiędzy klasteryzacją hierarchiczną metodą Ward.D2, a rozwiązaniem
k-means dla k = 6. ARI (complete vs k-means) = 0.81 co wskazuje na
bardzo wysoką zgodność struktur klastrów uzyskanych metodą complete
linkage oraz k-means. Metoda complete w tym przypadku lepiej odtwarza
podział uzyskany przez k-means, mimo że wcześniej charakteryzowała się
niższą wartością silhouette.
WNIOSKI
1 Dane wykazują silną tendencję klastrową (Hopkins 0.967832)
2 Dobór liczby klastrów dał konflikt kryteriów: silhouette sugerował
k=8, gap k=6. Przyjęto k=6 jako rozwiązanie główne i k=8 jako
porównawcze.
3 Segmentacja k-means (k=6) jest spójna z PAM (wysoki ARI).
4 W klasteryzacji hierarchicznej Ward.D2 osiąga lepszą silhouette,
natomiast complete ma wyższą zgodność z k-means (ARI)
5 Pokazano możliwość przypisywania nowych klientów do istniejących
segmentów (podejście train/test).