Skip to content

Commit 2adc187

Browse files
added ml-flow and infrastructure
1 parent 81cbe50 commit 2adc187

14 files changed

Lines changed: 598 additions & 3 deletions

.env.example

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Core model settings
2+
MODEL_SOURCE=local
3+
MODEL_NAME=rf_churn_model
4+
DEFAULT_PREDICTION_THRESHOLD=0.36
5+
6+
# MLflow settings
7+
MLFLOW_TRACKING_URI=http://localhost:5000
8+
MLFLOW_EXPERIMENT=Telco_Churn_Prediction
9+
MLFLOW_MODEL_ALIAS=champion
10+
ALLOW_LOCAL_MODEL_FALLBACK=1
11+
12+
# UI <-> API wiring
13+
API_URL=http://localhost:8000
14+
USE_API_INFERENCE=1
15+
16+
# Email alerts
17+
ALERT_SMTP_HOST=smtp.example.com
18+
ALERT_SMTP_PORT=587
19+
ALERT_SMTP_USER=mlops@example.com
20+
ALERT_SMTP_PASSWORD=change_me
21+
ALERT_FROM_EMAIL=mlops@example.com
22+
ALERT_TO_EMAILS=data-team@example.com,owner@example.com

.github/workflows/ml_pipeline.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: ml-pipeline-ci
2+
3+
on:
4+
push:
5+
branches: ["main", "develop"]
6+
pull_request:
7+
branches: ["**"]
8+
9+
jobs:
10+
ci:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Checkout
14+
uses: actions/checkout@v4
15+
16+
- name: Setup Python
17+
uses: actions/setup-python@v5
18+
with:
19+
python-version: "3.11"
20+
21+
- name: Install dependencies
22+
run: |
23+
python -m pip install --upgrade pip
24+
pip install -r requirements.txt
25+
26+
- name: Python syntax check
27+
run: |
28+
python -m compileall src app.py
29+
30+
- name: Import smoke tests
31+
run: |
32+
python -c "import src.deploy_model"
33+
python -c "import src.crm_integration"
34+
python -c "import src.drift_monitor"
35+
python -c "import src.ab_testing"
36+
python -c "import src.shap_analysis"

Dockerfile

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
FROM python:3.11-slim
2+
3+
WORKDIR /app
4+
5+
COPY requirements.txt /app/requirements.txt
6+
RUN pip install --no-cache-dir -r /app/requirements.txt
7+
8+
COPY app.py /app/app.py
9+
COPY src /app/src
10+
COPY data /app/data
11+
COPY models /app/models
12+
COPY reports /app/reports
13+
14+
ENV PYTHONPATH=/app/src
15+
16+
EXPOSE 8081
17+
EXPOSE 8501
18+
19+
CMD ["streamlit", "run", "app.py", "--server.port", "8501", "--server.address", "0.0.0.0"]

README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,67 @@
22

33
A full end-to-end churn prediction system built on the **IBM Telco Customer Churn** dataset — from raw EDA and preprocessing all the way through model training, SHAP explainability, CRM integration, data drift monitoring, A/B testing, and an interactive **Streamlit retention dashboard**.
44

