{"spec_id":"shap-waterfall","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nshap-waterfall: SHAP Waterfall Plot for Feature Attribution\nLibrary: seaborn 0.13.2 | Python 3.13.13\nQuality: 84/100 | Created: 2026-05-07\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent self-import: remove this script's directory from sys.path\n_script_dir = os.path.dirname(os.path.abspath(__file__))\nif _script_dir in sys.path:\n    sys.path.remove(_script_dir)\n\nimport matplotlib.patches as mpatches\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\nCOLOR_POS = \"#AE3030\"  # imprint red — positive SHAP (raises prediction)\nCOLOR_NEG = \"#4467A3\"  # imprint blue — negative SHAP (lowers prediction)\n\nsns.set_theme(\n    style=\"ticks\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"grid.color\": INK,\n        \"grid.alpha\": 0.10,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\n\n# Data — credit scoring model predicting loan default probability (single applicant)\nbase_value = 0.32\n\nfeature_names = [\n    \"Late Payment Count\",\n    \"Credit Score\",\n    \"Debt-to-Income Ratio\",\n    \"Loan Amount\",\n    \"Monthly Income\",\n    \"Employment Years\",\n    \"Public Records\",\n    \"Savings Balance\",\n    \"Account Age\",\n    \"Credit Lines\",\n]\n# SHAP values sorted by absolute magnitude descending (index 0 = largest impact)\nshap_values = np.array([0.24, -0.18, 0.16, 0.11, -0.10, -0.07, 0.06, -0.05, -0.04, 0.03])\nfinal_value = base_value + shap_values.sum()\n\nn = len(feature_names)\n\n# Display: largest |SHAP| at top (y = n-1), smallest at bottom (y = 0)\n# Cumulative flow builds bottom-to-top — smallest feature processed first from base_value\nshap_btt = shap_values[::-1]\nrunning = np.concatenate([[base_value], base_value + np.cumsum(shap_btt)])\nstarts_btt = running[:-1]\nends_btt = running[1:]\nfeatures_btt = feature_names[::-1]\n\nbar_lefts = np.where(shap_btt >= 0, starts_btt, ends_btt)\nbar_widths = np.abs(shap_btt)\nbar_colors = [COLOR_POS if v >= 0 else COLOR_NEG for v in shap_btt]\ny_pos = np.arange(n)\n\n# Plot\nfig, ax = plt.subplots(figsize=(16, 9), facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Waterfall bars — dominant feature (top bar, y=n-1) gets thicker edge for focal emphasis\nfor i, (yi, left, width, color) in enumerate(zip(y_pos, bar_lefts, bar_widths, bar_colors, strict=False)):\n    is_dominant = i == n - 1\n    ax.barh(\n        yi,\n        width,\n        left=left,\n        color=color,\n        height=0.55,\n        zorder=3,\n        edgecolor=INK_SOFT if is_dominant else PAGE_BG,\n        linewidth=1.5 if is_dominant else 0.5,\n    )\n\n# Vertical dotted connector lines at cumulative junctions between adjacent bars\nfor j in range(n - 1):\n    cx = ends_btt[j]\n    ax.plot([cx, cx], [j + 0.30, j + 0.70], color=INK_SOFT, linewidth=1.0, linestyle=\":\", zorder=2, alpha=0.65)\n\n# Connector dots using seaborn scatterplot — mark the running cumulative total at each junction\nconnector_df = pd.DataFrame({\"x\": ends_btt[:-1], \"y\": np.arange(n - 1) + 0.5})\nsns.scatterplot(data=connector_df, x=\"x\", y=\"y\", ax=ax, color=INK_SOFT, s=50, zorder=5, alpha=0.75)\n\n# SHAP value labels beside bars\nfor yi, left, width, shap in zip(y_pos, bar_lefts, bar_widths, shap_btt, strict=False):\n    label = f\"+{shap:.2f}\" if shap >= 0 else f\"{shap:.2f}\"\n    if shap >= 0:\n        ax.text(\n            left + width,\n            yi,\n            f\"  {label}\",\n            ha=\"left\",\n            va=\"center\",\n            color=COLOR_POS,\n            fontsize=14,\n            fontweight=\"bold\",\n            zorder=4,\n        )\n    else:\n        ax.text(\n            left, yi, f\"{label}  \", ha=\"right\", va=\"center\", color=COLOR_NEG, fontsize=14, fontweight=\"bold\", zorder=4\n        )\n\n# Reference lines\nax.axvline(base_value, color=INK_SOFT, linewidth=1.5, linestyle=\"--\", zorder=1, alpha=0.85)\nax.axvline(final_value, color=INK, linewidth=2.0, linestyle=\"-\", zorder=1, alpha=0.90)\n\n# Annotations for reference lines — 15pt for adequate secondary text legibility\nax.text(base_value, -0.75, f\"E[f(x)] = {base_value:.2f}\", ha=\"center\", va=\"center\", fontsize=15, color=INK_SOFT)\nax.text(\n    final_value,\n    n - 0.5,\n    f\"f(x) = {final_value:.2f}\",\n    ha=\"center\",\n    va=\"center\",\n    fontsize=15,\n    color=INK,\n    fontweight=\"bold\",\n)\n\n# Axes — dominant feature label is bold to create visual focal point\nax.set_yticks(y_pos)\nax.set_yticklabels(features_btt, fontsize=16)\nfor label_obj in ax.get_yticklabels():\n    if label_obj.get_text() == \"Late Payment Count\":\n        label_obj.set_fontweight(\"bold\")\n\nax.tick_params(axis=\"y\", length=0)\nax.tick_params(axis=\"x\", labelsize=16, colors=INK_SOFT)\nax.set_xlabel(\"Model Output  (Default Probability)\", fontsize=20, color=INK)\n\n# Subtle x-axis grid\nax.xaxis.grid(True, alpha=0.10, linewidth=0.8, color=INK, zorder=0)\nax.set_axisbelow(True)\n\n# Spine removal using seaborn's despine — idiomatic seaborn API\nsns.despine(ax=ax, left=True, top=True, right=True)\nax.spines[\"bottom\"].set_color(INK_SOFT)\n\n# Legend\npos_patch = mpatches.Patch(color=COLOR_POS, label=\"Positive SHAP  (↑ prediction)\")\nneg_patch = mpatches.Patch(color=COLOR_NEG, label=\"Negative SHAP  (↓ prediction)\")\nax.legend(\n    handles=[pos_patch, neg_patch],\n    loc=\"lower right\",\n    fontsize=16,\n    framealpha=0.9,\n    facecolor=ELEVATED_BG,\n    edgecolor=INK_SOFT,\n)\n\n# Axis limits\nx_all = np.concatenate([starts_btt, ends_btt])\nax.set_xlim(x_all.min() - 0.06, x_all.max() + 0.16)\nax.set_ylim(-1.0, n)\n\nax.set_title(\n    \"Credit Default Prediction · shap-waterfall · seaborn · anyplot.ai\",\n    fontsize=24,\n    fontweight=\"medium\",\n    color=INK,\n    pad=16,\n)\n\nplt.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=300, bbox_inches=\"tight\", facecolor=PAGE_BG)\n"}