FraudScope is a small, production-shaped ML microservice that trains a gradient-boosted fraud classifier offline and serves it online as a REST API, with SHAP-based explainability on every prediction and an honest, class-imbalance-aware evaluation report. It's the AI/ML differentiator in a 3-project banking-domain portfolio: Projects 1 and 2 cover backend/systems work (Java/Spring), and FraudScope demonstrates the "train offline, serve online, explain predictions, monitor drift" pattern used in real fraud systems — without ever claiming access to real bank data.
This project uses the public Kaggle "Credit Card Fraud Detection" dataset (anonymized, PCA-transformed features) — or, when that dataset isn't available, a locally-generated synthetic stand-in with an identical schema, clearly labeled
synthetic-for-demoeverywhere it appears. No real bank, customer, or transaction data is used anywhere in this project. This is a demonstration of ML engineering and serving practices — training pipelines, REST APIs, explainability, evaluation rigor, containerization — not a claim of production deployment at a financial institution.The build environment used to assemble this repo had no network access to Kaggle, so the artifacts currently in
models/andreports/were produced from the synthetic-for-demo fallback (seedata/README.mdandmodels/metadata.jsonfor the exact provenance flag:dataset_is_synthetic). Swapping in the real Kaggle CSV requires no code changes — see Setup below.
TransactionWatch (Java/Spring, Project 2)
│ on flagged/borderline transaction
│ POST /score (REST, JSON)
▼
FraudScope (Python FastAPI service)
│ loads trained model (joblib artifact) at startup
│ featurizes request payload
│ model.predict_proba() → risk score
│ SHAP TreeExplainer → top 3 contributing features
▼
Response: { risk_score, top_features[], model_version, latency_ms }
│
▼
TransactionWatch stores score + top features alongside the alert
Two clearly separated concerns:
- Offline / training pipeline (
src/train.py,src/evaluate.py) — run once or on demand to produce a model artifact + metrics report. - Online / serving pipeline (
app/) — loads the artifact and serves predictions fast; it does not retrain per-request.
fraudscope/
├── README.md
├── requirements.txt
├── Dockerfile
├── docker-compose.yml
├── .env.example
├── data/ # data/README.md explains dataset provenance; CSV is gitignored
├── models/ # trained artifact + metadata.json land here (gitignored)
├── reports/ # evaluation_report.md (generated, not fabricated)
├── src/ # offline pipeline: config, train, evaluate, explain, features, synthetic_data, logging_utils
├── app/ # online service: FastAPI app, schemas, routes (score/health/retrain)
├── tests/ # pytest suite
└── scripts/ # example_client.py — simulated TransactionWatch → FraudScope call
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txtDownload creditcard.csv from
https://www.kaggle.com/datasets/mlg-ulb/creditcardfraud and place it at
data/creditcard.csv. If you skip this step, src/train.py will
auto-generate a schema-matching synthetic-for-demo CSV in its place —
see data/README.md for details.
python -m src.trainTrains an XGBClassifier with scale_pos_weight class-imbalance handling,
sanity-checks a SHAP TreeExplainer against the trained model, and saves
models/fraud_model_v1.joblib + models/metadata.json.
python -m src.evaluateComputes ROC-AUC, PR-AUC, precision/recall/F1 at multiple thresholds on the
held-out test split, and writes reports/evaluation_report.md.
pytest -vNote: several tests (the model-metrics regression floor, the explainer
tests, the score-endpoint tests) run against the actual trained artifact,
so python -m src.train must be run first — CI should train the model as
a prerequisite step before running the test suite.
uvicorn app.main:app --host 0.0.0.0 --port 8000docker build -t fraudscope .
docker run -p 8000:8000 -v $(pwd)/models:/srv/fraudscope/models fraudscopeor, with compose:
docker compose up --buildRequest:
{
"transaction_id": "txn_watch_00042",
"Time": 39016.0,
"Amount": 8.02,
"V1": -1.165, "V2": -0.103, "V3": -1.520, "V4": -5.234,
"V5": 1.910, "V6": 0.697, "V7": 2.323, "V8": 3.508,
"V9": -1.946, "V10": -0.668, "V11": -5.256, "V12": 0.602,
"V13": -0.683, "V14": 2.582, "V15": -0.315, "V16": -0.043,
"V17": -5.355, "V18": -3.293, "V19": 0.944, "V20": 1.161,
"V21": 0.185, "V22": 2.122, "V23": -0.996, "V24": -1.470,
"V25": 0.704, "V26": 0.602, "V27": -2.241, "V28": 0.110
}Response:
{
"transaction_id": "txn_watch_00042",
"risk_score": 0.999988,
"risk_band": "high",
"top_features": [
{ "feature": "V17", "contribution": 7.141281, "direction": "increases risk" },
{ "feature": "V11", "contribution": 1.230214, "direction": "increases risk" },
{ "feature": "V4", "contribution": 1.066011, "direction": "increases risk" }
],
"model_version": "v1",
"scored_at": "2026-08-21T18:23:38.407663Z",
"latency_ms": 2.132
}(This example was captured live against a fraud-labeled row from the synthetic-for-demo dataset — the SHAP explainer correctly surfaces the features that were deliberately shifted to create the fraud signal in that generator.)
{ "status": "ok", "model_version": "v1", "uptime_s": 1.153 }Returns 202 Accepted immediately and runs src.train.train() as a
FastAPI background task, then hot-reloads the in-process model singleton.
This is a demo convenience, not a production retraining system — see
Limitations below.
Full report: reports/evaluation_report.md.
Headline numbers from the current artifact (synthetic-for-demo dataset,
10,000-row stratified held-out test split, 17 fraud rows):
| Metric | Value |
|---|---|
| ROC-AUC | 1.0000 |
| PR-AUC (average precision) | 0.9866 |
| Precision @ 0.5 | 1.0000 |
| Recall @ 0.5 | 0.9412 |
Why PR-AUC matters more than ROC-AUC here: at ~0.17% fraud prevalence, a model that predicts "legitimate" for nearly everything can still post a high ROC-AUC (and >99.8% raw accuracy), because ROC-AUC is dominated by the abundant negative class. PR-AUC isolates precision/recall trade-offs on the fraud class specifically — the number a fraud-ops team actually cares about. Note that these particular numbers are inflated by the synthetic generator's clean, strongly-separated signal; on the real Kaggle dataset, expect PR-AUC in the 0.80–0.87 range, which is still strong but more representative of real-world separability.
Every /score response includes the top-3 SHAP-attributed features driving
that specific prediction, with a direction (increases risk /
decreases risk) derived from the sign of the SHAP value — not just a
global feature-importance ranking. This matters both for analyst trust
(a fraud analyst can see why a transaction was flagged, not just a bare
number) and for the kind of explainability regulators increasingly expect
around automated lending/fraud decisions.
- XGBoost over a neural net. Tabular data with strong, sometimes
nonlinear-but-low-dimensional interactions (already PCA-compressed here)
is the classic case where gradient-boosted trees match or beat deep
learning, train in seconds instead of hours, and are directly explainable
with SHAP's
TreeExplainer— a much cheaper explainability story than gradient-based attribution on a neural net. scale_pos_weightover resampling (SMOTE, undersampling). Weighting the loss avoids duplicating or discarding real rows, which is simpler to reason about and doesn't risk synthesizing implausible minority-class samples via SMOTE on already-PCA'd features.- Threshold tuning is a business decision, not a fixed constant. The evaluation report shows precision/recall at three thresholds precisely because the "right" cutoff depends on the relative cost of a missed fraud versus an analyst wasting time on a false alarm — that's a fraud-ops policy call, not something to hardcode.
- What I'd change for real production use:
- A real feature store, so online (serving-time) and offline (training-time) feature computation can't silently drift apart.
- Online/offline skew monitoring — the
logging_utils.pystructured JSON log is a stub for this, not a working monitoring pipeline. - Model versioning + rollback (the
model_versionfield is a start, but there's no registry, no canary rollout, no automatic rollback here). - A/B testing of decision thresholds against real business outcomes rather than a single fixed default.
/retrainbehind an actual async job queue with approval gates, instead of a synchronous background task that mutates the serving model in place.
FraudScope is deliberately polyglot: a Java/Spring service calls this Python service over a plain REST/JSON boundary rather than forcing everything into one stack — a common real-world pattern for isolating ML workloads in Python behind a language-agnostic API.
Python client (see scripts/example_client.py
for the full runnable version):
curl -X POST http://localhost:8000/score \
-H "Content-Type: application/json" \
-d '{"transaction_id": "txn_1", "Time": 40000, "Amount": 149.62, "V1": 0, ... "V28": 0}'Java (java.net.http.HttpClient) snippet, illustrating how
TransactionWatch would call FraudScope:
HttpClient client = HttpClient.newHttpClient();
String body = """
{"transaction_id":"txn_1","Time":40000,"Amount":149.62,"V1":0,"V2":0, ... "V28":0}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://fraudscope:8000/score"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
// response.body() -> { risk_score, risk_band, top_features[], model_version, ... }- Dataset: public or synthetic-for-demo only — no real bank, card network, or customer transaction data, ever.
- No live transaction stream: there is no real-time ingestion pipeline;
/scorescores whatever payload it's given, on demand. - No real-time feature engineering: the served features are exactly
the dataset's columns (
Time,V1–V28,Amount); there's no online feature computation, joins, or aggregation pipeline. /retrainis a demo stub, not a production job queue — no approval gates, no atomic artifact swap, no versioned rollback.- Drift monitoring is a logging stub (
src/logging_utils.py) — the structured JSON schema is designed to feed a real pipeline like Evidently or a feature-store drift dashboard, but no such pipeline is wired up here. - TransactionWatch (Project 2) is not built in this repo — only a clean REST contract and example clients demonstrating how it would call FraudScope.
- "I trained and served a real gradient-boosted classifier on a public fraud dataset — this demonstrates the same engineering pattern (train offline, serve online, explain predictions, monitor drift) used in production fraud systems, without claiming access to real bank data."
- "Class imbalance is the central technical challenge in fraud detection
— I handled it via
scale_pos_weightand evaluated with PR-AUC rather than accuracy, since accuracy is misleading at <1% positive rate." - "Explainability isn't an afterthought — SHAP values on every scored transaction let a fraud analyst see why a transaction was flagged, which matters both for analyst trust and regulatory explainability requirements in lending/fraud decisions."
- "The service is deliberately polyglot: Project 2's Java service calls this Python service over REST, mirroring how real fintechs often isolate ML workloads in Python microservices behind a REST boundary rather than forcing everything into one stack."
- "Is this real bank data?" No — and that's fine. This project isn't a claim of data access; it's a demonstration of engineering practice: a properly separated train/serve pipeline, honest evaluation under severe class imbalance, per-prediction explainability, and a clean polyglot service boundary. Anyone can download the same public Kaggle dataset and reproduce these results end-to-end.