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:

AspekSupervervised (klasifikasi)Unsupervised (anomaly detection)
Data latihButuh contoh anomali berlabel, biasanya sedikitCukup data yang didominasi kondisi normal
Pertanyaan“Apakah mirip anomali yang dulu?”“Apakah menyimpang dari pola normal?”
KelemahanAnomali baru yang polanya beda tidak dikenaliPencilan ekstrem bisa dianggap “normal” jika banyak
Contoh alatRandomForestClassifier, XGBoostIsolationForest, 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.

python
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:

text
    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      40

Mari kita telaah baris-baris pentingnya:

  1. 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.
  2. 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=42 membuat pohon-pohon acak dapat direproduksi.
  3. Baris 19 (fit_predict): model belajar pola normal sekaligus mengembalikan label per baris — -1 untuk anomali, 1 untuk normal — yang kemudian diterjemahkan np.where menjadi teks agar mudah dibaca.
  4. 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:

python
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:

text
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   normal

Bedah hasilnya:

  1. Baris 14-15 (n_neighbors=5): setiap titik dinilai terhadap lima tetangga terdekatnya. Angka kecil membuat penilaian sangat lokal — dan mudah goyah.
  2. 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.
  3. 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 contamination mengatur porsi anomali yang diperkirakan.
  • Local Outlier Factor menilai kepadatan lokal relatif terhadap n_neighbors tetangga; 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: