Pagar IQR dan z-score mengukur seberapa jauh sebuah nilai dari pusat kolomnya — pendekatan yang menjadi mahal dan rapuh ketika fitur berjumlah banyak dan keanehan justru lahir dari kombinasi antar fitur. Isolation Forest membalik logika deteksi sepenuhnya: alih-alih memodelkan kerumunan data normal lalu mengukur jarak ke sana, algoritme ini bertanya seberapa mudah sebuah titik dipisahkan dari yang lain lewat pemotongan acak. Titik yang jarang di sekitarnya terpotong-sendiri dalam beberapa langkah; titik di tengah kerumunan butuh banyak potongan. Pendekatan berbasis pohon ini membuat Isolation Forest menjadi detektor anomali paling populer untuk data berdimensi banyak — cepat, hampir beban-konfigurasi, dan tidak menuntut penskalaan fitur.

Ide Inti: Mengisolasi, Bukan Mengukur #

Bayangkan sebuah pohon keputusan yang dibangun secara acak: pilih fitur secara acak, pilih nilai potong secara acak antara minimum dan maksimum, belah data jadi dua, ulangi sampai setiap titik sendirian di daunnya sendiri. Sebuah anomali yang berdiri jauh dari kerumunan akan terisolasi dalam beberapa potongan pertama — sebagian besar potongan acak pun langsung memisahkannya. Sebaliknya, titik normal di tengah kerumunan harus diiris berulang kali sampai irisannya cukup tipis untuk memisahkannya dari tetangga-tetangganya.

Dari eksperimen acak semacam itu lahir ukuran panjang jalur (path length) h(x): banyaknya potongan yang dibutuhkan untuk mengisolasi titik x. Isolation Forest membangun ratusan pohon acak (secara bawaan pada subsample 256 titik per pohon, agar potongan tetap jarang dan cepat), lalu merata-ratakan panjang jalur tiap titik. Skor anomali dihitung dengan menormalkan rata-rata jalur terhadap c(n) — panjang jalur rata-rata pada pencarian biner tidak seimbang untuk n titik:

text
s(x, n) = 2 ^ ( - E[h(x)] / c(n) )

Nilai s mendekati 1 berarti sangat mudah diisolasi (anomali), mendekati 0,5 berarti sulit (normal). scikit-learn membalik tandanya pada score_samples: keluarannya bernilai negatif, dan semakin negatif semakin anomalis titik tersebut.

Tiga API Skor pada scikit-learn #

Satu kebingungan paling sering di lapangan: Isolation Forest punya tiga metode keluaran yang tampak berbeda padahal berasal dari skor yang sama. Ketiganya berhubungan lewat satu konstanta geser offset_:

MetodeRentang / maknaHubunganKapan dipakai
score_samples(x)negatif; makin negatif = makin anomaliskor mentah s yang dinegasipemeringkatan, sweep ambang, evaluasi
decision_function(x)di sekitar 0; negatif = anomaliscore_samples - offset_skor yang nyaman dibaca manusia
predict(x)tepat 1 atau -1tanda decision_functionbutuh label keputusan langsung

offset_ ditetapkan dari persentil skor latih sesuai contamination — inilah sebab contamination murni kenop ambang, bukan parameter model.

Praktik: Dua Kerumunan Belanja dan Lima Belas Anomali #

Data TokoKita kembali dipakai, kali ini dengan struktur dua kerumunan yang lebih kaya: 150 pelanggan harian (nilai kecil, keranjang rapat) dan 150 pelanggan akhir pekan (nilai besar, keranjang longgar). Lima belas anomali disuntik di wilayah yang tidak dihuni kedua kerumunan — nilai besar dengan keranjang jauh lebih penuh. Setelah melatih model, kita tidak hanya melihat skornya: kita juga mengukur langsung kedalaman rata-rata jalur isolasi tiap titik pada 200 pohon, sehingga mekanisme “cepat terisolasi” terlihat sebagai angka, bukan slogan.

python
# Isolation Forest: anomali dipisahkan dengan potongan acak, bukan diukur.

import numpy as np
from sklearn.ensemble import IsolationForest

rng = np.random.default_rng(42)

