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).