Lompat ke konten
nalar

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.

Unduh lab.ipynb

Lab Minggu 10 — K-Means, K-Medoids & Hierarchical Clustering

Tujuan lab:

  1. Menjalankan K-Means langkah demi langkah dan melihat SSE turun.
  2. Menentukan k optimal dengan Elbow Method & Silhouette Score.
  3. Membandingkan K-Means vs K-Medoids saat ada outlier.
  4. Membuat dendrogram agglomerative clustering dengan berbagai linkage.
  5. 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

  1. Ulangi K-Means contoh materi dengan centroid awal B dan D. Apakah hasil akhirnya sama?
  2. Jalankan KMeans(4, n_init=1, init="random") dengan 5 random_state berbeda pada Xs. Catat SSE-nya — apa yang kamu simpulkan tentang inisialisasi?
  3. Coba clustering tanpa normalisasi. Fitur mana yang mendominasi? Bandingkan ARI-nya.
  4. 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.