Dua pendekatan deteksi kini berdiri berdampingan dengan klaim masing-masing: 84 transaksi ditandai aturan statistik, 60 oleh Isolation Forest, irisan keduanya hanya 32. Mana yang benar? Seluruh bagian proyek ini sengaja menunda jawabannya — deteksi bekerja unsupervised karena di dunia nyata label mahal dan jarang. Tetapi untuk menilai deteksi, label tak terhindarkan: tim audit historis telah memeriksa seluruh 5000 transaksi dan menyimpan verdiktinya di kolom is_fraud. Bagian ini membuka segel tersebut untuk pertama kalinya, mengukur kualitas kedua pendekatan dengan precision@k dan titik operasi, menggambar kurva trade-off presisi-recall, lalu memilih ambang operasional dengan satu kendala dunia nyata: kapasitas tinjauan manual tim — 50 transaksi per hari.

Membuka Label untuk Pertama Kalis #

Filosofi evaluasinya perlu ditegaskan sebelum kode. Deteksi tetaplah unsupervised saat bekerja — model dan aturan tidak pernah melihat label saat menandai. Label hanya masuk saat menilai: kita membandingkan daftar tertandai dengan daftar audit untuk menghitung presisi (dari yang kita tandai, berapa yang benar-benar anomali) dan recall (dari seluruh anomali, berapa yang berhasil kita tangkap). Dua angka ini hampir selalu berlawanan — menurunkan ambang menaikkan recall dengan menurunkan presisi — sehingga pilihan ambang bukan keputusan statistik semata, melainkan keputusan operasional: berapa transaksi yang sanggup ditinjau manusia per hari.

python
# Evaluasi dan laporan akhir proyek deteksi anomali transaksi.
# Di sinilah kolom is_fraud DIBUKA untuk pertama kali: label historis
# hasil audit dipakai untuk mengukur kualitas deteksi Bagian 2-3 yang
# selama ini bekerja tanpa label. Dievaluasi:
#   - precision@k tiga peringkat (nominal, skor statistik, skor model);
#   - titik operasi tiap metode pada ambang wajarnya;
#   - kurva trade-off presisi-recall pada ambang skor Isolation Forest;
#   - ambang operasional terpilih berbasis kapasitas tinjauan manual.
# Seluruh angka dirakit menjadi laporan/laporan-anomali.txt yang lalu
# dicetak isinya. Jalankan dari root proyek: python3 src/05_laporan.py

import os

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

KAPASITAS_TINJAU = 50  # transaksi maksimum yang bisa ditinjau manual per hari

df = pd.read_csv("data/transaksi.csv")
y = df["is_fraud"].to_numpy().astype(bool)
nominal = df["nominal_ribu"].to_numpy()
jam = df["jam_transaksi"].to_numpy()
durasi = df["durasi_detik"].to_numpy()
item = df["jumlah_item"].to_numpy()

# ---------- Bangun ulang detektor Bagian 2 dan Bagian 3 ----------
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

durasi_per_item = durasi / np.maximum(item, 1)
nominal_per_item = nominal / np.maximum(item, 1)
X = StandardScaler().fit_transform(
    np.column_stack(
        [
            np.log1p(nominal),
            jam,
            np.log1p(durasi),
            item,
            np.log1p(durasi_per_item),
            np.log1p(nominal_per_item),
        ]
    )
)
model = IsolationForest(
    n_estimators=200,
    contamination=0.012,
    random_state=42,
).fit(X)
skor_if = model.decision_function(X)
tertanda_if = model.predict(X) == -1
tertanda_lof = LocalOutlierFactor(n_neighbors=20).fit_predict(X) == -1


def presisi_recall(tertanda):
    """Kembalikan (jumlah tertandai, benar, presisi, recall)."""
    n = int(tertanda.sum())
    tp = int((tertanda & y).sum())
    presisi = tp / n if n else 0.0
    recall = tp / int(y.sum())
    return n, tp, presisi, recall


# ---------- Precision@k tiga peringkat ----------
urut_nominal = np.argsort(-nominal, kind="stable")
urut_stat = np.argsort(-skor_stat, kind="stable")
urut_if = np.argsort(skor_if, kind="stable")

