This project is a production-ready Machine Learning service that predicts credit default risk using XGBoost and FastAPI.
- Automated Pipeline: Data preprocessing, model training, and evaluation in one command.
- Hyperparameter Tuning:
GridSearchCVsweepsn_estimators/learning_rate/max_depthwith 5-fold cross-validation instead of hand-picked values, so the reported score reflects average performance across splits rather than one lucky train/test draw. - Class Imbalance Handling:
scale_pos_weightreweights the minority "Bad" class during training so the model is penalized for missing risky applicants, not just for being wrong on average. - REST API: Real-time inference using FastAPI, with Pydantic range validation (not just types) rejecting out-of-bounds applicant data before it ever reaches the model.
- Feature Alignment: Robust reindexing logic to handle categorical mismatches and missing features.
- Drift Monitoring: A standalone PSI (Population Stability Index) check that compares live prediction traffic against the training distribution and flags features that have shifted.
- Reproducible: Fully managed environment via
requirements.txt.
- Language: Python 3.11.9
- Model: XGBoost (Gradient Boosting), tuned via
GridSearchCV - API Framework: FastAPI / Uvicorn
- Libraries: Pandas, NumPy, Scikit-Learn, Joblib, Matplotlib, Seaborn
Evaluated on a held-out 20% test split (200 samples) of the German Credit Dataset, which is imbalanced — 141 "Good" vs. 59 "Bad" — so accuracy alone is a misleading headline number (always predicting "Good" would already score 70.5%).
| Class | Precision | Recall | F1 |
|---|---|---|---|
| Good (majority) | 0.82 | 0.80 | 0.81 |
| Bad (credit risk) | 0.55 | 0.58 | 0.56 |
Overall accuracy: 73%
This is a deliberate trade-off from an earlier 76%-accuracy version of the model. That version was tuned by hand and only caught 36% of actual defaults (recall on "Bad"). Adding scale_pos_weight to penalize missed "Bad" predictions during training raised that recall to 58% — catching nearly 3 in 5 risky applicants instead of roughly 1 in 3 — at the cost of a few points of raw accuracy. For a credit risk product, that's the right side of the trade: the cost of missing a real default is higher than the cost of a slightly lower headline accuracy number.
Validation happens at the API boundary: app.py's Borrower model constrains every field to its real-world range (e.g. Age 18–100, Duration 1–120 months), so malformed or out-of-distribution requests are rejected with a 422 before they reach the model.
Drift monitoring happens out-of-band: every served /predict request is logged to logs/prediction_log.csv. Running
python monitor_drift.pycomputes the Population Stability Index for key features (Age, Credit amount, Duration, Job) between that live traffic and the distribution the model was trained on, using the same PSI thresholds banks use in production (<0.1 stable, 0.1–0.2 monitor, >0.2 retrain candidate). It prints a report and saves drift_report.png. Run python monitor_drift.py --demo to see it work against a simulated drifted population without needing live traffic first.
The chart below is from --demo mode, which simulates an economic downturn by aging every applicant +15 years and inflating requested loan amounts ×1.6 — Age spikes far past the retrain threshold while untouched features (Duration, Job) correctly stay flat, showing the check only fires on features that actually shifted.
pip install -r requirements.txt
# Run the full pipeline: preprocess -> train -> predict -> visualize
# (training now runs a GridSearchCV hyperparameter sweep, so this takes
# a bit longer than a single model fit)
python run_pipline.py
# Serve the API
uvicorn app:app --reload
# -> visit http://127.0.0.1:8000/docs for the interactive API
# Check for data drift (once there's logged traffic, or with --demo)
python monitor_drift.py --demo
