Bagian 2 menutup dengan satu celah yang disebut namanya: aturan satu dimensi buta terhadap kombinasi — checkout kilat bernominal sedang pada jam normal lolos dari z-score, kuantil, maupun aturan jam. Bagian ini menutup celahnya dengan model multi-dimensi. Isolation Forest — algoritme yang mengisolasi anomali lewat pemisahan acak berulang, sudah kenal kita dari Section Anomaly Detection — dilatih pada enam fitur transaksi terskala, diperbandingkan dengan Local Outlier Factor, lalu diadu langsung dengan aturan statistik Bagian 2. Seperti sebelumnya, kolom is_fraud tetap tersegel: model belajar pola keramaian tanpa tahu mana anomali sejati.

Rekayasa Fitur: Membuat Kombinasi Terlihat #

Kekuatan model multi-dimensi berdiri di atas fitur yang dirancang. Empat kolom dasar (nominal, jam, durasi, jumlah item) dilengkapi dua transformasi dan dua fitur rasio. log1p pada nominal dan durasi meredam kemiringan +14,28 yang ditemukan Bagian 1 — tanpa ini, jarak Euclidean di ruang fitur didominasi ekor kanan nominal. Fitur durasi_per_item lahir dari penalaran domain: durasi checkout normal sebanding jumlah item, sehingga rasio yang terlalu kecil menandai proses yang tidak manusiawi cepatnya; nominal_per_item menangkap kebalikannya. Kolom menit dan hari-dalam-minggu sengaja dijatuhkan — eksplorasi Bagian 1 menunjukkan keduanya seragam, dan fitur tanpa sinyal hanya menambah derau pada pengukuran jarak.

Isolation Forest dan Local Outlier Factor Berdampingan #

python
# Model anomali: Isolation Forest dan Local Outlier Factor pada fitur
# transaksi terskala, dibandingkan dengan aturan statistik Bagian 2.
# Pipeline fitur:
#   - fitur dasar : nominal_ribu, jam_transaksi, durasi_detik, jumlah_item
#     (menit dan hari_dalam_minggu dijatuhkan: seragam, tanpa sinyal).
#   - transformasi log1p pada nominal dan durasi (skewness Bagian 1).
#   - fitur turunan durasi_per_item dan nominal_per_item: durasi normal
#     sebanding jumlah item, sehingga rasionya membidik pola kombinasi
#     yang tidak wajar.
# IsolationForest(n_estimators=200, contamination=0.012, random_state=42):
# skor predict -1 = anomali; decision_function = skor kontinu (makin
# kecil makin anomali). Skrip TIDAK membaca is_fraud. Jalankan dari
# root proyek: python3 src/04_model.py

import numpy as np
import pandas as pd
from sklearn.ensemble import IsolationForest
from sklearn.neighbors import LocalOutlierFactor
from sklearn.preprocessing import StandardScaler

df = pd.read_csv("data/transaksi.csv")

# ---------- Rekayasa fitur ----------
nominal = df["nominal_ribu"].to_numpy()
jam = df["jam_transaksi"].to_numpy()
durasi = df["durasi_detik"].to_numpy()
item = df["jumlah_item"].to_numpy()

durasi_per_item = durasi / np.maximum(item, 1)
nominal_per_item = nominal / np.maximum(item, 1)

matriks = np.column_stack(
    [
        np.log1p(nominal),
        jam,
        np.log1p(durasi),
        item,
        np.log1p(durasi_per_item),
        np.log1p(nominal_per_item),
    ]
)
NAMA_FITUR = [
    "log1p(nominal_ribu)",
    "jam_transaksi",
    "log1p(durasi_detik)",
    "jumlah_item",
    "log1p(durasi_per_item)",
    "log1p(nominal_per_item)",
]
X = StandardScaler().fit_transform(matriks)

print("== Rekayasa fitur model ==")
print(f"fitur dasar  : 4 kolom (menit, hari_dalam_minggu dijatuhkan - seragam)")
print(f"fitur turunan: log1p(nominal), log1p(durasi), 2 fitur rasio")
print(f"total fitur  : {len(NAMA_FITUR)} -> {NAMA_FITUR}")
print(f"penskalaan   : StandardScaler (mean 0, std 1 per fitur)")

# ---------- Isolation Forest ----------
model = IsolationForest(
    n_estimators=200,
    contamination=0.012,
    random_state=42,
)
model.fit(X)
label_if = model.predict(X)          # -1 anomali, 1 normal
skor_if = model.decision_function(X)  # makin kecil makin anomali
tertanda_if = label_if == -1

