import shap import numpy as np import pandas as pd import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt _cache = {} def get_explainer(model): key = id(model) if key not in _cache: try: _cache[key] = shap.TreeExplainer(model) except Exception: _cache[key] = None return _cache[key] def explain_prediction(model, features: pd.DataFrame): try: explainer = get_explainer(model) if explainer is None: return None, None shap_vals = explainer.shap_values(features) vals = shap_vals[1][0] if isinstance(shap_vals, list) else shap_vals[0] df = pd.DataFrame({ "Feature": features.columns.tolist(), "SHAP Value": vals, "Value": features.values[0], }).sort_values("SHAP Value", key=abs, ascending=False) return df, plot_shap(df) except Exception: return None, None def plot_shap(shap_df: pd.DataFrame): fig, ax = plt.subplots(figsize=(9, 3.5)) fig.patch.set_facecolor("#0d1520") ax.set_facecolor("#0d1520") colors = ["#f43f5e" if v > 0 else "#10b981" for v in shap_df["SHAP Value"]] labels = [ f"{r.Feature} = {r.Value:.1f}" if isinstance(r.Value, float) else f"{r.Feature} = {r.Value}" for r in shap_df.itertuples() ] ax.barh(labels, shap_df["SHAP Value"], color=colors, edgecolor="none", height=0.55) ax.axvline(0, color="rgba(255,255,255,0.15)", linewidth=0.8) ax.set_xlabel("SHAP Value — Impact on Churn Probability", color="#94a3b8", fontsize=9) ax.tick_params(colors="#94a3b8", labelsize=8) for sp in ax.spines.values(): sp.set_visible(False) ax.invert_yaxis() plt.tight_layout() return fig