Sepanjang section ini kita selalu bekerja dengan keuntungan tersembunyi: anomali kita suntik sendiri, sehingga kebenarannya diketahui dan tiap detektor bisa dinilai dengan jujur. Chapter penutup ini membuat penilaian tersebut menjadi disiplin tersendiri, karena di dunia nyata keuntungan itu hilang sebagian — label anomali mahal, jarang, dan tidak lengkap. Masalahnya dimulai dari angka yang paling sering disalahpahami: akurasi. Ketika hanya 2% transaksi yang anomali, model yang tidak pernah mendeteksi apa pun langsung mencatat akurasi 98%. Kita akan membangun kerangka pengukuran yang benar untuk kelas langka — presisi dan recall pada titik operasi, average precision sebagai ringkasan tunggal, dan kehati-hatian khusus terhadap ROC-AUC yang tampak manis.
Akurasi yang Menipu pada Kelas Langka #
Angka 98% pada contoh di atas bukan salah hitung — model “selalu jawab normal” memang benar pada 980 dari 1.000 transaksi. Kegagalan akurasi bersifat struktural: metrik ini menjumlahkan kebenaran pada dua kelas yang ukurannya 49 berbanding 1, sehingga kelas kecil praktis tidak berbobot. Menambahkan 19 anomali yang tertangkap hanya menggeser akurasi dari 0,980 ke 0,998 — selisih 0,018 yang tidak mengatakan apa-apa — padahal recall bergerak dari 0 ke 0,950. Metrik campuran lain pun perlu diperiksa ulang; balanced_accuracy atau F1 sudah lebih adil, tetapi keduanya masih menyembunyikan hal yang paling penting bagi operasional: berapa transaksi yang harus ditinjau manual untuk mendapatkan anomali sebanyak itu.
Presisi, Recall, dan Biaya Antrean Tinjauan #
Kerangka yang tepat memperlakukan deteksi anomali sebagai mesin pemeringkat: detektor memberi skor, kita memutuskan berapa banyak transaksi teratas yang masuk antrean peninjauan manual. Dua besaran mengukur kualitas antrean tersebut pada titik operasi k:
- Presisi = dari k transaksi yang ditandai, berapa yang benar-benar anomali — kualitas antrean, satu dikurangi pemborosan waktu peninjau.
- Recall = dari seluruh anomali yang ada, berapa yang berhasil masuk antrean — cakupan deteksi, satu dikurangi anomali yang lolos.
Kedua angka selalu tarik-menarik: mengecilkan k menaikkan presisi namun menurunkan recall. Titik operasi yang tepat adalah keputusan bisnis — biaya satu anomali yang lolos dibandingkan biaya satu peninjauan manual — bukan angka yang dioptimalkan secara membabi buta.
Average Precision dan Jebakan ROC-AUC #
Sapuan seluruh titik operasi menghasilkan kurva presisi-recall, dan average precision (AP) merangkum luas di bawahnya menjadi satu angka yang tidak bergantung ambang. Interpretasinya enak: AP ≈ presisi rata-rata yang kita dapat jika anomali ditarik satu per satu menurut skor. Baseline pembandingnya bukan 0,5, melainkan prevalensi — pengacakan skor memperoleh AP sama dengan proporsi anomali (0,020 pada data kita), sehingga AP harus dibaca relatif terhadap angka sekecil itu.
ROC-AUC perlu diperlakukan dengan curiga pada kelas langka. Kurva ROC membandingkan true positive rate terhadap false positive rate, dan dengan 980 transaksi normal, false positive rate yang “kecil” pun berarti antrean tinjauan yang besar: 8% false positive rate sudah berarti ~80 transaksi wajar salah masuk antrean dari setiap 1.000. AUC yang mendekati 1,0 tetap kompatibel dengan presisi yang menyedihkan pada titik operasi yang terjangkau — dan makin langka anomalinya, makin lebar jurang ini.
Praktik: 1.000 Transaksi, 20 Anomali, dan Sapuan Titik Operasi #
Data uji kita berisi 1.000 transaksi TokoKita: 980 normal dalam dua kerumunan, sepuluh anomali yang berdiri jauh, dan sepuluh anomali yang sengaja ditempatkan menyentuh tepi kerumunan akhir pekan — meniru kenyataan bahwa sebagian anomali menyamar baik. Isolation Forest memberi skornya (dinegasi agar “makin tinggi = makin anomali”), lalu kita sapukan seluruh kerangka evaluasi: paradoks akurasi, tabel titik operasi lengkap dengan jumlah salah-tandai, AP versus baseline prevalensi, ROC-AUC, dan contamination sebagai kenop ambang.
# Evaluasi anomaly detection: akurasi menipu saat anomali langka;
# presisi-recall, average precision, dan kontaminasi sebagai kenop ambang.
import numpy as np
from sklearn.ensemble import IsolationForest
from sklearn.metrics import (
average_precision_score,
precision_score,
recall_score,
roc_auc_score,
)
rng = np.random.default_rng(42)
# 1) 1.000 transaksi: 980 normal (dua kerumunan) + 20 anomali (prevalensi 2%).
normal = np.vstack(
[
rng.normal(loc=[90.0, 6.0], scale=[15.0, 2.0], size=(490, 2)),
rng.normal(loc=[210.0, 11.0], scale=[25.0, 3.0], size=(490, 2)),
]
)
jauh = rng.uniform(low=[300.0, 1.0], high=[430.0, 8.0], size=(10, 2))
dekat = rng.uniform(low=[230.0, 16.0], high=[320.0, 26.0], size=(10, 2))
X = np.vstack([normal, jauh, dekat])
y_anom = np.concatenate([np.zeros(980, dtype=int), np.ones(20, dtype=int)])
# 2) Skor Isolation Forest, dinegasi agar "makin tinggi = makin anomali"
# sesuai konvensi average_precision dan ROC.
iso = IsolationForest(n_estimators=300, contamination="auto", random_state=42)
iso.fit(X)
skor = -iso.score_samples(X)
urut = np.argsort(skor)[::-1] # indeks transaksi, skor tertinggi lebih dulu
# 3) Paradoks akurasi: bandingkan model "semua normal" dengan detektor.
pred20 = np.zeros_like(y_anom)
pred20[urut[:20]] = 1
akurasi_trivial = float(np.mean(np.zeros_like(y_anom) == y_anom))
akurasi_detektor = float(np.mean(pred20 == y_anom))
recall_trivial = 0.0
recall_detektor = float(recall_score(y_anom, pred20, zero_division=0))
print(f"Prevalensi anomali: {y_anom.mean():.3f} (20 dari 1.000 transaksi)")
print("\nParadoks akurasi:")
print(f" model 'semua normal' : akurasi {akurasi_trivial:.3f}, recall {recall_trivial:.3f}")
print(f" detektor (k=20) : akurasi {akurasi_detektor:.3f}, recall {recall_detektor:.3f}")
print(" -> selisih akurasi 0,018; selisih recall 0,950")
# 4) Titik operasi: tandai k transaksi berskor tertinggi, hitung konsekuensinya.
print("\nTitik operasi (tandai k skor tertinggi):")
print(" k TP FP presisi recall")
for k in (5, 10, 20, 50, 100, 200):
pred = np.zeros_like(y_anom)
pred[urut[:k]] = 1
tp = int(((pred == 1) & (y_anom == 1)).sum())
fp = int(((pred == 1) & (y_anom == 0)).sum())
p = precision_score(y_anom, pred, zero_division=0)
r = recall_score(y_anom, pred, zero_division=0)
print(f" {k:>4d} {tp:>3d} {fp:>3d} {p:>7.3f} {r:>6.3f}")
# 5) Ringkasan satu angka: average precision dan ROC-AUC.
ap = average_precision_score(y_anom, skor)
auc = roc_auc_score(y_anom, skor)
print(f"\naverage_precision_score : {ap:.4f} (baseline acak = prevalensi {y_anom.mean():.3f})")
print(f"roc_auc_score : {auc:.4f}")
# 6) contamination sebagai kenop ambang pada predict().
# Kebiasaan scikit-learn: predict mengeluarkan -1 = anomali, 1 = normal;
# metrik presisi/recall meminta kelas positif bernilai 1 -> konversi biner.
print("\ncontamination sebagai kenop ambang (predict):")
print(" contamination ditandai presisi recall")
for c in (0.01, 0.02, 0.05, 0.10):
model = IsolationForest(n_estimators=300, contamination=c, random_state=42)
model.fit(X)
label = model.predict(X) # -1 = anomali, 1 = normal
pred = (label == -1).astype(int) # kelas positif = 1
p = precision_score(y_anom, pred, zero_division=0)
r = recall_score(y_anom, pred, zero_division=0)
print(f" {c:>13.2f} {int(pred.sum()):>8d} {p:>7.3f} {r:>6.3f}")Output yang Dihasilkan:
Prevalensi anomali: 0.020 (20 dari 1.000 transaksi)
Paradoks akurasi:
model 'semua normal' : akurasi 0.980, recall 0.000
detektor (k=20) : akurasi 0.998, recall 0.950
-> selisih akurasi 0,018; selisih recall 0,950
Titik operasi (tandai k skor tertinggi):
k TP FP presisi recall
5 5 0 1.000 0.250
10 10 0 1.000 0.500
20 19 1 0.950 0.950
50 20 30 0.400 1.000
100 20 80 0.200 1.000
200 20 180 0.100 1.000
average_precision_score : 0.9955 (baseline acak = prevalensi 0.020)
roc_auc_score : 0.9999
contamination sebagai kenop ambang (predict):
contamination ditandai presisi recall
0.01 10 1.000 0.500
0.02 20 0.950 0.950
0.05 50 0.400 1.000
0.10 100 0.200 1.000Mari kita bedah hasil eksekusinya:
- Baris 15–25 (data dua tingkat kesulitan): sepuluh anomali berdiri di wilayah kosong (mudah), sepuluh lainnya menyentuh tepi kerumunan akhir pekan (sulit). Label disimpan sebagai
y_anombernilai 0/1 — kelas positif 1 adalah anomali. - Baris 29–32 (skor dinegasi):
score_samplesmengembalikan nilai yang makin negatif untuk makin anomali; tanda minus membaliknya agar fungsi metrik — yang menganggap “makin tinggi = makin positif” — bekerja tanpa kejutan.np.argsort(...)[::-1]lalu memberi peringkat transaksi dari skor tertinggi. - Baris 34–45 (paradoks akurasi): model “semua normal” mencatat akurasi 0,980 dengan recall 0,000; detektor pada k=20 mencatat 0,998 dengan recall 0,950. Selisih akurasi 0,018 terlihat sepele justru karena mayoritas kelas normal menguasai perhitungan — bukti angka bahwa akurasi layak dibuang pada masalah kelas langka.
- Baris 47–57 (tabel titik operasi): pada k=5 dan k=10 antrean murni (presisi 1,000) namun separuh anomali masih lepas (recall 0,250 dan 0,500). Titik manis jatuh di k=20: 19 anomali tertangkap, satu transaksi wajar ikut tertandai (presisi 0,950, recall 0,950). Pada k=100, recall mentok di 1,000 tetapi 80 transaksi wajar memenuhi antrean — presisi 0,200 — dan k=200 membuang 180 peninjauan untuk nol anomali tambahan. Kurva presisi-recall inilah yang dirangkum AP.
- Baris 59–63 (AP vs AUC): AP 0,9955 harus dibaca terhadap baseline prevalensi 0,020 — hampir 50 kali lipat pengacakan, pemeringkatan yang sangat baik. ROC-AUC 0,9999 tampak serupa, tetapi baris k=100 di tabel di atas sudah membuktikan angka ini tidak menjelaskan kualitas antrean — inilah jebakan yang dibahas callout sebelumnya.
- Baris 65–77 (kontaminasi sebagai kenop): kolom
contamination0,01/0,02/0,05/0,10 menghasilkan baris yang persis mengikuti k=10/20/50/100 pada tabel titik operasi. Konfirmasi terakhir bahwa kontaminasi hanyalah cara lain memilih titik operasi — dan bahwa labelpredict(-1/1) perlu dikonversi menjadi kelas positif 1 sebelum masukprecision_scoredanrecall_score.
Menutup Section Anomaly Detection #
Benang merah section ini kini utuh: kita memulai dari kepadatan Gaussian sebagai baseline, menambah ambang statistik univariat, mengisolasi lewat potongan acak, membaca kepadatan lokal, melengkungkan batas di ruang kernel, dan akhirnya mengukur semuanya dengan kerangka yang adil terhadap kelas langka. Satu jenis keanehan masih luput dari seluruh alat kita: keanehan yang lahir dari waktu. Transaksi Rp500.000 terasa wajar pukul 14.00 dan mencurigakan pukul 03.00; lonjakan penjualan yang sah saat kampanye diskon berbeda dari lonjakan saat sensor rusak. Deteksi anomali kontekstual menuntut kosakata deret waktu — tren, musiman, dan stasionaritas — yang menjadi pembukaan section berikutnya, dimulai dari Chapter Konsep Deret Waktu.
Referensi Resmi #
- Outlier detection — scikit-learn User Guide — kerangka deteksi anomali beserta perbandingan metode sepanjang section ini.
- sklearn.metrics.average_precision_score — API Reference — definisi AP dan makna baseline prevalensi.
- sklearn.metrics.roc_auc_score — API Reference — definisi ROC-AUC yang perlu dibaca dengan curiga pada kelas langka.
- Classification metrics — scikit-learn User Guide
— presisi, recall, dan kurva presisi-recall dari
sklearn.metrics.