Portfolio Case Study

Mobile-money fraud detection,
evaluated the way a risk team would

XGBoost classifier on 6.3M real PaySim transactions. Time-based split, walk-forward validated across 4 folds, calibrated probabilities, cost-sensitive thresholding, and a bug found and fixed mid-build, documented rather than hidden.

Precision 99.85% Recall 99.56% PR-AUC 0.9993 Brier 0.00017

Score a transaction, live

This runs the actual shipped model. The trained trees and calibration curve are re-implemented in pure Python for this always-on demo (see src/export_model_json.py and api/score.py), validated against the real scikit-learn/XGBoost output to within floating-point precision on 20,000 real held-out transactions. Not a mock, not a lookup table.

Why this is a precision problem, not an accuracy problem

PaySim's fraud rate is 0.13%. A model that predicts "not fraud" every single time scores 99.87% accuracy and catches nothing. Fraud-ops workflows freeze customer funds on a flag, so false positives carry real trust and regulatory cost. This project holds precision at or above 99% and evaluates on a strict time-based holdout, with no future-state leakage. PaySim is a common tutorial dataset; the contribution here isn't the dataset choice, it's the evaluation rigour: time-based split, calibration, cost-sensitive thresholds, and a five-model comparison on identical pipelines.

How it was built: seven stages, each answering one open question

1

Baseline model

Can a model tell fraud apart from legitimate transactions at all? (src/train.py)

2

Hyperparameter tuning

Were the model's settings ever actually tested against alternatives? (src/tune.py)

3

Walk-forward validation

Does it hold up on more than one train/test split? (src/validate.py)

4

Drift monitoring

How would we know if the model started drifting in production? (src/monitoring.py)

5

Live dashboard

How does someone without Python actually use this? (dashboard/app.py)

6

Feature fix

Live-testing the dashboard found the model flagging legitimate account closures as 100% fraud, traced to raw balance columns that were then removed from the model's inputs.

7

Diagnostics

Does one feature dominate the model's decisions, and where's the real precision/recall/cost trade-off? (src/explain.py)

Results: verified pipeline output, not notebook estimates

These numbers are the real output of src/train.py run end-to-end against the PaySim CSV. The script previously could not run at all: CalibratedClassifierCV(cv="prefit") was removed in scikit-learn ≥1.6. Fixed by wrapping the fitted model in sklearn.frozen.FrozenEstimator and calibrating on a held-out slice.

99.85%
Precision
99.56%
Recall
0.9993
PR-AUC
0.9998
ROC-AUC
0.00017
Brier score
0.40
Operating threshold

What this doesn't claim

Honest limitation #1: threshold instability

The cost-optimal decision threshold swings fold to fold in walk-forward validation. The near-perfect PR-AUC hides that no single fixed threshold is clearly "correct" across the whole time horizon. A real deployment would need to revisit it periodically.

Honest limitation #2: destination-balance blind spot

The model can't see whether the destination account's balance was updated consistently, because PaySim's CASH_OUT type doesn't strictly enforce destination-side accounting. A rule-based patch for this was tested and rejected: it produced 44,996 false alarms on the training split, up from 136. See MODEL_CARD.md.

Tech stack

Python 3.10+XGBoostscikit-learn SHAPimbalanced-learnStreamlit Optunajoblib