Isolation Forest menghakimi titik lewat kemudahan pemisahannya — ukuran yang berlaku seragam di seluruh ruang fitur. Data nyata sering tidak serumit itu: satu wilayah boleh jadi padat penghuni, wilayah lain memang selalu jarang, dan keduanya sama normalnya. Transaksi pelanggan harian TokoKita menumpuk rapat di nilai kecil, sedangkan pelanggan akhir pekan tersebar longgar di nilai besar; sebuah titik yang terlihat “jauh dari segalanya” menurut ukuran global bisa jadi penghuni sah wilayah yang memang longgar. Local Outlier Factor (LOF) mengganti pertanyaan global dengan pertanyaan lokal: seberapa kosong lingkungan titik ini dibandingkan dengan lingkungan tetangga-tetangganya sendiri?
Kepadatan Lokal sebagai Tolok Ukur #
LOF membangun ukurannya dari tetangga-k: untuk setiap titik, ambil k tetangga terdekat (parameter n_neighbors). Dari sana lahir dua besaran bertingkat. Pertama, reachability distance — jarak ke tetangga o yang tidak boleh lebih kecil daripada jarak tetangga-ke-o sendiri, sehingga titik yang sangat berhimpit tidak saling menghitung jarak nol:
reach-dist_k(x, o) = max( k-distance(o), d(x, o) )Kedua, local reachability density (lrd) — kebalikan dari rata-rata reachability distance ke k tetangga: lingkungan sempit berarti padat. Skor LOF lalu membandingkan kepadatan titik dengan kepadatan para tetangganya:
LOF_k(x) = rata-rata atas tetangga o dari lrd(o) / lrd(x)Hasilnya mudah dibaca: LOF ≈ 1 berarti kepadatan titik sebanding tetangganya — normal, di klaster padat maupun jarang. LOF jauh di atas 1 berarti tetangga-tetangga titik tersebut jauh lebih padat, sehingga titik itu mengangkang di wilayah yang jauh lebih kosong dari seharusnya menurut standar lokalnya. Tidak ada ambang global tentang “seberapa jauh dari pusat”; yang dihakimi adalah ketidakselarasan dengan lingkungan.
Dua Mode: Outlier dan Novelty #
scikit-learn menyediakan LOF dalam dua pengaturan yang tidak boleh tertukar:
| Aspek | Mode bawaan (outlier) | Mode novelty=True |
|---|---|---|
Data saat fit | Campuran normal + anomali | Hanya data normal bersih |
| Metode yang tersedia | fit_predict pada data latih; predict untuk data baru tidak tersedia | predict, decision_function, score_samples untuk data baru |
| Pertanyaan yang dijawab | Titik mana di data ini yang menyimpang? | Apakah titik baru ini sepadan dengan data normal? |
Atribut negative_outlier_factor_ tersedia di kedua mode dan berisi LOFT yang sudah dinegasi: nilai −1,0 berarti normal sempurna, makin negatif makin anomali. Pada mode novelty=True, score_samples(x_baru) memberi LOF titik baru yang juga dinegasi, dan predict menerapkan ambang dari contamination (bawaan "auto" memakai ambang LOF 1,5 dari makalah aslinya).
Praktik: Dua Kerumunan Berkepadatan Beda #
Eksperimen kita memakai 260 transaksi normal bersih dalam dua kerumunan: klaster padat pelanggan harian (nilai Rp60 ribu, sebaran sempit) dan klaster jarang pelanggan akhir pekan (nilai Rp170 ribu, sebaran lebar). Demo berjalan tiga babak: mode outlier dengan tiga titik suntikan, mode novelty dengan enam titik baru — termasuk sepasang titik yang berjarak sama persis (16 unit) dari pusat klaster masing-masing — dan sapuan n_neighbors untuk melihat betapa menentukannya definisi “lokal”.
# Local Outlier Factor: kepadatan lokal dibandingkan kepadatan tetangga.
import numpy as np
from sklearn.neighbors import LocalOutlierFactor
rng = np.random.default_rng(42)
# 1) Data normal TokoKita: dua kerumunan dengan kepadatan sangat berbeda.
# Klaster padat: pelanggan harian. Klaster jarang: pelanggan akhir pekan.
padat = rng.normal(loc=[60.0, 4.0], scale=[4.0, 0.8], size=(200, 2))
jarang = rng.normal(loc=[170.0, 11.0], scale=[25.0, 3.0], size=(60, 2))
X_latih = np.vstack([padat, jarang]) # 260 transaksi normal bersih
# 2) Mode outlier detection: tiga titik disuntik lalu LOF difit biasa.
suntikan = np.array(
[
[110.0, 20.0], # jurang antara dua kerumunan
[35.0, 8.0], # jauh di luar klaster padat
[215.0, 14.0], # tepi klaster jarang (kasus borderline)
]
)
X_campur = np.vstack([X_latih, suntikan])
lof = LocalOutlierFactor(n_neighbors=20)
label = lof.fit_predict(X_campur) # 1 = normal, -1 = anomali
lof_nilai = -lof.negative_outlier_factor_ # LOF positif; ~1 = normal
print("Mode outlier detection (fit pada data yang berisi anomali):")
print(" nilai_ribu item LOF pred jenis")
titik_pilihan = [
(0, "padat (normal)"),
(200, "jarang (normal)"),
(260, "jurang antar kerumunan"),
(261, "jauh dari klaster padat"),
(262, "tepi klaster jarang"),
]
for i, jenis in titik_pilihan:
print(
f" {X_campur[i, 0]:>9.1f} {X_campur[i, 1]:>4.1f} {lof_nilai[i]:>5.2f}"
f" {label[i]:>4d} {jenis}"
)
# 3) Mode novelty detection: LOF dilatih HANYA pada data bersih,
# lalu menghakimi titik baru yang belum pernah dilihat.
lof_baru = LocalOutlierFactor(n_neighbors=20, novelty=True).fit(X_latih)
baru = np.array(
[
[63.0, 4.4], # di dalam klaster padat
[180.0, 11.5], # di dalam klaster jarang
[76.0, 4.0], # 16 unit dari pusat padat (4,0 sigma)
[186.0, 11.0], # 16 unit dari pusat jarang (0,6 sigma)
[115.0, 18.0], # jurang antar kerumunan
[240.0, 30.0], # jauh dari semua kerumunan
]
)
jenis_baru = [
"dalam klaster padat",
"dalam klaster jarang",
"16 unit dari pusat padat",
"16 unit dari pusat jarang",
"jurang antar kerumunan",
"jauh dari semua",
]
pred_baru = lof_baru.predict(baru) # 1 = normal, -1 = novelty
skor_baru = lof_baru.score_samples(baru) # -LOF titik baru
print("\nMode novelty detection (fit hanya pada 260 data bersih):")
print(" nilai_ribu item LOF pred jenis")
for i in range(len(baru)):
print(
f" {baru[i, 0]:>9.1f} {baru[i, 1]:>4.1f} {-skor_baru[i]:>5.2f}"
f" {pred_baru[i]:>4d} {jenis_baru[i]}"
)
# 4) n_neighbors mendefinisikan seberapa "lokal" kata lokal itu.
print("\nPengaruh n_neighbors pada LOF titik baru:")
print(" jenis k=5 k=20 k=50")
indeks_pilih = [2, 3, 4, 5] # empat titik terakhir
lof_per_k = {}
for k in (5, 20, 50):
model = LocalOutlierFactor(n_neighbors=k, novelty=True).fit(X_latih)
lof_per_k[k] = -model.score_samples(baru)
for i in indeks_pilih:
baris = (
f" {jenis_baru[i]:<26s}"
+ "".join(f" {lof_per_k[k][i]:>4.2f}" for k in (5, 20, 50))
)
print(baris)Output yang Dihasilkan:
Mode outlier detection (fit pada data yang berisi anomali):
nilai_ribu item LOF pred jenis
61.2 3.2 1.06 1 padat (normal)
165.5 11.6 0.97 1 jarang (normal)
110.0 20.0 3.46 -1 jurang antar kerumunan
35.0 8.0 8.12 -1 jauh dari klaster padat
215.0 14.0 1.51 -1 tepi klaster jarang
Mode novelty detection (fit hanya pada 260 data bersih):
nilai_ribu item LOF pred jenis
63.0 4.4 0.96 1 dalam klaster padat
180.0 11.5 0.98 1 dalam klaster jarang
76.0 4.0 3.43 -1 16 unit dari pusat padat
186.0 11.0 1.05 1 16 unit dari pusat jarang
115.0 18.0 2.17 -1 jurang antar kerumunan
240.0 30.0 2.78 -1 jauh dari semua
Pengaruh n_neighbors pada LOF titik baru:
jenis k=5 k=20 k=50
16 unit dari pusat padat 3.17 3.43 4.14
16 unit dari pusat jarang 0.99 1.05 0.99
jurang antar kerumunan 1.36 2.17 6.30
jauh dari semua 4.86 2.78 1.59Mari kita bedah hasil eksekusinya:
- Baris 23–25 (mode outlier):
fit_predictmengembalikan label dannegative_outlier_factor_menyimpan LOF yang dinegasi; tanda minus di baris 25 mengembalikannya ke bentuk positif yang enak dibaca (≈1 berarti normal). - Baris 29–40 (tabel mode outlier): penghuni klaster padat dan klaster jarang sama-sama mencatat LOF dekat 1 (1,06 dan 0,97) meski kepadatan keduanya puluhan kali berbeda — inilah makna “lokal”. Titik di jurang antar kerumunan mencatat 3,46, titik jauh di luar klaster padat mencatat 8,12, dan titik tepi klaster jarang mencatat 1,51 — hanya sedikit di atas ambang bawaan 1,5 sehingga tetap tertandai. LOF memberi gradien kecurigaan yang berkesinambungan, bukan vonis biner.
- Baris 44–71 (mode novelty): pasangan baris ketiga-keempat adalah inti chapter ini. Titik (76,0; 4,0) berjarak 16 unit dari pusat klaster padat — empat simpangan baku — dan dihukum LOF 3,43 dengan label novelty. Titik (186,0; 11,0) berjarak 16 unit yang sama dari pusat klaster jarang — hanya 0,6 simpangan baku — dan dinyatakan normal dengan LOF 1,05. Jarak absolut yang sama, vonis berlawanan, karena standar pemberiannya adalah kepadatan lingkungan masing-masing.
- Baris 73–86 (sapuan
n_neighbors): parameter k ternyata menentukan hasil secara hakiki. Pada k=5, titik jurang hanya mencatat LOF 1,36 — di bawah ambang 1,5 dan lolos dari deteksi — karena lima tetangganya yang kebetulan berjarak mirip membuat lingkungannya tampak wajar. Pada k=50, titik “jauh dari semua” turun ke LOF 1,59: lingkungan yang diperluas sampai mencakup klaster jarang membuat perbandingannya semakin global dan anomali murni justru tampak biasa saja. k=20 memberi titik tengah yang masuk akal untuk kedua kasus.
Memilih n_neighbors #
Sapuan di atas merangkum dilema pemilihan k. Nilai kecil membuat LOF peka terhadap struktur halus namun berisiko bising — klaster kecil beranggota kurang dari k bisa seluruhnya dianggap anomali. Nilai besar membuat perbandingan makin global dan keunggulan LOF atas metode jarak biasa semakin menipis. Dokumentasi scikit-learn memberi patokan yang tepat: pilih n_neighbors lebih besar daripada jumlah anggota minimum klaster sah terkecil di data, agar klaster tersebut punya cukup anggota untuk menjadi acuan normalitas; praktik umumnya berkisar 10–50, lalu diperiksa lewat stabilitas skor seperti sapuan kita.
LOF menuntaskan masalah kepadatan berbeda-beda dengan anggun, tetapi menyisakan dua keinginan: model yang ringkas untuk dipakai berulang, dan cara bekerja yang andal ketika dimensi fitur bertambah banyak. Kedua keinginan itu mengarah ke pendekatan kernel: melengkungkan batas di ruang fitur transformasi. Chapter One-Class SVM membangun batas tersebut, membahas makna parameter nu yang mengikat pecahan anomali latih, dan menunjukkan kenapa penskalaan fitur menjadi syarat mutlak di sana.
Referensi Resmi #
- Local Outlier Factor — scikit-learn User Guide
— penjelasan resmi lrd, LOF, dan aturan pemakaian mode
novelty. - sklearn.neighbors.LocalOutlierFactor — API Reference
— definisi
negative_outlier_factor_,score_samples, dann_neighbors.