SECTION 1: LEARNING OBJECTIVES
By the end of this lesson, you will be able to:
-
Prepare models for production deployment – serialisation, containerisation, and API development.
-
Build a model serving API using Flask or FastAPI for real-time predictions.
-
Implement a monitoring framework to track model performance, data drift, and concept drift.
-
Set up automated alerts for performance degradation and drift detection.
-
Create a monitoring dashboard for stakeholders to visualise model health.
-
Develop a model retraining strategy based on performance triggers.
-
Document deployment procedures for regulatory compliance and operational handover.
SECTION 2: DEPLOYMENT ARCHITECTURE
2.1 Production Architecture
┌─────────────────────────────────────────────────────────────────────────────┐ │ DEPLOYMENT ARCHITECTURE │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Loan │ │ API │ │ Model │ │ │ │ Application│ ──→ │ Gateway │ ──→ │ Serving │ ──→ Prediction │ │ │ System │ │ (Flask) │ │ (Container)│ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ │ │ v v │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Monitoring │ │ Logging │ │ Model │ │ │ │ Dashboard │ ←── │ (Prometheus│ ←── │ Registry │ │ │ │ (Grafana) │ │ + Loki) │ │ (MLflow) │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ │ │ v v │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Alerting │ │ Retraining │ │ CI/CD │ │ │ │ (PagerDuty)│ │ Pipeline │ │ Pipeline │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘
2.2 Key Components
| Component | Technology | Purpose |
|---|---|---|
| API Gateway | Flask/FastAPI | Accept loan applications, return predictions. |
| Model Serving | Docker container + model file | Host the model for inference. |
| Model Registry | MLflow | Version and track models. |
| Monitoring | Prometheus + Grafana | Track performance and drift metrics. |
| Logging | ELK Stack or Loki | Centralised logging. |
| Alerting | PagerDuty/OpsGenie | Notify team of issues. |
| CI/CD | GitHub Actions/Jenkins | Automated testing and deployment. |
| Retraining | Airflow/Dagster | Scheduled retraining pipeline. |
SECTION 3: IMPLEMENTATION IN PYTHON – DEPLOYMENT AND MONITORING
# =================================================================== # MODULE 9, LESSON 5: MODEL DEPLOYMENT AND MONITORING # =================================================================== import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns import joblib import json from datetime import datetime, timedelta import warnings warnings.filterwarnings('ignore') # Set style sns.set_style("whitegrid") np.random.seed(42) print("="*70) print("CAPSTONE PROJECT – MODEL DEPLOYMENT AND MONITORING") print("="*70) # ---------------------------------------------------------------- # PART A: MODEL SERIALISATION AND PREPARATION # ---------------------------------------------------------------- print("\n" + "-"*60) print("PART A: Model Serialisation and Preparation") print("-"*60) # Load models and scaler lr_model = joblib.load('logistic_regression_model.pkl') best_xgb = joblib.load('xgboost_model.pkl') scaler = joblib.load('scaler.pkl') # Feature names feature_names = ['applicant_age', 'income', 'credit_score', 'dti', 'loan_amount', 'loan_term', 'employment_years', 'home_owner', 'marital_status', 'education', 'loan_to_income', 'dti_credit_interaction', 'dti_squared', 'loan_amount_log'] print(f"Loaded {len(feature_names)} features.") print("Models and scaler loaded successfully.") # ---------------------------------------------------------------- # PART B: SIMULATED API SERVICE (FLASK-LIKE) # ---------------------------------------------------------------- print("\n" + "-"*60) print("PART B: Simulated API Service") print("-"*60) class ModelAPI: """Simulated model serving API.""" def __init__(self, model, scaler, feature_names, model_name='XGBoost', threshold=0.5): self.model = model self.scaler = scaler self.feature_names = feature_names self.model_name = model_name self.threshold = threshold self.request_log = [] self.prediction_log = [] self.start_time = datetime.now() self.request_count = 0 def predict(self, data): """ Make a prediction from input data. Expects a dictionary with feature values. """ self.request_count += 1 request_time = datetime.now() # Validate input missing = [f for f in self.feature_names if f not in data] if missing: return { 'error': f'Missing features: {missing}', 'status': 'error' } # Create feature vector X = np.array([[data.get(f, 0) for f in self.feature_names]]) # Standardise X_scaled = self.scaler.transform(X) # Predict prob = self.model.predict_proba(X_scaled)[0, 1] pred = 1 if prob >= self.threshold else 0 # Log self.prediction_log.append({ 'request_id': self.request_count, 'timestamp': request_time.isoformat(), 'input': data, 'probability': float(prob), 'prediction': int(pred), 'model': self.model_name }) return { 'request_id': self.request_count, 'probability': prob, 'prediction': pred, 'decision': 'Approved' if pred == 0 else 'Declined', 'model': self.model_name, 'timestamp': request_time.isoformat() } def get_stats(self): """Get API statistics.""" total_requests = len(self.prediction_log) if total_requests == 0: return {'total_requests': 0} approved = sum(1 for p in self.prediction_log if p['prediction'] == 0) declined = sum(1 for p in self.prediction_log if p['prediction'] == 1) return { 'total_requests': total_requests, 'approved': approved, 'declined': declined, 'approval_rate': approved / total_requests, 'uptime': (datetime.now() - self.start_time).total_seconds() / 3600 } # Create API instance api = ModelAPI(best_xgb, scaler, feature_names, model_name='XGBoost (Tuned)', threshold=0.5) # Test with a sample application sample_application = { 'applicant_age': 42, 'income': 65.4, 'credit_score': 710, 'dti': 28.5, 'loan_amount': 145, 'loan_term': 36, 'employment_years': 8, 'home_owner': 1, 'marital_status': 1, 'education': 2, 'loan_to_income': 145 / 65.4, 'dti_credit_interaction': 28.5 * 710 / 1000, 'dti_squared': 28.5 ** 2, 'loan_amount_log': np.log(145 + 1) } result = api.predict(sample_application) print("Sample Prediction:") print(json.dumps(result, indent=2)) # ---------------------------------------------------------------- # PART C: SIMULATED PRODUCTION TRAFFIC # ---------------------------------------------------------------- print("\n" + "-"*60) print("PART C: Simulating Production Traffic") print("-"*60) def generate_sample_application(): """Generate a random loan application.""" age = np.random.normal(42, 14).clip(18, 80) income = np.random.gamma(5, 15) + 20 credit_score = np.random.normal(700, 50).clip(550, 850) dti = np.random.beta(2, 5) * 60 loan_amount = np.random.gamma(4, 50) + 30 loan_term = np.random.choice([12, 24, 36, 48, 60, 72]) employment_years = np.random.gamma(3, 5).clip(0, 40) home_owner = np.random.binomial(1, 0.65) marital_status = np.random.choice([0, 1, 2], p=[0.35, 0.45, 0.20]) education = np.random.choice([0, 1, 2, 3], p=[0.15, 0.25, 0.35, 0.25]) return { 'applicant_age': int(age), 'income': float(income), 'credit_score': int(credit_score), 'dti': float(dti), 'loan_amount': float(loan_amount), 'loan_term': int(loan_term), 'employment_years': int(employment_years), 'home_owner': int(home_owner), 'marital_status': int(marital_status), 'education': int(education), 'loan_to_income': loan_amount / income if income > 0 else 0, 'dti_credit_interaction': dti * credit_score / 1000, 'dti_squared': dti ** 2, 'loan_amount_log': np.log(loan_amount + 1) } # Simulate 1000 requests print("Simulating 1000 API requests...") for i in range(1000): app = generate_sample_application() api.predict(app) stats = api.get_stats() print(f"\nAPI Statistics:") print(f" Total Requests: {stats['total_requests']}") print(f" Approved: {stats['approved']} ({stats['approval_rate']*100:.1f}%)") print(f" Declined: {stats['declined']} ({(1-stats['approval_rate'])*100:.1f}%)") print(f" Uptime: {stats['uptime']:.1f} hours") # ---------------------------------------------------------------- # PART D: MODEL MONITORING – PERFORMANCE OVER TIME # ---------------------------------------------------------------- print("\n" + "-"*60) print("PART D: Model Monitoring – Performance Over Time") print("-"*60) def generate_monitoring_data(n_days=30): """Generate simulated monitoring data over time.""" dates = [datetime.now() - timedelta(days=i) for i in range(n_days)] dates = sorted(dates) data = [] for i, date in enumerate(dates): # Simulate performance drift drift = np.sin(i / 10) * 0.03 auc = 0.85 + drift + np.random.normal(0, 0.01) ks = 0.40 + drift * 0.5 + np.random.normal(0, 0.01) psi = np.random.gamma(0.1, 0.05) + max(0, drift * 2) requests = np.random.poisson(500 + i * 2) latency = np.random.normal(50 + drift * 10, 10).clip(20, 150) data.append({ 'date': date, 'auc': auc.clip(0, 1), 'ks': ks.clip(0, 1), 'psi': psi, 'requests': requests, 'latency': latency, 'approval_rate': np.random.normal(0.75, 0.02).clip(0.6, 0.9) }) return pd.DataFrame(data) monitoring_df = generate_monitoring_data(30) print("Monitoring data generated.") # ---------------------------------------------------------------- # PART E: MONITORING DASHBOARD VISUALISATION # ---------------------------------------------------------------- print("\n" + "-"*60) print("PART E: Monitoring Dashboard Visualisation") print("-"*60) fig, axes = plt.subplots(2, 3, figsize=(15, 10)) # 1. AUC over time ax = axes[0, 0] ax.plot(monitoring_df['date'], monitoring_df['auc'], 'b-', linewidth=2) ax.axhline(y=0.80, color='green', linestyle='--', label='Acceptable (0.80)') ax.axhline(y=0.75, color='orange', linestyle='--', label='Warning (0.75)') ax.axhline(y=0.70, color='red', linestyle='--', label='Critical (0.70)') ax.set_xlabel('Date') ax.set_ylabel('AUC') ax.set_title('Model Performance (AUC)') ax.legend(fontsize=8) ax.grid(True, alpha=0.3) # 2. KS over time ax = axes[0, 1] ax.plot(monitoring_df['date'], monitoring_df['ks'], 'g-', linewidth=2) ax.axhline(y=0.35, color='green', linestyle='--', label='Acceptable (0.35)') ax.axhline(y=0.30, color='orange', linestyle='--', label='Warning (0.30)') ax.axhline(y=0.25, color='red', linestyle='--', label='Critical (0.25)') ax.set_xlabel('Date') ax.set_ylabel('KS') ax.set_title('Model Performance (KS)') ax.legend(fontsize=8) ax.grid(True, alpha=0.3) # 3. PSI (Data Drift) ax = axes[0, 2] ax.plot(monitoring_df['date'], monitoring_df['psi'], 'r-', linewidth=2) ax.axhline(y=0.10, color='green', linestyle='--', label='Stable (<0.10)') ax.axhline(y=0.25, color='orange', linestyle='--', label='Warning (0.10-0.25)') ax.axhline(y=0.25, color='red', linestyle='--', label='Critical (>0.25)') ax.set_xlabel('Date') ax.set_ylabel('PSI') ax.set_title('Data Drift (PSI)') ax.legend(fontsize=8) ax.grid(True, alpha=0.3) # 4. Request volume ax = axes[1, 0] ax.bar(monitoring_df['date'], monitoring_df['requests'], color='blue', alpha=0.7) ax.set_xlabel('Date') ax.set_ylabel('Requests') ax.set_title('API Request Volume') ax.grid(True, alpha=0.3) plt.xticks(rotation=45) # 5. Latency ax = axes[1, 1] ax.plot(monitoring_df['date'], monitoring_df['latency'], 'purple', linewidth=2) ax.axhline(y=100, color='red', linestyle='--', label='SLA (100ms)') ax.set_xlabel('Date') ax.set_ylabel('Latency (ms)') ax.set_title('API Latency') ax.legend(fontsize=8) ax.grid(True, alpha=0.3) # 6. Approval Rate ax = axes[1, 2] ax.plot(monitoring_df['date'], monitoring_df['approval_rate'], 'orange', linewidth=2) ax.set_xlabel('Date') ax.set_ylabel('Approval Rate') ax.set_title('Loan Approval Rate') ax.axhline(y=0.70, color='red', linestyle='--', label='Target (70%)') ax.legend(fontsize=8) ax.grid(True, alpha=0.3) plt.tight_layout() plt.savefig('monitoring_dashboard.png', dpi=300, bbox_inches='tight') plt.show() print("Monitoring dashboard saved as 'monitoring_dashboard.png'") # ---------------------------------------------------------------- # PART F: DRIFT DETECTION AND ALERTING # ---------------------------------------------------------------- print("\n" + "-"*60) print("PART F: Drift Detection and Alerting") print("-"*60) def detect_drift(monitoring_df, thresholds): """ Detect drift and generate alerts based on thresholds. """ alerts = [] latest = monitoring_df.iloc[-1] # Check performance metrics if latest['auc'] < thresholds.get('auc_warning', 0.75): alerts.append({ 'severity': 'WARNING' if latest['auc'] >= 0.70 else 'CRITICAL', 'metric': 'AUC', 'value': latest['auc'], 'threshold': thresholds.get('auc_warning', 0.75), 'message': f"AUC dropped to {latest['auc']:.3f}" }) if latest['ks'] < thresholds.get('ks_warning', 0.30): alerts.append({ 'severity': 'WARNING' if latest['ks'] >= 0.25 else 'CRITICAL', 'metric': 'KS', 'value': latest['ks'], 'threshold': thresholds.get('ks_warning', 0.30), 'message': f"KS dropped to {latest['ks']:.3f}" }) if latest['psi'] > thresholds.get('psi_warning', 0.10): alerts.append({ 'severity': 'WARNING' if latest['psi'] < 0.25 else 'CRITICAL', 'metric': 'PSI', 'value': latest['psi'], 'threshold': thresholds.get('psi_warning', 0.10), 'message': f"PSI increased to {latest['psi']:.3f}" }) if latest['latency'] > thresholds.get('latency_warning', 100): alerts.append({ 'severity': 'WARNING', 'metric': 'Latency', 'value': latest['latency'], 'threshold': thresholds.get('latency_warning', 100), 'message': f"Latency exceeded {thresholds.get('latency_warning', 100)}ms" }) return alerts # Define thresholds thresholds = { 'auc_warning': 0.75, 'auc_critical': 0.70, 'ks_warning': 0.30, 'ks_critical': 0.25, 'psi_warning': 0.10, 'psi_critical': 0.25, 'latency_warning': 100, 'latency_critical': 150 } alerts = detect_drift(monitoring_df, thresholds) print("Current Alerts:") if alerts: for alert in alerts: print(f" [{alert['severity']}] {alert['metric']}: {alert['message']}") else: print(" ✅ All systems nominal") # ---------------------------------------------------------------- # PART G: AUTOMATED RETRAINING STRATEGY # ---------------------------------------------------------------- print("\n" + "-"*60) print("PART G: Automated Retraining Strategy") print("-"*60) def retraining_check(monitoring_df, thresholds): """ Determine if retraining is needed based on monitoring data. """ latest = monitoring_df.iloc[-1] retrain = False reasons = [] # Check performance degradation if latest['auc'] < thresholds.get('auc_critical', 0.70): retrain = True reasons.append("AUC below critical threshold") if latest['ks'] < thresholds.get('ks_critical', 0.25): retrain = True reasons.append("KS below critical threshold") if latest['psi'] > thresholds.get('psi_critical', 0.25): retrain = True reasons.append("PSI above critical threshold (data drift)") # Check trend: if performance has been declining for 7 days last_7 = monitoring_df.tail(7) auc_trend = np.polyfit(range(len(last_7)), last_7['auc'], 1)[0] if auc_trend < -0.01: # Decreasing more than 1% per day retrain = True reasons.append("Performance decline trend detected") return retrain, reasons retrain_needed, reasons = retraining_check(monitoring_df, thresholds) print("Retraining Analysis:") print(f" Retraining Needed: {'YES' if retrain_needed else 'NO'}") if retrain_needed: print(" Reasons:") for reason in reasons: print(f" • {reason}") else: print(" ✅ Model performance stable; retraining not required.") # ---------------------------------------------------------------- # PART H: MODEL CARD FOR DEPLOYMENT # ---------------------------------------------------------------- print("\n" + "-"*60) print("PART H: Model Card for Deployment") print("-"*60) model_card = """ --- MODEL CARD: XGBoost Default Prediction Model --- 1. MODEL DETAILS - Model Name: XGBoost Default Predictor v1.0 - Model Type: XGBoost Classifier - Development Date: 2024-01-15 - Deployment Date: 2024-06-01 - Owner: Atlantic Bank Data Science Team - Contact: datascience@atlanticbank.com 2. INTENDED USE - Primary Use: Credit risk assessment for personal loans. - Secondary Use: Portfolio risk monitoring and stress testing. - Out-of-Scope: Automated lending without human review. 3. PERFORMANCE (At Deployment) - AUC: 0.85 - KS Statistic: 0.42 - Gini Coefficient: 0.70 - Calibration: HL p-value = 0.18 - Accuracy: 0.81 4. FEATURES - Number of Features: 14 (original + engineered) - Key Features: credit_score, dti, loan_to_income, dti_credit_interaction - Feature Engineering: loan_to_income, dti_credit_interaction, dti_squared, loan_amount_log 5. DATA - Training Data: 2019-2023 loan applications (70,000 rows) - Validation Data: 2023-2024 loan applications (30,000 rows) - Default Rate: 4.2% 6. MONITORING - Performance: AUC, KS, Calibration (daily) - Data Drift: PSI (weekly) - Alerts: AUC < 0.75, KS < 0.30, PSI > 0.25 - Retraining: Triggered by alerts or monthly schedule 7. GOVERNANCE - Model Validator: Model Validation Team - Approval Date: 2024-05-15 - Review Frequency: Annual (or trigger-based) - Compliance: SR 11-7, Fair Lending 8. DEPLOYMENT - Environment: Production (AWS) - Serving: Containerised (Docker) + API (Flask) - Monitoring: Prometheus + Grafana - CI/CD: GitHub Actions """ print(model_card) # ---------------------------------------------------------------- # PART I: DEPLOYMENT CHECKLIST # ---------------------------------------------------------------- print("\n" + "-"*60) print("PART I: Deployment Checklist") print("-"*60) deployment_checklist = [ "✅ Model validated and approved by independent validator", "✅ Model performance meets thresholds (AUC > 0.80, KS > 0.35)", "✅ Model documentation (Model Card) completed", "✅ API developed and tested", "✅ Performance monitoring dashboard set up", "✅ Drift detection and alerting configured", "✅ Retraining strategy documented", "✅ Rollback plan in place", "✅ User training materials prepared", "✅ Regulatory compliance review completed", "✅ Security review completed (access controls, encryption)", "✅ Load testing completed (1000+ requests/second)", "✅ Business continuity plan in place", "✅ Go-live approval obtained from steering committee" ] print("Deployment Checklist:") for item in deployment_checklist: print(item) # ---------------------------------------------------------------- # PART J: SUMMARY # ---------------------------------------------------------------- print("\n" + "="*70) print("PART J: Summary") print("="*70) print(""" Model Deployment and Monitoring – Key Takeaways: 1. Model Serialisation: - Models are saved using joblib/pickle for production. - Feature engineering logic must be packaged with the model. 2. API Service: - Flask/FastAPI provides a RESTful interface for predictions. - Input validation and standardisation are critical. 3. Monitoring: - Track performance metrics (AUC, KS) over time. - Monitor data drift using PSI (Population Stability Index). - Monitor concept drift via performance decline. 4. Alerting: - Alerts for performance drops, drift, and latency issues. - Severity levels: WARNING and CRITICAL. 5. Retraining: - Triggered by alerts (critical thresholds) or schedule. - Automated retraining pipeline (Airflow/Dagster). 6. Documentation: - Model Card summarises model details, performance, and governance. - Deployment checklist ensures all requirements are met. 7. Regulatory Compliance: - SR 11-7 requires validation and monitoring. - Fair Lending requires ongoing bias testing. """) print("="*70) print("END OF LESSON 5 – MODULE 9") print("="*70)
SECTION 4: SUMMARY FOR THE DATA PRACTITIONER
-
Model deployment requires serialisation, containerisation, and API development.
-
Monitoring tracks performance metrics, data drift (PSI), and concept drift.
-
Alerting ensures timely response to performance degradation.
-
Retraining is triggered by performance alerts or scheduled intervals.
-
Documentation (Model Card) is essential for regulatory compliance.
-
Deployment checklist ensures all requirements are met before go-live.
SECTION 5: RECOMMENDED NEXT STEPS
-
Review the deployment checklist and ensure all items are addressed.
-
Prepare for Lesson 6: Change Management and Business Adoption.
-
Consider additional monitoring metrics (e.g., business impact, customer satisfaction).
-
Explore containerisation (Docker) and orchestration (Kubernetes) for production deployment.
[END OF LESSON 5 – MODULE 9]