Spaces:
Configuration error
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](https://www.kaggle.com/datasets/ealaxi/paysim1) 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](#quick-start) | |
| - [Environment configuration](#environment-configuration) | |
| - [Dataset acquisition](#dataset-acquisition) | |
| - [Database setup](#database-setup) | |
| - [Running the API](#running-the-api) | |
| - [Authentication & roles](#authentication--roles) | |
| - [Using the UI](#using-the-ui) | |
| - [Demo script for a panel presentation](#demo-script-for-a-panel-presentation) | |
| - [Running tests](#running-tests) | |
| - [Model performance summary](#model-performance-summary) | |
| - [Project structure](#project-structure) | |
| ## Quick start | |
| ```bash | |
| 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 | |
| ```bash | |
| 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.** | |
| ```bash | |
| 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 | |
| ```bash | |
| 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](TESTING.md) for the full guide. Short version: | |
| ```bash | |
| 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 `step`s 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) | |
| ``` | |