A production-grade Credit Scoring ML API. Predicts loan default probability using an ensemble ML model (Random Forest + Gradient Boosting), tracks experiments with MLflow, and serves predictions via a FastAPI REST API with full audit logging.
Built as a portfolio project applicable to fintech, banking, insurance, and any risk assessment platform.
┌─────────────────────────────────────────────────────────────────────┐
│ CREDIT SCORING PIPELINE │
│ │
│ Training (offline) Inference (online) │
│ ────────────────── ───────────────── │
│ │
│ Synthetic data (2000+ rows) POST /api/v1/predict │
│ │ │ │
│ ▼ ▼ │
│ FeatureEngineer FeatureEngineer │
│ (26 features: DTI, LTI, (same pipeline — │
│ payment score, one-hot...) no train/test leakage) │
│ │ │ │
│ ▼ ▼ │
│ sklearn Pipeline: CreditScorePredictor.predict() │
│ StandardScaler + │ │
│ VotingClassifier ▼ │
│ (RF + GradientBoosting) default_probability (0–1) │
│ │ risk_score (300–850) │
│ ▼ decision (APPROVED/REVIEW/REJECT) │
│ MLflow tracking: feature_contributions │
│ - params, metrics │ │
│ - model artifact ▼ │
│ - model registry PostgreSQL audit log │
│ │ (regulatory compliance) │
│ ▼ │
│ AUC-ROC, Gini, KS-stat Drift monitoring │
│ Precision, Recall, F1 (distribution tracking) │
└─────────────────────────────────────────────────────────────────────┘
| Component | Details |
|---|---|
| Algorithm | Ensemble: Random Forest + Gradient Boosting (VotingClassifier) |
| Preprocessing | StandardScaler inside sklearn Pipeline |
| Features | 26 engineered features (see Feature Engineering) |
| Class imbalance | class_weight="balanced" in Random Forest |
| Evaluation | AUC-ROC, Gini coefficient, KS statistic, Precision/Recall |
| Tracking | MLflow experiments + model registry |
| Explainability | Feature contributions per prediction |
| Feature | Description |
|---|---|
debt_to_income_ratio |
existing debt / monthly income — #1 credit metric |
loan_to_income_ratio |
loan amount / annual income |
monthly_payment_estimate |
estimated monthly payment on new loan (annuity formula) |
total_monthly_debt_ratio |
(existing + new) monthly debt / monthly income |
payment_history_score |
penalizes late payments relative to history length |
credit_density |
number of loans per year of credit history |
| + 20 more | one-hot encodings, raw features |
| Metric | Value | Description |
|---|---|---|
| AUC-ROC | ~0.82 | Discriminatory power (1 = perfect) |
| Gini coefficient | ~0.64 | 2×AUC − 1, industry standard |
| KS statistic | ~0.50 | Max separation between good/bad |
| Accuracy | ~0.77 | Overall classification accuracy |
- Python 3.11 (pinned dependency versions in
requirements.txtmay lack prebuilt wheels on newer Python versions without a C compiler installed) - Docker & Docker Compose
git clone https://github.com/M-TOUITI/credit-scoring-api.git
cd credit-scoring-api
# Start infrastructure (PostgreSQL + MLflow)
docker-compose up -d postgres mlflow
# Install dependencies
pip install -r requirements.txt
# Train the model (generates synthetic data + logs to MLflow)
make train
# Run migrations + start API
make migrate
make dev
# Open docs
open http://localhost:8000/docs
open http://localhost:5000 # MLflow UImake test-unit # 25+ unit tests, instant
make test # all tests with coveragePOST /api/v1/predict
{
"age": 35,
"annual_income": 55000,
"loan_amount": 12000,
"loan_term_months": 36,
"credit_history_months": 72,
"num_existing_loans": 1,
"existing_debt_monthly": 400,
"num_late_payments": 0,
"num_credit_inquiries": 1,
"employment_years": 6.0,
"employment_type": "employed",
"home_ownership": "mortgage",
"loan_purpose": "personal"
}Response:
{
"prediction_id": "uuid",
"default_probability": 0.1832,
"risk_score": 749,
"risk_category": "LOW",
"decision": "APPROVED",
"confidence": 0.87,
"feature_contributions": [
{
"feature": "Debt To Income Ratio",
"value": 0.0873,
"contribution": -0.142,
"direction": "reduces_risk"
},
{
"feature": "Payment History Score",
"value": 1.0,
"contribution": -0.089,
"direction": "reduces_risk"
}
],
"model_version": "mlflow:Production",
"predicted_at": "2025-06-01T14:30:00Z"
}POST /api/v1/predict/batch
{
"applications": [...],
"return_explanations": false
}GET /api/v1/models/info # model version + top features
GET /api/v1/models/monitoring # drift detection + distribution stats
GET /api/v1/models/features # global feature importances
POST /api/v1/models/reload # hot-reload from MLflow registryGET /api/v1/models/predictions/{id}/audit # full audit record
POST /api/v1/models/predictions/{id}/outcome # record actual outcome| Probability | Risk Score | Category | Decision |
|---|---|---|---|
| < 0.35 | 657–850 | 🟢 LOW / MEDIUM | APPROVED |
| 0.35–0.64 | 497–657 | 🟡 MEDIUM / HIGH | MANUAL REVIEW |
| ≥ 0.65 | 300–496 | 🔴 HIGH / VERY HIGH | REJECTED |
After training, open the MLflow UI at http://localhost:5000 to:
- Compare experiments (different hyperparameters, feature sets)
- Inspect model artifacts (feature importances, ROC curve)
- Promote a model version to Production
# Promote a model to Production via CLI
mlflow models set-model-version-tag \
--model-name credit-scoring-model \
--version 1 \
--key stage --value ProductionThen reload the API model: POST /api/v1/models/reload
credit-scoring-api/
├── app/
│ ├── main.py # FastAPI app, model loading at startup
│ ├── core/ # config, logging
│ ├── domain/schemas.py # Pydantic request/response models
│ ├── ml/
│ │ ├── features/engineer.py # 26-feature engineering pipeline
│ │ └── models/
│ │ ├── trainer.py # sklearn training + MLflow logging
│ │ └── predictor.py # inference + explainability
│ ├── infrastructure/ # SQLAlchemy models + repositories
│ └── api/routers/ # predict, models (monitoring)
├── scripts/
│ └── train_model.py # CLI training script
├── tests/
│ ├── unit/ml/ # 25+ unit tests (no model needed)
│ └── integration/ # API tests with mock predictor
├── alembic/ # DB migrations
├── docker-compose.yml # PostgreSQL + MLflow + API
├── Makefile
└── requirements.txt
Why Random Forest + Gradient Boosting ensemble?
Both models capture different aspects of the data. Random Forest is robust to noise and overfitting via bagging. Gradient Boosting captures sequential patterns. VotingClassifier(voting="soft") averages probabilities — reducing variance while keeping the discriminatory power of each model.
Why engineer features before sklearn Pipeline?
The FeatureEngineer applies identical transformations at training and inference time. Putting it inside sklearn's Pipeline would complicate serving (serialization issues with Pydantic inputs). Keeping it separate as a stateless class is cleaner.
Why store input features in the audit log as JSON? Regulatory compliance (GDPR Art. 22, ECOA) requires that automated credit decisions can be explained. Storing the full input allows reconstructing the decision and generating explanations after the fact.
Why Gini and KS instead of just accuracy? Accuracy is misleading for imbalanced classes (25% default rate). Gini coefficient (2×AUC−1) and KS statistic are the standard metrics used by banks and credit bureaus — they measure how well the model separates good from bad applicants.
M-TOUITI — GitHub · hello@mtouiti.dev
MIT — see LICENSE.