5+
## MLOps Upgrade (In Progress)
6+
7+
This project now includes a production-oriented MLOps path in addition to the local notebook flow:
8+
9+
- MLflow tracking and model registry integration in `src/deploy_model.py`
10+
- FastAPI batch inference service in `src/api.py`
11+
- API-based Streamlit scoring mode (`USE_API_INFERENCE=1`)
12+
- Drift job entrypoint with MLflow logging and SMTP alerts in `src/drift_monitor_job.py`
13+
- Containerized multi-service stack via `docker-compose.yml`
14+
- CI workflow in `.github/workflows/ml_pipeline.yml`
15+
16+
### Service Architecture
17+
18+
1. `mlflow` service: experiment tracking and model registry.
19+
2. `api` service: batch prediction endpoint (`POST /predict`) and health endpoint (`GET /health`).
20+
3. `streamlit` service: dashboard UI that calls API for scoring.
21+
22+
### Run Containerized Stack
23+
24+
```bash
25+
docker compose up --build
26+
```
27+
28+
Endpoints:
29+
30+
- MLflow UI: `http://localhost:5001`
31+
- FastAPI: `http://localhost:18000` (`/health`, `/predict`)
32+
- Streamlit: `http://localhost:18501`
33+
34+
### Environment Variables
35+
36+
Use `.env.example` as a template for local configuration.
37+
38+
Key variables:
39+
40+
- `MODEL_SOURCE` (`local` or `mlflow`)
41+
- `MLFLOW_TRACKING_URI`
42+
- `MLFLOW_EXPERIMENT`
43+
- `MLFLOW_MODEL_ALIAS`
44+
- `API_URL`
45+
- `USE_API_INFERENCE`
46+
- `ALERT_SMTP_*`
47+
48+
### Drift Monitoring Job (Airflow/Cron Ready)
49+
50+
Run manually:
51+
52+
```bash
53+
python -m src.drift_monitor_job --reference-path data/telco_churn_processed.csv --current-path data/telco_churn_processed.csv
54+
```
55+
56+
This job computes PSI, logs drift metrics to MLflow, and sends email alerts when retraining is recommended.
57+
58+
### Quick MLflow Logging (Existing Model)
59+
60+
Use this helper to log your current local model artifact to MLflow without opening a notebook:
61+
62+
```bash
63+
./venv/python.exe scripts/log_existing_model_to_mlflow.py --tracking-uri http://localhost:5000 --experiment Telco_Churn_Prediction
64+
```
65+
566
---
667

768
## Dashboard Preview
@@ -49,6 +110,8 @@ This project answers:
49110

50111
```
51112
churn-analyze-crm/
113+
├── docker-compose.yml # Multi-service local orchestration
114+
├── Dockerfile # Shared image for API + Streamlit services
52115
├── app.py # Streamlit retention dashboard (entry point)
53116
├── requirements.txt # Python dependencies
54117
├── data/
@@ -61,11 +124,15 @@ churn-analyze-crm/
61124
│ ├── eda_prac.ipynb # Full EDA + preprocessing notebook
62125
│ └── model_evaluation.ipynb # Model training & evaluation notebook
63126
├── src/
127+
│ ├── api.py # FastAPI batch inference service
64128
│ ├── deploy_model.py # Model packaging, loading & model card
65129
│ ├── shap_analysis.py # SHAP explainability (waterfall, bar, beeswarm)
66130
│ ├── crm_integration.py # Customer scoring, risk tiers, CRM CSV export
67131
│ ├── drift_monitor.py # PSI-based data drift detection & alerts
132+
│ ├── drift_monitor_job.py # Scheduler-friendly drift monitoring entrypoint
68133
│ └── ab_testing.py # A/B test simulation & power analysis
134+
├── .github/workflows/
135+
│ └── ml_pipeline.yml # CI checks (syntax/import smoke)
69136
├── reports/ # Auto-generated SHAP plots & drift reports
70137
└── img/ # Dashboard screenshots
71138
```

app.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
matplotlib.use('Agg')
1010
import matplotlib.pyplot as plt
1111
import streamlit as st
12+
import requests
1213

1314
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
1415

@@ -20,6 +21,7 @@
2021

2122
ROOT = os.path.dirname(__file__)
2223
DATA_PATH = os.path.join(ROOT, 'data', 'telco_churn_processed.csv')
24+
API_URL = os.environ.get('API_URL', 'http://localhost:8000')
2325

2426
AVG_MONTHLY_REVENUE = 65 # £ per customer (MonthlyCharges proxy)
2527

@@ -87,7 +89,15 @@ def plain(name: str) -> str:
8789

8890
@st.cache_resource(show_spinner='Loading prediction system…')
8991
def get_model():
90-
return load_model('rf_churn_model')
92+
try:
93+
return load_model('rf_churn_model')
94+
except Exception:
95+
# API inference mode can run without local model artifacts.
96+
return {
97+
'model': None,
98+
'feature_names': [],
99+
'threshold': float(os.environ.get('DEFAULT_PREDICTION_THRESHOLD', '0.36')),
100+
}
91101

92102

93103
@st.cache_data(show_spinner='Loading customer data…')
@@ -96,9 +106,15 @@ def get_data():
96106

