Spaces:
Sleeping
Sleeping
File size: 6,044 Bytes
38c1a14 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | import os
import joblib
import json
import numpy as np
import xgboost as xgb
import pandas as pd
from typing import Dict, Any, List
from src.api.config import MODEL_PATH, CHALLENGER_PATH, CUSTOMERS_JSON_PATH, CONFIG, logger
from src.api.models import CustomerInput
# Global variables for models and in-memory customer data
champion_model = None
challenger_model = None
memory_customers = {}
def load_models_and_data() -> None:
"""Loads all models and loads customer record assets into memory."""
global champion_model, challenger_model, memory_customers
# Load Champion model
logger.info(f"Attempting to load champion model from {MODEL_PATH}...")
if os.path.exists(MODEL_PATH):
try:
champion_model = joblib.load(MODEL_PATH)
logger.info("Successfully loaded champion XGBoost model.")
except Exception as e:
logger.error(f"Error loading champion model: {str(e)}")
else:
logger.warning(f"Champion model file missing at {MODEL_PATH}.")
# Load Challenger model
logger.info(f"Attempting to load challenger model from {CHALLENGER_PATH}...")
if os.path.exists(CHALLENGER_PATH):
try:
challenger_model = joblib.load(CHALLENGER_PATH)
logger.info("Successfully loaded challenger Random Forest model.")
except Exception as e:
logger.error(f"Error loading challenger model: {str(e)}")
else:
logger.warning(f"Challenger model file missing at {CHALLENGER_PATH}.")
# Load customer records into memory for live WebSocket streaming
if os.path.exists(CUSTOMERS_JSON_PATH):
try:
with open(CUSTOMERS_JSON_PATH, "r") as f:
data = json.load(f)
for cust in data:
memory_customers[str(cust["id"])] = cust
logger.info(f"Successfully loaded {len(memory_customers)} customer records into memory for WebSocket streaming.")
except Exception as e:
logger.error(f"Failed to load customers.json into memory: {str(e)}")
else:
logger.warning(f"customers.json missing at {CUSTOMERS_JSON_PATH}. WebSocket streaming fallback mock data will be used.")
def get_realtime_recommendation(recency: int, frequency: int, monetary: float, churn_prob: float) -> str:
"""Generates actionable retention strategy based on client segment classification."""
is_high_value = (frequency >= 3) or (monetary >= 300.0)
if churn_prob >= 0.70:
if is_high_value:
return "At-Risk VIP: High historic value. Route to customer relations manager for direct feedback outreach. Offer priority recovery benefits."
else:
return "Hibernating Win-back: Inactive standard customer. Target with automated email re-engagement flow offering aggressive discount vouchers."
elif churn_prob >= 0.30:
if is_high_value:
return "Proactive VIP Retention: High-value showing drop-off signs. Send customized recommendations based on past purchases. Avoid direct discount spam."
else:
return "Standard Retention: Nurture with standard newsletter promotions and seasonal discounts."
else:
if is_high_value:
return "Maintain & Upsell: Core loyal customer. Exclude from margin-diluting discount codes. Send early access and premium alerts."
else:
return "Nurture Campaign: Keep engaged with standard marketing updates."
def preprocess_features(customer: CustomerInput) -> np.ndarray:
"""Preprocesses input parameters and fills in feature engineered attributes."""
single_buyer_impute = CONFIG["parameters"]["single_order_imputation_days"]
default_uk = CONFIG["parameters"]["default_is_uk"]
avg_days = customer.avg_days_between if customer.avg_days_between is not None else (single_buyer_impute if customer.frequency == 1 else 30.0)
recent_ratio = customer.recent_orders_ratio if customer.recent_orders_ratio is not None else (1.0 if customer.recency <= 60 else 0.0)
recency_ratio = customer.recency / (avg_days + 1e-5)
is_uk = customer.is_uk if customer.is_uk is not None else default_uk
return np.array([[
customer.recency,
customer.frequency,
customer.monetary,
customer.basket_size,
avg_days,
recency_ratio,
recent_ratio,
is_uk
]])
def run_champion_inference(features: np.ndarray) -> float:
"""Computes prediction probability using the Champion XGBoost model."""
if champion_model is None:
raise ValueError("Champion model is not loaded.")
return float(champion_model.predict_proba(features)[:, 1][0])
def run_challenger_inference(features: np.ndarray) -> float:
"""Computes prediction probability using the Challenger Random Forest model."""
if challenger_model is None:
return 0.0
df_features = pd.DataFrame(features, columns=[
'Recency', 'Frequency', 'Monetary', 'AvgBucketSize',
'AvgDaysBetween', 'Recency_to_AvgDaysRatio', 'Recent_Orders_Ratio', 'Is_UK'
])
return float(challenger_model.predict_proba(df_features)[:, 1][0])
def compute_shap_values(features: np.ndarray) -> Dict[str, float]:
"""Calculates TreeSHAP feature attribution scores for explainability plots."""
if champion_model is None:
return {}
booster = champion_model.get_booster()
dmat = xgb.DMatrix(features, feature_names=[
'Recency', 'Frequency', 'Monetary', 'AvgBucketSize',
'AvgDaysBetween', 'Recency_to_AvgDaysRatio', 'Recent_Orders_Ratio', 'Is_UK'
])
contribs = booster.predict(dmat, pred_contribs=True)[0]
return {
"Recency": float(contribs[0]),
"Frequency": float(contribs[1]),
"Monetary": float(contribs[2]),
"AvgBucketSize": float(contribs[3]),
"AvgDaysBetween": float(contribs[4]),
"Recency_to_AvgDaysRatio": float(contribs[5]),
"Recent_Orders_Ratio": float(contribs[6]),
"Is_UK": float(contribs[7])
}
|