Bagian 1 menutup dengan dua temuan yang ditabung sengaja: kemiringan nominal +14,28 dan jam tersepi di 02.00–05.59. Bagian ini mengubah kedua kebiasaan tersebut menjadi aturan keras — pendeteksi transparan yang bisa dijelaskan ke tim operasional dalam satu kalimat, tanpa satu baris model pun. Tiga aturan dipasang: z-score nominal terhadap rata-rata data, kuantil ekor 99%, dan aturan jam dini hari. Ketiganya lalu digabung menjadi skor sederhana 0–3, dan kita menghitung berapa transaksi yang tertandai. Skrip tetap tidak menyentuh kolom is_fraud — deteksi bekerja unsupervised; label audit itu baru dibuka di Bagian 4 untuk menilai seberapa bagus aturan-aturan ini sebenarnya.
Tiga Aturan dan Alasan Pemilihannya #
Mengapa tepat tiga aturan ini, bukan yang lain? z-score menjawab pertanyaan paling klasik:berapa jauh satu transaksi menyimpang dari rata-rata populasi, dalam satuan simpangan baku. Kuantil ekor menjawab pertanyaan yang lebih tahan terhadap kemiringan:berapa persen transaksi termahal yang kita mau periksa, apa pun bentuk distribusinya. Aturan jam menangkap keanehan waktu — transaksi 02.00–05.59 nyaris tidak muncul pada data Bagian 1, sehingga kehadirannya sendiri sudah bernilai sinyal. Ketiganya sengaja satu dimensi: itulah kekuatan sekaligus batasnya.
| Aturan | Pertanyaan yang dijawab | Ambang | Karakter |
|---|---|---|---|
| z-score nominal ≥ 3 | Seberapa jauh dari rata-rata? | 1495,2 ribu | Tahan derau, rapuh pada kemiringan |
| nominal ≥ kuantil 99% | Berapa persen ekor yang disorot? | 730,8 ribu | Bebas asumsi bentuk distribusi |
| jam 02..05 | Kapan transaksi terjadi? | 02:00–05:59 | Sinyal waktu, buta nominal |
# Deteksi statistik: aturan transparan pada nominal dan jam sebagai
# baseline sebelum model. Tiga aturan dipasang:
# 1. z-score nominal : |z| >= 3 terhadap mean/std seluruh data.
# 2. kuantil ekor nominal : nominal >= kuantil 99% (ekor 1% teratas).
# 3. aturan jam dini hari : jam_transaksi 02..05 (dari eksplorasi
# Bagian 1, jam tersepi).
# Skor sederhana = jumlah aturan yang terpicu (0..3). Skrip TIDAK
# membaca is_fraud - deteksi unsupervised. Jalankan dari root proyek:
# python3 src/03_statistik.py
import numpy as np
import pandas as pd
df = pd.read_csv("data/transaksi.csv")
nominal = df["nominal_ribu"].to_numpy()
jam = df["jam_transaksi"].to_numpy()
# ---------- Aturan 1: z-score nominal ----------
rata = float(nominal.mean())
std = float(nominal.std())
z = (nominal - rata) / std
ambang_z = rata + 3.0 * std
aturan_z = z >= 3.0
# ---------- Aturan 2: kuantil ekor nominal ----------
q99 = float(np.quantile(nominal, 0.99))
aturan_kuantil = nominal >= q99
# ---------- Aturan 3: jam dini hari ----------
aturan_jam = (jam >= 2) & (jam <= 5)
# ---------- Tabel aturan ----------
print("== Tiga aturan statistik ==")
print(f"{'aturan':34s} {'ambang':>12s} {'tertandai':>10s}")
print(f"{'z-score nominal >= 3':34s} {ambang_z:12.1f} {int(aturan_z.sum()):10d}")
print(f"{'nominal >= kuantil 99%':34s} {q99:12.1f} {int(aturan_kuantil.sum()):10d}")
print(f"{'jam transaksi 02..05':34s} {'02:00-05:59':>12s} {int(aturan_jam.sum()):10d}")
print(f"\nnominal : mean {rata:.1f} ribu, std {std:.1f} ribu "
f"(std terdistorsi ekor kanan)")
print(f"z-score maks : {z.max():.1f} (transaksi terbesar "
f"{nominal.max():.1f} ribu)")
# ---------- Skor gabungan ----------
skor = aturan_z.astype(int) + aturan_kuantil.astype(int) + aturan_jam.astype(int)
df_skor = df[
["id_transaksi", "nominal_ribu", "jam_transaksi", "durasi_detik",
"jumlah_item"]
].copy()
df_skor["skor"] = skor
print("\n== Distribusi skor gabungan (0..3) ==")
for s in range(4):
print(f"skor {s}: {int((skor == s).sum()):5d} transaksi")
print(f"skor >= 1 (minimal satu aturan) : {int((skor >= 1).sum())} transaksi "
f"({(skor >= 1).mean():.2%})")
print(f"skor >= 2 (dua aturan atau lebih): {int((skor >= 2).sum())} transaksi "
f"({(skor >= 2).mean():.2%})")
# ---------- Contoh transaksi tertandai ----------
print("\n== Delapan transaksi dengan skor tertinggi ==")
atas = df_skor[df_skor["skor"] >= 2].sort_values(
["skor", "nominal_ribu"], ascending=[False, False]
).head(8)
print(atas.to_string(index=False))
print("\n== Contoh transaksi jam dini hari ber skor 1 (lima pertama) ==")
bawah = df_skor[(df_skor["skor"] == 1) & aturan_jam].head(5)
print(bawah.to_string(index=False))Output yang Dihasilkan:
== Tiga aturan statistik ==
aturan ambang tertandai
z-score nominal >= 3 1495.2 20
nominal >= kuantil 99% 730.8 50
jam transaksi 02..05 02:00-05:59 34
nominal : mean 278.9 ribu, std 405.4 ribu (std terdistorsi ekor kanan)
z-score maks : 19.7 (transaksi terbesar 8268.3 ribu)
== Distribusi skor gabungan (0..3) ==
skor 0: 4916 transaksi
skor 1: 64 transaksi
skor 2: 20 transaksi
skor 3: 0 transaksi
skor >= 1 (minimal satu aturan) : 84 transaksi (1.68%)
skor >= 2 (dua aturan atau lebih): 20 transaksi (0.40%)
== Delapan transaksi dengan skor tertinggi ==
id_transaksi nominal_ribu jam_transaksi durasi_detik jumlah_item skor
336 8268.3 18 89 1 2
427 8260.1 14 112 1 2
87 7938.6 21 96 2 2
1534 7491.7 13 118 1 2
370 7428.9 14 71 1 2
519 7284.9 14 93 1 2
1510 7248.3 15 95 1 2
158 6756.5 19 89 1 2
== Contoh transaksi jam dini hari ber skor 1 (lima pertama) ==
id_transaksi nominal_ribu jam_transaksi durasi_detik jumlah_item skor
53 254.5 4 219 3 1
432 269.5 2 179 3 1
606 255.0 4 135 2 1
777 231.2 5 134 2 1
1115 361.3 4 166 4 1Mari kita bedah aturan demi aturan:
- Baris 18–23 (z-score): rata-rata 278,9 ribu dan simpangan 405,4 ribu menghasilkan ambang 1495,2 ribu — hanya 20 transaksi tertandai. Perhatikan std yang jauh lebih besar dari mean: ekor kanan nominal mendistorsi simpangan baku sehingga ambang z terangkat tinggi. z-score maksimum 19,7 (transaksi 8268,3 ribu) menegaskan betapa ekstremnya ekor tersebut.
- Baris 25–27 (kuantil): tanpa asumsi bentuk distribusi, kuantil 99% memotong tepat 1% data teratas pada 730,8 ribu — 50 transaksi. Aturan ini kebal terhadap kemiringan yang membuat z-score kehilangan sensitivitas.
- Baris 29–30 (jam): 34 transaksi jatuh pada jendela 02.00–05.59 — jam yang pada Bagian 1 nyaris kosong penghuni.
- Baris 43–57 (skor gabungan): skor 0..3 dibentuk dari penjumlahan tiga penanda boolean. Hasilnya bertingkat rapi: 84 transaksi (1,68%) memicu minimal satu aturan, dan hanya 20 transaksi (0,40%) yang memicu dua aturan sekaligus — tidak ada yang memicu tiga.
- Baris 60–68 (jendela inspeksi): kedelapan transaksi ber skor 2 semuanya ber nominal 6756–8268 ribu — kombinasi nominal ekstrem yang memicu z-score dan kuantil bersamaan. Sebaliknya, transaksi jam dini hari ber skor 1 justru bernominal kecil (231–361 ribu): anomali waktu yang sama sekali tak terlihat oleh aturan nominal.
Membaca Hasil: Harga Sebuah Transparansi #
Distribusi skor memberi tiga informasi berharga. Pertama, ambang alami bekerja: 20 transaksi skor-2 adalah kandidat anomali paling meyakinkan — nominal raksasa yang menembus dua aturan sekaligus. Kedua, zona abu-abu terukur: 64 transaksi skor-1 harus dipilah manual antara jam dini hari dan nominal tinggi. Ketiga, yang tidak tertandai tetap pertanyaan terbuka: 4916 transaksi skor-0 dinyatakan normal oleh ketiga aturan — tetapi apakah benar normal? Aturan satu dimensi buta terhadap kombinasi: transaksi bernominal sedang pada jam normal namun berdurasi 40 detik untuk 8 item (checkout kilat) lolos dari semua aturan kita. Celah inilah yang menuntut model multi-dimensi.
Aturan transparan telah memberi peta awal: 84 transaksi layak ditinjau, 20 di antaranya nyaris pasti anomali. Namun seluruh aturan tersebut melihat transaksi dari satu dimensi saja — nominal, atau jam, tidak pernah keduanya bersama rasio durasi per item. Pertanyaan berikutnya: apa yang ditemukan ketika model melihat keenam dimensi sekaligus? Pertanyaan itu dijawab oleh Bagian 3: Model Anomali.
Referensi Resmi #
- numpy.quantile — API — kuantil ekor tanpa asumsi distribusi.
- scikit-learn User Guide: Outlier detection — konteks aturan statistik sebagai baseline deteksi anomali.
- pandas.DataFrame.mean — API — statistik dasar yang dipakai z-score.