bank-fraud / README.md
root
init
942b115
|
Raw
History Blame Contribute Delete
14.7 kB

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

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 hashes
  • sessions β€” 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 the users row that logs into it, and which officer created it
  • transactions, 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_id is nullable: set for client-submitted transactions, NULL for 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_PASSWORD is 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 a users row (role client) and the linked clients row, 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 own clients row β€” there is no client_id parameter 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

  1. Open /login, sign in as the seed officer.
  2. On /officer/clients, create a client β€” copy the one-time temp password shown.
  3. Log out, log back in as that client using the email + temp password.
  4. 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."
  5. Check /client/alerts β€” the alert is there, read-only, in plain language.
  6. Log out, log back in as the officer.
  7. 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.
  8. Optionally walk through /officer/insights and /officer/model for 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)