print("\n== Isolation Forest ==")
print(f"parameter    : n_estimators=200, contamination=0.012, random_state=42")
print(f"tertandai    : {int(tertanda_if.sum())} transaksi "
      f"(skor predict -1, {tertanda_if.mean():.2%} dari data)")
print(f"skor decision: min {skor_if.min():.3f}, median {np.median(skor_if):.3f}, "
      f"maks {skor_if.max():.3f}")

# ---------- Local Outlier Factor ----------
lof = LocalOutlierFactor(n_neighbors=20)
label_lof = lof.fit_predict(X)
tertanda_lof = label_lof == -1
print("\n== Local Outlier Factor (pembanding) ==")
print(f"parameter    : n_neighbors=20")
print(f"tertandai    : {int(tertanda_lof.sum())} transaksi "
      f"({tertanda_lof.mean():.2%} dari data)")

# ---------- Sepuluh transaksi paling anomali versi model ----------
urut = np.argsort(skor_if)
df_top = df.iloc[urut[:10]][
    ["id_transaksi", "nominal_ribu", "jam_transaksi", "durasi_detik",
     "jumlah_item"]
].copy()
df_top["skor_if"] = skor_if[urut[:10]]
print("\n== Sepuluh transaksi dengan skor Isolation Forest terendah ==")
print(df_top.to_string(index=False))

# ---------- Perbandingan dengan aturan statistik Bagian 2 ----------
rata = float(nominal.mean())
std = float(nominal.std())
q99 = float(np.quantile(nominal, 0.99))
aturan_z = (nominal - rata) / std >= 3.0
aturan_kuantil = nominal >= q99
aturan_jam = (jam >= 2) & (jam <= 5)
skor_stat = (
    aturan_z.astype(int) + aturan_kuantil.astype(int) + aturan_jam.astype(int)
)
tertanda_stat = skor_stat >= 1

print("\n== Perbandingan pendekatan ==")
print(f"{'metode':38s} {'tertandai':>10s}")
print(f"{'aturan statistik (skor >= 1)':38s} {int(tertanda_stat.sum()):10d}")
print(f"{'Isolation Forest (contamination 1,2%)':38s} {int(tertanda_if.sum()):10d}")
print(f"{'Local Outlier Factor (n=20)':38s} {int(tertanda_lof.sum()):10d}")

keduanya = tertanda_stat & tertanda_if
hanya_stat = tertanda_stat & ~tertanda_if
hanya_if = ~tertanda_stat & tertanda_if
print(f"\naturan & model sekaligus : {int(keduanya.sum())}")
print(f"hanya aturan statistik    : {int(hanya_stat.sum())}")
print(f"hanya Isolation Forest    : {int(hanya_if.sum())}")
print(f"tidak keduanya            : {int((~tertanda_stat & ~tertanda_if).sum())}")

print("\n== Sebaran skor statistik pada 60 transaksi tertanda model ==")
for s in range(3):
    n = int((skor_stat[tertanda_if] == s).sum())
    print(f"skor statistik {s}: {n:3d} transaksi")
print("-> transaksi ber skor statistik 0 luput dari seluruh aturan Bagian 2")

Output yang Dihasilkan:

text
== Rekayasa fitur model ==
fitur dasar  : 4 kolom (menit, hari_dalam_minggu dijatuhkan - seragam)
fitur turunan: log1p(nominal), log1p(durasi), 2 fitur rasio
total fitur  : 6 -> ['log1p(nominal_ribu)', 'jam_transaksi', 'log1p(durasi_detik)', 'jumlah_item', 'log1p(durasi_per_item)', 'log1p(nominal_per_item)']
penskalaan   : StandardScaler (mean 0, std 1 per fitur)

== Isolation Forest ==
parameter    : n_estimators=200, contamination=0.012, random_state=42
tertandai    : 60 transaksi (skor predict -1, 1.20% dari data)
skor decision: min -0.116, median 0.169, maks 0.232

== Local Outlier Factor (pembanding) ==
parameter    : n_neighbors=20
tertandai    : 94 transaksi (1.88% dari data)