print("== Precision@k: kualitas peringkat tiga pendekatan ==")
print(f"{'k':>5s} {'nominal':>9s} {'statistik':>10s} {'IF model':>9s}")
baris_pk = []
for k in [20, 40, 60, 100]:
    p_nom = y[urut_nominal[:k]].sum() / k
    p_stat = y[urut_stat[:k]].sum() / k
    p_if = y[urut_if[:k]].sum() / k
    baris_pk.append((k, p_nom, p_stat, p_if))
    print(f"{k:5d} {p_nom:9.3f} {p_stat:10.3f} {p_if:9.3f}")

# ---------- Titik operasi tiap metode ----------
titik_operasi = [
    ("z-score nominal >= 3", aturan_z),
    ("nominal >= kuantil 99%", aturan_kuantil),
    ("jam 02..05", aturan_jam),
    ("skor statistik >= 1", tertanda_stat),
    ("skor statistik >= 2", skor_stat >= 2),
    ("durasi <= 70 s (kandidat ditolak)", durasi <= 70),
    ("Isolation Forest (contamination 1,2%)", tertanda_if),
    ("Local Outlier Factor (n=20)", tertanda_lof),
]
print("\n== Titik operasi: tertandai, benar, presisi, recall ==")
print(f"{'metode':38s} {'n':>5s} {'benar':>6s} {'presisi':>8s} {'recall':>8s}")
hasil_operasi = []
for nama, tandai in titik_operasi:
    n, tp, presisi, recall = presisi_recall(tandai)
    hasil_operasi.append((nama, n, tp, presisi, recall))
    print(f"{nama:38s} {n:5d} {tp:6d} {presisi:8.3f} {recall:8.3f}")

# ---------- Kurva trade-off ambang Isolation Forest ----------
AMBANG_UJI = [-0.08, -0.06, -0.04, -0.02, 0.0, 0.02]
print("\n== Kurva trade-off presisi-recall (ambang skor Isolation Forest) ==")
print(f"{'ambang':>8s} {'tertandai':>10s} {'presisi':>8s} {'recall':>8s}")
hasil_ambang = []
for amb in AMBANG_UJI:
    n, tp, presisi, recall = presisi_recall(skor_if <= amb)
    hasil_ambang.append((amb, n, tp, presisi, recall))
    print(f"{amb:+8.2f} {n:10d} {presisi:8.3f} {recall:8.3f}")

# ---------- Pilihan ambang operasional ----------
# Antrean tinjauan manual dibatasi kapasitas; pilih ambang dengan
# jumlah tertandai terbanyak yang masih muat dalam kapasitas.
muat = [h for h in hasil_ambang if h[1] <= KAPASITAS_TINJAU]
ambang_pilihan = max(muat, key=lambda h: h[1])
tandai_pilihan = skor_if <= ambang_pilihan[0]
antrean_manual = presisi_recall(tandai_pilihan)
tambahan_jam = int((aturan_jam & ~tandai_pilihan).sum())
gabungan = presisi_recall(tandai_pilihan | aturan_jam)
print(f"\n== Ambang operasional terpilih (kapasitas tinjau {KAPASITAS_TINJAU}/hari) ==")
print(f"ambang            : {ambang_pilihan[0]:+.2f}")
print(f"antrean manual    : {antrean_manual[0]} transaksi, presisi {antrean_manual[2]:.3f}, "
      f"recall {antrean_manual[3]:.3f}")
print(f"saringan jam 02-05: +{tambahan_jam} transaksi (verifikasi otomatis)")
print(f"gabungan          : {gabungan[0]} transaksi, presisi {gabungan[2]:.3f}, "
      f"recall {gabungan[3]:.3f}")

# ---------- Rakit laporan ----------
n_total, n_fraud = len(df), int(y.sum())
p_nom, p_stat, p_if = baris_pk[2][1], baris_pk[2][2], baris_pk[2][3]
baris = []
baris.append("=" * 64)
baris.append(" LAPORAN PROYEK: DETEKSI ANOMALI TRANSAKSI")
baris.append(f" Dataset sintetis {n_total} transaksi e-commerce, "
             f"{n_fraud} anomali terlabel")
