Lompat ke konten
nalar

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.

Unduh lab.ipynb

Lab Minggu 11 — DBSCAN & Outlier Detection Dasar

Tujuan lab:

  1. Melihat kegagalan K-Means pada data non-sferis.
  2. Menjalankan DBSCAN, memahami titik core/border/noise.
  3. Memilih eps dengan k-distance plot.
  4. Menerapkan DBSCAN pada data segmentasi pelanggan & mendeteksi outlier (z-score, LOF).

Baca lesson.md dulu. Jalankan sel satu per satu (Shift+Enter).

[1]
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

[2]
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.

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

[4]
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

[5]
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:

[6]
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.

[7]
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()
[8]
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)
[9]
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

[10]
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)

[11]
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

  1. Pada make_circles, cari kombinasi eps & MinPts yang menghasilkan ARI = 1.
  2. Buat data make_blobs dengan cluster_std=[0.3, 1.5]. Apakah satu eps bisa memisahkan keduanya? Coba sklearn.cluster.HDBSCAN.
  3. Pada data RFM, ubah eps (0.3, 0.5, 0.8). Tabulasikan jumlah klaster, noise, dan ARI.
  4. Bandingkan pelanggan yang ditandai noise DBSCAN dengan yang ditandai LOF pada data RFM. Berapa yang sama?
[12]
# 1.
[13]
# 2.
[14]
# 3.
[15]
# 4.

Selanjutnya

Kerjakan kuis dan exercise.py, lalu jalankan python check.py 11 dari folder utama.