adwitiyashukla commited on
Commit
855d16d
·
verified ·
1 Parent(s): 63cb41d

Upload folder using huggingface_hub

Browse files
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: false
11
  license: mit
 
 
 
 
 
 
 
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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