A dynamic credit limit recommendation engine that combines spend forecasting, default-risk modeling, and a learned recommendation layer to suggest a risk-adjusted credit limit for each customer — the same kind of problem a credit-card issuer's portfolio risk team works on.
Given a customer's spending history, payment behavior, utilization, and demographics, recommend an optimal credit limit that balances growth opportunity against default risk.
Most "default prediction" or "churn prediction" portfolios look the same. This one is different because it's a full decision pipeline, not a single classifier: it forecasts future behavior, prices risk, and turns both into a single actionable number a card issuer could actually act on — which is closer to how risk and growth teams at issuers like Amex or Chase actually operate.
┌────────────────────────┐
customer → │ Feature Engineering │
history │ (utilization, payment │
│ ratios, late-pay, │
│ spend trend) │
└───────────┬─────────────┘
│
┌─────────────┼─────────────┐
▼ ▼
┌───────────────────────┐ ┌───────────────────────────┐
│ Spend Forecast Model │ │ Default Risk Model │
│ Linear Regression │ │ Logistic Regression │
│ → predicted next-month │ │ → probability of default │
│ spend │ │ next month │
└───────────┬───────────┘ └─────────────┬──────────────┘
│ │
└──────────────┬───────────────┘
▼
┌──────────────────────────────┐
│ Limit Recommendation Engine │
│ XGBoost (learns a risk- │
│ adjusted limit policy) │
│ → recommended credit limit │
└──────────────────────────────┘
| # | Model | Type | Predicts |
|---|---|---|---|
| 1 | Spend Forecast Model | Linear Regression | Next month's spend, from 5 months of billing history |
| 2 | Default Risk Model | Logistic Regression | Probability of default next month |
| 3 | Limit Recommendation Engine | XGBoost (gradient boosted trees) | Final recommended credit limit |
No public dataset includes a ground-truth "ideal credit limit" — that's an internal business decision issuers make, not something that gets published. So model 3 is trained in two honest steps:
- A simple, explicit business rule (
compute_business_rule_limitinsrc/limit_recommendation_engine.py) gives each customer enough headroom to cover ~1.2×–3× their predicted spend, where the multiplier shrinks as default probability rises, capped so no limit can more than double or less than halve in one cycle (real issuers don't swing limits 5x overnight). - XGBoost is trained to learn and generalize that policy from the engineered features, rather than just looking it up. In a real deployment, this is the part you'd continuously retrain on actual realized outcomes (did the customer default after their limit changed? did they grow their spend?) so the model adapts past the hand-written starting rule.
- A hard guardrail clip at inference time (
recommend_credit_limit) re-applies the same growth/shrink bounds to the model's output. A learned model is an approximation, not a guarantee — it can drift slightly past the policy bounds on individual predictions, so the final serving step never lets a recommendation violate the stated policy, even by a little.
This is a standard pattern when you don't have outcome-labeled "optimal action" data, and it's worth explaining exactly this way in an interview — it shows you understand the difference between a label you have and a label you're approximating.
Default of Credit Card Clients Dataset
— 30,000 real customers with 6 months of billing/payment history and a real,
labeled default outcome. See data/README.md for download
instructions. If you run the pipeline without the real CSV, it automatically
falls back to a synthetic dataset with an identical schema so you can smoke
test it.
Spend Forecast Model (Linear Regression)
| MAE | RMSE | R² |
|---|---|---|
| 8,856.92 | 22,665.54 | 0.91 |
Default Risk Model (Logistic Regression)
| Accuracy | Precision | Recall | F1 | ROC-AUC |
|---|---|---|---|---|
| 0.738 | 0.433 | 0.607 | 0.506 | 0.748 |
A ROC-AUC of ~0.75 from plain logistic regression is in line with published
benchmarks for this dataset — gradient-boosted models (the recommendation
engine below) typically push that closer to ~0.78. Recall is prioritized over
precision here (class_weight="balanced") since for a credit issuer, missing
an actual default is more costly than over-flagging a safe customer.
Limit Recommendation Engine (Gradient Boosted Trees)
| MAE | RMSE | R² |
|---|---|---|
| 4,880.28 | 9,921.65 | 1.00 |
The R² here is near 1.0 because this model is trained to approximate the explicit business rule (see below) from features that mostly determine that rule directly — it's measuring how well the model learned the policy, not an independent predictive task. That's expected and worth explaining as such.
ROC curve, confusion matrix, and feature importance plots are saved to
outputs/plots/ after running the pipeline.
LimitForge/
├── data/
│ └── README.md # how to get the real dataset
├── src/
│ ├── data_loader.py # loads real data, falls back to synthetic
│ ├── synthetic_data.py # synthetic demo dataset (same schema)
│ ├── feature_engineering.py # utilization, payment ratios, trends
│ ├── spend_forecast_model.py # Model 1: Linear Regression
│ ├── default_risk_model.py # Model 2: Logistic Regression
│ ├── limit_recommendation_engine.py # Model 3: XGBoost + business-rule target
│ └── utils.py # plotting + save/load helpers
├── outputs/
│ ├── models/ # saved .pkl artifacts (gitignored)
│ └── plots/ # ROC curve, confusion matrix, feature importance
├── main.py # runs the full pipeline end to end
├── requirements.txt
└── README.md
git clone https://github.com/<your-username>/LimitForge.git
cd LimitForge
pip install -r requirements.txt
# Optional but recommended: download the real dataset (see data/README.md)
# and place it at data/UCI_Credit_Card.csv
python main.pyThis trains all three models, saves them to outputs/models/, saves
evaluation plots to outputs/plots/, and prints sample recommendations for
the first 5 customers, e.g.:
{
"current_limit": 50000.0,
"predicted_next_month_spend": 49174.86,
"default_probability": 0.3715,
"recommended_limit": 100000.0,
"change": 50000.0,
"pct_change": 100.0
}from src.data_loader import load_data
from src.feature_engineering import add_engineered_features
from src.spend_forecast_model import train_spend_model, predict_future_spend
from src.default_risk_model import train_default_model, predict_default_probability
from src.limit_recommendation_engine import (
build_recommendation_dataset, train_recommendation_engine, recommend_credit_limit
)
df = add_engineered_features(load_data())
spend_artifacts, _ = train_spend_model(df)
predicted_spend = predict_future_spend(spend_artifacts, df)
risk_artifacts, _, _ = train_default_model(df)
default_prob = predict_default_probability(risk_artifacts, df)
X, y = build_recommendation_dataset(df, predicted_spend, default_prob)
rec_artifacts, _ = train_recommendation_engine(X, y)
customer = df.iloc[0]
print(recommend_credit_limit(rec_artifacts, customer, predicted_spend[0], default_prob[0]))