Upload folder using huggingface_hub
Browse files- README.md +42 -4
- app.py +556 -0
- assets/config.json +41 -0
- assets/demo_display.csv +0 -0
- assets/demo_meta.csv +232 -0
- assets/demo_windows.npz +3 -0
- assets/gru.pth +3 -0
- assets/lightgbm.txt +0 -0
- assets/test_scores.npz +3 -0
- model.py +48 -0
- requirements.txt +13 -0
README.md
CHANGED
|
@@ -1,14 +1,52 @@
|
|
| 1 |
---
|
| 2 |
title: Sequential Fraud Detection
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: gray
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.22.0
|
| 8 |
-
python_version: '3.13'
|
| 9 |
app_file: app.py
|
| 10 |
-
pinned:
|
| 11 |
license: mit
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
---
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Sequential Fraud Detection
|
| 3 |
+
emoji: 🛡️
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: gray
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.22.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
+
pinned: true
|
| 10 |
license: mit
|
| 11 |
+
short_description: A GRU scoring card transaction sequences, live
|
| 12 |
+
tags:
|
| 13 |
+
- fraud-detection
|
| 14 |
+
- pytorch
|
| 15 |
+
- deep-learning
|
| 16 |
+
- fintech
|
| 17 |
+
- gru
|
| 18 |
---
|
| 19 |
|
| 20 |
+
# Sequential Fraud Detection
|
| 21 |
+
|
| 22 |
+
Most credit card fraud models score each transaction in isolation. Fraud is a behavioural
|
| 23 |
+
signal: what matters is that *this card* has never behaved this way before.
|
| 24 |
+
|
| 25 |
+
This demo runs a GRU over each card's last 10 transactions and compares it against a
|
| 26 |
+
LightGBM model given the identical features without sequence context. On a held out time
|
| 27 |
+
period of 555,719 transactions the GRU reaches **0.965 PR-AUC** against **0.899**, and at the
|
| 28 |
+
cost minimising threshold it misses **31** of 2,145 frauds where LightGBM misses **112**.
|
| 29 |
+
|
| 30 |
+
**Every score in the first tab is a real PyTorch forward pass**, computed when you click.
|
| 31 |
+
Nothing is looked up.
|
| 32 |
+
|
| 33 |
+
## The two tabs
|
| 34 |
+
|
| 35 |
+
**Score a transaction.** Pick a scenario, including the interesting one where the sequence
|
| 36 |
+
model catches fraud the flat model misses, and see the card's recent history alongside both
|
| 37 |
+
models' verdicts.
|
| 38 |
+
|
| 39 |
+
**Cost explorer.** A missed fraud costs the transaction amount; a false positive costs a
|
| 40 |
+
manual review. Move the review cost and watch the optimal threshold move with it. The
|
| 41 |
+
decision threshold is a business parameter, not a modelling constant.
|
| 42 |
+
|
| 43 |
+
## Limits worth stating
|
| 44 |
+
|
| 45 |
+
The data is simulated (Sparkov via Kaggle). Rule generated fraud is far more learnable than
|
| 46 |
+
the adversarial kind, so 0.965 PR-AUC is not a production number; the relative comparison
|
| 47 |
+
between models is the meaningful output. The cost minimising threshold is also selected on
|
| 48 |
+
the test set, which makes the dollar figures optimistic.
|
| 49 |
+
|
| 50 |
+
Trained on CPU: 22,577 parameters, 4 epochs, 7.1 minutes on a laptop with no GPU.
|
| 51 |
+
|
| 52 |
+
[Full code and methodology on GitHub](https://github.com/adwitiyashukla/DL-based-sequential-fraud-detection)
|
app.py
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Sequential Fraud Detection, live demo.
|
| 3 |
+
|
| 4 |
+
Two things happen here that a static repository cannot show:
|
| 5 |
+
|
| 6 |
+
1. The GRU actually runs. Every score on the first tab is a real forward pass
|
| 7 |
+
over that card's last 10 transactions, computed when you click.
|
| 8 |
+
2. The cost model is interactive. The decision threshold is not a modelling
|
| 9 |
+
constant, it is a business parameter, and the second tab lets you move the
|
| 10 |
+
inputs and watch the optimum move with them.
|
| 11 |
+
|
| 12 |
+
Repository: https://github.com/adwitiyashukla/DL-based-sequential-fraud-detection
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import json
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
|
| 20 |
+
import gradio as gr
|
| 21 |
+
import lightgbm as lgb
|
| 22 |
+
import numpy as np
|
| 23 |
+
import pandas as pd
|
| 24 |
+
import plotly.graph_objects as go
|
| 25 |
+
import torch
|
| 26 |
+
|
| 27 |
+
from model import GRUFraudModel
|
| 28 |
+
|
| 29 |
+
ASSETS = Path(__file__).parent / "assets"
|
| 30 |
+
|
| 31 |
+
# --------------------------------------------------------------------- loading
|
| 32 |
+
|
| 33 |
+
with open(ASSETS / "config.json", encoding="utf-8") as f:
|
| 34 |
+
CFG = json.load(f)
|
| 35 |
+
|
| 36 |
+
SEQ_LEN = CFG["seq_len"]
|
| 37 |
+
N_NUMERIC = CFG["n_numeric"]
|
| 38 |
+
N_FEAT = N_NUMERIC - 1
|
| 39 |
+
THR_GRU = CFG["threshold_gru"]
|
| 40 |
+
THR_LGB = CFG["threshold_lgb"]
|
| 41 |
+
|
| 42 |
+
_w = np.load(ASSETS / "demo_windows.npz")
|
| 43 |
+
WIN_NUM = _w["win_num"]
|
| 44 |
+
WIN_CAT = _w["win_cat"]
|
| 45 |
+
|
| 46 |
+
META = pd.read_csv(ASSETS / "demo_meta.csv")
|
| 47 |
+
DISPLAY = pd.read_csv(ASSETS / "demo_display.csv")
|
| 48 |
+
|
| 49 |
+
_s = np.load(ASSETS / "test_scores.npz")
|
| 50 |
+
S_GRU = _s["gru"].astype(np.float64)
|
| 51 |
+
S_LGB = _s["lgb"].astype(np.float64)
|
| 52 |
+
Y_TEST = _s["y"].astype(np.float64)
|
| 53 |
+
AMT_TEST = _s["amt"].astype(np.float64)
|
| 54 |
+
|
| 55 |
+
N_TEST = len(Y_TEST)
|
| 56 |
+
N_FRAUD = int(Y_TEST.sum())
|
| 57 |
+
TOTAL_FRAUD_AMT = float((Y_TEST * AMT_TEST).sum())
|
| 58 |
+
|
| 59 |
+
GRU = GRUFraudModel(n_numeric=N_NUMERIC, n_categories=CFG["n_categories"])
|
| 60 |
+
GRU.load_state_dict(torch.load(ASSETS / "gru.pth", map_location="cpu", weights_only=True))
|
| 61 |
+
GRU.eval()
|
| 62 |
+
torch.set_num_threads(2)
|
| 63 |
+
|
| 64 |
+
BOOSTER = lgb.Booster(model_file=str(ASSETS / "lightgbm.txt"))
|
| 65 |
+
|
| 66 |
+
RNG = np.random.default_rng()
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
# ------------------------------------------------------------------ inference
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def score_gru(sample_id: int) -> float:
|
| 73 |
+
"""A genuine forward pass over the card's last 10 transactions."""
|
| 74 |
+
with torch.no_grad():
|
| 75 |
+
logit = GRU(
|
| 76 |
+
torch.from_numpy(WIN_NUM[sample_id : sample_id + 1]),
|
| 77 |
+
torch.from_numpy(WIN_CAT[sample_id : sample_id + 1]),
|
| 78 |
+
)
|
| 79 |
+
return float(torch.sigmoid(logit).item())
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def score_lgb(sample_id: int) -> float:
|
| 83 |
+
"""LightGBM sees only the final timestep: no sequence, same features."""
|
| 84 |
+
flat = np.concatenate(
|
| 85 |
+
[
|
| 86 |
+
WIN_NUM[sample_id, -1, :N_FEAT].astype(np.float64),
|
| 87 |
+
[float(WIN_CAT[sample_id, -1])],
|
| 88 |
+
]
|
| 89 |
+
).reshape(1, -1)
|
| 90 |
+
return float(BOOSTER.predict(flat)[0])
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
# ----------------------------------------------------------- cost machinery
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _cumulative(scores: np.ndarray) -> dict:
|
| 97 |
+
order = np.argsort(-scores, kind="stable")
|
| 98 |
+
ys = Y_TEST[order]
|
| 99 |
+
return {
|
| 100 |
+
"s_desc": scores[order],
|
| 101 |
+
"tp": np.concatenate([[0.0], np.cumsum(ys)]),
|
| 102 |
+
"famt": np.concatenate([[0.0], np.cumsum(ys * AMT_TEST[order])]),
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
CUM = {"GRU": _cumulative(S_GRU), "LightGBM": _cumulative(S_LGB)}
|
| 107 |
+
K_AXIS = np.arange(N_TEST + 1, dtype=np.float64)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def cost_curve(model: str, review_cost: float) -> np.ndarray:
|
| 111 |
+
c = CUM[model]
|
| 112 |
+
return review_cost * (K_AXIS - c["tp"]) + (TOTAL_FRAUD_AMT - c["famt"])
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def optimal_threshold(model: str, review_cost: float) -> tuple[float, float]:
|
| 116 |
+
curve = cost_curve(model, review_cost)
|
| 117 |
+
k = int(np.argmin(curve))
|
| 118 |
+
thr = 1.0 if k == 0 else float(CUM[model]["s_desc"][k - 1])
|
| 119 |
+
return thr, float(curve[k])
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def confusion_at(model: str, threshold: float, review_cost: float) -> dict:
|
| 123 |
+
c = CUM[model]
|
| 124 |
+
k = int(np.searchsorted(-c["s_desc"], -threshold, side="right"))
|
| 125 |
+
tp = float(c["tp"][k])
|
| 126 |
+
fp = k - tp
|
| 127 |
+
fn = N_FRAUD - tp
|
| 128 |
+
tn = N_TEST - k - fn
|
| 129 |
+
cost = review_cost * fp + (TOTAL_FRAUD_AMT - float(c["famt"][k]))
|
| 130 |
+
return {
|
| 131 |
+
"alerts": k,
|
| 132 |
+
"tp": int(tp),
|
| 133 |
+
"fp": int(fp),
|
| 134 |
+
"fn": int(fn),
|
| 135 |
+
"tn": int(tn),
|
| 136 |
+
"cost": cost,
|
| 137 |
+
"recall": tp / N_FRAUD if N_FRAUD else 0.0,
|
| 138 |
+
"precision": tp / k if k else 0.0,
|
| 139 |
+
"alert_rate": k / N_TEST,
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
# ------------------------------------------------------------------- tab one
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def _history_table(sample_id: int) -> str:
|
| 147 |
+
rows = DISPLAY[DISPLAY.sample_id == sample_id].sort_values("position")
|
| 148 |
+
body = []
|
| 149 |
+
for _, r in rows.iterrows():
|
| 150 |
+
target = bool(r.is_target)
|
| 151 |
+
cls = "target" if target else ""
|
| 152 |
+
marker = "▶" if target else ""
|
| 153 |
+
gap = "first seen" if r.hours_since_prev >= 719 else f"{r.hours_since_prev:,.1f} h"
|
| 154 |
+
body.append(
|
| 155 |
+
f"<tr class='{cls}'>"
|
| 156 |
+
f"<td class='marker'>{marker}</td>"
|
| 157 |
+
f"<td>{r.timestamp}</td>"
|
| 158 |
+
f"<td class='num'>${r.amount:,.2f}</td>"
|
| 159 |
+
f"<td>{r.category}</td>"
|
| 160 |
+
f"<td class='num'>{r.distance_km:,.0f} km</td>"
|
| 161 |
+
f"<td class='num'>{gap}</td>"
|
| 162 |
+
f"<td class='num'>{r.amt_vs_card_mean:,.2f}x</td>"
|
| 163 |
+
f"</tr>"
|
| 164 |
+
)
|
| 165 |
+
return (
|
| 166 |
+
"<div class='tablewrap'><table class='hist'>"
|
| 167 |
+
"<thead><tr><th></th><th>Timestamp</th><th>Amount</th><th>Category</th>"
|
| 168 |
+
"<th>Distance</th><th>Since previous</th><th>vs card average</th></tr></thead>"
|
| 169 |
+
f"<tbody>{''.join(body)}</tbody></table></div>"
|
| 170 |
+
"<p class='caption'>The highlighted row is the transaction being scored. "
|
| 171 |
+
"The rows above it are the context the GRU reads.</p>"
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def _score_panel(name: str, prob: float, threshold: float, subtitle: str) -> str:
|
| 176 |
+
flagged = prob >= threshold
|
| 177 |
+
pct = prob * 100
|
| 178 |
+
tone = "flag" if flagged else "clear"
|
| 179 |
+
verdict = "FLAG FOR REVIEW" if flagged else "ALLOW"
|
| 180 |
+
return (
|
| 181 |
+
f"<div class='scorecard {tone}'>"
|
| 182 |
+
f"<div class='sc-name'>{name}</div>"
|
| 183 |
+
f"<div class='sc-sub'>{subtitle}</div>"
|
| 184 |
+
f"<div class='sc-prob'>{pct:.1f}<span>%</span></div>"
|
| 185 |
+
f"<div class='sc-bar'><div class='sc-fill' style='width:{min(pct, 100):.1f}%'></div>"
|
| 186 |
+
f"<div class='sc-thr' style='left:{threshold * 100:.1f}%'></div></div>"
|
| 187 |
+
f"<div class='sc-verdict'>{verdict}</div>"
|
| 188 |
+
f"<div class='sc-thrlab'>threshold {threshold:.3f}</div>"
|
| 189 |
+
f"</div>"
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def _outcome_banner(is_fraud: int, p_gru: float, p_lgb: float) -> str:
|
| 194 |
+
gru_flag = p_gru >= THR_GRU
|
| 195 |
+
lgb_flag = p_lgb >= THR_LGB
|
| 196 |
+
truth = "FRAUDULENT" if is_fraud else "LEGITIMATE"
|
| 197 |
+
|
| 198 |
+
if is_fraud and gru_flag and not lgb_flag:
|
| 199 |
+
tone, msg = "good", "The sequence model caught it. The flat model did not."
|
| 200 |
+
elif is_fraud and gru_flag:
|
| 201 |
+
tone, msg = "good", "Both models caught it."
|
| 202 |
+
elif is_fraud and not gru_flag:
|
| 203 |
+
tone, msg = "bad", "Both models missed this one. It is one of the 31 the GRU lets through."
|
| 204 |
+
elif not is_fraud and gru_flag:
|
| 205 |
+
tone, msg = "warn", "A false alarm. This costs a review, not a chargeback."
|
| 206 |
+
else:
|
| 207 |
+
tone, msg = "good", "Correctly cleared, with no analyst time spent."
|
| 208 |
+
|
| 209 |
+
return (
|
| 210 |
+
f"<div class='banner {tone}'>"
|
| 211 |
+
f"<span class='b-label'>Ground truth</span>"
|
| 212 |
+
f"<span class='b-truth'>{truth}</span>"
|
| 213 |
+
f"<span class='b-msg'>{msg}</span></div>"
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def load_case(scenario: str):
|
| 218 |
+
pool = META[META.scenario == scenario]
|
| 219 |
+
if pool.empty:
|
| 220 |
+
pool = META
|
| 221 |
+
row = pool.iloc[int(RNG.integers(len(pool)))]
|
| 222 |
+
sid = int(row.sample_id)
|
| 223 |
+
|
| 224 |
+
p_gru = score_gru(sid)
|
| 225 |
+
p_lgb = score_lgb(sid)
|
| 226 |
+
|
| 227 |
+
header = (
|
| 228 |
+
f"<div class='caseheader'>"
|
| 229 |
+
f"<div><span class='ch-label'>Card</span><span class='ch-val'>{row.card}</span></div>"
|
| 230 |
+
f"<div><span class='ch-label'>Transaction</span>"
|
| 231 |
+
f"<span class='ch-val'>${row.amount:,.2f}</span></div>"
|
| 232 |
+
f"<div><span class='ch-label'>History available</span>"
|
| 233 |
+
f"<span class='ch-val'>{int(row.history_length)} of {SEQ_LEN} steps</span></div>"
|
| 234 |
+
f"</div>"
|
| 235 |
+
)
|
| 236 |
+
panels = (
|
| 237 |
+
"<div class='panelrow'>"
|
| 238 |
+
+ _score_panel("GRU", p_gru, THR_GRU, "reads the last 10 transactions")
|
| 239 |
+
+ _score_panel("LightGBM", p_lgb, THR_LGB, "reads this transaction only")
|
| 240 |
+
+ "</div>"
|
| 241 |
+
)
|
| 242 |
+
banner = _outcome_banner(int(row.is_fraud), p_gru, p_lgb)
|
| 243 |
+
return header, _history_table(sid), panels, banner
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
# ------------------------------------------------------------------- tab two
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
def _metric_tile(label: str, value: str, sub: str = "") -> str:
|
| 250 |
+
return (
|
| 251 |
+
f"<div class='tile'><div class='t-label'>{label}</div>"
|
| 252 |
+
f"<div class='t-value'>{value}</div><div class='t-sub'>{sub}</div></div>"
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
def _style_fig(fig: go.Figure) -> go.Figure:
|
| 257 |
+
"""
|
| 258 |
+
Transparent background and neutral greys.
|
| 259 |
+
|
| 260 |
+
A plotly_white template renders as a white rectangle on a dark page, which
|
| 261 |
+
looks broken. Letting the page show through, with axis text in a mid grey
|
| 262 |
+
that has contrast against both, works in either mode.
|
| 263 |
+
"""
|
| 264 |
+
fig.update_layout(
|
| 265 |
+
paper_bgcolor="rgba(0,0,0,0)",
|
| 266 |
+
plot_bgcolor="rgba(0,0,0,0)",
|
| 267 |
+
font=dict(color="#94a3b8", size=12),
|
| 268 |
+
legend=dict(orientation="h", yanchor="bottom", y=1.0, x=0),
|
| 269 |
+
margin=dict(l=68, r=20, t=34, b=50),
|
| 270 |
+
)
|
| 271 |
+
grid = "rgba(148,163,184,0.20)"
|
| 272 |
+
line = "rgba(148,163,184,0.35)"
|
| 273 |
+
fig.update_xaxes(gridcolor=grid, zerolinecolor=line, linecolor=line)
|
| 274 |
+
fig.update_yaxes(gridcolor=grid, zerolinecolor=line, linecolor=line)
|
| 275 |
+
return fig
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def explore(review_cost: float, threshold: float):
|
| 279 |
+
gru = confusion_at("GRU", threshold, review_cost)
|
| 280 |
+
opt_thr, opt_cost = optimal_threshold("GRU", review_cost)
|
| 281 |
+
opt_thr_l, opt_cost_l = optimal_threshold("LightGBM", review_cost)
|
| 282 |
+
naive = confusion_at("GRU", 0.5, review_cost)
|
| 283 |
+
|
| 284 |
+
tiles = (
|
| 285 |
+
"<div class='tilehead'>GRU, at your chosen threshold</div>"
|
| 286 |
+
"<div class='tilerow'>"
|
| 287 |
+
+ _metric_tile("Alerts raised", f"{gru['alerts']:,}",
|
| 288 |
+
f"{gru['alert_rate'] * 100:.2f}% of transactions")
|
| 289 |
+
+ _metric_tile("Fraud caught", f"{gru['tp']:,}",
|
| 290 |
+
f"{gru['recall'] * 100:.1f}% recall")
|
| 291 |
+
+ _metric_tile("Fraud missed", f"{gru['fn']:,}", "escaped review")
|
| 292 |
+
+ _metric_tile("False alarms", f"{gru['fp']:,}",
|
| 293 |
+
f"{gru['precision'] * 100:.1f}% precision")
|
| 294 |
+
+ _metric_tile("Total cost", f"${gru['cost']:,.0f}",
|
| 295 |
+
f"vs ${naive['cost']:,.0f} at threshold 0.5")
|
| 296 |
+
+ "</div>"
|
| 297 |
+
+ f"<div class='optnote'>At ${review_cost:,.2f} per review, each model's own cost "
|
| 298 |
+
f"minimising threshold is <b>{opt_thr:.3f}</b> for the GRU "
|
| 299 |
+
f"(<b>${opt_cost:,.0f}</b>) and <b>{opt_thr_l:.3f}</b> for LightGBM "
|
| 300 |
+
f"(<b>${opt_cost_l:,.0f}</b>). Sequence context is worth "
|
| 301 |
+
f"<b>${opt_cost_l - opt_cost:,.0f}</b>.</div>"
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
fig = go.Figure()
|
| 305 |
+
for name, colour in (("GRU", "#4f46e5"), ("LightGBM", "#f59e0b")):
|
| 306 |
+
curve = cost_curve(name, review_cost)
|
| 307 |
+
s = CUM[name]["s_desc"]
|
| 308 |
+
step = max(1, len(s) // 1500)
|
| 309 |
+
fig.add_trace(
|
| 310 |
+
go.Scatter(
|
| 311 |
+
x=s[::step], y=curve[1:][::step], mode="lines", name=name,
|
| 312 |
+
line=dict(color=colour, width=2),
|
| 313 |
+
hovertemplate="threshold %{x:.3f}<br>cost $%{y:,.0f}<extra></extra>",
|
| 314 |
+
)
|
| 315 |
+
)
|
| 316 |
+
fig.add_vline(x=threshold, line_dash="dash", line_color="#94a3b8",
|
| 317 |
+
annotation_text="your threshold", annotation_position="top")
|
| 318 |
+
fig.add_trace(
|
| 319 |
+
go.Scatter(x=[opt_thr], y=[opt_cost], mode="markers", name="GRU optimum",
|
| 320 |
+
marker=dict(color="#4f46e5", size=12, symbol="circle"),
|
| 321 |
+
hovertemplate="optimum %{x:.3f}<br>$%{y:,.0f}<extra></extra>")
|
| 322 |
+
)
|
| 323 |
+
fig.update_layout(
|
| 324 |
+
yaxis_type="log",
|
| 325 |
+
xaxis_title="Decision threshold",
|
| 326 |
+
yaxis_title="Total cost, dollars (log scale)",
|
| 327 |
+
height=420,
|
| 328 |
+
hovermode="x unified",
|
| 329 |
+
)
|
| 330 |
+
# Decade ticks only. Plotly's default log minor ticks produce a cluttered
|
| 331 |
+
# axis of 2s and 3s next to the 100k labels.
|
| 332 |
+
fig.update_yaxes(dtick=1, tickprefix="$", tickformat="~s")
|
| 333 |
+
return tiles, _style_fig(fig)
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
def snap_to_optimal(review_cost: float):
|
| 337 |
+
thr, _ = optimal_threshold("GRU", review_cost)
|
| 338 |
+
return thr
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
# ----------------------------------------------------------------- tab three
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
def pr_figure() -> go.Figure:
|
| 345 |
+
fig = go.Figure()
|
| 346 |
+
for name, colour in (("GRU", "#4f46e5"), ("LightGBM", "#f59e0b")):
|
| 347 |
+
c = CUM[name]
|
| 348 |
+
k = np.arange(1, N_TEST + 1)
|
| 349 |
+
recall = c["tp"][1:] / N_FRAUD
|
| 350 |
+
precision = c["tp"][1:] / k
|
| 351 |
+
step = max(1, N_TEST // 2000)
|
| 352 |
+
fig.add_trace(
|
| 353 |
+
go.Scatter(x=recall[::step], y=precision[::step], mode="lines", name=name,
|
| 354 |
+
line=dict(color=colour, width=2)))
|
| 355 |
+
fig.add_hline(y=N_FRAUD / N_TEST, line_dash="dot", line_color="#94a3b8",
|
| 356 |
+
annotation_text="random classifier")
|
| 357 |
+
fig.update_layout(
|
| 358 |
+
xaxis_title="Recall", yaxis_title="Precision",
|
| 359 |
+
yaxis_range=[0, 1.02], height=400,
|
| 360 |
+
)
|
| 361 |
+
return _style_fig(fig)
|
| 362 |
+
|
| 363 |
+
|
| 364 |
+
M = CFG["metrics"]
|
| 365 |
+
ABOUT = f"""
|
| 366 |
+
### What this is
|
| 367 |
+
|
| 368 |
+
Most credit card fraud models score each transaction in isolation. But fraud is a
|
| 369 |
+
behavioural signal: what matters is that *this card* has never behaved this way before.
|
| 370 |
+
This model builds a per card sequence of recent transactions and feeds it to a GRU.
|
| 371 |
+
|
| 372 |
+
The comparison is against a LightGBM model given the **identical features** for the
|
| 373 |
+
transaction being scored, but no sequence context. That isolates what the sequence adds.
|
| 374 |
+
|
| 375 |
+
### Results on the held out time period
|
| 376 |
+
|
| 377 |
+
{CFG['n_test']:,} transactions, {CFG['n_fraud']:,} fraudulent ({CFG['n_fraud'] / CFG['n_test'] * 100:.3f} percent).
|
| 378 |
+
|
| 379 |
+
| Model | PR-AUC | ROC-AUC | Recall @ 0.1% | Recall @ 1% |
|
| 380 |
+
|---|---|---|---|---|
|
| 381 |
+
| **GRU (sequence)** | **{M['gru']['pr_auc']:.4f}** | {M['gru']['roc_auc']:.4f} | {M['gru']['recall_at_0.001']:.3f} | **{M['gru']['recall_at_0.01']:.3f}** |
|
| 382 |
+
| LightGBM (no sequence) | {M['lightgbm']['pr_auc']:.4f} | {M['lightgbm']['roc_auc']:.4f} | {M['lightgbm']['recall_at_0.001']:.3f} | {M['lightgbm']['recall_at_0.01']:.3f} |
|
| 383 |
+
| Logistic regression | {M['logreg']['pr_auc']:.4f} | {M['logreg']['roc_auc']:.4f} | {M['logreg']['recall_at_0.001']:.3f} | {M['logreg']['recall_at_0.01']:.3f} |
|
| 384 |
+
|
| 385 |
+
Accuracy is deliberately absent. Predicting "legitimate" for every transaction scores
|
| 386 |
+
**{CFG['trivial_accuracy'] * 100:.3f} percent** while catching nothing.
|
| 387 |
+
|
| 388 |
+
### How it was built
|
| 389 |
+
|
| 390 |
+
- **Chronological split by timestamp.** Every test row is strictly later than every training row.
|
| 391 |
+
- **Causal features only.** A card's average spend is an expanding mean over prior rows, so a
|
| 392 |
+
transaction never contributes to its own baseline.
|
| 393 |
+
- **Windows sliced on demand.** One flat array per split rather than materialising 1.29M
|
| 394 |
+
sequences, which keeps the whole thing inside 16 GB.
|
| 395 |
+
- **Trained on CPU.** 22,577 parameters, 4 epochs, 7.1 minutes on a laptop with no GPU.
|
| 396 |
+
|
| 397 |
+
### Honest limits
|
| 398 |
+
|
| 399 |
+
The data is simulated (Sparkov), and rule generated fraud is far more learnable than the
|
| 400 |
+
adversarial kind, so a PR-AUC of 0.965 is not a production number. The cost minimising
|
| 401 |
+
threshold is also selected on the test set, which makes those dollar figures optimistic.
|
| 402 |
+
Both points are covered in more detail in the repository.
|
| 403 |
+
|
| 404 |
+
[Full code, methodology and limitations on GitHub](https://github.com/adwitiyashukla/DL-based-sequential-fraud-detection)
|
| 405 |
+
"""
|
| 406 |
+
|
| 407 |
+
# Every colour below is either a Gradio theme variable or a translucent overlay,
|
| 408 |
+
# so the demo reads correctly in both light and dark mode. Hardcoding light
|
| 409 |
+
# greys here puts dark text on a dark background for anyone whose browser
|
| 410 |
+
# prefers dark, which is most people.
|
| 411 |
+
CSS = """
|
| 412 |
+
.gradio-container { max-width: 1180px !important; }
|
| 413 |
+
#hero { padding: 4px 0 2px 0; }
|
| 414 |
+
#hero h1 { font-size: 2rem; font-weight: 700; margin: 0 0 6px 0; letter-spacing: -0.02em;
|
| 415 |
+
color: var(--body-text-color); }
|
| 416 |
+
#hero p { color: var(--body-text-color-subdued); margin: 0; font-size: 1.02rem; }
|
| 417 |
+
|
| 418 |
+
.caseheader { display:flex; gap:34px; padding:14px 18px;
|
| 419 |
+
background: var(--background-fill-secondary);
|
| 420 |
+
border:1px solid var(--border-color-primary); border-radius:10px;
|
| 421 |
+
margin-bottom:6px; flex-wrap:wrap; }
|
| 422 |
+
.caseheader > div { display:flex; flex-direction:column; }
|
| 423 |
+
.ch-label { font-size:0.7rem; text-transform:uppercase; letter-spacing:0.07em;
|
| 424 |
+
color: var(--body-text-color-subdued); }
|
| 425 |
+
.ch-val { font-size:1.12rem; font-weight:650; color: var(--body-text-color); }
|
| 426 |
+
|
| 427 |
+
.tablewrap { overflow-x:auto; border:1px solid var(--border-color-primary); border-radius:10px; }
|
| 428 |
+
table.hist { width:100%; border-collapse:collapse; font-size:0.87rem; }
|
| 429 |
+
table.hist th { background: var(--background-fill-secondary); text-align:left;
|
| 430 |
+
padding:9px 11px; font-weight:600; color: var(--body-text-color-subdued);
|
| 431 |
+
border-bottom:1px solid var(--border-color-primary); white-space:nowrap;
|
| 432 |
+
font-size:0.78rem; text-transform:uppercase; letter-spacing:0.04em; }
|
| 433 |
+
table.hist td { padding:8px 11px; border-bottom:1px solid var(--border-color-primary);
|
| 434 |
+
color: var(--body-text-color); white-space:nowrap; }
|
| 435 |
+
table.hist td.num { text-align:right; font-variant-numeric:tabular-nums; }
|
| 436 |
+
table.hist td.marker { color:#818cf8; width:18px; }
|
| 437 |
+
table.hist tr.target td { background: rgba(99,102,241,0.18); font-weight:700; }
|
| 438 |
+
.caption { color: var(--body-text-color-subdued); font-size:0.8rem; margin:7px 2px 0 2px; }
|
| 439 |
+
|
| 440 |
+
.panelrow { display:flex; gap:16px; flex-wrap:wrap; }
|
| 441 |
+
.scorecard { flex:1; min-width:250px; border:1px solid var(--border-color-primary);
|
| 442 |
+
border-radius:12px; padding:16px 18px; background: var(--background-fill-primary); }
|
| 443 |
+
.scorecard.flag { border-color: rgba(239,68,68,0.45); background: rgba(239,68,68,0.09); }
|
| 444 |
+
.scorecard.clear { border-color: rgba(34,197,94,0.45); background: rgba(34,197,94,0.09); }
|
| 445 |
+
.sc-name { font-size:1.02rem; font-weight:700; color: var(--body-text-color); }
|
| 446 |
+
.sc-sub { font-size:0.78rem; color: var(--body-text-color-subdued); margin-bottom:10px; }
|
| 447 |
+
.sc-prob { font-size:2.5rem; font-weight:700; line-height:1; color: var(--body-text-color);
|
| 448 |
+
font-variant-numeric:tabular-nums; }
|
| 449 |
+
.sc-prob span { font-size:1.1rem; color: var(--body-text-color-subdued); margin-left:2px; }
|
| 450 |
+
.sc-bar { position:relative; height:8px; background: rgba(148,163,184,0.30);
|
| 451 |
+
border-radius:4px; margin:12px 0 10px 0; }
|
| 452 |
+
.sc-fill { position:absolute; height:100%; border-radius:4px; background:#6366f1; }
|
| 453 |
+
.scorecard.flag .sc-fill { background:#ef4444; }
|
| 454 |
+
.scorecard.clear .sc-fill { background:#22c55e; }
|
| 455 |
+
.sc-thr { position:absolute; top:-3px; width:2px; height:14px; background: var(--body-text-color); }
|
| 456 |
+
.sc-verdict { font-size:0.82rem; font-weight:700; letter-spacing:0.06em; }
|
| 457 |
+
.scorecard.flag .sc-verdict { color:#ef4444; }
|
| 458 |
+
.scorecard.clear .sc-verdict { color:#22c55e; }
|
| 459 |
+
.sc-thrlab { font-size:0.72rem; color: var(--body-text-color-subdued); margin-top:3px; }
|
| 460 |
+
|
| 461 |
+
.banner { display:flex; align-items:center; gap:14px; padding:13px 18px; border-radius:10px;
|
| 462 |
+
margin-top:4px; flex-wrap:wrap; }
|
| 463 |
+
.banner.good { background: rgba(34,197,94,0.10); border:1px solid rgba(34,197,94,0.40); }
|
| 464 |
+
.banner.bad { background: rgba(239,68,68,0.10); border:1px solid rgba(239,68,68,0.40); }
|
| 465 |
+
.banner.warn { background: rgba(245,158,11,0.12); border:1px solid rgba(245,158,11,0.40); }
|
| 466 |
+
.b-label { font-size:0.7rem; text-transform:uppercase; letter-spacing:0.07em;
|
| 467 |
+
color: var(--body-text-color-subdued); }
|
| 468 |
+
.b-truth { font-weight:750; font-size:0.95rem; letter-spacing:0.03em;
|
| 469 |
+
color: var(--body-text-color); }
|
| 470 |
+
.b-msg { color: var(--body-text-color); opacity:0.85; font-size:0.9rem; }
|
| 471 |
+
|
| 472 |
+
.tilehead { font-size:0.72rem; text-transform:uppercase; letter-spacing:0.07em;
|
| 473 |
+
color: var(--body-text-color-subdued); margin:2px 2px 7px 2px; }
|
| 474 |
+
.tilerow { display:flex; gap:12px; flex-wrap:wrap; margin-bottom:10px; }
|
| 475 |
+
.tile { flex:1; min-width:150px; border:1px solid var(--border-color-primary);
|
| 476 |
+
border-radius:10px; padding:13px 15px; background: var(--background-fill-primary); }
|
| 477 |
+
.t-label { font-size:0.7rem; text-transform:uppercase; letter-spacing:0.07em;
|
| 478 |
+
color: var(--body-text-color-subdued); }
|
| 479 |
+
.t-value { font-size:1.6rem; font-weight:700; color: var(--body-text-color);
|
| 480 |
+
font-variant-numeric:tabular-nums; line-height:1.2; }
|
| 481 |
+
.t-sub { font-size:0.76rem; color: var(--body-text-color-subdued); }
|
| 482 |
+
.optnote { padding:12px 16px; background: rgba(99,102,241,0.12);
|
| 483 |
+
border:1px solid rgba(99,102,241,0.40); border-radius:10px;
|
| 484 |
+
color: var(--body-text-color); font-size:0.92rem; }
|
| 485 |
+
"""
|
| 486 |
+
|
| 487 |
+
THEME = gr.themes.Base(
|
| 488 |
+
primary_hue=gr.themes.colors.indigo,
|
| 489 |
+
secondary_hue=gr.themes.colors.slate,
|
| 490 |
+
neutral_hue=gr.themes.colors.slate,
|
| 491 |
+
)
|
| 492 |
+
|
| 493 |
+
with gr.Blocks(title="Sequential Fraud Detection") as demo:
|
| 494 |
+
gr.HTML(
|
| 495 |
+
"<div id='hero'><h1>Sequential Fraud Detection</h1>"
|
| 496 |
+
"<p>A GRU reads each card's last 10 transactions. A LightGBM baseline sees the same "
|
| 497 |
+
"features without the sequence. Every score below is computed live.</p></div>"
|
| 498 |
+
)
|
| 499 |
+
|
| 500 |
+
with gr.Tabs():
|
| 501 |
+
with gr.Tab("Score a transaction"):
|
| 502 |
+
with gr.Row():
|
| 503 |
+
scenario = gr.Dropdown(
|
| 504 |
+
choices=CFG["scenario_order"],
|
| 505 |
+
value=CFG["scenario_order"][0],
|
| 506 |
+
label="Pick a scenario",
|
| 507 |
+
scale=3,
|
| 508 |
+
)
|
| 509 |
+
shuffle = gr.Button("Load another case", variant="primary", scale=1)
|
| 510 |
+
|
| 511 |
+
case_header = gr.HTML()
|
| 512 |
+
history = gr.HTML()
|
| 513 |
+
panels = gr.HTML()
|
| 514 |
+
banner = gr.HTML()
|
| 515 |
+
|
| 516 |
+
scenario.change(load_case, scenario, [case_header, history, panels, banner])
|
| 517 |
+
shuffle.click(load_case, scenario, [case_header, history, panels, banner])
|
| 518 |
+
demo.load(load_case, scenario, [case_header, history, panels, banner])
|
| 519 |
+
|
| 520 |
+
with gr.Tab("Cost explorer"):
|
| 521 |
+
gr.Markdown(
|
| 522 |
+
"A missed fraud costs the full transaction amount. A false positive costs a "
|
| 523 |
+
"manual review. Move the inputs and watch where the optimum goes: as reviews "
|
| 524 |
+
"get more expensive, the threshold rises and you alert less."
|
| 525 |
+
)
|
| 526 |
+
with gr.Row():
|
| 527 |
+
review_cost = gr.Slider(1, 50, value=CFG["default_review_cost"], step=0.5,
|
| 528 |
+
label="Cost of one manual review ($)")
|
| 529 |
+
threshold = gr.Slider(0.0, 1.0, value=THR_GRU, step=0.001,
|
| 530 |
+
label="Decision threshold")
|
| 531 |
+
snap = gr.Button("Snap to the cost minimising threshold")
|
| 532 |
+
|
| 533 |
+
tiles = gr.HTML()
|
| 534 |
+
cost_plot = gr.Plot(show_label=False)
|
| 535 |
+
|
| 536 |
+
for control in (review_cost, threshold):
|
| 537 |
+
control.change(explore, [review_cost, threshold], [tiles, cost_plot])
|
| 538 |
+
snap.click(snap_to_optimal, review_cost, threshold)
|
| 539 |
+
demo.load(explore, [review_cost, threshold], [tiles, cost_plot])
|
| 540 |
+
|
| 541 |
+
with gr.Tab("How it works"):
|
| 542 |
+
gr.Markdown(ABOUT)
|
| 543 |
+
gr.Plot(pr_figure(), show_label=False)
|
| 544 |
+
|
| 545 |
+
|
| 546 |
+
if __name__ == "__main__":
|
| 547 |
+
# Sanity check: live inference must reproduce the scores computed during
|
| 548 |
+
# the original evaluation run. A mismatch means the export is misaligned.
|
| 549 |
+
ids = META.sample_id.values[:25]
|
| 550 |
+
d_gru = max(abs(score_gru(int(i)) - float(META.score_gru[i])) for i in ids)
|
| 551 |
+
d_lgb = max(abs(score_lgb(int(i)) - float(META.score_lgb[i])) for i in ids)
|
| 552 |
+
print(f"[check] max score drift GRU {d_gru:.2e} LightGBM {d_lgb:.2e}")
|
| 553 |
+
|
| 554 |
+
# Gradio 6 takes theme and css at launch time rather than on the Blocks
|
| 555 |
+
# constructor. The Space pins this same version, so behaviour matches.
|
| 556 |
+
demo.launch(theme=THEME, css=CSS)
|
assets/config.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"seq_len": 10,
|
| 3 |
+
"n_numeric": 11,
|
| 4 |
+
"n_categories": 14,
|
| 5 |
+
"threshold_gru": 0.7138904333114624,
|
| 6 |
+
"threshold_lgb": 0.5880337498766687,
|
| 7 |
+
"default_review_cost": 5.0,
|
| 8 |
+
"n_test": 555719,
|
| 9 |
+
"n_fraud": 2145,
|
| 10 |
+
"total_fraud_amount": 1133324.75,
|
| 11 |
+
"trivial_accuracy": 0.9961401224136353,
|
| 12 |
+
"train_rows": 1296675,
|
| 13 |
+
"train_frauds": 7506,
|
| 14 |
+
"scenario_order": [
|
| 15 |
+
"Fraud caught by the GRU, missed by LightGBM",
|
| 16 |
+
"Fraud caught by both models",
|
| 17 |
+
"Fraud missed by the GRU",
|
| 18 |
+
"False alarm raised by the GRU",
|
| 19 |
+
"Legitimate, correctly cleared"
|
| 20 |
+
],
|
| 21 |
+
"metrics": {
|
| 22 |
+
"gru": {
|
| 23 |
+
"pr_auc": 0.9652347549329161,
|
| 24 |
+
"roc_auc": 0.9992351169901056,
|
| 25 |
+
"recall_at_0.001": 0.2592074592074592,
|
| 26 |
+
"recall_at_0.01": 0.9892773892773893
|
| 27 |
+
},
|
| 28 |
+
"lightgbm": {
|
| 29 |
+
"pr_auc": 0.8985085792471785,
|
| 30 |
+
"roc_auc": 0.9974172780171616,
|
| 31 |
+
"recall_at_0.001": 0.25874125874125875,
|
| 32 |
+
"recall_at_0.01": 0.9501165501165502
|
| 33 |
+
},
|
| 34 |
+
"logreg": {
|
| 35 |
+
"pr_auc": 0.21899742390107182,
|
| 36 |
+
"roc_auc": 0.919594279505511,
|
| 37 |
+
"recall_at_0.001": 0.11701631701631701,
|
| 38 |
+
"recall_at_0.01": 0.5538461538461539
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
}
|
assets/demo_display.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
assets/demo_meta.csv
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
sample_id,scenario,card,is_fraud,score_gru,score_lgb,amount,history_length
|
| 2 |
+
0,"Fraud caught by the GRU, missed by LightGBM",card ending 0731,1,0.9999901,0.58049184,7.15,10
|
| 3 |
+
1,"Fraud caught by the GRU, missed by LightGBM",card ending 0710,1,0.99999774,0.07683075,7.23,10
|
| 4 |
+
2,"Fraud caught by the GRU, missed by LightGBM",card ending 1577,1,0.83218426,0.5303817,7.83,10
|
| 5 |
+
3,"Fraud caught by the GRU, missed by LightGBM",card ending 2529,1,0.99983704,0.2679808,9.03,10
|
| 6 |
+
4,"Fraud caught by the GRU, missed by LightGBM",card ending 0168,1,0.99998915,0.46935344,9.1,10
|
| 7 |
+
5,"Fraud caught by the GRU, missed by LightGBM",card ending 9669,1,0.9999827,0.23812468,7.74,10
|
| 8 |
+
6,"Fraud caught by the GRU, missed by LightGBM",card ending 9817,1,0.99994063,0.003563515,26.34,10
|
| 9 |
+
7,"Fraud caught by the GRU, missed by LightGBM",card ending 3035,1,0.9972844,0.2954881,801.38,10
|
| 10 |
+
8,"Fraud caught by the GRU, missed by LightGBM",card ending 2988,1,0.9999976,0.0026371698,229.83,10
|
| 11 |
+
9,"Fraud caught by the GRU, missed by LightGBM",card ending 0127,1,0.9999708,0.3038156,18.72,10
|
| 12 |
+
10,"Fraud caught by the GRU, missed by LightGBM",card ending 0167,1,0.99998975,0.10531434,21.55,10
|
| 13 |
+
11,"Fraud caught by the GRU, missed by LightGBM",card ending 6071,1,0.99999845,0.46532136,21.88,10
|
| 14 |
+
12,"Fraud caught by the GRU, missed by LightGBM",card ending 2620,1,0.99944407,0.09026146,95.52,10
|
| 15 |
+
13,"Fraud caught by the GRU, missed by LightGBM",card ending 3773,1,0.99996734,0.5627058,10.23,10
|
| 16 |
+
14,"Fraud caught by the GRU, missed by LightGBM",card ending 3773,1,0.9999467,0.24042638,874.71,10
|
| 17 |
+
15,"Fraud caught by the GRU, missed by LightGBM",card ending 9724,1,0.9999778,0.00018027819,219.46,10
|
| 18 |
+
16,"Fraud caught by the GRU, missed by LightGBM",card ending 2311,1,0.9977331,0.19513614,107.46,10
|
| 19 |
+
17,"Fraud caught by the GRU, missed by LightGBM",card ending 8828,1,0.9988404,0.06663319,21.19,10
|
| 20 |
+
18,"Fraud caught by the GRU, missed by LightGBM",card ending 1404,1,0.9997193,0.32762414,137.61,10
|
| 21 |
+
19,"Fraud caught by the GRU, missed by LightGBM",card ending 9816,1,0.9994796,0.31162152,16.69,10
|
| 22 |
+
20,"Fraud caught by the GRU, missed by LightGBM",card ending 3698,1,0.9998939,0.43868282,22.55,10
|
| 23 |
+
21,"Fraud caught by the GRU, missed by LightGBM",card ending 7959,1,0.94743615,0.053698935,5.6,10
|
| 24 |
+
22,"Fraud caught by the GRU, missed by LightGBM",card ending 2095,1,0.99940634,0.0029671884,218.12,10
|
| 25 |
+
23,"Fraud caught by the GRU, missed by LightGBM",card ending 0073,1,0.99999297,0.54631144,10.18,10
|
| 26 |
+
24,"Fraud caught by the GRU, missed by LightGBM",card ending 9543,1,0.9994685,0.32986715,139.84,10
|
| 27 |
+
25,"Fraud caught by the GRU, missed by LightGBM",card ending 3197,1,0.9999287,0.016789751,102.42,10
|
| 28 |
+
26,"Fraud caught by the GRU, missed by LightGBM",card ending 9375,1,0.99993527,0.22732805,24.84,10
|
| 29 |
+
27,"Fraud caught by the GRU, missed by LightGBM",card ending 9446,1,0.9999646,0.33600897,949.25,10
|
| 30 |
+
28,"Fraud caught by the GRU, missed by LightGBM",card ending 8561,1,0.7230288,0.024998164,241.45,10
|
| 31 |
+
29,"Fraud caught by the GRU, missed by LightGBM",card ending 8268,1,0.99992776,0.16690575,23.36,10
|
| 32 |
+
30,"Fraud caught by the GRU, missed by LightGBM",card ending 8268,1,0.9998417,0.0011061538,232.25,10
|
| 33 |
+
31,"Fraud caught by the GRU, missed by LightGBM",card ending 9806,1,0.9999981,0.56434464,6.67,10
|
| 34 |
+
32,"Fraud caught by the GRU, missed by LightGBM",card ending 8423,1,0.9998785,0.0022294042,17.51,10
|
| 35 |
+
33,"Fraud caught by the GRU, missed by LightGBM",card ending 3829,1,0.99996555,0.0075690732,216.01,10
|
| 36 |
+
34,"Fraud caught by the GRU, missed by LightGBM",card ending 3829,1,0.99970335,0.021006443,234.54,10
|
| 37 |
+
35,"Fraud caught by the GRU, missed by LightGBM",card ending 7162,1,0.9422511,0.0007981081,10.53,1
|
| 38 |
+
36,"Fraud caught by the GRU, missed by LightGBM",card ending 9660,1,0.99995136,0.0044562295,16.75,10
|
| 39 |
+
37,"Fraud caught by the GRU, missed by LightGBM",card ending 1180,1,0.99995935,0.2331846,9.94,10
|
| 40 |
+
38,"Fraud caught by the GRU, missed by LightGBM",card ending 9750,1,0.92301565,0.11316425,1029.47,10
|
| 41 |
+
39,"Fraud caught by the GRU, missed by LightGBM",card ending 0532,1,0.9999695,0.17258067,18.86,10
|
| 42 |
+
40,"Fraud caught by the GRU, missed by LightGBM",card ending 0532,1,0.99971026,0.23307104,10.46,10
|
| 43 |
+
41,"Fraud caught by the GRU, missed by LightGBM",card ending 8276,1,0.999998,0.011943006,236.42,10
|
| 44 |
+
42,"Fraud caught by the GRU, missed by LightGBM",card ending 9997,1,0.99991167,0.256229,7.12,10
|
| 45 |
+
43,"Fraud caught by the GRU, missed by LightGBM",card ending 5522,1,0.9998301,0.013598998,168.06,10
|
| 46 |
+
44,"Fraud caught by the GRU, missed by LightGBM",card ending 2306,1,0.99999404,0.4513615,16.91,10
|
| 47 |
+
45,"Fraud caught by the GRU, missed by LightGBM",card ending 4121,1,0.99993837,0.014097717,9.89,10
|
| 48 |
+
46,"Fraud caught by the GRU, missed by LightGBM",card ending 9027,1,0.9993548,0.0077013858,52.82,10
|
| 49 |
+
47,"Fraud caught by the GRU, missed by LightGBM",card ending 6016,1,0.99965966,0.43362147,101.04,10
|
| 50 |
+
48,"Fraud caught by the GRU, missed by LightGBM",card ending 5730,1,0.99998176,0.3565554,9.65,10
|
| 51 |
+
49,"Fraud caught by the GRU, missed by LightGBM",card ending 5369,1,0.9999312,0.58591455,21.11,10
|
| 52 |
+
50,Fraud caught by both models,card ending 3109,1,0.9999857,0.99878836,8.45,10
|
| 53 |
+
51,Fraud caught by both models,card ending 9986,1,0.9999591,0.99986035,315.39,10
|
| 54 |
+
52,Fraud caught by both models,card ending 7795,1,0.99991953,0.9993055,988.01,10
|
| 55 |
+
53,Fraud caught by both models,card ending 7795,1,0.99993825,0.9994888,859.65,10
|
| 56 |
+
54,Fraud caught by both models,card ending 2597,1,0.99997354,0.9966072,916.04,10
|
| 57 |
+
55,Fraud caught by both models,card ending 0710,1,0.99999964,0.999809,301.37,10
|
| 58 |
+
56,Fraud caught by both models,card ending 5974,1,0.99999976,0.99975175,897.8,10
|
| 59 |
+
57,Fraud caught by both models,card ending 3689,1,0.9959369,0.9997346,722.27,10
|
| 60 |
+
58,Fraud caught by both models,card ending 3689,1,0.99996936,0.99966264,969.05,10
|
| 61 |
+
59,Fraud caught by both models,card ending 8158,1,0.9998592,0.90191764,641.44,10
|
| 62 |
+
60,Fraud caught by both models,card ending 5985,1,0.99981207,0.98782736,114.9,10
|
| 63 |
+
61,Fraud caught by both models,card ending 0127,1,0.9919852,0.9975527,855.54,10
|
| 64 |
+
62,Fraud caught by both models,card ending 7183,1,0.98905444,0.9995207,319.23,10
|
| 65 |
+
63,Fraud caught by both models,card ending 7661,1,0.9999988,0.99849623,1009.4,10
|
| 66 |
+
64,Fraud caught by both models,card ending 0192,1,0.9999728,0.9880377,780.45,10
|
| 67 |
+
65,Fraud caught by both models,card ending 3109,1,0.9999682,0.999895,789.22,10
|
| 68 |
+
66,Fraud caught by both models,card ending 6333,1,0.9999944,0.98842525,1019.51,10
|
| 69 |
+
67,Fraud caught by both models,card ending 3058,1,0.99999857,0.99954045,950.19,10
|
| 70 |
+
68,Fraud caught by both models,card ending 2684,1,0.9975304,0.9878894,1043.4,10
|
| 71 |
+
69,Fraud caught by both models,card ending 2684,1,0.99853396,0.98807144,127.17,10
|
| 72 |
+
70,Fraud caught by both models,card ending 8609,1,0.9999846,0.9974542,313.62,10
|
| 73 |
+
71,Fraud caught by both models,card ending 6425,1,0.9999471,0.9995023,8.65,10
|
| 74 |
+
72,Fraud caught by both models,card ending 6425,1,0.9999515,0.9998004,314.74,10
|
| 75 |
+
73,Fraud caught by both models,card ending 6555,1,0.9999876,0.9994462,1024.42,10
|
| 76 |
+
74,Fraud caught by both models,card ending 9497,1,0.999969,0.99967915,10.64,10
|
| 77 |
+
75,Fraud caught by both models,card ending 9543,1,0.9998535,0.9995925,1111.35,10
|
| 78 |
+
76,Fraud caught by both models,card ending 9330,1,0.99999595,0.9927563,250.32,10
|
| 79 |
+
77,Fraud caught by both models,card ending 2582,1,0.99999297,0.99987465,312.5,10
|
| 80 |
+
78,Fraud caught by both models,card ending 2582,1,0.99995697,0.9998308,306.79,10
|
| 81 |
+
79,Fraud caught by both models,card ending 2582,1,0.9999763,0.9998508,332.24,10
|
| 82 |
+
80,Fraud caught by both models,card ending 7350,1,0.99997413,0.9998084,727.32,10
|
| 83 |
+
81,Fraud caught by both models,card ending 8089,1,0.999998,0.9997776,332.59,2
|
| 84 |
+
82,Fraud caught by both models,card ending 5425,1,0.9999975,0.9998568,348.61,10
|
| 85 |
+
83,Fraud caught by both models,card ending 2580,1,0.99994457,0.98760927,1003.86,10
|
| 86 |
+
84,Fraud caught by both models,card ending 3890,1,0.999099,0.9928431,22.11,10
|
| 87 |
+
85,Fraud caught by both models,card ending 8268,1,0.9999881,0.9938878,1001.99,10
|
| 88 |
+
86,Fraud caught by both models,card ending 3829,1,0.9979564,0.9988224,829.36,10
|
| 89 |
+
87,Fraud caught by both models,card ending 9716,1,0.9884985,0.99763703,739.85,1
|
| 90 |
+
88,Fraud caught by both models,card ending 1412,1,0.99950457,0.998244,18.9,10
|
| 91 |
+
89,Fraud caught by both models,card ending 1241,1,0.99998593,0.9998611,725.34,10
|
| 92 |
+
90,Fraud caught by both models,card ending 0532,1,0.9999294,0.9979735,709.07,10
|
| 93 |
+
91,Fraud caught by both models,card ending 6057,1,0.99999154,0.99513406,1034.66,10
|
| 94 |
+
92,Fraud caught by both models,card ending 8276,1,0.9999776,0.9970941,214.51,10
|
| 95 |
+
93,Fraud caught by both models,card ending 8648,1,0.999652,0.8172538,15.99,10
|
| 96 |
+
94,Fraud caught by both models,card ending 4802,1,0.9994924,0.99964267,309.7,10
|
| 97 |
+
95,Fraud caught by both models,card ending 1314,1,0.9999496,0.99935716,758.6,10
|
| 98 |
+
96,Fraud caught by both models,card ending 2306,1,0.99999475,0.99945134,750.68,10
|
| 99 |
+
97,Fraud caught by both models,card ending 2193,1,0.99993896,0.99701965,267.74,10
|
| 100 |
+
98,Fraud caught by both models,card ending 9245,1,0.9998385,0.9600009,18.85,10
|
| 101 |
+
99,Fraud caught by both models,card ending 6883,1,0.99975866,0.99970233,960.14,10
|
| 102 |
+
100,Fraud missed by the GRU,card ending 2387,1,0.16554838,0.9301583,8.08,10
|
| 103 |
+
101,Fraud missed by the GRU,card ending 2621,1,0.5525818,0.99771875,305.1,10
|
| 104 |
+
102,Fraud missed by the GRU,card ending 2455,1,0.0011045086,0.007584502,19.02,10
|
| 105 |
+
103,Fraud missed by the GRU,card ending 2455,1,0.0015910426,0.30770463,9.25,10
|
| 106 |
+
104,Fraud missed by the GRU,card ending 2455,1,0.6191622,0.998649,1130.56,10
|
| 107 |
+
105,Fraud missed by the GRU,card ending 7768,1,0.4106906,0.90742165,260.44,10
|
| 108 |
+
106,Fraud missed by the GRU,card ending 3689,1,0.43455544,0.99966955,274.08,10
|
| 109 |
+
107,Fraud missed by the GRU,card ending 9584,1,0.0056192502,0.03374157,8.49,10
|
| 110 |
+
108,Fraud missed by the GRU,card ending 9584,1,0.26173532,0.99981177,710.45,10
|
| 111 |
+
109,Fraud missed by the GRU,card ending 9584,1,0.1275148,0.9977829,48.95,10
|
| 112 |
+
110,Fraud missed by the GRU,card ending 0127,1,0.030776095,0.6068254,133.43,10
|
| 113 |
+
111,Fraud missed by the GRU,card ending 2290,1,0.6597321,0.9996876,283.58,10
|
| 114 |
+
112,Fraud missed by the GRU,card ending 2311,1,0.00012275545,0.0031429573,50.59,10
|
| 115 |
+
113,Fraud missed by the GRU,card ending 4065,1,0.18986869,0.9817645,837.53,10
|
| 116 |
+
114,Fraud missed by the GRU,card ending 3386,1,0.172274,0.07801823,111.17,10
|
| 117 |
+
115,Fraud missed by the GRU,card ending 0662,1,0.5255446,0.70071393,119.72,10
|
| 118 |
+
116,Fraud missed by the GRU,card ending 7879,1,0.025860542,0.60389924,21.38,10
|
| 119 |
+
117,Fraud missed by the GRU,card ending 6568,1,0.604364,0.36144868,140.13,10
|
| 120 |
+
118,Fraud missed by the GRU,card ending 0341,1,0.09775736,0.01590944,46.36,10
|
| 121 |
+
119,Fraud missed by the GRU,card ending 5374,1,0.0062118103,0.0067008287,21.09,10
|
| 122 |
+
120,Fraud missed by the GRU,card ending 4931,1,0.00041542365,7.388653e-05,17.45,10
|
| 123 |
+
121,Fraud missed by the GRU,card ending 6057,1,0.20173854,0.8969215,966.98,10
|
| 124 |
+
122,Fraud missed by the GRU,card ending 7745,1,0.012767666,3.5899473e-05,5.26,10
|
| 125 |
+
123,Fraud missed by the GRU,card ending 0186,1,0.061968446,0.67524207,19.05,10
|
| 126 |
+
124,Fraud missed by the GRU,card ending 0186,1,0.038256023,0.95467687,149.42,10
|
| 127 |
+
125,Fraud missed by the GRU,card ending 0186,1,0.0202298,0.7926477,8.41,10
|
| 128 |
+
126,Fraud missed by the GRU,card ending 0186,1,0.013994445,0.9801244,17.77,10
|
| 129 |
+
127,Fraud missed by the GRU,card ending 0915,1,0.06988248,0.835683,17.71,10
|
| 130 |
+
128,Fraud missed by the GRU,card ending 0915,1,0.4762789,0.22455584,132.33,10
|
| 131 |
+
129,Fraud missed by the GRU,card ending 9027,1,0.022901328,0.60879016,52.45,10
|
| 132 |
+
130,Fraud missed by the GRU,card ending 6408,1,0.6043448,0.06975564,107.67,10
|
| 133 |
+
131,False alarm raised by the GRU,card ending 6219,0,0.85642076,0.222954,21.71,10
|
| 134 |
+
132,False alarm raised by the GRU,card ending 0592,0,0.9562068,0.9891122,15.62,10
|
| 135 |
+
133,False alarm raised by the GRU,card ending 0168,0,0.99896085,0.000475615,21.17,10
|
| 136 |
+
134,False alarm raised by the GRU,card ending 9857,0,0.71894515,0.23299862,8.36,10
|
| 137 |
+
135,False alarm raised by the GRU,card ending 9857,0,0.76245147,1.9675595e-05,21.18,10
|
| 138 |
+
136,False alarm raised by the GRU,card ending 1492,0,0.99582773,0.73138404,24.55,10
|
| 139 |
+
137,False alarm raised by the GRU,card ending 3023,0,0.7675419,0.10415603,1380.66,10
|
| 140 |
+
138,False alarm raised by the GRU,card ending 2315,0,0.9180552,0.000408209,156.99,10
|
| 141 |
+
139,False alarm raised by the GRU,card ending 8427,0,0.996307,0.2102755,6.33,10
|
| 142 |
+
140,False alarm raised by the GRU,card ending 5770,0,0.9905577,0.20583712,2.03,10
|
| 143 |
+
141,False alarm raised by the GRU,card ending 9449,0,0.98763496,0.44082472,524.65,10
|
| 144 |
+
142,False alarm raised by the GRU,card ending 0895,0,0.99955565,0.18312402,19.31,10
|
| 145 |
+
143,False alarm raised by the GRU,card ending 7270,0,0.99845445,0.0012007295,53.68,10
|
| 146 |
+
144,False alarm raised by the GRU,card ending 0336,0,0.96198565,0.0001142461,30.95,10
|
| 147 |
+
145,False alarm raised by the GRU,card ending 4907,0,0.9772613,0.05348462,1722.76,10
|
| 148 |
+
146,False alarm raised by the GRU,card ending 4333,0,0.99542135,0.0034557006,35.2,10
|
| 149 |
+
147,False alarm raised by the GRU,card ending 2492,0,0.8759521,0.027181638,506.95,10
|
| 150 |
+
148,False alarm raised by the GRU,card ending 3960,0,0.9686354,0.21529111,895.63,10
|
| 151 |
+
149,False alarm raised by the GRU,card ending 1233,0,0.99328494,0.8937972,6.92,10
|
| 152 |
+
150,False alarm raised by the GRU,card ending 4120,0,0.9995865,0.00012935966,84.55,10
|
| 153 |
+
151,False alarm raised by the GRU,card ending 4120,0,0.9847552,0.00047943555,210.84,10
|
| 154 |
+
152,False alarm raised by the GRU,card ending 9419,0,0.8189587,6.085334e-05,3.67,10
|
| 155 |
+
153,False alarm raised by the GRU,card ending 3625,0,0.9877529,0.544954,7.68,10
|
| 156 |
+
154,False alarm raised by the GRU,card ending 1552,0,0.90577024,1.9137733e-05,4.93,10
|
| 157 |
+
155,False alarm raised by the GRU,card ending 9773,0,0.9941491,0.17576227,914.1,10
|
| 158 |
+
156,False alarm raised by the GRU,card ending 3522,0,0.8205868,0.012814419,1012.41,10
|
| 159 |
+
157,False alarm raised by the GRU,card ending 6548,0,0.9071076,0.05155802,376.85,10
|
| 160 |
+
158,False alarm raised by the GRU,card ending 9494,0,0.72276217,0.0066942046,1232.74,10
|
| 161 |
+
159,False alarm raised by the GRU,card ending 0035,0,0.7807672,0.00034983282,32.69,10
|
| 162 |
+
160,False alarm raised by the GRU,card ending 4285,0,0.7865164,0.00022320634,172.9,10
|
| 163 |
+
161,False alarm raised by the GRU,card ending 0341,0,0.94774354,0.0006213321,25.42,10
|
| 164 |
+
162,False alarm raised by the GRU,card ending 5478,0,0.99317884,0.040101014,20.18,10
|
| 165 |
+
163,False alarm raised by the GRU,card ending 5751,0,0.9223014,0.92138296,1855.84,10
|
| 166 |
+
164,False alarm raised by the GRU,card ending 7671,0,0.9948413,0.051075343,2606.86,10
|
| 167 |
+
165,False alarm raised by the GRU,card ending 4630,0,0.9174663,0.04582901,1310.91,10
|
| 168 |
+
166,False alarm raised by the GRU,card ending 4931,0,0.8713996,0.022155289,1348.88,10
|
| 169 |
+
167,False alarm raised by the GRU,card ending 6294,0,0.72085,0.9251436,2274.48,10
|
| 170 |
+
168,False alarm raised by the GRU,card ending 6400,0,0.7788446,0.019208128,18.77,10
|
| 171 |
+
169,False alarm raised by the GRU,card ending 9750,0,0.8714391,0.087344326,18.41,10
|
| 172 |
+
170,False alarm raised by the GRU,card ending 8276,0,0.8704047,0.16692759,985.13,10
|
| 173 |
+
171,False alarm raised by the GRU,card ending 5903,0,0.82258725,0.0012041178,5.45,10
|
| 174 |
+
172,False alarm raised by the GRU,card ending 9997,0,0.7182004,0.7337843,11.11,10
|
| 175 |
+
173,False alarm raised by the GRU,card ending 8160,0,0.7933024,0.00012148686,65.88,10
|
| 176 |
+
174,False alarm raised by the GRU,card ending 4500,0,0.99545586,0.9975607,19.73,10
|
| 177 |
+
175,False alarm raised by the GRU,card ending 8216,0,0.7510039,0.0001805572,43.94,10
|
| 178 |
+
176,False alarm raised by the GRU,card ending 0186,0,0.727426,0.62168807,22.59,10
|
| 179 |
+
177,False alarm raised by the GRU,card ending 5434,0,0.9461209,0.13520549,10.79,10
|
| 180 |
+
178,False alarm raised by the GRU,card ending 1302,0,0.79255116,0.005438155,6.26,10
|
| 181 |
+
179,False alarm raised by the GRU,card ending 2777,0,0.99631643,0.26869652,23.98,10
|
| 182 |
+
180,False alarm raised by the GRU,card ending 9432,0,0.9905041,0.01538513,13.34,10
|
| 183 |
+
181,"Legitimate, correctly cleared",card ending 6709,0,8.893039e-06,4.6896497e-05,101.21,10
|
| 184 |
+
182,"Legitimate, correctly cleared",card ending 5126,0,4.690343e-05,1.6899228e-05,9.05,10
|
| 185 |
+
183,"Legitimate, correctly cleared",card ending 7322,0,6.79495e-05,0.00012251438,51.55,10
|
| 186 |
+
184,"Legitimate, correctly cleared",card ending 2499,0,1.1658574e-05,0.0041574873,23.49,10
|
| 187 |
+
185,"Legitimate, correctly cleared",card ending 2499,0,1.8802855e-05,0.00016973798,91.8,10
|
| 188 |
+
186,"Legitimate, correctly cleared",card ending 8703,0,0.008520837,0.003156836,11.22,10
|
| 189 |
+
187,"Legitimate, correctly cleared",card ending 8794,0,0.00029792296,5.9022426e-07,3.94,10
|
| 190 |
+
188,"Legitimate, correctly cleared",card ending 6504,0,0.00018796635,0.0029864553,1.7,10
|
| 191 |
+
189,"Legitimate, correctly cleared",card ending 4871,0,0.0004225552,0.008189974,267.25,10
|
| 192 |
+
190,"Legitimate, correctly cleared",card ending 5320,0,0.0009930147,0.0029659308,35.39,10
|
| 193 |
+
191,"Legitimate, correctly cleared",card ending 8506,0,7.513256e-05,5.612957e-05,86.48,10
|
| 194 |
+
192,"Legitimate, correctly cleared",card ending 9178,0,0.00018110925,8.3055365e-06,83.26,10
|
| 195 |
+
193,"Legitimate, correctly cleared",card ending 6888,0,0.00020197514,0.00044528677,520.62,10
|
| 196 |
+
194,"Legitimate, correctly cleared",card ending 7660,0,1.2356696e-05,3.6854017e-06,6.42,10
|
| 197 |
+
195,"Legitimate, correctly cleared",card ending 5802,0,2.5458876e-05,3.687818e-05,60.47,10
|
| 198 |
+
196,"Legitimate, correctly cleared",card ending 0030,0,0.0004964879,2.1163083e-05,76.16,10
|
| 199 |
+
197,"Legitimate, correctly cleared",card ending 3780,0,6.929844e-06,3.1729007e-05,89.56,10
|
| 200 |
+
198,"Legitimate, correctly cleared",card ending 2314,0,0.00823937,0.00092781946,37.16,10
|
| 201 |
+
199,"Legitimate, correctly cleared",card ending 8499,0,2.368849e-06,0.00015747253,106.33,10
|
| 202 |
+
200,"Legitimate, correctly cleared",card ending 6537,0,0.00012586365,0.0010625104,2.54,10
|
| 203 |
+
201,"Legitimate, correctly cleared",card ending 1125,0,1.2372806e-06,6.0120063e-05,71.87,10
|
| 204 |
+
202,"Legitimate, correctly cleared",card ending 7633,0,1.1300421e-05,1.4568015e-05,135.9,10
|
| 205 |
+
203,"Legitimate, correctly cleared",card ending 3058,0,7.087911e-06,7.715945e-07,4.82,10
|
| 206 |
+
204,"Legitimate, correctly cleared",card ending 1789,0,2.5901396e-05,1.5876813e-05,2.16,10
|
| 207 |
+
205,"Legitimate, correctly cleared",card ending 6919,0,3.9364128e-05,0.00016834475,78.84,10
|
| 208 |
+
206,"Legitimate, correctly cleared",card ending 7350,0,3.409065e-05,1.2461516e-05,15.34,10
|
| 209 |
+
207,"Legitimate, correctly cleared",card ending 4278,0,0.0019649353,6.125634e-05,5.25,10
|
| 210 |
+
208,"Legitimate, correctly cleared",card ending 5774,0,0.0005637887,9.8339355e-05,26.35,10
|
| 211 |
+
209,"Legitimate, correctly cleared",card ending 4745,0,6.3677367e-06,0.00031404654,40.64,10
|
| 212 |
+
210,"Legitimate, correctly cleared",card ending 3463,0,0.0018401808,2.4977902e-07,1.58,10
|
| 213 |
+
211,"Legitimate, correctly cleared",card ending 4947,0,2.3110211e-05,0.0028097834,15.45,10
|
| 214 |
+
212,"Legitimate, correctly cleared",card ending 5002,0,1.3803499e-05,5.791114e-05,86.63,10
|
| 215 |
+
213,"Legitimate, correctly cleared",card ending 0341,0,8.223805e-06,0.00014266005,114.35,10
|
| 216 |
+
214,"Legitimate, correctly cleared",card ending 6900,0,3.785761e-05,6.4106007e-06,4.17,10
|
| 217 |
+
215,"Legitimate, correctly cleared",card ending 0989,0,0.00022232455,4.787023e-06,2.22,10
|
| 218 |
+
216,"Legitimate, correctly cleared",card ending 5790,0,3.1901227e-06,1.8629911e-05,84.42,10
|
| 219 |
+
217,"Legitimate, correctly cleared",card ending 9043,0,0.00062586844,0.001228773,11.7,10
|
| 220 |
+
218,"Legitimate, correctly cleared",card ending 9010,0,8.922933e-06,0.00018945723,85.89,10
|
| 221 |
+
219,"Legitimate, correctly cleared",card ending 5522,0,1.5236932e-05,0.0003380949,113.86,10
|
| 222 |
+
220,"Legitimate, correctly cleared",card ending 9333,0,6.0215778e-05,3.928301e-05,62.8,10
|
| 223 |
+
221,"Legitimate, correctly cleared",card ending 5248,0,0.00013961516,7.062526e-07,212.23,10
|
| 224 |
+
222,"Legitimate, correctly cleared",card ending 2105,0,3.7629125e-06,1.040725e-05,8.26,10
|
| 225 |
+
223,"Legitimate, correctly cleared",card ending 7171,0,0.0014116894,0.0013639225,8.22,10
|
| 226 |
+
224,"Legitimate, correctly cleared",card ending 9027,0,0.0009955311,0.000100095225,3.59,10
|
| 227 |
+
225,"Legitimate, correctly cleared",card ending 7611,0,4.2383122e-05,2.2544796e-06,90.79,10
|
| 228 |
+
226,"Legitimate, correctly cleared",card ending 1852,0,1.5716314e-06,0.0009727804,65.45,10
|
| 229 |
+
227,"Legitimate, correctly cleared",card ending 1773,0,0.00035255958,6.711299e-05,3.44,10
|
| 230 |
+
228,"Legitimate, correctly cleared",card ending 1815,0,3.370073e-05,2.2217191e-05,1.32,10
|
| 231 |
+
229,"Legitimate, correctly cleared",card ending 9245,0,4.040741e-05,6.0880648e-05,90.42,10
|
| 232 |
+
230,"Legitimate, correctly cleared",card ending 4250,0,0.00010561431,1.0367631e-05,58.19,10
|
assets/demo_windows.npz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5ed5b3a0ed9dcb4b4389f6246a5e740eb3e2f2b3321e893b6610e26dee8d2e0b
|
| 3 |
+
size 46154
|
assets/gru.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fea103c036220dce52ac9b974be1cc8e798fce565541501d973ef53a85a81a15
|
| 3 |
+
size 93581
|
assets/lightgbm.txt
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
assets/test_scores.npz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e8f45794458e58cad3eb31c02856271c9d3d7a80a17a6a7352f7b70d1545b34b
|
| 3 |
+
size 5658831
|
model.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
The GRU, copied verbatim from the training repository so the Space is
|
| 3 |
+
self-contained. Architecture must match the checkpoint exactly.
|
| 4 |
+
|
| 5 |
+
Source: https://github.com/adwitiyashukla/DL-based-sequential-fraud-detection
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
from torch import nn
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class GRUFraudModel(nn.Module):
|
| 15 |
+
"""
|
| 16 |
+
Per timestep the model sees a learned embedding of the merchant category
|
| 17 |
+
and a linear projection of the standardised numeric features. Those are
|
| 18 |
+
concatenated and fed to a single layer GRU. The hidden state at the final
|
| 19 |
+
timestep, which is the transaction being scored, goes through dropout and a
|
| 20 |
+
linear head to one logit.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
def __init__(
|
| 24 |
+
self,
|
| 25 |
+
n_numeric: int,
|
| 26 |
+
n_categories: int,
|
| 27 |
+
emb_dim: int = 16,
|
| 28 |
+
proj_dim: int = 32,
|
| 29 |
+
hidden: int = 64,
|
| 30 |
+
dropout: float = 0.2,
|
| 31 |
+
) -> None:
|
| 32 |
+
super().__init__()
|
| 33 |
+
self.cat_emb = nn.Embedding(n_categories + 1, emb_dim, padding_idx=0)
|
| 34 |
+
self.num_proj = nn.Linear(n_numeric, proj_dim)
|
| 35 |
+
self.gru = nn.GRU(
|
| 36 |
+
input_size=proj_dim + emb_dim,
|
| 37 |
+
hidden_size=hidden,
|
| 38 |
+
num_layers=1,
|
| 39 |
+
batch_first=True,
|
| 40 |
+
)
|
| 41 |
+
self.dropout = nn.Dropout(dropout)
|
| 42 |
+
self.head = nn.Linear(hidden, 1)
|
| 43 |
+
|
| 44 |
+
def forward(self, x_num: torch.Tensor, x_cat: torch.Tensor) -> torch.Tensor:
|
| 45 |
+
emb = self.cat_emb(x_cat)
|
| 46 |
+
proj = torch.relu(self.num_proj(x_num))
|
| 47 |
+
out, _ = self.gru(torch.cat([proj, emb], dim=-1))
|
| 48 |
+
return self.head(self.dropout(out[:, -1, :])).squeeze(-1)
|
requirements.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# gradio itself is installed by the Space runtime, so it is not listed here.
|
| 2 |
+
#
|
| 3 |
+
# torch is pinned to the CPU build. The default PyPI wheel bundles CUDA, which
|
| 4 |
+
# is roughly 2.5 GB of dead weight on a CPU Space and slows every cold start.
|
| 5 |
+
# The "+cpu" local tag only resolves from PyTorch's own index, hence the extra
|
| 6 |
+
# index URL below.
|
| 7 |
+
--extra-index-url https://download.pytorch.org/whl/cpu
|
| 8 |
+
|
| 9 |
+
torch==2.13.0+cpu
|
| 10 |
+
numpy
|
| 11 |
+
pandas
|
| 12 |
+
lightgbm
|
| 13 |
+
plotly
|