Spaces:
Configuration error
Banking Fraud Detection System
An AI/ML system that scores banking transactions for fraud risk in real time. Given a transaction, it returns a probability, a low/medium/high risk tier, and the top SHAP features driving that score β not just a black-box number. Built on the PaySim synthetic mobile-money dataset (~6.36M transactions), served through a FastAPI + MySQL scoring service, with two role-scoped web apps on top: a banking officer console and a client self-service app.
Contents
- Quick start
- Environment configuration
- Dataset acquisition
- Database setup
- Running the API
- Authentication & roles
- Using the UI
- Demo script for a panel presentation
- Running tests
- Model performance summary
- Project structure
Quick start
python3.12 -m venv venv
source venv/bin/activate
pip install -r requirements-dev.txt # includes requirements.txt
cp .env.example .env # fill in real DB/Kaggle credentials + a seed officer login (see below)
python -m training.fetch_data # download PaySim into data/
python -c "from app.db.session import create_all; create_all()" # review migrations/schema.sql first!
python -m training.eda # optional: regenerate reports/EDA_REPORT.md
python -m training.train compare --variant conservative # optional: regenerate model comparison
python -m training.train finalize --variant conservative --model xgboost --threshold 0.5 # optional: retrain model_v1.pkl
uvicorn app.main:app --host 0.0.0.0 --port $APP_PORT
Then open http://localhost:$APP_PORT/login and sign in with the seed officer credentials from .env (SEED_OFFICER_EMAIL / SEED_OFFICER_PASSWORD) β that account is created automatically the first time the app starts.
A trained model artifact (models/model_v1.pkl + models/model_v1.meta.json) is already checked in, so the training steps above are optional unless you want to reproduce or retrain it.
Environment configuration
All configuration is read from .env (via app/config.py) β nothing is hardcoded, and missing/invalid required values fail loudly at startup rather than silently defaulting. See .env.example for the full list:
| Variable | Purpose |
|---|---|
DB_HOST / DB_PORT / DB_NAME / DB_USER / DB_PASSWORD |
MySQL connection parameters |
DATABASE_URL |
Full SQLAlchemy connection string (mysql+pymysql://...) |
APP_PORT |
Port the API binds 0.0.0.0 on (default 8000) |
ALLOWED_ORIGINS |
Comma-separated CORS origins |
FRAUD_ALERT_THRESHOLD |
Probability (0β1) above which a prediction becomes an alert. Currently 0.5 |
MODEL_PATH |
Path to the joblib model artifact (models/model_v1.pkl) |
SEED_OFFICER_EMAIL / SEED_OFFICER_PASSWORD |
The one banking-officer account created automatically on first startup, so there's always a way in. Every other officer or client account is created through the app afterward (see below) |
SESSION_EXPIRE_MINUTES |
How long a login session lasts before it must be renewed (default 1440 = 24h) |
COOKIE_SECURE |
Optional, defaults to secure-on. Set to false only for local dev when hitting the app directly over http:// (bypassing the reverse proxy) β a Secure cookie is never sent back over plain HTTP, so login would otherwise appear to silently fail locally. Never set this to false in a deployed environment |
KAGGLE_USERNAME / KAGGLE_KEY |
Only needed to (re)download the PaySim dataset |
There is no MySQL/SQLite fallback β the app will not start against anything other than the configured MySQL database. There is also no reverse-proxy/SSL/hostname configuration in this codebase; the app just binds 0.0.0.0:$APP_PORT and expects an external reverse proxy (already handled for bank-fraud.hdev.rw) in front of it β that proxy is what makes COOKIE_SECURE's default of "on" correct in production (the browser's connection to the proxy is HTTPS, even though the proxy may forward to this app over plain HTTP internally).
Dataset acquisition
python -m training.fetch_data
Downloads the PaySim CSV via the Kaggle API (using KAGGLE_USERNAME/KAGGLE_KEY from .env) into data/. Requires a Kaggle account with API access enabled.
Database setup
The schema is defined as SQLAlchemy 2.0 typed models in app/db/models.py, with a reproducible, human-reviewable DDL export at migrations/schema.sql. Review that file before running any migration against a real database.
python -c "from app.db.session import create_all; create_all()"
create_all() is additive-only (it creates any tables from Base.metadata that don't already exist; it never drops or alters existing ones). Six tables:
usersβ login accounts (officer or client role), bcrypt password hashessessionsβ server-side session tokens (not JWT β deleting a row is an instant, unconditional revocation)clientsβ a bank customer's account: banking fields (starting balance, account type) plus a 1:1 link to theusersrow that logs into it, and which officer created ittransactions,fraud_predictions,fraud_alertsβ every scored transaction (both officer-submitted backfill/testing and client-submitted), one unified pipeline so the alert queue has a single source of truth.transactions.client_idis nullable: set for client-submitted transactions,NULLfor officer-submitted ones
Running the API
uvicorn app.main:app --host 0.0.0.0 --port $APP_PORT
# or, for local development with auto-reload:
uvicorn app.main:app --host 0.0.0.0 --port $APP_PORT --reload
The model and its SHAP explainer are loaded once at startup (not per-request), and the seed officer account is created if it doesn't already exist. Every JSON endpoint lives under /api/... (kept separate from the page routes below, which use the clean /officer/... / /client/... URLs):
| Method | Path | Who | Purpose |
|---|---|---|---|
POST |
/api/auth/login |
anyone | Log in, sets the session cookie |
POST |
/api/auth/logout |
anyone | Log out, clears the session |
GET |
/api/auth/me |
logged in | Current user + role |
POST |
/api/predict |
officer | Score a single raw transaction (backfill/testing) |
POST |
/api/transactions/batch |
officer | Score a batch of raw transactions |
POST |
/api/officer/clients |
officer | Create a client account (returns a one-time temp password) |
GET |
/api/officer/clients |
officer | List all clients |
GET |
/api/officer/clients/{id} |
officer | One client's transaction history + alerts |
GET |
/api/officer/alerts |
officer | All fraud alerts across all clients (filterable) |
PATCH |
/api/officer/alerts/{id} |
officer | Approve (reviewed), dismissed, or rejected β rejecting also reverses the underlying transaction, excluding it from the client's balance |
GET |
/api/officer/stats/overview | /eda | /model-performance |
officer | Dashboard/insights/model-performance data |
POST |
/api/client/transactions |
client | Submit a transaction on your own account |
GET |
/api/client/transactions |
client | Your own transaction history |
GET |
/api/client/alerts |
client | Alerts on your own account (read-only) |
GET |
/api/client/profile |
client | Your name, account type, current balance, open alerts |
GET |
/health |
anyone | Health check for the reverse proxy |
Full request/response schemas and a try-it-now request builder are auto-generated at /docs (Swagger UI) and /redoc.
Authentication & roles
Two roles: banking officer (back-office β creates/manages clients, reviews the cross-client fraud alert queue) and client (self-service β transacts and reviews alerts on their own account only).
- Login: email + password, server-side session (an opaque token in an httpOnly cookie, checked by a DB lookup on every request) β not JWT, so an account can be instantly locked out by deleting its session rows, without waiting for a token to expire.
- First login: the officer account named by
SEED_OFFICER_EMAIL/SEED_OFFICER_PASSWORDis created automatically on first startup. There is no self-service officer signup β every other officer or client account is created by an existing officer. - Creating a client: an officer uses "+ Create client" on
/officer/clients(name, email, starting balance, account type) β this creates both ausersrow (roleclient) and the linkedclientsrow, and shows a one-time temporary password for the officer to hand to that client. - Role enforcement: every
/api/officer/*route rejects non-officers (403); every/api/client/*route rejects non-clients (403) and is scoped to the caller's ownclientsrow β there is noclient_idparameter anywhere in a client's own routes, so a client can't even ask for another client's data by guessing an ID.
Using the UI
| Page | Path | Who | What it's for |
|---|---|---|---|
| Login | /login |
anyone | Single login page for both roles; redirects to the right home by role |
| Officer dashboard | /officer |
officer | Totals (clients, transactions, fraud rate, open alerts) + recent cross-client activity |
| Clients | /officer/clients |
officer | Every client account, "+ Create client" |
| Client detail | /officer/clients/{id} |
officer | One client's transaction history + alert history |
| Alerts queue | /officer/alerts |
officer | Every alert across every client, filterable, with an expandable SHAP breakdown and approve/reject/dismiss actions β rejecting reverses the transaction |
| EDA insights | /officer/insights |
officer | Class imbalance, fraud rate by type/amount/time, plain-language captions |
| Model performance | /officer/model |
officer | Model comparison, PR curve, why accuracy alone would mislead here |
| Client dashboard | /client |
client | Your balance, a calm security notice for any open alert, recent transactions |
| Make a transaction | /client/transactions/new |
client | Submit a transaction, see the result in plain language |
| Transaction history | /client/transactions |
client | Every transaction on your account |
| My alerts | /client/alerts |
client | Read-only alerts on your own account, plain-language status |
The officer console (dense, dark rail nav, sharp corners, hairline borders) and the client app (light header nav, rounded 12β16px cards, generous spacing) share the same color palette and type system (Space Grotesk / Inter / IBM Plex Mono) but are deliberately different products visually β one reads as a back-office tool, the other as a consumer banking app.
Demo script for a panel presentation
- Open
/login, sign in as the seed officer. - On
/officer/clients, create a client β copy the one-time temp password shown. - Log out, log back in as that client using the email + temp password.
- On
/client/transactions/new, submit a transaction equal to the account's full balance (this reproduces PaySim's fraud-drain signature) β the result shows "being reviewed for your security." - Check
/client/alertsβ the alert is there, read-only, in plain language. - Log out, log back in as the officer.
- On
/officer/alerts, find the alert, click "Review" to see the SHAP breakdown, then Approve, Reject & reverse (returns the amount to the client's balance), or Dismiss. - Optionally walk through
/officer/insightsand/officer/modelfor the EDA/model-selection story behind the score.
Running tests
See TESTING.md for the full guide. Short version:
pytest # unit + integration tests
python -m scripts.test_live --base-url http://127.0.0.1:8811 # live smoke test (officer/client flow + fixture replay)
Model performance summary
Trained on a time-respecting split (80% earliest steps for training, most recent 20% held out for testing β never a random shuffle, to avoid leaking future transactions into training). Full comparison methodology and the leakage investigation that led to the final feature set are in reports/MODEL_COMPARISON_conservative.md and reports/EDA_REPORT.md.
Final model: XGBoost, "conservative" feature variant (excludes the two balance-ratio columns that let a model memorize a PaySim-specific simulation artifact rather than a generalizable pattern β see reports/MODEL_COMPARISON_conservative.md for the full writeup), SMOTE resampling within the training fold only.
| Threshold | Precision | Recall | F1 |
|---|---|---|---|
| 0.3 | 0.9981 | 0.9981 | 0.9981 |
| 0.5 (chosen) | 0.9995 | 0.9976 | 0.9986 |
| 0.7 | 1.0000 | 0.9972 | 0.9986 |
PR-AUC: 0.99997 Β· ROC-AUC: 0.99999 (held-out test fold, ~1.25M transactions)
Chosen alert threshold: 0.5 (FRAUD_ALERT_THRESHOLD in .env) β the best F1 balance between catching fraud and not overwhelming officers with false positives at this operating point.
Risk tiers shown throughout the UI are a fixed, threshold-independent display band: low < 0.30, medium 0.30β0.70, high β₯ 0.70.
Training-time library versions are recorded alongside the artifact in models/model_v1.meta.json β check this before assuming a re-trained model is safe to load into a different environment.
Project structure
app/
auth.py Password hashing, sessions, role-enforcement dependencies
crud.py DB read/write helpers shared by all routers
scoring.py Model + SHAP loading, feature-row assembly, scoring
routers/ auth, predict (officer), officer, client, health, pages
templates/ officer/, client/, login.html, base.html
static/ css/app.css (shared design system), js/ (per-page + auth-guard.js)
training/ EDA, feature-model comparison, and final-model training scripts
scripts/ Fixture generator + live smoke test (auth-aware)
tests/ Unit tests (features, scoring, config) + integration tests (full API, auth, roles)
migrations/ schema.sql (reproducible DDL, human-reviewed before every migration)
models/ Versioned model artifact + metadata sidecar
reports/ EDA report, model comparison reports, and their charts
data/ PaySim CSV (gitignored; fetch via training/fetch_data.py)