== Sepuluh transaksi dengan skor Isolation Forest terendah ==
 id_transaksi  nominal_ribu  jam_transaksi  durasi_detik  jumlah_item   skor_if
         3926         361.6             16            42            8 -0.115590
         3645         625.5             19            26            8 -0.114725
         3881         450.5             17            29            8 -0.113452
         3845         517.0             18            41            8 -0.109419
         4085         376.1             11            53            8 -0.107813
         4629         658.0             13            27            8 -0.100251
         3699         557.7             11            37            8 -0.099983
         3654         429.4             17            26            7 -0.099676
         3591         580.3             15            41            8 -0.097617
         1043        6595.1             21           127            1 -0.095591

== Perbandingan pendekatan ==
metode                                  tertandai
aturan statistik (skor >= 1)                   84
Isolation Forest (contamination 1,2%)          60
Local Outlier Factor (n=20)                    94

aturan & model sekaligus : 32
hanya aturan statistik    : 52
hanya Isolation Forest    : 28
tidak keduanya            : 4888

== Sebaran skor statistik pada 60 transaksi tertanda model ==
skor statistik 0:  28 transaksi
skor statistik 1:  12 transaksi
skor statistik 2:  20 transaksi
-> transaksi ber skor statistik 0 luput dari seluruh aturan Bagian 2

Mari kita bedah arsitektur dan hasilnya:

  1. Baris 24–50 (matriks fitur): dua rasio dibangun dengan np.maximum(item, 1) menjaga pembagian dari nol, log1p meredam kemiringan, dan StandardScaler menyetarakan enam dimensi. Tanpa penskalaan, jam (0–23) dan nominal terskala-log akan berjarak berbeda orde — model berbasis pemisahan acak kehilangan arti.
  2. Baris 59–67 (Isolation Forest): contamination=0.012 memberi tahu model seberapa banyak anomali yang diharapkan (1,2% — estimasi dari desain data), random_state=42 mengunci determinisme pemisahan acak, n_estimators=200 menstabilkan skor. predict() menghasilkan label biner; decision_function() menghasilkan skor kontinu — skor inilah yang dipakai Bagian 4 untuk menggeser ambang.
  3. Baris 77–83 (Local Outlier Factor): pembanding berbasis kepadatan lokal dengan n_neighbors=20. LOF menandai 94 transaksi — hampir 60% lebih banyak dari IF.
  4. Sepuluh teratas IF: perhatikan baris pertama — transaksi 3926 bernominal 361,6 ribu pada jam 16, durasi 42 detik, 8 item. Tidak ada satu aturan Bagian 2 pun yang akan menandai transaksi ini; durasi_per_item-lah yang membuatnya menonjol sebagai checkout yang tidak wajar cepatnya. Ini bukti hidup nilai fitur kombinasi. Baris terakhir tabel (transaksi 1043, nominal 6595,1) menunjukkan sisi lain: anomali nominal ekstrem juga tetap terdeteksi model.
  5. Baris 95–119 (perbandingan tiga pendekatan): 84 (aturan) vs 60 (IF) vs 94 (LOF) — tetapi yang lebih penting adalah irisan: hanya 32 transaksi ditandai aturan sekaligus IF, 52 hanya aturan, 28 hanya IF. Dua pendekatan melihat dunia berbeda dan saling melengkapi.
  6. Sebaran skor statistik pada 60 tertanda IF: 28 dari 60 transaksi tertanda model memiliki skor statistik 0 — luput dari seluruh aturan Bagian 2. Inilah kuantifikasi celah yang diduga di awal bagian ini.

Dua Model, Dua Karakter #

Isolation Forest dan Local Outlier Factor menandai jumlah berbeda (60 vs 94) bukan kebetulan. Isolation Forest mengukur seberapa mudah satu titik dipisahkan dari keramaian lewat pemotongan acak — anomali nominal raksasa atau checkout kilat terisolasi dalam beberapa potongan saja. LOF mengukur seberapa jauh kepadatan lokal satu titik dari tetangganya — sensitif terhadap jembatan antar klaster kepadatan, tetapi pada data 5000 baris dengan parameter n_neighbors=20 ia cenderung memperluas wilayah tandai. Keduanya sama-sama sah; pilihan di antara mereka — atau keputusan menggabungkan — hanya bisa diambil setelah diukur terhadap label. Dan sekarang, seluruh pertanyaan mengarah ke sana: 60 tertanda IF dan 84 tertanda aturan, mana yang lebih benar? Segel is_fraud dibuka di Bagian 4: Evaluasi dan Ambang.

Referensi Resmi #