# 1) Transaksi normal TokoKita, dua kelompok belanja (ribu Rp, jumlah item):
#    pelanggan harian (kecil, rapat) dan pelanggan akhir pekan (besar).
harian = rng.normal(loc=[80.0, 5.0], scale=[12.0, 1.5], size=(150, 2))
pekan = rng.normal(loc=[190.0, 12.0], scale=[20.0, 2.0], size=(150, 2))

# 2) Lima belas anomali suntikan: nilai besar, keranjang jauh lebih penuh
#    daripada kebiasaan kedua kelompok manapun.
anomali = rng.uniform(low=[260.0, 20.0], high=[430.0, 38.0], size=(15, 2))

X = np.vstack([harian, pekan, anomali])
y_anomali = np.array([0] * 300 + [1] * 15)

# 3) Latih Isolation Forest. contamination=0.05 -> kira-kira 5% skor
#    paling ekstrem dilabeli anomali oleh predict().
iso = IsolationForest(n_estimators=200, contamination=0.05, random_state=42)
iso.fit(X)
prediksi = iso.predict(X)  # 1 = normal, -1 = anomali
skor_mentah = iso.score_samples(X)  # makin negatif = makin anomali
skor_geser = iso.decision_function(X)  # skor_mentah - offset_

ditandai = prediksi == -1
benar = int((ditandai & (y_anomali == 1)).sum())
salah = int((ditandai & (y_anomali == 0)).sum())
print(f"predict menandai {int(ditandai.sum())} transaksi:")
print(f"  {benar} anomali suntikan + {salah} transaksi normal")
print(f"offset_ = {iso.offset_:.3f}")
selisih = float(np.max(np.abs(skor_geser - (skor_mentah - iso.offset_))))
print(f"cek: max |decision_function - (score_samples - offset_)| = {selisih:.6f}")

# 4) Skor mentah: bandingkan kelompok anomali vs kelompok normal.
print("\nSkor mentah score_samples (makin negatif = makin anomali):")
print(f"  rata-rata transaksi normal : {skor_mentah[y_anomali == 0].mean():.3f}")
print(f"  rata-rata anomali suntikan : {skor_mentah[y_anomali == 1].mean():.3f}")

# 5) Mekanisme inti: kedalaman rata-rata jalur isolasi pada tiap pohon.
def kedalaman_rata(model, x_baris):
    total = 0.0
    for pohon in model.estimators_:
        jalur = pohon.decision_path(x_baris)[0]
        total += jalur.nnz - 1  # jumlah simpul yang dilalui dikurangi akar
    return total / len(model.estimators_)


idx_normal_tersulit = int(np.argmin(skor_mentah[y_anomali == 0]))
idx_anom_termudah = 300 + int(np.argmin(skor_mentah[y_anomali == 1]))
idx_anom_tersulit = 300 + int(np.argmax(skor_mentah[y_anomali == 1]))

pilihan = [
    (0, "harian (normal)"),
    (150, "pekan (normal)"),
    (idx_normal_tersulit, "normal tersulit"),
    (idx_anom_tersulit, "anomali tersulit"),
    (idx_anom_termudah, "anomali termudah"),
]
print("\nLima titik terpilih (nilai_ribu, item):")
print("  nilai_ribu  item  kedalaman  skor_mentah  skor_geser  pred  jenis")
for i, jenis in pilihan:
    baris = X[i].reshape(1, -1)
    print(
        f"  {X[i, 0]:>9.1f}  {X[i, 1]:>4.1f}  {kedalaman_rata(iso, baris):>9.2f}"
        f"  {skor_mentah[i]:>12.3f}  {skor_geser[i]:>11.3f}"
        f"  {prediksi[i]:>4d}  {jenis}"
    )

# 6) Kontaminasi hanya menggeser offset_, tidak mengubah skor mentah.
iso10 = IsolationForest(n_estimators=200, contamination=0.10, random_state=42)
iso10.fit(X)
sama = np.allclose(iso.score_samples(X), iso10.score_samples(X))
ditandai10 = iso10.predict(X) == -1
print(f"\ncontamination 0.05 -> 0.10:")
print(f"  skor mentah identik : {sama}")
print(f"  offset_ berubah     : {iso.offset_:.3f} -> {iso10.offset_:.3f}")
print(f"  jumlah ditandai     : {int(ditandai.sum())} -> {int(ditandai10.sum())}")

Output yang Dihasilkan:

