customerAI / ml /explainability /shap_explainer.py
Vishu2006's picture
Upload 31 files
73cb5e7 verified
Raw History Blame Contribute Delete
4.5 kB
import numpy as np
import pandas as pd
import shap
FEATURE_LABEL_MAP = {
"recency_days": "Days Inactive",
"frequency_purchases": "Total Purchases",
"monetary_total_spend": "Lifetime Spend",
"avg_order_value": "Avg Order Value",
"website_visits_30d": "30-Day Website Visits",
"product_views_30d": "30-Day Product Views",
"cart_additions_30d": "Cart Additions",
"cart_abandonments_30d": "Abandoned Carts",
"email_opens_30d": "Email Opens",
"email_clicks_30d": "Email Clicks",
"support_tickets_30d": "Support Tickets",
"engagement_change_pct": "Engagement Trend %",
"return_rate": "Return Rate %",
"discount_dependency_ratio": "Discount Dependency",
"spend_30d": "30-Day Spend",
"spend_90d": "90-Day Spend",
"purchase_velocity": "Days Between Purchases",
"cart_abandon_ratio": "Cart Abandon Rate",
"email_click_ratio": "Email Click Rate",
"rfm_score": "RFM Score"
}
class ShapExplainabilityEngine:
def __init__(self, trained_model, feature_names):
self.trained_model = trained_model
self.feature_names = feature_names
try:
# TreeExplainer for XGBoost/RandomForest
self.explainer = shap.TreeExplainer(self.trained_model)
except Exception:
# Fallback
self.explainer = None
def explain_instance(self, customer_features: pd.DataFrame, top_n: int = 5) -> list[dict]:
"""
Calculates SHAP feature attributions for a single customer sample.
Returns top_n positive & negative behavioral drivers.
"""
if customer_features.ndim == 1:
customer_features = customer_features.to_frame().T
features_df = customer_features[self.feature_names]
if self.explainer is not None:
try:
shap_values = self.explainer.shap_values(features_df)
# Handle binary classification 2D or 3D output
if isinstance(shap_values, list):
shap_vals = shap_values[1][0]
elif shap_values.ndim == 2:
shap_vals = shap_values[0]
else:
shap_vals = shap_values[0]
except Exception:
shap_vals = self._fallback_feature_importance(features_df)
else:
shap_vals = self._fallback_feature_importance(features_df)
explanations = []
for feat_name, val, s_val in zip(self.feature_names, features_df.iloc[0], shap_vals):
label = FEATURE_LABEL_MAP.get(feat_name, feat_name)
# Format value for human display
if "pct" in feat_name or "rate" in feat_name or "ratio" in feat_name:
formatted_val = f"{val * 100:.1f}%" if "pct" not in feat_name else f"{val:.1f}%"
elif "spend" in feat_name or "value" in feat_name or "clv" in feat_name:
formatted_val = f"Rs.{val:,.0f}"
else:
formatted_val = f"{val:g}" if isinstance(val, (int, float)) else str(val)
impact_direction = "INCREASES_RISK" if s_val > 0 else "DECREASES_RISK"
explanations.append({
"feature": feat_name,
"label": label,
"value": formatted_val,
"raw_value": float(val),
"shap_value": float(round(s_val, 4)),
"impact": impact_direction,
"abs_shap": abs(float(s_val))
})
# Sort by absolute SHAP impact
explanations.sort(key=lambda x: x["abs_shap"], reverse=True)
return explanations[:top_n]
def _fallback_feature_importance(self, features_df: pd.DataFrame) -> np.ndarray:
# Fallback heuristic impact calculation if SHAP tree evaluation fails
row = features_df.iloc[0]
impacts = []
for feat in self.feature_names:
val = row[feat]
if feat == "recency_days":
impacts.append(0.005 * (val - 30))
elif feat == "cart_abandonments_30d":
impacts.append(0.08 * val)
elif feat == "support_tickets_30d":
impacts.append(0.12 * val)
elif feat == "engagement_change_pct":
impacts.append(-0.004 * val)
elif feat == "product_views_30d":
impacts.append(-0.003 * val)
else:
impacts.append(0.01 * (val / (abs(val) + 1.0)))
return np.array(impacts)