{"spec_id":"shap-waterfall","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nshap-waterfall: SHAP Waterfall Plot for Feature Attribution\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 85/100 | Created: 2026-05-07\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\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\"\n\nPOS_COLOR = \"#AE3030\"  # imprint red — positive SHAP (pushes prediction up)\nNEG_COLOR = \"#4467A3\"  # Okabe-Ito blue — negative SHAP (pushes prediction down)\n\n# Data — loan application: individual credit default prediction\nnp.random.seed(42)\nbase_value = 0.32\n\nfeature_names = [\n    \"Debt-to-Income Ratio\",\n    \"Credit Score\",\n    \"Annual Income\",\n    \"Late Payments (Count)\",\n    \"Home Ownership\",\n    \"Employment Length\",\n    \"Savings Balance\",\n    \"Loan Amount\",\n    \"Credit Utilization\",\n    \"Credit Age (Years)\",\n    \"Open Accounts\",\n    \"Recent Inquiries\",\n]\nshap_values_raw = [0.21, 0.17, -0.14, 0.13, -0.09, 0.08, -0.06, 0.05, -0.04, -0.03, 0.02, -0.01]\n\n# Sort by absolute SHAP magnitude (largest at top)\norder = np.argsort(np.abs(shap_values_raw))[::-1]\nfeatures_sorted = [feature_names[i] for i in order]\nshap_sorted = [shap_values_raw[i] for i in order]\n\n# Compute cumulative bar start/end positions (waterfall stacking from base_value)\nx_starts, x_ends = [], []\ncumulative = base_value\nfor v in shap_sorted:\n    x_starts.append(cumulative)\n    x_ends.append(cumulative + v)\n    cumulative += v\n\nfinal_value = round(base_value + sum(shap_values_raw), 3)\n\ndf = pd.DataFrame(\n    {\n        \"feature\": features_sorted,\n        \"shap_value\": shap_sorted,\n        \"x_start\": x_starts,\n        \"x_end\": x_ends,\n        \"sign\": [\"positive\" if v > 0 else \"negative\" for v in shap_sorted],\n        \"shap_label\": [f\"+{v:.2f}\" if v > 0 else f\"{v:.2f}\" for v in shap_sorted],\n    }\n)\n\ndf_pos = df[df[\"shap_value\"] > 0].copy()\ndf_neg = df[df[\"shap_value\"] <= 0].copy()\n\n# X-axis domain with padding for text labels\nx_pad = 0.08\nx_min = min(df[\"x_start\"].min(), df[\"x_end\"].min()) - x_pad\nx_max = max(df[\"x_start\"].max(), df[\"x_end\"].max()) + x_pad\nx_scale = alt.Scale(domain=[x_min, x_max])\n\n# Waterfall bars\nbars = (\n    alt.Chart(df)\n    .mark_bar(opacity=0.88)\n    .encode(\n        x=alt.X(\"x_start:Q\", scale=x_scale, axis=alt.Axis(title=\"Probability of Default\", format=\".2f\")),\n        x2=\"x_end:Q\",\n        y=alt.Y(\"feature:N\", sort=features_sorted, axis=alt.Axis(title=None, labelLimit=260)),\n        color=alt.Color(\n            \"sign:N\",\n            scale=alt.Scale(domain=[\"positive\", \"negative\"], range=[POS_COLOR, NEG_COLOR]),\n            legend=alt.Legend(title=\"SHAP Direction\", orient=\"bottom-right\"),\n        ),\n        tooltip=[\n            alt.Tooltip(\"feature:N\", title=\"Feature\"),\n            alt.Tooltip(\"shap_value:Q\", title=\"SHAP Value\", format=\"+.4f\"),\n            alt.Tooltip(\"x_start:Q\", title=\"From\", format=\".3f\"),\n            alt.Tooltip(\"x_end:Q\", title=\"To\", format=\".3f\"),\n        ],\n    )\n)\n\n# SHAP value text labels — positive bars (to the right of bar end)\ntext_pos = (\n    alt.Chart(df_pos)\n    .mark_text(align=\"left\", dx=8, fontSize=16, fontWeight=\"bold\")\n    .encode(\n        x=alt.X(\"x_end:Q\", scale=x_scale),\n        y=alt.Y(\"feature:N\", sort=features_sorted),\n        text=\"shap_label:N\",\n        color=alt.value(POS_COLOR),\n    )\n)\n\n# SHAP value text labels — negative bars (to the left of bar end)\ntext_neg = (\n    alt.Chart(df_neg)\n    .mark_text(align=\"right\", dx=-8, fontSize=16, fontWeight=\"bold\")\n    .encode(\n        x=alt.X(\"x_end:Q\", scale=x_scale),\n        y=alt.Y(\"feature:N\", sort=features_sorted),\n        text=\"shap_label:N\",\n        color=alt.value(NEG_COLOR),\n    )\n)\n\n# Base value reference line (dashed)\nbase_rule = (\n    alt.Chart(pd.DataFrame({\"x\": [base_value]}))\n    .mark_rule(strokeDash=[6, 4], strokeWidth=2)\n    .encode(x=alt.X(\"x:Q\", scale=x_scale), color=alt.value(INK_SOFT))\n)\n\n# Annotation for base value positioned above the top feature\nbase_ann = (\n    alt.Chart(pd.DataFrame({\"x\": [base_value], \"feature\": [features_sorted[0]], \"label\": [f\"Base = {base_value:.2f}\"]}))\n    .mark_text(align=\"center\", dy=-20, fontSize=15, fontStyle=\"italic\")\n    .encode(\n        x=alt.X(\"x:Q\", scale=x_scale),\n        y=alt.Y(\"feature:N\", sort=features_sorted),\n        text=\"label:N\",\n        color=alt.value(INK_SOFT),\n    )\n)\n\n# Final prediction reference line (solid)\nfinal_rule = (\n    alt.Chart(pd.DataFrame({\"x\": [final_value]}))\n    .mark_rule(strokeWidth=2.5)\n    .encode(x=alt.X(\"x:Q\", scale=x_scale), color=alt.value(INK))\n)\n\n# Annotation for final prediction value\nfinal_ann = (\n    alt.Chart(\n        pd.DataFrame(\n            {\"x\": [final_value], \"feature\": [features_sorted[0]], \"label\": [f\"Prediction = {final_value:.2f}\"]}\n        )\n    )\n    .mark_text(align=\"center\", dy=-20, fontSize=15, fontWeight=\"bold\")\n    .encode(\n        x=alt.X(\"x:Q\", scale=x_scale), y=alt.Y(\"feature:N\", sort=features_sorted), text=\"label:N\", color=alt.value(INK)\n    )\n)\n\n# Combine all layers\nchart = (\n    alt.layer(base_rule, final_rule, bars, text_pos, text_neg, base_ann, final_ann)\n    .properties(\n        width=1600, height=900, title=\"Credit Default Risk · shap-waterfall · altair · anyplot.ai\", background=PAGE_BG\n    )\n    .configure_view(fill=PAGE_BG, stroke=None)\n    .configure_axis(\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.10,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=18,\n        titleFontSize=22,\n    )\n    .configure_title(color=INK, fontSize=28)\n    .configure_legend(\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=16,\n        titleFontSize=16,\n    )\n)\n\n# Save\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}