97107

98108
def model_ready() -> bool:
109+
if use_api_inference():
110+
return True
99111
return os.path.exists(os.path.join(ROOT, 'models', 'rf_churn_model.joblib'))
100112

101113

114+
def use_api_inference() -> bool:
115+
return os.environ.get('USE_API_INFERENCE', '0') == '1'
116+
117+
102118
def show_deploy_prompt():
103119
st.warning(
104120
"⚠️ The prediction system has not been set up yet.\n\n"
@@ -112,6 +128,22 @@ def show_deploy_prompt():
112128
@st.cache_data(show_spinner='Scoring customers…')
113129
def get_scored(_model, _feature_names, _threshold, _data_hash):
114130
df = get_data()
131+
if use_api_inference():
132+
try:
133+
response = requests.post(
134+
f'{API_URL}/predict',
135+
json={'records': df.to_dict(orient='records')},
136+
timeout=60,
137+
)
138+
response.raise_for_status()
139+
payload = response.json()
140+
preds = pd.DataFrame(payload.get('predictions', []), index=df.index)
141+
if preds.empty:
142+
raise ValueError('No predictions returned by API')
143+
return pd.concat([df, preds], axis=1)
144+
except Exception as exc:
145+
st.warning(f'API scoring unavailable, using local model instead. ({exc})')
146+
115147
return score_customers(df, _model, _feature_names, _threshold)
116148

117149

@@ -384,6 +416,14 @@ def get_scored(_model, _feature_names, _threshold, _data_hash):
384416
feature_names = artifact['feature_names']
385417
df = get_data()
386418

419+
if model is None:
420+
st.info(
421+
'SHAP explainability requires local model access. '
422+
'Run with local model artifacts (MODEL_SOURCE=local) to use this page.',
423+
icon='ℹ️',
424+
)
425+
st.stop()
426+
387427
X = df.drop(columns=['Churn_Numeric'])
388428

389429
with st.sidebar:

docker-compose.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
version: "3.8"
2+
3+
services:
4+
mlflow:
5+
image: ghcr.io/mlflow/mlflow:v2.13.2
6+
command: mlflow server --host 0.0.0.0 --port 5000 --backend-store-uri sqlite:////mlrunsdb/mlflow.db --default-artifact-root /mlartifacts
7+
ports:
8+
- "5000:5000"
9+
volumes:
10+
- mlflow_data:/mlartifacts
11+
- mlflow_db:/mlrunsdb
12+
13+
api:
14+
build:
15+
context: .
16+
dockerfile: Dockerfile
17+
command: uvicorn src.api:app --host 0.0.0.0 --port 8081
18+
ports:
19+
- "8081:8081"
20+
environment:
21+
- MODEL_SOURCE=mlflow
22+
- MODEL_NAME=rf_churn_model
23+
- MLFLOW_TRACKING_URI=http://mlflow:5000
24+
- ALLOW_LOCAL_MODEL_FALLBACK=1
25+
depends_on:
26+
- mlflow
27+
28+
streamlit:
29+
build:
30+
context: .
31+
dockerfile: Dockerfile
32+
command: streamlit run app.py --server.port 8501 --server.address 0.0.0.0
33+
ports:
34+
- "8501:8501"
35+
environment:
36+
- API_URL=http://api:8081
37+
- USE_API_INFERENCE=1
38+
depends_on:
39+
- api
40+
41+
volumes:
42+
mlflow_data:
43+
mlflow_db:

models/rf_churn_model.joblib

70 Bytes
Binary file not shown.

models/rf_churn_model_meta.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"PaymentMethod_Mailed check"
2828
],
2929
"threshold": 0.3632911392405064,
30-
"trained_at": "2026-03-07T15:09:07.719001",
30+
"trained_at": "2026-03-15T21:04:45.844238",
3131
"model_type": "RandomForestClassifier",
3232
"n_features": 25
3333
}

requirements.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,9 @@ shap
1010
streamlit
1111
scipy
1212
joblib
13+
mlflow
14+
fastapi
15+
uvicorn
16+
requests
17+
python-dotenv
18+
docker

0 commit comments

Comments
 (0)