baris.append("=" * 64)
baris.append("")
baris.append("1. RINGKASAN EKSEKUTIF")
baris.append(
    "   Isolation Forest pada 6 fitur terskala (log1p nominal dan"
)
baris.append(
    f"   durasi, jam, jumlah item, dua fitur rasio) menandai "
    f"{int(tertanda_if.sum())} transaksi"
)
baris.append(
    f"   dengan presisi {presisi_recall(tertanda_if)[2]:.3f} dan recall "
    f"{presisi_recall(tertanda_if)[3]:.3f} terhadap label audit."
)
baris.append(
    "   Aturan jam dini hari menangkap anomali yang luput dari model;"
)
baris.append(
    f"   desain dua tingkat mencapai recall {gabungan[3]:.3f} pada "
    f"{gabungan[0]} transaksi tertandai."
)
baris.append("")
baris.append("2. DATA")
baris.append(f"   Baris            : {n_total} transaksi")
baris.append(f"   Anomali terlabel : {n_fraud} ({n_fraud / n_total:.1%}) - tipe:")
baris.append("                     nominal ekstrem, jam 02..05,")
baris.append("                     checkout kilat (banyak item, durasi")
baris.append("                     terlalu cepat untuk sebanyak itu item).")
baris.append(f"   Nominal          : mean {rata:.1f} ribu, median "
             f"{float(np.median(nominal)):.1f} ribu")
baris.append("   Label            : hanya dibuka di evaluasi ini;")
baris.append("                     deteksi Bagian 2-3 unsupervised.")
baris.append("")
baris.append("3. METODE STATISTIK (Bagian 2)")
for nama, tandai in [
    ("z-score >= 3", aturan_z),
    ("kuantil 99%", aturan_kuantil),
    ("jam 02..05", aturan_jam),
    ("skor >= 1", tertanda_stat),
]:
    n, tp, presisi, recall = presisi_recall(tandai)
    baris.append(f"   {nama:16s}: {n:4d} tertandai, presisi {presisi:.3f}, "
                 f"recall {recall:.3f}")
baris.append("   Aturan satu dimensi; buta terhadap kombinasi fitur:")
baris.append("   anomali checkout kilat 0 dari 20 tertangkap aturan.")
baris.append("")
baris.append("4. MODEL ANOMALI (Bagian 3)")
n_if, tp_if, pr_if, rc_if = presisi_recall(tertanda_if)
n_lof, tp_lof, pr_lof, rc_lof = presisi_recall(tertanda_lof)
baris.append(f"   Isolation Forest : {n_if:4d} tertandai, presisi {pr_if:.3f}, recall {rc_if:.3f}")
baris.append(f"   LocalOutlierFact.: {n_lof:4d} tertandai, presisi {pr_lof:.3f}, recall {rc_lof:.3f}")
baris.append(f"   Precision@60     : IF {p_if:.3f} vs statistik {p_stat:.3f} "
             f"vs nominal {p_nom:.3f}")
baris.append("   Fitur rasio durasi_per_item membuka anomali yang tak")
baris.append("   terlihat pada aturan nominal/jam mana pun.")
baris.append("")
baris.append("5. EVALUASI TERHADAP LABEL AUDIT")
baris.append("   peringkat-k   nominal  statistik  IF model")
for k, pn, ps, pi in baris_pk:
    baris.append(f"   {k:10d}   {pn:6.3f}    {ps:6.3f}    {pi:6.3f}")
baris.append("   ambang IF  tertandai  presisi  recall")
for amb, n, tp, presisi, recall in hasil_ambang:
    baris.append(f"   {amb:+9.2f}  {n:7d}  {presisi:7.3f}  {recall:7.3f}")
n_dur, tp_dur, pr_dur, rc_dur = presisi_recall(durasi <= 70)
baris.append(f"   Aturan durasi <= 70 s ditolak: {n_dur} tertandai, "
             f"presisi {pr_dur:.3f}.")
