Pada chapter Mendeteksi Pola dan Anomali kita sudah menandai hari penjualan janggal memakai z-score dan IQR — aturan ambang yang kita tetapkan sendiri di atas satu kolom. Di lapangan, anomali jarang sebersederhana itu: penipuan kartu, sensor rusak, atau kesalahan input tidak selalu ekstrem di satu kolom, melainkan berada pada kombinasi nilai yang tidak lazim — jumlah gelas enam cup dengan total Rp110 ribu terlihat wajar secara terpisah, tetapi janggal ketika keduanya dibaca bersama. Anomaly detection menyediakan model yang belajar pola “normal” dari data historis tanpa label, lalu menandai baris yang menyimpang dari pola tersebut. Pada chapter ini kita bandingkan dua pendekatan tak berlabel yang paling sering dipakai: Isolation Forest dan Local Outlier Factor.
Supervised dan Unsupervised Deteksi Anomali #
Cara menangani anomali bergantung pada satu pertanyaan: apakah kita punya contoh anomali yang sudah berlabel? Jawabannya menentukan dua alur yang sangat berbeda:
| Aspek | Supervervised (klasifikasi) | Unsupervised (anomaly detection) |
|---|---|---|
| Data latih | Butuh contoh anomali berlabel, biasanya sedikit | Cukup data yang didominasi kondisi normal |
| Pertanyaan | “Apakah mirip anomali yang dulu?” | “Apakah menyimpang dari pola normal?” |
| Kelemahan | Anomali baru yang polanya beda tidak dikenali | Pencilan ekstrem bisa dianggap “normal” jika banyak |
| Contoh alat | RandomForestClassifier, XGBoost | IsolationForest, LocalOutlierFactor |
Alur unsupervised menjadi andalan justru karena anomali langka: labelnya nyaris tidak pernah cukup untuk melatih classifier yang adil, dan modus penipuan terus berubah sehingga contoh lama cepat kedaluwarsa. Isolation Forest dan Local Outlier Factor bekerja tanpa label sama sekali — kita hanya perlu menebak seberapa besar porsi anomali di data, parameter yang dikenal sebagai contamination.
Isolation Forest: Mengisolasi yang Berbeda #
Isolation Forest memakai intuisi sederhana: titik anomali mudah dipisahkan. Algoritma membangun pohon pemisah acak — pilih satu fitur, pilih satu nilai ambang acak, belah data menjadi dua, ulangi sampai tiap titik berdiri sendiri. Titik yang jauh dari kerumunan hanya butuh sedikit pemisahan sebelum terisolasi; titik di tengah keramaian butuh pemisahan yang panjang. Rata-rata kedalaman isolasi di banyak pohon kemudian diringkas menjadi skor: makin pendek jalur isolasi, makin kuat kandidat anomali. Pendekatan ini membuatnya hemat komputasi dan tidak menuntut asumsi bentuk distribusi.
Kita coba pada data transaksi fiktif Toko Kopi Kita: 40 transaksi normal (1-5 gelas, harga Rp18-22 ribu per gelas) ditambah empat transaksi janggal yang kita tanam sendiri — dua borongan katering, satu total yang melonjak untuk tiga gelas, dan satu pembelian tepat di batas atas pola normal. Keempatnya hanya bisa dikenali dari kombinasi dua kolom, bukan dari satu kolom saja.
import numpy as np
import pandas as pd
from sklearn.ensemble import IsolationForest
rng = np.random.default_rng(42)
# 40 transaksi normal: jumlah gelas wajar, harga per gelas 18-22 ribu rupiah
jumlah_normal = rng.integers(1, 6, 40)
harga_normal = rng.uniform(18, 22, 40)
# 4 transaksi janggal: dua borongan katering, satu total melonjak, satu di batas
jumlah_anomali = np.array([6, 12, 3, 2])
total_anomali = np.array([115, 240, 160, 110])
transaksi = pd.DataFrame({
"jumlah_gelas": np.concatenate([jumlah_normal, jumlah_anomali]),
"total_ribu": np.concatenate([jumlah_normal * harga_normal, total_anomali]),
})
label_sebenarnya = np.array(["normal"] * 40 + ["anomali"] * 4)
model = IsolationForest(contamination=0.09, random_state=42)
prediksi = model.fit_predict(transaksi)
hasil = transaksi.copy()
hasil["sebenarnya"] = label_sebenarnya
hasil["prediksi"] = np.where(prediksi == -1, "anomali", "normal")
print(hasil[hasil["sebenarnya"] == "anomali"].to_string())
print()
print("Tabel silang label sebenarnya vs prediksi:")
print(pd.crosstab(hasil["sebenarnya"], hasil["prediksi"]))Output yang Dihasilkan:
jumlah_gelas total_ribu sebenarnya prediksi
40 6 115.0 anomali anomali
41 12 240.0 anomali anomali
42 3 160.0 anomali anomali
43 2 110.0 anomali anomali
Tabel silang label sebenarnya vs prediksi:
prediksi anomali normal
sebenarnya
anomali 4 0
normal 0 40Mari kita telaah baris-baris pentingnya:
- Baris 7-11 (pembuatan data): 40 transaksi normal dibangkitkan dengan
rng(42)— jumlah gelas 1 sampai 5, harga satuan seragam di rentang Rp18-22 ribu, sehingga total transaksinya berimpit di pita Rp20-110 ribu. Empat transaksi janggal diletakkan sebagai kejutan yang hanya terlihat dari gabungan kedua kolom. - Baris 18 (
contamination=0.09): kita memberi tahu model bahwa sekitar 9% data adalah anomali (4 dari 44). Angka ini mengatur ambang skor, bukan jumlah pohon;random_state=42membuat pohon-pohon acak dapat direproduksi. - Baris 19 (
fit_predict): model belajar pola normal sekaligus mengembalikan label per baris —-1untuk anomali,1untuk normal — yang kemudian diterjemahkannp.wheremenjadi teks agar mudah dibaca. - Bacaan hasilnya: keempat transaksi janggal tertangkap tanpa satu pun transaksi normal yang salah tandai. Skor transaksi (2, 110) memang paling tipis — nilainya nyaris menyentuh batas atas pola normal.
Local Outlier Factor: Menimbang Kepadatan Tetangga #
Local Outlier Factor (LOF) memandang anomali lewat keramaian sekitar: setiap titik dibandingkan kepadatan lokalnya dengan kepadatan k tetangga terdekatnya. Titik yang kepadatannya jauh lebih renggang dari para tetangganya mendapat LOF di atas 1 dan ditandai sebagai pencilan lokal. Kekuatan sekaligus kelemahannya ada pada kata “lokal”: LOF pandai menangkap anomali yang hanya janggal dibandingkan kelompoknya, tetapi sangat peka terhadap pilihan n_neighbors. Kita jalankan pada data yang sama:
import numpy as np
import pandas as pd
from sklearn.ensemble import IsolationForest
from sklearn.neighbors import LocalOutlierFactor
rng = np.random.default_rng(42)
jumlah_normal = rng.integers(1, 6, 40)
harga_normal = rng.uniform(18, 22, 40)
jumlah_anomali = np.array([6, 12, 3, 2])
total_anomali = np.array([115, 240, 160, 110])
transaksi = pd.DataFrame({
"jumlah_gelas": np.concatenate([jumlah_normal, jumlah_anomali]),
"total_ribu": np.concatenate([jumlah_normal * harga_normal, total_anomali]),
})
label_sebenarnya = np.array(["normal"] * 40 + ["anomali"] * 4)
lof = LocalOutlierFactor(n_neighbors=5)
prediksi_lof = lof.fit_predict(transaksi)
hasil_lof = transaksi.copy()
hasil_lof["sebenarnya"] = label_sebenarnya
hasil_lof["prediksi"] = np.where(prediksi_lof == -1, "anomali", "normal")
print("LocalOutlierFactor, tabel silang:")
print(pd.crosstab(hasil_lof["sebenarnya"], hasil_lof["prediksi"]))
model_if = IsolationForest(contamination=0.09, random_state=42)
model_if.fit(transaksi)
skor = model_if.decision_function(transaksi)
urutan = np.argsort(skor)[:5]
print()
print("Lima skor isolasi terendah (makin negatif = makin mudah diisolasi):")
peringkat = transaksi.iloc[urutan].copy()
peringkat["skor_isolasi"] = skor[urutan]
peringkat["label"] = label_sebenarnya[urutan]
print(peringkat.to_string())Output yang Dihasilkan:
LocalOutlierFactor, tabel silang:
prediksi anomali normal
sebenarnya
anomali 3 1
normal 4 36
Lima skor isolasi terendah (makin negatif = makin mudah diisolasi):
jumlah_gelas total_ribu skor_isolasi label
41 12 240.000000 -0.255345 anomali
42 3 160.000000 -0.096310 anomali
40 6 115.000000 -0.074177 anomali
43 2 110.000000 -0.041547 anomali
6 1 19.866884 0.006208 normalBedah hasilnya:
- Baris 14-15 (
n_neighbors=5): setiap titik dinilai terhadap lima tetangga terdekatnya. Angka kecil membuat penilaian sangat lokal — dan mudah goyah. - Tabel silang LOF: tiga anomali tertangkap, satu luput (transaksi (2, 110) yang memang bertengger di batas), tetapi empat transaksi normal ikut tertandai. Bandingkan dengan Isolation Forest yang bersih 44 dari 44 — bukan berarti LOF buruk, melainkan bahwa parameter dan struktur data menentukan hasil.
- Peringkat skor isolasi: empat skor terendah seluruhnya milik anomali buatan, dan skor kelima (sebuah transaksi normal satu gelas) sudah positif. Urutan skor semacam ini berguna di dunia nyata: alih-alih ambang biner, kita bisa menyerahkan 20 transaksi teratas kepada tim pemeriksa manual.
Rangkuman #
- Anomali sering hanya janggal pada kombinasi beberapa fitur, sehingga aturan satu kolom seperti z-score dan IQR tidak cukup; model tak berlabel menangkap pola bersama.
- Pendekatan supervised menuntut contoh anomali berlabel yang langka; pendekatan unsupervised cukup berpegang pada data yang didominasi kondisi normal.
- Isolation Forest mengisolasi titik dengan pemisahan acak: anomali terisolasi dalam sedikit langkah, dan
contaminationmengatur porsi anomali yang diperkirakan. - Local Outlier Factor menilai kepadatan lokal relatif terhadap
n_neighborstetangga; hasilnya peka pada parameter tersebut, seperti terlihat pada empat transaksi normal yang ikut tertandai. - Skor kontinu (
decision_function) memungkinkan anomali diperingkat sebagai daftar prioritas pemeriksaan manual.
Anomali pada data ini masih tersembunyi secara rapi di antara dua kolom. Bab berikutnya membahas masalah kebalikannya: data yang labelnya kita ketahui, tetapi jumlahnya timpang antara kelas yang satu dan lainnya — kita masuk ke Imbalanced Learning .
Referensi: