churnflow-api / src /api /ml_services.py
aleem1234's picture
Fix requirements.txt dependencies
38c1a14
Raw
History Blame Contribute Delete
6.04 kB
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])
}