baris.append("")
baris.append("6. AMBANG OPERASIONAL TERPILIH")
baris.append(f"   Kapasitas tinjau manual : {KAPASITAS_TINJAU} transaksi/hari")
baris.append(f"   Ambang skor IF terpilih : {ambang_pilihan[0]:+.2f}")
baris.append(f"   Antrean manual          : {antrean_manual[0]} transaksi, presisi "
             f"{antrean_manual[2]:.3f}, recall {antrean_manual[3]:.3f}")
baris.append(f"   Saringan jam 02..05     : +{tambahan_jam} transaksi "
             "(verifikasi otomatis)")
baris.append(f"   Gabungan                : {gabungan[0]} transaksi, presisi "
             f"{gabungan[2]:.3f}, recall {gabungan[3]:.3f}")
baris.append("   Kriteria: ambang dengan antrean terbanyak yang masih")
baris.append("   muat dalam kapasitas - presisi antrean tetap terjaga.")
baris.append("")
baris.append("7. REKOMENDASI OPERASIONAL")
baris.append("   a. Jalankan Isolation Forest dengan ambang terpilih")
baris.append("      sebagai antrean utama tinjauan manual.")
baris.append("   b. Pasang aturan jam 02..05 sebagai saringan kedua")
baris.append("      yang memicu verifikasi otomatis - murah dan")
baris.append("      menangkap anomali dini hari yang luput dari model.")
baris.append("   c. Tolak aturan durasi mentah (presisi 0.143); rasio")
baris.append("      durasi per item sudah terwakili sebagai fitur model.")
baris.append("   d. Latih ulang model secara berkala; pantau presisi")
baris.append("      antrean - penurunan presisi menandai pergeseran")
baris.append("      pola transaksi yang menuntut fitur baru.")
baris.append("")
baris.append("=" * 64)

isi_laporan = "\n".join(baris) + "\n"
os.makedirs("laporan", exist_ok=True)
with open("laporan/laporan-anomali.txt", "w", encoding="utf-8") as f:
    f.write(isi_laporan)

print("\n== Laporan ditulis: laporan/laporan-anomali.txt ==")
print(isi_laporan, end="")

Output yang Dihasilkan:

text
== Precision@k: kualitas peringkat tiga pendekatan ==
    k   nominal  statistik  IF model
   20     1.000      1.000     1.000
   40     0.500      0.575     0.925
   60     0.333      0.550     0.717
  100     0.230      0.400     0.450

== Titik operasi: tertandai, benar, presisi, recall ==
metode                                     n  benar  presisi   recall
z-score nominal >= 3                      20     20    1.000    0.333
nominal >= kuantil 99%                    50     20    0.400    0.333
jam 02..05                                34     20    0.588    0.333
skor statistik >= 1                       84     40    0.476    0.667
skor statistik >= 2                       20     20    1.000    0.333
durasi <= 70 s (kandidat ditolak)        154     22    0.143    0.367
Isolation Forest (contamination 1,2%)     60     43    0.717    0.717
Local Outlier Factor (n=20)               94     17    0.181    0.283

== Kurva trade-off presisi-recall (ambang skor Isolation Forest) ==
  ambang  tertandai  presisi   recall
   -0.08         16    1.000    0.267
   -0.06         28    1.000    0.467
   -0.04         36    0.972    0.583
   -0.02         45    0.844    0.633
   +0.00         60    0.717    0.717
   +0.02         94    0.479    0.750

== Ambang operasional terpilih (kapasitas tinjau 50/hari) ==
ambang            : -0.02
antrean manual    : 45 transaksi, presisi 0.844, recall 0.633
saringan jam 02-05: +33 transaksi (verifikasi otomatis)
gabungan          : 78 transaksi, presisi 0.731, recall 0.950

== Laporan ditulis: laporan/laporan-anomali.txt ==
================================================================
 LAPORAN PROYEK: DETEKSI ANOMALI TRANSAKSI
 Dataset sintetis 5000 transaksi e-commerce, 60 anomali terlabel
================================================================

