Partitioning & Hierarchical Clustering
Clustering tanpa label — K-Means (SSE), K-Medoids/PAM, Agglomerative Hierarchical Clustering (single, complete, average, Ward), Elbow Method, dan Silhouette Score untuk segmentasi pelanggan.
Notebook lab (pratinjau)
Unduh lalu jalankan di Jupyter, VS Code, atau Google Colab.
Lab Minggu 10 — K-Means, K-Medoids & Hierarchical Clustering
Tujuan lab:
- Menjalankan K-Means langkah demi langkah dan melihat SSE turun.
- Menentukan k optimal dengan Elbow Method & Silhouette Score.
- Membandingkan K-Means vs K-Medoids saat ada outlier.
- Membuat dendrogram agglomerative clustering dengan berbagai linkage.
- Membuat profil segmen pelanggan (RFM) yang siap dipakai tim marketing.
[1]
import sys
sys.path.insert(0, "..")
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans, AgglomerativeClustering
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import silhouette_score, silhouette_samples, adjusted_rand_score
from scipy.cluster.hierarchy import linkage, dendrogram, fcluster
from common.datasets import segmentasi_pelanggan
1. K-Means langkah demi langkah (contoh materi)
[2]
X7 = np.array([[1, 1], [1.5, 2], [3, 4], [5, 7], [3.5, 5], [4.5, 5], [3.5, 4.5]])
nama = list("ABCDEFG")
C = X7[[0, 2]].copy() # centroid awal = A dan C
for it in range(1, 4):
D = ((X7[:, None] - C[None]) ** 2).sum(axis=2)
label = D.argmin(axis=1)
print(f"Iterasi {it}: klaster = {dict(zip(nama, label.tolist()))}")
C_baru = np.array([X7[label == j].mean(axis=0) for j in range(2)])
sse = ((X7 - C_baru[label]) ** 2).sum()
print(f" centroid baru = {C_baru.round(3).tolist()} SSE = {sse:.3f}")
if np.allclose(C_baru, C):
print(" konvergen!")
break
C = C_baru
[3]
plt.scatter(X7[:, 0], X7[:, 1], c=label, cmap="coolwarm", s=80)
plt.scatter(C[:, 0], C[:, 1], marker="X", s=200, c="k")
for n, (x, y) in zip(nama, X7):
plt.annotate(n, (x + 0.08, y + 0.08))
plt.title("Hasil K-Means (k=2)"); plt.show()
2. Data segmentasi pelanggan (RFM)
[4]
df = segmentasi_pelanggan()
fitur = ["recency_hari", "frekuensi", "monetary_ribu"]
df[fitur].describe().round(1)
Skala sangat berbeda (monetary ribuan, frekuensi puluhan) → z-score dulu.
[5]
Xs = StandardScaler().fit_transform(df[fitur])
3. Elbow & Silhouette
[6]
ks = range(1, 10)
sse = [KMeans(k, n_init=10, random_state=0).fit(Xs).inertia_ for k in ks]
sil = [silhouette_score(Xs, KMeans(k, n_init=10, random_state=0).fit_predict(Xs)) for k in ks if k > 1]
fig, ax = plt.subplots(1, 2, figsize=(11, 3.5))
ax[0].plot(list(ks), sse, "o-"); ax[0].set_title("Elbow (SSE vs k)"); ax[0].set_xlabel("k")
ax[1].plot(list(ks)[1:], sil, "s-", color="C1"); ax[1].set_title("Silhouette vs k"); ax[1].set_xlabel("k")
plt.tight_layout(); plt.show()
print("k terbaik (silhouette):", list(ks)[1:][int(np.argmax(sil))])
Plot silhouette per titik untuk k = 4
[7]
km = KMeans(4, n_init=10, random_state=0).fit(Xs)
s = silhouette_samples(Xs, km.labels_)
y0 = 0
for j in range(4):
sj = np.sort(s[km.labels_ == j])
plt.fill_betweenx(np.arange(y0, y0 + len(sj)), 0, sj, alpha=0.7, label=f"klaster {j}")
y0 += len(sj) + 10
plt.axvline(s.mean(), color="k", ls="--", label=f"rata-rata {s.mean():.2f}")
plt.xlabel("silhouette"); plt.legend(); plt.title("Silhouette per titik (k=4)"); plt.show()
4. Profil segmen → strategi bisnis
[8]
df["klaster"] = km.labels_
profil = df.groupby("klaster")[fitur].mean().round(1)
profil["jumlah"] = df["klaster"].value_counts().sort_index()
profil["porsi_pendapatan_%"] = (df.groupby("klaster")["monetary_ribu"].sum() / df["monetary_ribu"].sum() * 100).round(1)
profil
[9]
# Karena data sintetis, kita bisa cek terhadap segmen asli (di dunia nyata label ini tidak ada!)
print("Adjusted Rand Index vs segmen asli:", round(adjusted_rand_score(df["segmen_asli"], km.labels_), 3))
5. K-Means vs K-Medoids dengan outlier
[10]
rng = np.random.default_rng(0)
A = np.vstack([rng.normal([0, 0], 0.5, (30, 2)), rng.normal([4, 4], 0.5, (30, 2)), [[25, 25], [26, 24]]])
def k_medoids(X, medoid, iters=50):
D = np.sqrt(((X[:, None] - X[None]) ** 2).sum(axis=2))
for _ in range(iters):
lab = D[:, medoid].argmin(axis=1)
baru = [int(np.where(lab == j)[0][D[np.ix_(lab == j, lab == j)].sum(axis=1).argmin()]) for j in range(len(medoid))]
if baru == medoid:
break
medoid = baru
return lab, X[medoid]
km2 = KMeans(2, init=A[[0, 30]], n_init=1).fit(A)
lab_m, pusat_m = k_medoids(A, [0, 30])
fig, ax = plt.subplots(1, 2, figsize=(11, 4))
ax[0].scatter(A[:, 0], A[:, 1], c=km2.labels_, cmap="coolwarm"); ax[0].scatter(*km2.cluster_centers_.T, marker="X", s=200, c="k")
ax[0].set_title("K-Means: centroid tertarik outlier")
ax[1].scatter(A[:, 0], A[:, 1], c=lab_m, cmap="coolwarm"); ax[1].scatter(*pusat_m.T, marker="X", s=200, c="k")
ax[1].set_title("K-Medoids: medoid = titik nyata")
plt.show()
print("centroid K-Means :", km2.cluster_centers_.round(2).tolist())
print("medoid :", pusat_m.round(2).tolist())
6. Agglomerative clustering & dendrogram
[11]
sampel = Xs[rng.choice(len(Xs), 40, replace=False)]
fig, ax = plt.subplots(1, 3, figsize=(15, 4))
for a, metode in zip(ax, ["single", "complete", "average"]):
Z = linkage(sampel, method=metode)
dendrogram(Z, ax=a, no_labels=True)
a.set_title(f"{metode} linkage")
plt.tight_layout(); plt.show()
Perhatikan chaining pada single linkage (penggabungan bertangga), sedangkan complete/average lebih seimbang.
[12]
for metode in ["single", "complete", "average", "ward"]:
lab = AgglomerativeClustering(n_clusters=4, linkage=metode).fit_predict(Xs)
print(f"{metode:9s} silhouette={silhouette_score(Xs, lab):.3f} ARI vs asli={adjusted_rand_score(df['segmen_asli'], lab):.3f} ukuran={np.bincount(lab).tolist()}")
✍️ Coba sendiri
- Ulangi K-Means contoh materi dengan centroid awal B dan D. Apakah hasil akhirnya sama?
- Jalankan
KMeans(4, n_init=1, init="random")dengan 5random_stateberbeda padaXs. Catat SSE-nya — apa yang kamu simpulkan tentang inisialisasi? - Coba clustering tanpa normalisasi. Fitur mana yang mendominasi? Bandingkan ARI-nya.
- Beri nama bisnis untuk tiap klaster di tabel profil (bagian 4) dan usulkan satu aksi marketing per klaster.
[13]
# 1.
[14]
# 2.
[15]
# 3.
Jawaban no. 4:
| Klaster | Nama segmen | Aksi |
|---|---|---|
| 0 | ... | ... |
Selanjutnya
Kerjakan kuis dan exercise.py, lalu python check.py 10.