Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Credit Default Risk Model

An end-to-end credit risk pipeline: EDA-ready data, a trained default-prediction model, SHAP-based explanations, and a live scoring API — the shape of a project a bank's risk team would actually ship, not a one-off notebook.

Why explainability matters here: credit decisions are regulated (Consumer Credit Act / FCA in the UK, ECOA in the US) — a lender generally has to be able to say why an applicant was scored the way they were, not just produce a number. This project treats the explanation as a first-class output, not an afterthought.

Data

UCI "Default of Credit Card Clients" — 30,000 credit card clients in Taiwan (Apr–Sep 2005). Target: whether the client defaulted on their payment the following month (22.1% positive rate).

Features: credit limit, demographics (age/sex/education/marriage), 6 months of repayment status, bill amounts, and payment amounts.

Pipeline

src/data_prep.py   Clean raw data (fix undocumented category codes, collapse
                    repayment-status edge cases) -> data/processed/
src/train.py        Train Logistic Regression (baseline) + XGBoost, evaluate,
                     pick the winner by ROC-AUC -> models/, reports/
src/explain.py       SHAP TreeExplainer over the winning model -> reports/
api/main.py          FastAPI service: POST an applicant, get back a
                     probability + risk band + top SHAP drivers for that score

Results (held-out 20% test set)

Model ROC-AUC PR-AUC Recall (default class)
Logistic Regression 0.744 0.519 0.54
XGBoost 0.776 0.553 0.62

XGBoost is the selected model. Recall on the default class is prioritized over raw accuracy — for a lender, missing an actual default is more costly than a false alarm. See reports/roc_curve.png.

Explainability

reports/shap_importance.png and reports/shap_summary.png show which features drive predictions globally. Unsurprisingly, recent repayment status (PAY_1, PAY_2 — how many months late the client was) dominates, followed by credit limit and recent bill amounts.

Every API prediction also returns the top 5 SHAP factors for that specific applicant — a real analyst or underwriter could read the response and explain the decision to a customer.

Fairness audit

src/fairness_audit.py checks whether the model produces disparate outcomes across SEX, AGE band, EDUCATION, and MARRIAGE — using the "four-fifths rule" (the standard US EEOC adverse-impact threshold: the lowest group's positive rate should be at least 80% of the highest group's) plus fairlearn's demographic parity and equalized odds metrics.

It also trains a second model with SEX/EDUCATION/MARRIAGE removed as features ("blinded") and re-audits it, to test a real question a UK lender has to ask: under the Equality Act 2010, using a protected characteristic as a direct model input risks a direct discrimination claim, regardless of how predictive it is — but does removing it actually fix disparate outcomes?

Findings on this dataset:

Attribute Full model — 4/5 rule Blinded model — 4/5 rule
SEX pass (ratio 0.85) pass (ratio 0.88)
MARRIAGE pass (ratio 0.91) pass (ratio 0.88)
EDUCATION FAIL (ratio 0.19) FAIL (ratio 0.39)
AGE band FAIL (ratio 0.66) FAIL (ratio 0.63)

Removing the protected attributes costs almost nothing in accuracy (ROC-AUC 0.7758 → 0.7739) — but it doesn't fix the EDUCATION and AGE disparities. That's the actual lesson: other features (credit limit, repayment history, bill amounts) are correlated proxies for education and age, so the model keeps re-deriving what it "shouldn't" know. Removing protected fields is necessary but not sufficient — a real deployment would need proxy analysis and likely a fairness-constrained training objective (fairlearn's ExponentiatedGradient, for example) to actually close the gap. Full breakdown: reports/fairness_report.json, charts: reports/fairness_sex.png, fairness_education.png, fairness_marriage.png, fairness_age_band.png.

Run it: python3 src/fairness_audit.py

Running it

python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

python3 src/data_prep.py   # clean data
python3 src/train.py       # train + evaluate, saves models/
python3 src/explain.py     # SHAP artifacts

uvicorn api.main:app --app-dir . --reload

Example request

curl -X POST http://127.0.0.1:8000/predict -H "Content-Type: application/json" -d '{
  "LIMIT_BAL": 20000, "AGE": 24, "SEX": 2, "EDUCATION": 2, "MARRIAGE": 1,
  "PAY_1": 2, "PAY_2": 2, "PAY_3": 0, "PAY_4": 0, "PAY_5": 0, "PAY_6": 0,
  "BILL_AMT1": 3913, "BILL_AMT2": 3102, "BILL_AMT3": 689, "BILL_AMT4": 0, "BILL_AMT5": 0, "BILL_AMT6": 0,
  "PAY_AMT1": 0, "PAY_AMT2": 689, "PAY_AMT3": 0, "PAY_AMT4": 0, "PAY_AMT5": 0, "PAY_AMT6": 0
}'
{
  "default_probability": 0.9275,
  "risk_band": "high",
  "top_factors": [
    {"feature": "PAY_1", "value": 2.0, "shap_contribution": 1.57},
    {"feature": "LIMIT_BAL", "value": 20000.0, "shap_contribution": 0.31},
    {"feature": "PAY_2", "value": 2.0, "shap_contribution": 0.26}
  ]
}

Docker

docker build -t credit-risk-api .
docker run -p 8000:8000 credit-risk-api

Possible next steps

  • Calibrate probabilities (Platt scaling) so the score is a true probability, not just a ranking.
  • Try a fairness-constrained training objective (fairlearn.reductions.ExponentiatedGradient) to close the EDUCATION/AGE gaps found above rather than just measuring them.
  • Deploy to a free-tier host (Render/Fly.io) and link the live endpoint here.

About

End-to-end credit default risk model: XGBoost, SHAP explainability, fairness audit, FastAPI serving

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages