Density-Based Clustering (DBSCAN) & Outlier Detection Dasar
Keterbatasan K-Means pada data non-sferis, parameter eps dan MinPts, titik core/border/noise, pemilihan eps dengan k-distance, serta deteksi outlier dengan z-score dan LOF.
Notebook lab (pratinjau)
Unduh lalu jalankan di Jupyter, VS Code, atau Google Colab.
Lab Minggu 11 — DBSCAN & Outlier Detection Dasar
Tujuan lab:
- Melihat kegagalan K-Means pada data non-sferis.
- Menjalankan DBSCAN, memahami titik core/border/noise.
- Memilih
epsdengan k-distance plot. - Menerapkan DBSCAN pada data segmentasi pelanggan & mendeteksi outlier (z-score, LOF).
Baca lesson.md dulu. Jalankan sel satu per satu (Shift+Enter).
import sys
sys.path.insert(0, "..")
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.cluster import DBSCAN, KMeans
from sklearn.datasets import make_moons, make_circles, make_blobs
from sklearn.metrics import adjusted_rand_score, silhouette_score
from sklearn.neighbors import NearestNeighbors, LocalOutlierFactor
from sklearn.preprocessing import StandardScaler
from common.datasets import segmentasi_pelanggan
1. K-Means vs DBSCAN pada data berbentuk arbitrer
X_moon, y_moon = make_moons(n_samples=300, noise=0.06, random_state=0)
X_circ, y_circ = make_circles(n_samples=300, factor=0.45, noise=0.05, random_state=0)
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
for row, (X, y, eps) in enumerate([(X_moon, y_moon, 0.2), (X_circ, y_circ, 0.2)]):
km = KMeans(n_clusters=2, n_init=10, random_state=0).fit_predict(X)
db = DBSCAN(eps=eps, min_samples=5).fit_predict(X)
axes[row, 0].scatter(X[:, 0], X[:, 1], c=km, cmap="coolwarm", s=12)
axes[row, 0].set_title(f"K-Means ARI={adjusted_rand_score(y, km):.2f}")
axes[row, 1].scatter(X[:, 0], X[:, 1], c=db, cmap="coolwarm", s=12)
axes[row, 1].set_title(f"DBSCAN ARI={adjusted_rand_score(y, db):.2f}")
plt.tight_layout()
plt.show()
2. Hitung manual: core / border / noise
Contoh di materi bagian 4: 1, 2, 3, 8, 9, 10, 25, eps=1.5, MinPts=3.
X1 = np.array([1, 2, 3, 8, 9, 10, 25], dtype=float).reshape(-1, 1)
db = DBSCAN(eps=1.5, min_samples=3).fit(X1)
jenis = np.full(len(X1), "noise", dtype=object)
jenis[db.labels_ != -1] = "border"
jenis[db.core_sample_indices_] = "core"
pd.DataFrame({"x": X1.ravel(), "label": db.labels_, "jenis": jenis})
3. DBSCAN dari nol (versi ringkas)
Implementasi lengkap kamu tulis di exercise.py. Di sini kita lihat langkahnya.
from collections import deque
def dbscan_sederhana(X, eps, min_pts):
n = len(X)
label = np.full(n, -2) # -2 = belum dikunjungi
c = -1
for p in range(n):
if label[p] != -2:
continue
N = np.flatnonzero(np.linalg.norm(X - X[p], axis=1) <= eps)
if len(N) < min_pts:
label[p] = -1 # noise (sementara)
continue
c += 1
label[p] = c
antrian = deque(N)
while antrian:
q = antrian.popleft()
if label[q] == -1:
label[q] = c # noise -> border
if label[q] != -2:
continue
label[q] = c
Nq = np.flatnonzero(np.linalg.norm(X - X[q], axis=1) <= eps)
if len(Nq) >= min_pts:
antrian.extend(Nq)
return label
lab = dbscan_sederhana(X_moon, 0.2, 5)
ref = DBSCAN(eps=0.2, min_samples=5).fit_predict(X_moon)
print("ARI vs sklearn:", adjusted_rand_score(ref, lab))
4. Memilih eps dengan k-distance plot
k = 4
nn = NearestNeighbors(n_neighbors=k + 1).fit(X_moon)
dist, _ = nn.kneighbors(X_moon)
kdist = np.sort(dist[:, k])
plt.plot(kdist)
plt.axhline(0.2, ls="--", c="r", label="eps = 0.2")
plt.xlabel("titik (urut)")
plt.ylabel(f"jarak ke tetangga ke-{k}")
plt.title("k-distance plot")
plt.legend()
plt.show()
Efek eps yang terlalu kecil / besar:
fig, axes = plt.subplots(1, 4, figsize=(16, 3.5))
for ax, eps in zip(axes, [0.05, 0.12, 0.2, 0.5]):
lab = DBSCAN(eps=eps, min_samples=5).fit_predict(X_moon)
n_k = len(set(lab) - {-1})
ax.scatter(X_moon[:, 0], X_moon[:, 1], c=lab, cmap="tab10", s=10)
ax.set_title(f"eps={eps}: {n_k} klaster, {np.sum(lab == -1)} noise")
plt.tight_layout()
plt.show()
5. Studi kasus: segmentasi pelanggan (RFM) dengan DBSCAN
Wajib standarisasi karena skala monetary_ribu jauh lebih besar.
seg = segmentasi_pelanggan()
fitur = ["recency_hari", "frekuensi", "monetary_ribu"]
Xs = StandardScaler().fit_transform(seg[fitur])
kd = np.sort(NearestNeighbors(n_neighbors=7).fit(Xs).kneighbors(Xs)[0][:, 6])
plt.plot(kd); plt.title("k-distance (k=6) data RFM"); plt.show()
db = DBSCAN(eps=0.5, min_samples=6).fit(Xs)
seg["klaster_dbscan"] = db.labels_
print("Jumlah klaster:", len(set(db.labels_) - {-1}), "| noise:", (db.labels_ == -1).sum())
print("ARI vs segmen asli:", round(adjusted_rand_score(seg.segmen_asli, db.labels_), 3))
seg.groupby("klaster_dbscan")[fitur].median().round(1)
mask = db.labels_ != -1
print("Silhouette (tanpa noise):", round(silhouette_score(Xs[mask], db.labels_[mask]), 3))
6. Deteksi outlier
6a. Z-score pada satu atribut
x = seg["monetary_ribu"].values
z = (x - x.mean()) / x.std()
print("Outlier |z|>3:", np.sum(np.abs(z) > 3))
seg.loc[np.abs(z) > 3, fitur]
6b. Local Outlier Factor (multivariat)
rng = np.random.default_rng(0)
padat = rng.normal([0, 0], 0.3, (150, 2))
jarang = rng.normal([4, 4], 1.2, (80, 2))
aneh = np.array([[1.5, 1.5], [0, 2.2], [-2, -1]])
Xo = np.vstack([padat, jarang, aneh])
lof = LocalOutlierFactor(n_neighbors=20, contamination=0.03)
pred = lof.fit_predict(Xo)
skor = -lof.negative_outlier_factor_
plt.scatter(Xo[:, 0], Xo[:, 1], c=skor, cmap="viridis", s=15)
plt.scatter(Xo[pred == -1, 0], Xo[pred == -1, 1], facecolors="none", edgecolors="r", s=120, label="LOF outlier")
plt.colorbar(label="skor LOF"); plt.legend(); plt.title("LOF menangkap outlier lokal"); plt.show()
Perhatikan: titik (1.5, 1.5) tidak terlalu jauh secara absolut, tetapi jauh lebih jarang dibanding klaster padat di dekatnya → LOF tinggi. Titik-titik di klaster jarang (4, 4) tidak ikut ditandai.
✍️ Coba sendiri
- Pada
make_circles, cari kombinasi eps & MinPts yang menghasilkan ARI = 1. - Buat data
make_blobsdengancluster_std=[0.3, 1.5]. Apakah satu eps bisa memisahkan keduanya? Cobasklearn.cluster.HDBSCAN. - Pada data RFM, ubah
eps(0.3, 0.5, 0.8). Tabulasikan jumlah klaster, noise, dan ARI. - Bandingkan pelanggan yang ditandai noise DBSCAN dengan yang ditandai LOF pada data RFM. Berapa yang sama?
# 1.
# 2.
# 3.
# 4.
Selanjutnya
Kerjakan kuis dan exercise.py, lalu jalankan python check.py 11 dari folder utama.