1. RINGKASAN EKSEKUTIF
   Isolation Forest pada 6 fitur terskala (log1p nominal dan
   durasi, jam, jumlah item, dua fitur rasio) menandai 60 transaksi
   dengan presisi 0.717 dan recall 0.717 terhadap label audit.
   Aturan jam dini hari menangkap anomali yang luput dari model;
   desain dua tingkat mencapai recall 0.950 pada 78 transaksi tertandai.

2. DATA
   Baris            : 5000 transaksi
   Anomali terlabel : 60 (1.2%) - tipe:
                     nominal ekstrem, jam 02..05,
                     checkout kilat (banyak item, durasi
                     terlalu cepat untuk sebanyak itu item).
   Nominal          : mean 278.9 ribu, median 232.1 ribu
   Label            : hanya dibuka di evaluasi ini;
                     deteksi Bagian 2-3 unsupervised.

3. METODE STATISTIK (Bagian 2)
   z-score >= 3    :   20 tertandai, presisi 1.000, recall 0.333
   kuantil 99%     :   50 tertandai, presisi 0.400, recall 0.333
   jam 02..05      :   34 tertandai, presisi 0.588, recall 0.333
   skor >= 1       :   84 tertandai, presisi 0.476, recall 0.667
   Aturan satu dimensi; buta terhadap kombinasi fitur:
   anomali checkout kilat 0 dari 20 tertangkap aturan.

4. MODEL ANOMALI (Bagian 3)
   Isolation Forest :   60 tertandai, presisi 0.717, recall 0.717
   LocalOutlierFact.:   94 tertandai, presisi 0.181, recall 0.283
   Precision@60     : IF 0.717 vs statistik 0.550 vs nominal 0.333
   Fitur rasio durasi_per_item membuka anomali yang tak
   terlihat pada aturan nominal/jam mana pun.

5. EVALUASI TERHADAP LABEL AUDIT
   peringkat-k   nominal  statistik  IF model
           20    1.000     1.000     1.000
           40    0.500     0.575     0.925
           60    0.333     0.550     0.717
          100    0.230     0.400     0.450
   ambang IF  tertandai  presisi  recall
       -0.08       16    1.000    0.267
       -0.06       28    1.000    0.467
       -0.04       36    0.972    0.583
       -0.02       45    0.844    0.633
       +0.00       60    0.717    0.717
       +0.02       94    0.479    0.750
   Aturan durasi <= 70 s ditolak: 154 tertandai, presisi 0.143.

6. AMBANG OPERASIONAL TERPILIH
   Kapasitas tinjau manual : 50 transaksi/hari
   Ambang skor IF terpilih : -0.02
   Antrean manual          : 45 transaksi, presisi 0.844, recall 0.633
   Saringan jam 02..05     : +33 transaksi (verifikasi otomatis)
   Gabungan                : 78 transaksi, presisi 0.731, recall 0.950
   Kriteria: ambang dengan antrean terbanyak yang masih
   muat dalam kapasitas - presisi antrean tetap terjaga.

7. REKOMENDASI OPERASIONAL
   a. Jalankan Isolation Forest dengan ambang terpilih
      sebagai antrean utama tinjauan manual.
   b. Pasang aturan jam 02..05 sebagai saringan kedua
      yang memicu verifikasi otomatis - murah dan
      menangkap anomali dini hari yang luput dari model.
   c. Tolak aturan durasi mentah (presisi 0.143); rasio
      durasi per item sudah terwakili sebagai fitur model.
   d. Latih ulang model secara berkala; pantau presisi
      antrean - penurunan presisi menandai pergeseran
      pola transaksi yang menuntut fitur baru.

================================================================

