Fraud Detection System
An end-to-end fraud-detection system trained and evaluated on more than 13 million card transactions — from imbalanced-data handling and feature design through to threshold selection, model evaluation and a containerised app serving the model in production.
- Python
- XGBoost
- Optuna
- MLflow
- pandas
- SQL
- FastAPI
- Docker
Read moreShow less
The problem
Card fraud is a needle-in-a-haystack problem, and the haystack is the point. In the test set, 2,562 of 1,691,468 transactions are fraudulent — a rate of about 0.15%. A model that predicts “legitimate” for every single transaction is 99.85% accurate and worth nothing, which is why accuracy never appears in this project’s reporting.
The asymmetry runs the other way too. A missed fraud costs the chargeback; a false positive declines a real customer at a real till. Neither error is free, they are not free in the same way, and no metric collapses them into one number honestly. So the work was less about squeezing out a score than about being explicit on what was being traded for what.
Approach
The data is the Caixabank Tech synthetic transactions dataset — transactions, cardholders and cards across the 2010s, joined and explored in SQL before any model was fitted.
Feature engineering stayed deliberately close to the transaction: hour, day of week, month and day; days since the account opened; days until the card expires; years since the last PIN change; years since retirement. The date arithmetic doubles as a data-quality filter — rows where a card had already expired but recorded no error, or where a transaction predated its own account opening, are generation artefacts and get dropped. Eight categorical columns (merchant category, merchant city, card brand and type, error codes, and so on) are kept as native categoricals rather than one-hot expanded, since the model handles them directly.
The split is a stratified 80/20 hold-out, stratified so the fraud rate is preserved on both sides — with 0.15% positives, an unstratified split leaves the test fraud rate to chance.
Technical decisions
- PR-AUC as the tuning objective, not ROC-AUC — the final model scores
0.9900 ROC-AUC, which sounds excellent and means very little here. ROC-AUC
rewards ranking the 1.69M negatives correctly, and at this imbalance that is
nearly free. PR-AUC (0.7998) only measures the part anyone cares about.
Optuna optimised average precision directly, and XGBoost’s own
eval_metricwas set toaucprso early stopping agreed with the objective. scale_pos_weighttuned as a hyperparameter, not resampling — no SMOTE and no undersampling. Reweighting the loss leaves the real class prior untouched, so the predicted probabilities stay interpretable and threshold selection remains a meaningful exercise instead of an artefact of a rebalanced training set. The weight was searched over 1–100 rather than pinned at the imbalance ratio, letting the data decide how hard to push.- Optuna with a TPE sampler over a 50% stratified subsample — 15 trials of 3-fold stratified CV, with early stopping (50 rounds against a 200-tree ceiling) deciding the tree count per fold. Subsampling for the search and refitting on the full training set was the compromise that made tuning fit the compute available; it is also the most obvious place where more budget would buy more performance.
- MLflow around every run — parameters, metrics and the serialised model logged per run, so the artefact that ended up behind the API is traceable to the run that produced it rather than to a pickle of unknown provenance.
Results
Evaluated on the held-out 1,691,468 transactions:
| Metric | Value |
|---|---|
| PR-AUC | 0.7998 |
| ROC-AUC | 0.9900 |
| Recall (fraud) | 0.854 |
| Precision (fraud) | 0.263 |
| F1 (fraud) | 0.402 |
The model catches 85.4% of fraud. It also flags roughly four transactions for every one that is genuinely fraudulent — at production scale that is a lot of friction pushed onto legitimate customers, and the 14.6% it misses is a lot of fraud left on the table. Both numbers are real and neither is hidden here.
What that trade-off should be is not a modelling question. Where the threshold belongs depends on the cost of a chargeback against the cost of a declined customer, and setting it is a conversation with fraud analysts and risk owners, not something to be inferred from a validation curve.
From model to service
The model is not left as a notebook artefact. The pipeline is refactored into importable modules with separated concerns — load, preprocess, tune, train, evaluate — and served behind a FastAPI endpoint with Pydantic validation, with a Gradio front end so the model can be interrogated without writing code. The whole thing is containerised and deployed to Hugging Face Spaces. The Phase 1 SQL exploration also became a public Tableau dashboard, which is a different audience than this page and a useful thing to have had to build.
What I would do differently
The leakage is the honest headline. Several engineered features —
avg_amount_cli, trans_count_cli, years_since_pin_change — are computed
across the entire dataset before splitting, so each training row carries a
trace of information from the test period. The 0.80 PR-AUC is therefore an
optimistic estimate, and I would not defend it as a clean out-of-sample number.
The correct fix is to compute those aggregates inside the training fold only,
or as time-windowed features that can only look backwards from each
transaction. That is the first thing I would rebuild.
The split should be temporal. A random hold-out on data spanning a decade lets the model train on the future and predict the past. Fraud patterns drift; a train-on-past, test-on-future split would be harder to score well on and far more indicative of live behaviour.
Tuning was under-resourced. 15 trials on half the training data is a sanity-check budget, not a search. The reported PR-AUC is a floor on what this feature set supports, not a ceiling.
Cardholder attributes are static snapshots. Age, yearly income and per-capita income are recorded as of dataset creation, not as of the transaction — so a 2012 transaction carries 2024’s income figure. It is a property of the source data rather than something the pipeline introduced, but it limits how much any conclusion about income and fraud can be trusted.