A production-ready MLOps project that predicts and explains customer churn for a telecom company.
Live API: https://churn-api-0cwo.onrender.com/docs
| Layer | Technology |
|---|---|
| API | FastAPI + Uvicorn |
| ML Model | scikit-learn Logistic Regression + LIME explanations |
| Experiment Tracking | MLflow (SQLite backend) |
| Containerisation | Docker (multi-stage, non-root user) |
| Deployment | Render (free tier) |
| CI/CD | GitHub Actions — test → deploy on every push |
| Monitoring | Data drift detection (PSI + Chi-Squared) |
| Authentication | API key via X-API-Key header |
churn_api/
├── main.py # FastAPI app — endpoints, auth, drift logging
├── inference.py # ModelService — loads model, predict/explain
├── churnexplainer.py # ExplainedModel + CategoricalEncoder
├── mlflow_logger.py # MLflow tracking helpers (best-effort)
├── monitoring.py # DriftMonitor — PSI + Chi-Squared detection
├── auth.py # API key authentication dependency
├── config.py # All settings via environment variables
├── train.py # Full training pipeline — train → save → MLflow
├── simulate_traffic.py # Send realistic traffic to test drift monitor
├── models/
│ └── telco_linear/
│ ├── telco_linear.pkl # Pre-trained model (pickle)
│ └── reference_stats.json # Training distribution stats for drift
├── tests/
│ ├── conftest.py # Mocks ModelService — no pickle needed in CI
│ ├── test_main.py # Root + health endpoint tests
│ └── test_predict_explain.py # Predict, explain, validation tests
├── data/
│ └── README.md # Instructions to download Telco dataset
├── Dockerfile # Multi-stage production image
├── docker-compose.yml # Local dev: API + MLflow server
├── render.yaml # Render deployment config
├── requirements.txt # Pinned dependencies
├── .env.example # Copy to .env for local dev
└── .github/
└── workflows/
├── deploy.yml # CI/CD: test → deploy to Render
└── mlflow-server.yml # One-time MLflow server setup (optional)
Developer pushes to main
│
▼
┌─────────────────────────────────────┐
│ GitHub Actions │
│ 1. pytest — 7 tests │
│ 2. Trigger Render deploy hook │
└─────────────────────────────────────┘
│
▼
Render (free tier)
https://churn-api-0cwo.onrender.com
└── Docker container (non-root)
├── FastAPI + Uvicorn
├── MLflow → SQLite (/tmp/mlruns.db)
└── Drift log → SQLite (same DB)
git clone https://github.com/S-Shetty/churn-api.git
cd churn_api
cp .env.example .env
# Edit .env — set API_KEY and other valuesStarts both the API and a local MLflow server:
docker compose up --build| Service | URL |
|---|---|
| Churn API | http://localhost:8000 |
| Swagger UI | http://localhost:8000/docs |
| MLflow UI | http://localhost:5000 |
pip install -r requirements.txt
uvicorn main:app --reload --port 8000All endpoints except /health require an X-API-Key header.
curl -H "X-API-Key: your-key" https://churn-api-0cwo.onrender.com/healthPublic. Returns {"status": "ok"}. Used by Render health checks.
Request:
{
"gender": "Female",
"SeniorCitizen": 0,
"Partner": "Yes",
"Dependents": "No",
"tenure": 5,
"PhoneService": "Yes",
"MultipleLines": "No",
"InternetService": "DSL",
"OnlineSecurity": "Yes",
"OnlineBackup": "No",
"DeviceProtection": "Yes",
"TechSupport": "No",
"StreamingTV": "No",
"StreamingMovies": "No",
"Contract": "Month-to-month",
"PaperlessBilling": "Yes",
"PaymentMethod": "Electronic check",
"MonthlyCharges": 75.5,
"TotalCharges": 400.2
}Response:
{ "prediction": 1, "probability": 0.73 }prediction: 1 = likely to churn, 0 = likely to stay
Same request body. Returns LIME feature importance weights showing which factors drove the prediction.
{
"probability": 0.73,
"explanations": {
"tenure": -0.21,
"Contract": 0.18,
"MonthlyCharges": 0.14,
"InternetService": -0.09
}
}Positive weight = pushes towards churn. Negative = pushes away from churn.
Returns data drift report comparing recent traffic against training distribution.
{
"status": "no_drift",
"samples": 120,
"features": {
"tenure": { "psi": 0.03, "type": "numeric" },
"MonthlyCharges": { "psi": 0.07, "type": "numeric" },
"Contract": { "chi2": 0.04, "type": "categorical" }
},
"alerts": [],
"warnings": []
}pip install -r requirements.txt
pytest -v7 tests — model is mocked in conftest.py so no pickle file or AWS credentials needed.
| Test | Type |
|---|---|
test_root_endpoint |
Smoke |
test_health_endpoint |
Smoke |
test_predict_endpoint |
Integration |
test_explain_endpoint |
Integration |
test_predict_missing_field_returns_422 |
Validation |
test_predict_invalid_senior_citizen_returns_422 |
Validation |
test_predict_negative_tenure_returns_422 |
Validation |
The pre-trained model is already in models/telco_linear/. To retrain:
https://www.kaggle.com/datasets/blastchar/telco-customer-churn
Save to data/WA_Fn-UseC_-Telco-Customer-Churn.csv
python train.py
python train.py --test-size 0.2 --no-mlflow # skip MLflowtrain.py outputs:
models/telco_linear/telco_linear.pkl— model picklemodels/telco_linear/reference_stats.json— drift reference statistics
| Metric | Score |
|---|---|
| Accuracy | ~0.76 |
| ROC AUC | ~0.84 |
| Precision | ~0.58 |
| Recall | ~0.75 |
| F1 | ~0.65 |
class_weight="balanced" prioritises recall — catching true churners matters more than overall accuracy for a retention use case.
Every prediction request is logged to SQLite. The /drift endpoint compares recent traffic against reference statistics saved during training.
# Test with realistic traffic
python simulate_traffic.py --mode normal --n 60 # expect: no_drift
python simulate_traffic.py --mode shifted --n 60 # expect: drift_detected| Method | Used for | Thresholds |
|---|---|---|
| PSI (Population Stability Index) | Numeric features | >0.1 warning, >0.2 alert |
| Chi-Squared distance | Categorical features | >0.2 warning, >0.5 alert |
push to main → Run Tests ✅ → Deploy to Render ✅
| Job | Trigger | What it does |
|---|---|---|
Run Tests |
Every push + PR | Runs 7 pytest tests |
Deploy to Render |
Push to main only | Triggers Render deploy hook |
| Variable | Value |
|---|---|
API_KEY |
Any secret string — used for X-API-Key auth |
MODEL_NAME |
telco_linear |
MLFLOW_TRACKING_URI |
sqlite:////tmp/mlruns.db |
MLFLOW_EXPERIMENT_NAME |
churn-prediction |
LOG_LEVEL |
INFO |
| Secret | Description |
|---|---|
RENDER_DEPLOY_HOOK_URL |
From Render → Settings → Deploy Hook |
All inferences logged under the churn-prediction experiment.
| Event | What's logged |
|---|---|
| API startup | Model registered in MLflow Model Registry |
/predict |
probability + prediction as metrics |
/explain |
probability as metric + explanations.json artifact |
View locally:
mlflow ui --backend-store-uri sqlite:///mlruns.db
# Open http://localhost:5000| Variable | Default | Description |
|---|---|---|
API_KEY |
(empty — auth disabled) | Secret key for X-API-Key header |
MLFLOW_TRACKING_URI |
sqlite:////tmp/mlruns.db |
MLflow backend |
MLFLOW_EXPERIMENT_NAME |
churn-prediction |
MLflow experiment name |
MODEL_NAME |
telco_linear |
Model artifact to load |
LOG_LEVEL |
INFO |
Python logging level |
Copy .env.example to .env for local development.