Mari kita bedah alur evaluasinya langkah demi langkah:

  1. Baris 20 (konstanta operasional): KAPASITAS_TINJAU = 50 — satu-satunya angka pada skrip ini yang bukan statistik melainkan kendala bisnis. Ia akan menentukan pilihan ambang; mengubahnya mengubah keputusan, dan itulah inti desain berbasis biaya.
  2. Baris 29–62 (detektor dibangun ulang): aturan Bagian 2 dan model Bagian 3 direkonstruksi persis — parameter, seed, urutan fitur sama. Evaluasi yang jujur menuntut objek yang dinilai identik dengan objek yang akan dipakai produksi.
  3. Baris 65–71 (fungsi ukur): satu fungsi kecil mengembalikan keempat angka untuk vektor penanda mana pun — pola yang membuat delapan metode bisa dibandingkan pada satu tabel tanpa duplikasi kode.
  4. Baris 74–87 (precision@k): ketiga pendekatan diurutkan menurut skor masing-masing, lalu dihitung proporsi anomali sejati pada k teratas. Pada k=40, peringkat IF mencapai presisi 0,925 — jauh di atas statistik (0,575) dan nominal mentah (0,500); pada k=100 pun IF masih 0,450. Peringkat yang baik berarti tim audit memeriksa yang paling berisiko lebih dulu.
  5. Baris 89–106 (delapan titik operasi): setiap metode dievaluasi pada ambang wajarnya. Isolation Forest mencatat presisi 0,717 sekaligus recall 0,717 — satu-satunya yang seimbang tinggi. Aturan durasi mentah durasi <= 70 s diuji sebagai kandidat dan langsung ditolak: 154 tertandai dengan presisi 0,143 — terlalu banyak transaksi sah berdurasi pendek. LOF mencatat presisi 0,181: memang menandai banyak, tetapi mayoritas keliru.
  6. Baris 108–116 (kurva trade-off): ambang skor IF digeser dari −0,08 hingga +0,02. Polanya klasik: ambang −0,08 memberi presisi sempurna 1,000 dengan recall 0,267; ambang +0,02 menaikkan recall ke 0,750 tetapi presisi jatuh ke 0,479. Tidak ada ambang yang menang semua — pilihan harus datang dari kendala operasional.
  7. Baris 118–133 (keputusan berbasis kapasitas): dari seluruh ambang yang antreiannya muat 50 transaksi, dipilih yang antreiannya paling banyak: ambang −0,02 dengan 45 transaksi, presisi 0,844, recall 0,633. Kemudian aturan jam 02–05 ditambahkan sebagai saringan kedua (+33 transaksi verifikasi otomatis) — gabungan mencapai recall 0,950: 57 dari 60 anomali tertangkap oleh desain dua tingkat.
  8. Baris 135–239 (laporan dirakit): tujuh bagian laporan ditulis dari variabel yang baru dihitung — ringkasan eksekutif, data, kedua metode, tabel evaluasi lengkap, ambang terpilih beserta kriterianya, dan empat rekomendasi operasional. Laporan tersimpan di laporan/laporan-anomali.txt dan bisa dibaca pemangku kepentingan tanpa membuka Python.

Membaca Kemenangan dan Kelemahannya #

Hasil evaluasi memberi Isolation Forest kemenangan yang meyakinkan namun jujur. Presisi 0,717 dan recall 0,717 pada titik default berarti dari 60 transaksi yang ditandai, 43 benar-benar anomali; dan dari 60 anomali sejati, 43 tertangkap. Desain dua tingkat menutup celahnya dengan elegan: anomali jam dini hari yang luput dari model (28 dari 60 tertanda IF ber skor statistik 0 — sebagian adalah anomali jam) ditangkap saringan murah yang bahkan tidak butuh model. Namun tiga anomali tetap lolos dari seluruh sistem — kejujuran yang dicatat laporan sebagai batas, bukan disembunyikan. Kelemahan struktural lainnya: evaluasi memakai data yang sama dengan pelatihan (unsupervised tetap menyentuh seluruh baris), sehingga angka ini adalah batas atas kualitas; pengukuran sejati menanti data transaksi baru.

Lima proyek telah lengkap: regresi, klasifikasi imbalance, segmentasi tanpa label, forecasting deret waktu, dan deteksi anomali. Satu tantangan terakhir menanti — proyek akhir yang merangkai semuanya menjadi satu sistem utuh dari eksplorasi hingga serving dan monitoring: Proyek Akhir: Prediksi Keberhasilan Kampanye.

Referensi Resmi #