End-to-end, interpretable machine-learning system that predicts the probability of a Home Equity Line of Credit (HELOC) applicant becoming seriously delinquent (90+ days past due) within 24 months, using real credit-bureau data from the FICO Explainable ML Challenge.
The project demonstrates a full, explainability-first ML lifecycle: EDA → leakage-aware preprocessing → penalised logistic regression → cost-optimised decision threshold → FastAPI inference → Streamlit dashboard with SHAP explanations → Dockerised deployment.
Prediction tool with per-applicant SHAP waterfall explanation — red bars push the default probability up, blue bars pull it down, from the average applicant's base rate:
Model evaluation — honest holdout performance at the cost-optimised threshold:
Global feature importance (mean |SHAP|) — which credit behaviours drive default risk across the portfolio:
The previous version of this project used the synthetic AmExpert dataset, whose engineered columns do not reward honest, leakage-free modelling. It was replaced with the FICO HELOC dataset (~10.4k real applicants, 23 credit-bureau attributes, ~52% "Bad" rate within 24 months). It is a well-known benchmark for interpretable credit scoring, with a strong, honest baseline achievable by a linear model.
Test-set ROC-AUC: 0.793 with a plain penalised logistic regression - no leakage, no feature magic.
- Algorithm: penalised (elastic-net) logistic regression - interpretable by design.
- Tuning: 5-fold GridSearchCV (C and L1 ratio) optimising ROC-AUC on a stratified 80/20 split.
- Decision threshold: chosen to minimise an asymmetric cost function weighting a false negative (approving a future defaulter, cost 150) higher than a false positive (declining a good customer, cost 100) - mirroring a lender's real loss asymmetry.
- Validation: stratified holdout; all imputation statistics fitted on the training set only.
| Metric | Value |
|---|---|
| ROC-AUC | 0.793 |
| Precision (Bad) | 0.61 |
| Recall (Bad) | 0.95 |
| Decision threshold | 0.25 (cost-optimised) |
At that threshold the model catches 95% of future defaulters while keeping precision at 61%, reflecting the deliberate bias towards avoiding bad approvals.
FICO records missing data as special value codes rather than blanks:
| Code | Meaning |
|---|---|
| -9 | No Bureau record (thin file) |
| -8 | No usable / valid trades |
| -7 | Condition not met (e.g. never delinquent) |
| -2 | No / insufficient data |
These are treated as meaningful signals, not random missingness:
- Binary flags are derived before imputation (
no_bureau,never_delinquent) - an applicant who has never been delinquent is genuinely lower risk, so this must not be discarded. - Sentinels are then removed and remaining gaps are median-imputed using statistics fitted on the training split only (no leakage).
NumInqLast6Mexcl7daysis excluded as a near-duplicate ofNumInqLast6M(r > 0.99); keeping both destabilised the elastic-net coefficients (opposite-sign artifacts) and hurt interpretability.
- Lowers default risk: higher external risk score, more satisfactory trades, longer credit history, "never delinquent".
- Raises default risk: revolving utilisation, high-utilisation bank/national trades, recent hard inquiries.
api/ FastAPI inference endpoint (input validation, batch predict)
dashboard/ Streamlit app (dataset explorer, metrics, prediction + SHAP)
src/data/preprocess.py Sentinel handling, flags, median imputation + feature metadata
src/data/pipeline.py sklearn transformer wrapper (fit/transform)
src/data/eda.ipynb Exploratory analysis of the HELOC dataset
src/models/model1/train.py Training + evaluation + threshold selection
src/models/model1/predict.py Inference + SHAP LinearExplainer
artifacts/ Trained model, scaler/pipeline, threshold, splits (gitignored)
data/raw/ Dataset (gitignored)
- Python 3.14+ (Docker image
python:3.14.3) - Dataset placed at
data/raw/train.csv
# get the data
mkdir -p data/raw
curl -L -o data/raw/train.csv https://raw.githubusercontent.com/Ali-Abbass313/DSPP-HELOC/main/heloc_dataset_v1.csvpython -m venv venv
source venv/bin/activate # or venv\Scripts\activate on Windows
pip install -r requirements.txtpython src/models/model1/train.pyWrites model, best hyperparameters, cost-optimised threshold and train/test splits to artifacts/.
uvicorn api.main:app --reload
# docs: http://localhost:8000/docsstreamlit run dashboard/app.py
# http://localhost:8501The image bakes in the trained artefacts and dataset, so train before building:
docker build -t credit-api .
docker run -p 8000:8000 credit-apicurl -X POST http://localhost:8000/predict \
-H 'Content-Type: application/json' \
-d '[{
"ExternalRiskEstimate": 90, "MSinceOldestTradeOpen": 240,
"MSinceMostRecentTradeOpen": 5, "AverageMInFile": 120,
"NumSatisfactoryTrades": 25, "NumTrades60Ever2DerogPubRec": 0,
"NumTrades90Ever2DerogPubRec": 0, "PercentTradesNeverDelq": 100,
"MSinceMostRecentDelq": -7, "MaxDelq2PublicRecLast12M": -7,
"MaxDelqEver": -7, "NumTotalTrades": 28, "NumTradesOpeninLast12M": 1,
"PercentInstallTrades": 30, "MSinceMostRecentInqexcl7days": 8,
"NumInqLast6M": 0, "NetFractionRevolvingBurden": 12,
"NetFractionInstallBurden": 50, "NumRevolvingTradesWBalance": 2,
"NumInstallTradesWBalance": 2,
"NumBank2NatlTradesWHighUtilization": 0, "PercentTradesWBalance": 55
}]'The three delinquency fields accept -7 (FICO's "never delinquent" code); the API enforces that they are all -7 or all real values.
Interpretability is the point of the model, not an afterthought:
- By design: linear, penalised model with stable, monotonic-sensible coefficients.
- Per prediction: the dashboard renders a SHAP waterfall showing exactly which credit behaviours pushed an applicant's default probability up or down from the base rate.
- Global view: mean-|SHAP| feature importance shows the portfolio-level drivers of risk.
This mirrors the transparency expected of credit models under responsible-lending and regulatory (e.g. model risk / assurance) review.


