{"spec_id":"shap-waterfall","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nshap-waterfall: SHAP Waterfall Plot for Feature Attribution\nLibrary: plotnine 0.15.4 | Python 3.13.13\nQuality: 88/100 | Created: 2026-05-08\n\"\"\"\n\nimport importlib.util\nimport os\nimport sys\n\nimport numpy as np\nimport pandas as pd\n\n\n# Handle import conflicts: remove the implementations/python dir from sys.path\nsys.path = [p for p in sys.path if not p.endswith(\"python\")]\nif \"plotnine\" in sys.modules:\n    del sys.modules[\"plotnine\"]\n\n# Load plotnine explicitly from site-packages\n_pn_spec = importlib.util.find_spec(\"plotnine\")\n_pn = importlib.util.module_from_spec(_pn_spec)\nsys.modules[\"plotnine\"] = _pn\n_pn_spec.loader.exec_module(_pn)\n\naes = _pn.aes\ncoord_cartesian = _pn.coord_cartesian\nelement_blank = _pn.element_blank\nelement_line = _pn.element_line\nelement_rect = _pn.element_rect\nelement_text = _pn.element_text\ngeom_rect = _pn.geom_rect\ngeom_segment = _pn.geom_segment\ngeom_text = _pn.geom_text\ngeom_vline = _pn.geom_vline\nggplot = _pn.ggplot\nlabs = _pn.labs\nscale_fill_manual = _pn.scale_fill_manual\nscale_y_continuous = _pn.scale_y_continuous\ntheme = _pn.theme\n\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\"\nGRID_COLOR = \"#C8C7C0\" if THEME == \"light\" else \"#2E2E2B\"\n\nCOLOR_POS = \"#AE3030\"  # imprint red — positive SHAP contributions\nCOLOR_NEG = \"#4467A3\"  # imprint blue — negative SHAP contributions\n\n# Data: credit scoring model — explaining a single loan approval prediction\nnp.random.seed(42)\nfeatures = [\n    \"Annual Income\",\n    \"Credit Score\",\n    \"Debt-to-Income\",\n    \"Employment Years\",\n    \"Loan Amount\",\n    \"Payment History\",\n    \"Num Credit Lines\",\n    \"Savings Balance\",\n    \"Age\",\n    \"Recent Inquiries\",\n    \"Education Level\",\n    \"Home Ownership\",\n]\nshap_raw = [0.18, 0.14, -0.12, 0.09, -0.08, 0.06, -0.05, 0.05, 0.04, -0.03, 0.02, -0.01]\nbase_value = 0.34\nfinal_value = round(base_value + sum(shap_raw), 4)\n\ndf = pd.DataFrame({\"feature\": features, \"shap_value\": shap_raw})\ndf = df.reindex(df[\"shap_value\"].abs().sort_values(ascending=False).index).reset_index(drop=True)\nn = len(df)\n\n# Cumulative start/end x positions (index 0 = largest |SHAP| = top of chart)\ndf[\"start\"] = base_value + df[\"shap_value\"].cumsum().shift(1).fillna(0)\ndf[\"end\"] = df[\"start\"] + df[\"shap_value\"]\n\n# Y positions: largest |SHAP| at top (y=n), smallest at bottom (y=1)\ndf[\"y_pos\"] = list(range(n, 0, -1))\ndf[\"direction\"] = df[\"shap_value\"].apply(lambda v: \"Positive\" if v >= 0 else \"Negative\")\ndf[\"bar_label\"] = df[\"shap_value\"].apply(lambda v: f\"+{v:.3f}\" if v >= 0 else f\"{v:.3f}\")\n\nBAR_H = 0.65\ndf[\"ymin\"] = df[\"y_pos\"] - BAR_H / 2\ndf[\"ymax\"] = df[\"y_pos\"] + BAR_H / 2\n\n# Connector dashes between adjacent bars at their shared x boundary\nconn_rows = []\nfor i in range(n - 1):\n    conn_rows.append(\n        {\"x\": df.iloc[i][\"end\"], \"y0\": df.iloc[i + 1][\"y_pos\"] + BAR_H / 2, \"y1\": df.iloc[i][\"y_pos\"] - BAR_H / 2}\n    )\ndf_conn = pd.DataFrame(conn_rows)\n\n# Annotations for the baseline and final prediction reference lines\ndf_base_ann = pd.DataFrame({\"x\": [base_value], \"y\": [n + 0.85], \"label\": [f\"Base = {base_value:.2f}\"]})\ndf_final_ann = pd.DataFrame({\"x\": [final_value], \"y\": [0.15], \"label\": [f\"Pred = {final_value:.2f}\"]})\n\n# Separate bar-label DataFrames (need different ha alignment per sign)\ndf_pos_bars = df[df[\"shap_value\"] >= 0].copy()\ndf_neg_bars = df[df[\"shap_value\"] < 0].copy()\nNUDGE = 0.007\n\n# Axis limits\nx_left = min(df[\"start\"].min(), df[\"end\"].min(), base_value) - 0.02\nx_right = max(df[\"start\"].max(), df[\"end\"].max(), final_value) + 0.10\ny_breaks = sorted(df[\"y_pos\"].tolist())\ny_labels = df.sort_values(\"y_pos\")[\"feature\"].tolist()\n\nanyplot_theme = theme(\n    figure_size=(16, 9),\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG),\n    panel_grid_major_x=element_line(color=GRID_COLOR, size=0.4),\n    panel_grid_major_y=element_blank(),\n    panel_grid_minor=element_blank(),\n    panel_border=element_blank(),\n    axis_title_x=element_text(color=INK, size=20),\n    axis_title_y=element_blank(),\n    axis_text_x=element_text(color=INK_SOFT, size=16),\n    axis_text_y=element_text(color=INK, size=16),\n    axis_line_x=element_line(color=INK_SOFT),\n    axis_line_y=element_blank(),\n    axis_ticks_major_x=element_blank(),\n    axis_ticks_major_y=element_blank(),\n    plot_title=element_text(color=INK, size=22, ha=\"left\"),\n    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    legend_text=element_text(color=INK_SOFT, size=14),\n    legend_title=element_text(color=INK, size=16),\n    legend_position=\"right\",\n)\n\nplot = (\n    ggplot(df)\n    + geom_rect(aes(xmin=\"start\", xmax=\"end\", ymin=\"ymin\", ymax=\"ymax\", fill=\"direction\"))\n    + geom_segment(\n        data=df_conn, mapping=aes(x=\"x\", xend=\"x\", y=\"y0\", yend=\"y1\"), color=INK_SOFT, linetype=\"dashed\", size=0.6\n    )\n    + geom_vline(xintercept=base_value, color=INK_SOFT, linetype=\"dotted\", size=1.0)\n    + geom_vline(xintercept=final_value, color=INK_SOFT, linetype=\"dotted\", size=1.0)\n    + geom_text(\n        data=df_pos_bars,\n        mapping=aes(x=\"end\", y=\"y_pos\", label=\"bar_label\"),\n        nudge_x=NUDGE,\n        ha=\"left\",\n        color=INK,\n        size=12,\n    )\n    + geom_text(\n        data=df_neg_bars,\n        mapping=aes(x=\"end\", y=\"y_pos\", label=\"bar_label\"),\n        nudge_x=-NUDGE,\n        ha=\"right\",\n        color=INK,\n        size=12,\n    )\n    + geom_text(\n        data=df_base_ann, mapping=aes(x=\"x\", y=\"y\", label=\"label\"), ha=\"center\", va=\"bottom\", color=INK_SOFT, size=13\n    )\n    + geom_text(\n        data=df_final_ann, mapping=aes(x=\"x\", y=\"y\", label=\"label\"), ha=\"center\", va=\"top\", color=INK_SOFT, size=13\n    )\n    + scale_fill_manual(values={\"Positive\": COLOR_POS, \"Negative\": COLOR_NEG}, name=\"Contribution\")\n    + scale_y_continuous(breaks=y_breaks, labels=y_labels, expand=(0.05, 0))\n    + coord_cartesian(xlim=(x_left, x_right), ylim=(-0.2, n + 1.5))\n    + labs(x=\"SHAP Value (Feature Contribution)\", y=\"\", title=\"Loan Approval · shap-waterfall · plotnine · anyplot.ai\")\n    + anyplot_theme\n)\n\nplot.save(f\"plot-{THEME}.png\", dpi=300, width=16, height=9)\n"}