{"spec_id":"shap-waterfall","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nshap-waterfall: SHAP Waterfall Plot for Feature Attribution\nLibrary: matplotlib 3.10.9 | Python 3.13.13\nQuality: 90/100 | Created: 2026-05-07\n\"\"\"\n\nimport os\n\nimport matplotlib.patches as mpatches\nimport matplotlib.pyplot as plt\nimport numpy as np\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\n# Semantic direction colors — imprint diverging anchors\nCOLOR_POS = \"#AE3030\"  # red: positive SHAP (pushes prediction up)\nCOLOR_NEG = \"#4467A3\"  # blue: negative SHAP (pushes prediction down)\n\n# Data — credit loan approval model, individual applicant explanation\n# Features ordered by absolute SHAP magnitude, largest first (top of chart)\nfeatures_desc = [\n    \"Monthly Income\",\n    \"Debt-to-Income Ratio\",\n    \"Credit Score\",\n    \"Employment Duration\",\n    \"Prior Defaults\",\n    \"Loan Amount\",\n    \"Age\",\n    \"Open Credit Lines\",\n    \"Savings Balance\",\n    \"Recent Inquiries\",\n]\nshap_desc = [0.24, -0.18, 0.15, 0.12, -0.10, -0.08, 0.06, 0.04, 0.03, -0.02]\nbase_value = 0.42  # E[f(x)]: mean approval probability across training set\nfinal_value = base_value + sum(shap_desc)  # f(x) = 0.68\n\n# Reverse for matplotlib bottom-to-top axis (largest magnitude at top)\nfeatures = features_desc[::-1]\nshap_values = shap_desc[::-1]\nn = len(features)\ny_pos = np.arange(n)\n\n# Compute bar left edges and cumulative running totals (bottom-to-top)\nrunning = base_value\nbar_lefts = []\ncum_after = []\nfor sv in shap_values:\n    bar_lefts.append(running + sv if sv < 0 else running)\n    running += sv\n    cum_after.append(running)\n\nbar_widths = [abs(sv) for sv in shap_values]\nbar_colors = [COLOR_POS if sv >= 0 else COLOR_NEG for sv in shap_values]\n\n# Plot\nfig, ax = plt.subplots(figsize=(16, 9), facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\nax.set_axisbelow(True)\n\nax.barh(y_pos, bar_widths, left=bar_lefts, color=bar_colors, height=0.55, edgecolor=PAGE_BG, linewidth=1.5, zorder=3)\n\n# Vertical connector lines in the gaps between bars at cumulative position\nfor i in range(n - 1):\n    ax.plot(\n        [cum_after[i], cum_after[i]],\n        [y_pos[i] + 0.32, y_pos[i + 1] - 0.32],\n        color=INK_MUTED,\n        linewidth=1.2,\n        linestyle=\"dotted\",\n        alpha=0.7,\n        zorder=2,\n    )\n\n# Reference lines for base value and final prediction\nax.axvline(base_value, color=INK_MUTED, linewidth=1.5, linestyle=\"--\", alpha=0.6)\nax.axvline(final_value, color=INK_SOFT, linewidth=2.0, linestyle=\"--\", alpha=0.7)\n\n# SHAP value text labels on (wide bars) or beside (narrow bars) each segment\nLABEL_THRESH = 0.07\nfor i, (sv, left, width) in enumerate(zip(shap_values, bar_lefts, bar_widths, strict=False)):\n    txt = f\"+{sv:.2f}\" if sv > 0 else f\"{sv:.2f}\"\n    if width >= LABEL_THRESH:\n        ax.text(\n            left + width / 2,\n            y_pos[i],\n            txt,\n            ha=\"center\",\n            va=\"center\",\n            fontsize=14,\n            fontweight=\"bold\",\n            color=\"white\",\n            zorder=5,\n        )\n    else:\n        gap = 0.007\n        if sv > 0:\n            ax.text(\n                left + width + gap,\n                y_pos[i],\n                txt,\n                ha=\"left\",\n                va=\"center\",\n                fontsize=14,\n                fontweight=\"bold\",\n                color=COLOR_POS,\n                zorder=5,\n            )\n        else:\n            ax.text(\n                left - gap,\n                y_pos[i],\n                txt,\n                ha=\"right\",\n                va=\"center\",\n                fontsize=14,\n                fontweight=\"bold\",\n                color=COLOR_NEG,\n                zorder=5,\n            )\n\n# Annotations for base value and final prediction above the top bar\ntop_y = n + 0.08\nax.text(\n    base_value,\n    top_y,\n    f\"E[f(x)] = {base_value:.2f}\",\n    ha=\"center\",\n    va=\"bottom\",\n    fontsize=13,\n    color=INK_MUTED,\n    bbox={\"facecolor\": ELEVATED_BG, \"edgecolor\": INK_MUTED, \"boxstyle\": \"round,pad=0.35\", \"alpha\": 0.9},\n)\nax.text(\n    final_value,\n    top_y,\n    f\"f(x) = {final_value:.2f}\",\n    ha=\"center\",\n    va=\"bottom\",\n    fontsize=13,\n    fontweight=\"bold\",\n    color=INK,\n    bbox={\"facecolor\": ELEVATED_BG, \"edgecolor\": INK_SOFT, \"boxstyle\": \"round,pad=0.35\", \"alpha\": 0.9},\n)\n\n# Style\nax.set_yticks(y_pos)\nax.set_yticklabels(features, fontsize=16)\nax.set_xlabel(\"Loan Approval Probability\", fontsize=20, color=INK)\nax.set_title(\"Credit Approval · shap-waterfall · matplotlib · anyplot.ai\", fontsize=24, fontweight=\"medium\", color=INK)\n\nall_x = bar_lefts + [b + w for b, w in zip(bar_lefts, bar_widths, strict=False)] + [base_value, final_value]\nax.set_xlim(min(all_x) - 0.06, max(all_x) + 0.09)\nax.set_ylim(-0.6, n + 0.9)\n\nax.tick_params(axis=\"x\", labelsize=16, colors=INK_SOFT)\nax.tick_params(axis=\"y\", labelsize=16, colors=INK_SOFT, length=0)\n\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\nax.spines[\"left\"].set_visible(False)\nax.spines[\"bottom\"].set_color(INK_SOFT)\n\nax.xaxis.grid(True, alpha=0.10, linewidth=0.8, color=INK)\n\n# Legend\npos_patch = mpatches.Patch(color=COLOR_POS, label=\"Positive contribution\")\nneg_patch = mpatches.Patch(color=COLOR_NEG, label=\"Negative contribution\")\nleg = ax.legend(handles=[pos_patch, neg_patch], fontsize=16, loc=\"lower right\", frameon=True)\nleg.get_frame().set_facecolor(ELEVATED_BG)\nleg.get_frame().set_edgecolor(INK_SOFT)\nplt.setp(leg.get_texts(), color=INK_SOFT)\n\nplt.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=300, bbox_inches=\"tight\", facecolor=PAGE_BG)\n"}