01 · SYNTHETIC DATA
Data sintetik, deterministik, dan sudah tahu jawabannya
Lima entitas dibuat berurutan mengikuti dependency foreign key, lalu dua jenis pola mencurigakan (5 tipologi AML, 5 teknik sanctions evasion) disuntikkan sebagai ground truth — dipakai nanti untuk mengukur seberapa bagus tiap tahap deteksi.
Volume data (seed = 42)
| Tipologi AML | Cara kerja | Ground truth |
|---|---|---|
| Structuring | 3–5 setoran tunai $8,500–$9,900 dalam 72 jam — di bawah ambang pelaporan $10,000 | 8 |
| Smurfing | 5–10 transfer masuk dari counterparty berbeda dalam 96 jam (fan-in) | 34 |
| Layering | 3–5 wire keluar berturut-turut ke counterparty berbeda, tiap hop 2–24 jam | 11 |
| Rapid movement | Setoran besar diikuti penarikan/wire hampir senilai dalam 1–48 jam (pass-through) | 4 |
| Round-tripping | Wire keluar ke counterparty X, dana serupa kembali dari X dalam 2–10 hari | 6 |
| Teknik sanctions evasion | Contoh nyata dari data |
|---|---|
| name_variation | Kapitalisasi satu huruf diubah acak — "AnThony Clark" |
| alias_use | Memakai alias yang memang tercatat di watchlist |
| front_company | Nama asli + embel bisnis — "Hannah Russell Trading LLC" |
| transliteration | Homoglyph a→4, e→3, i→1, o→0, s→5 — "C4ldw3ll-M1ll3r" |
| split_transaction | Satu wire besar dipecah jadi 2–4 wire kecil, total nominal sama |
02 · SUPABASE LOCAL RAW TABLES
Skema dengan foreign key penuh dan kolom pgvector
Lima tabel raw.*, diindeks untuk join transaksi-level dan
rolling window. Tiga kolom vector(384) disiapkan untuk
contextual sanctions screening, walau isinya untuk saat ini masih placeholder.
flowchart LR
customers["raw.customers"] -->|1:N| accounts["raw.accounts"]
accounts -->|1:N| transactions["raw.transactions"]
counterparties["raw.counterparties"] -.->|0:N nullable| transactions
watchlist["raw.watchlist_entities"]
transactions --> aml["ground_truth.aml_scenarios"]
transactions --> evasion["ground_truth.sanctions_evasion"]
counterparties --> evasion
watchlist --> evasion
Garis putus-putus = foreign key nullable (transaksi tunai tidak
punya counterparty). name_embedding ada di
counterparties & watchlist_entities;
description_embedding ada di transactions.
03 · SQL DATA VALIDATION & STANDARDIZATION
Tidak ada baris yang dibuang, hanya dicatat
Setiap tabel raw.* punya pasangan 1:1 di
staging.* setelah dibersihkan (trim, upper-case kode
negara/mata uang). Masalah data dicatat ke satu log audit, bukan menghapus datanya.
4 pemeriksaan yang dijalankan
- ✓ Tanggal transaksi/pembukaan akun di masa depan
- ✓ Akun dibuka sebelum tanggal onboarding nasabahnya
- ✓ Mata uang transaksi tidak cocok dengan mata uang akun
- ✓ Kemungkinan transaksi duplikat
staging.run_validation() tetap penting untuk data dari
sumber lain yang tidak sebersih data sintetik ini.
04 · SQL ANALYTICS BASE TABLE
Menjahit lima tabel jadi satu materialized view
analytics.transactions_base menghitung join 5-arah sekali,
menyimpannya sebagai tabel fisik, lalu di-refresh eksplisit — supaya tahap
feature engineering yang men-scan berulang-ulang tidak perlu join ulang tiap kali.
flowchart LR
st["staging.transactions"] --> base(("analytics.
transactions_base"))
sa["staging.accounts"] --> base
sc["staging.customers"] --> base
scp["staging.counterparties"] --> base
ga["ground_truth.aml_scenarios"] --> base
gs["ground_truth.sanctions_evasion"] --> base
110,896 baris keluar — sama persis dengan jumlah baris
raw.transactions: satu baris analytics per transaksi, tidak
ada fan-out dari join.
05 · SQL FEATURE ENGINEERING
14 kolom fitur, 13 di antaranya rolling-window
Tiap fitur menjawab "bagaimana kondisi akun ini sampai saat transaksi ini
terjadi" — tidak melihat masa depan, karena sistem deteksi nyata pun begitu.
Satu pengecualian: amount bukan agregasi rolling, cuma
nominal transaksi itu sendiri (lihat baris terakhir tabel).
Before / after: optimisasi distinct-counterparty count
Postgres tidak mendukung
count(distinct x) over (...). Versi pertama query ulang
tabel dasar per baris (110,896×). Versi final: array_agg(...)
over w7d mengumpulkan array kecil per baris (window function biasa), lalu
dedupe array itu di scalar subquery — tidak menyentuh tabel lagi.
| Fitur | Window | Kenapa penting |
|---|---|---|
| txn_count_1d | 1 hari | Velocity sangat jangka pendek — ledakan mendadak |
| txn_count_7d | 7 hari | Basis rule High Velocity |
| txn_count_30d | 30 hari | Baseline aktivitas normal akun |
| total_amount_1d | 1 hari | Basis rule Rapid Movement |
| total_amount_7d | 7 hari | Basis rule Structuring & Smurfing |
| total_amount_30d | 30 hari | Baseline volume bulanan |
| avg_amount_7d | 7 hari | Profil "ukuran transaksi wajar" akun ini |
| max_amount_7d | 7 hari | Outlier tunggal di balik rata-rata yang tampak wajar |
| credit_amount_7d | 7 hari | Arah dana masuk — pembeda fan-in (smurfing) |
| debit_amount_7d | 7 hari | Arah dana keluar — pembeda fan-out (layering) |
| near_threshold_cash_deposits_7d | 7 hari | Sinyal structuring paling langsung |
| distinct_counterparties_7d | 7 hari | Sinyal smurfing & layering |
| seconds_since_prev_txn | — | Sinyal pass-through / rapid movement |
| amount | — | Baseline magnitude mentah untuk model ML |
06 · RULE-BASED AML DETECTION
8 rule SQL, dikalibrasi ulang setelah dicek ke ground truth
Threshold awal R004 ternyata menyalakan alarm di ~13% dari SEMUA transaksi — diperbaiki ke persentil-95. Layering awalnya 0% tertangkap karena polanya murni debit; R008 ditambahkan khusus untuk itu.
Jumlah hit per rule (110,896 transaksi)
Recall terhadap ground truth, per tipologi
Recall yang tidak 100% ini bukan bug: semua fitur di atas adalah trailing window, jadi transaksi pertama dalam sebuah "ledakan" pola belum punya riwayat untuk memicu threshold. Ini alasan konkret kenapa tahap ML dibutuhkan.
07 · MACHINE LEARNING / ANOMALY DETECTION
Isolation Forest, dilatih tanpa label
Model belajar murni dari fitur — ground_truth.*
tidak pernah dipakai untuk training, hanya untuk mengevaluasi ranking anomali
setelahnya. Ini meniru kondisi nyata: label suspicious yang terkonfirmasi selalu
langka.
Input model — 20 kolom, bukan 14
| Sumber | Isi | Kolom |
|---|---|---|
| Fitur rolling window (§05) | Semua kolom features.account_transaction_features kecuali amount | 13 |
amount | Diambil terpisah dari analytics.transactions_base, bukan hasil agregasi rolling | 1 |
transaction_type (one-hot) | 6 kategori jadi kolom biner — wire_transfer, cash_deposit, cash_withdrawal, card, ach, check | 6 |
"14 fitur" di §05 merujuk ke tabel SQL-nya saja. Kolom transaction_type ditambahkan khusus di tahap ML supaya model bisa membedakan — misalnya — nominal $9.000 yang wajar untuk wire_transfer tapi mencurigakan untuk cash_deposit.
Ringkasan evaluasi
Recall bertambah seiring makin banyak transaksi teratas yang direview
COBA SENDIRI · LIVE INFERENCE
Simulasi skor anomali, langsung dari model terlatih
Form ini memanggil backend FastAPI sungguhan (backend/api/main.py)
yang memuat model isolation_forest_v1.joblib hasil training di
§07 — bukan angka pura-pura. Pilih preset tipologi atau isi manual, lalu jalankan.
Preset tipologi
Hasil
Belum ada hasil — pilih preset atau isi form, lalu klik "Jalankan inference".
STATUS PIPELINE