text
predict menandai 16 transaksi:
  15 anomali suntikan + 1 transaksi normal
offset_ = -0.587
cek: max |decision_function - (score_samples - offset_)| = 0.000000

Skor mentah score_samples (makin negatif = makin anomali):
  rata-rata transaksi normal : -0.443
  rata-rata anomali suntikan : -0.690

Lima titik terpilih (nilai_ribu, item):
  nilai_ribu  item  kedalaman  skor_mentah  skor_geser  pred  jenis
       83.7   3.4       7.93        -0.429        0.158     1  harian (normal)
      224.5   8.9       7.58        -0.528        0.059     1  pekan (normal)
       97.7   1.2       6.00        -0.623       -0.035    -1  normal tersulit
      299.9  31.2       5.98        -0.662       -0.075    -1  anomali tersulit
      396.4  36.6       4.46        -0.739       -0.152    -1  anomali termudah

contamination 0.05 -> 0.10:
  skor mentah identik : True
  offset_ berubah     : -0.587 -> -0.530
  jumlah ditandai     : 16 -> 32

Mari kita bedah hasil eksekusinya:

  1. Baris 22–26 (tiga API): model dilatih dengan n_estimators=200 pohon acak; ketiga metode dipanggil pada data yang sama. Angka verifikasi pada baris 34–35 membuktikan hubungan ketiganya: selisih maksimum decision_function terhadap score_samples - offset_ tepat nol.
  2. Baris 31–32 (hasil predict): 16 transaksi ditandai — 15 anomali suntikan penuh plus satu transaksi normal. Dengan contamination=0.05 pada 315 titik, kuota ditandai memang sekitar 16; satu di antaranya jatuh ke pelanggan tepi, seperti kita lihat di tabel.
  3. Baris 38–40 (skor kelompok): rata-rata skor mentah −0,443 untuk transaksi normal versus −0,690 untuk anomali — pemisahan yang jelas, meski kedua distribusi bersinggungan di tepinya.
  4. Baris 42–48 (kedalaman jalur): fungsi kedalaman_rata berjalan menelusuri decision_path tiap pohon lalu menghitung banyaknya simpul yang dilalui. Pelanggan harian dan akhir pekan yang representatif perlu sekitar 7,6–7,9 potongan sebelum terisolasi; anomali termudah (Rp396,4 ribu, 36,6 item) cukup 4,46 potongan — teori “anomali terisolasi lebih cepat” tampak sebagai angka nyata.
  5. Baris 51–70 (tabel lima titik): dua baris tengah adalah pelajaran terpenting tabel ini. Normal tersulit (Rp97,7 ribu, 1,2 item — pelanggan sah di tepi klaster harian) terisolasi pada kedalaman 6,00 dengan skor −0,623, sedangkan anomali tersulit (Rp299,9 ribu, 31,2 item) terisolasi pada 5,98 dengan skor −0,662. Kedalaman yang nyaris sama membuat keduanya sama-sama tertandai: di perbatasan, deteksi anomali memang bersifat kelabu, bukan hitam-putih.
  6. Baris 72–80 (kenop kontaminasi): melatih ulang dengan contamination=0.10 menghasilkan skor mentah yang identik (True) — hanya offset_ bergeser dari −0,587 ke −0,530 sehingga jumlah yang ditandai naik dari 16 ke 32. Konfirmasi langsung bahwa kontaminasi adalah kenop ambang, bukan bahan pembelajaran.

Kekuatan dan Batas #

Kompleksitas Isolation Forest nyaris linier pada jumlah data dan ia bekerja baik pada dimensi tinggi karena tiap pohon hanya melihat sebagian kecil potongan. Batasnya juga khas keluarga pohon: anomali yang hidup di dalam wilayah berkerumun (misalnya dua transaksi identik berulang dari satu akun) tidak akan pernah terisolasi lebih cepat dari tetangganya, dan “kepadatan lokal” tidak masuk dalam kosakatanya — potongan acak tidak peduli bahwa sebuah titik berdiri di wilayah yang memang jarang penghuninya. Masalah tepat itulah yang dijawab metode berikutnya. Chapter Local Outlier Factor mengganti pertanyaan “seberapa cepat titik ini terisolasi” dengan “seberapa kosong lingkungan titik ini dibanding lingkungan tetangganya”.

Referensi Resmi #