{"spec_id":"shap-waterfall","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nshap-waterfall: SHAP Waterfall Plot for Feature Attribution\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 85/100 | Created: 2026-05-07\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# Prevent this file from shadowing the bokeh package\n_this_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != _this_dir and p != \"\"]\n\nfrom bokeh.io import output_file, save\nfrom bokeh.models import Span\nfrom bokeh.plotting import figure\nfrom bokeh.resources import INLINE\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\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)\nLABEL_INK = \"#F0EFE8\"  # near-white for text on colored bar segments\n\n# Data — credit default risk model: explaining one loan applicant's prediction\nfeatures = [\n    \"Credit Score (715)\",\n    \"Debt-to-Income Ratio\",\n    \"Monthly Income\",\n    \"Late Payments (2)\",\n    \"Employment Duration\",\n    \"Loan Amount\",\n    \"Savings Balance\",\n    \"Credit History Age\",\n    \"Credit Inquiries (3)\",\n    \"Credit Cards Count\",\n    \"Housing (Renter)\",\n    \"Education Level\",\n]\nshap_values = [-0.18, 0.15, -0.12, 0.11, -0.08, 0.07, -0.05, -0.04, 0.04, 0.03, 0.02, -0.02]\nbase_value = 0.35\n\n# Cumulative waterfall positions\nn = len(features)\ncumulative = [base_value]\nfor sv in shap_values:\n    cumulative.append(round(cumulative[-1] + sv, 6))\n\nfinal_value = cumulative[-1]\n\nbar_lefts = [min(cumulative[i], cumulative[i + 1]) for i in range(n)]\nbar_rights = [max(cumulative[i], cumulative[i + 1]) for i in range(n)]\nbar_colors = [POS_COLOR if sv > 0 else NEG_COLOR for sv in shap_values]\nbar_labels = [f\"{sv:+.2f}\" for sv in shap_values]\n\n# Y-axis: features[0] (largest |SHAP|) at top — Bokeh places y_range[0] at bottom\ny_range = list(reversed(features))\n\n# Plot\np = figure(\n    width=4800,\n    height=2700,\n    y_range=y_range,\n    title=\"Credit Default Risk · shap-waterfall · bokeh · anyplot.ai\",\n    toolbar_location=None,\n    x_axis_label=\"Cumulative Default Probability\",\n)\n\n# Waterfall bars\np.hbar(y=features, left=bar_lefts, right=bar_rights, height=0.6, color=bar_colors, alpha=0.88)\n\n# Connector lines: vertical dashed segments at each cumulative transition\np.segment(\n    x0=[cumulative[i + 1] for i in range(n - 1)],\n    y0=[features[i] for i in range(n - 1)],\n    x1=[cumulative[i + 1] for i in range(n - 1)],\n    y1=[features[i + 1] for i in range(n - 1)],\n    line_color=INK_SOFT,\n    line_dash=\"dashed\",\n    line_width=2,\n    line_alpha=0.6,\n)\n\n# SHAP value labels centered inside bars (p.text supports categorical y-axis)\np.text(\n    x=[(l + r) / 2 for l, r in zip(bar_lefts, bar_rights)],\n    y=features,\n    text=bar_labels,\n    text_align=\"center\",\n    text_baseline=\"middle\",\n    text_font_size=\"18pt\",\n    text_color=LABEL_INK,\n    text_font_style=\"bold\",\n)\n\n# Base value annotation above the topmost bar — right-aligned so it stays inside plot\np.text(\n    x=[base_value],\n    y=[features[0]],\n    text=[f\"Base value: {base_value:.2f}\"],\n    x_offset=-8,\n    y_offset=28,\n    text_align=\"right\",\n    text_font_size=\"18pt\",\n    text_color=INK_SOFT,\n)\n\n# Final prediction annotation below the bottommost bar\np.text(\n    x=[final_value],\n    y=[features[-1]],\n    text=[f\"Prediction: {final_value:.2f}\"],\n    x_offset=6,\n    y_offset=-28,\n    text_font_size=\"18pt\",\n    text_color=INK,\n    text_font_style=\"bold\",\n)\n\n# Reference lines for base value and final prediction\np.add_layout(\n    Span(location=base_value, dimension=\"height\", line_color=INK_SOFT, line_dash=\"dashed\", line_width=3, line_alpha=0.8)\n)\np.add_layout(Span(location=final_value, dimension=\"height\", line_color=INK, line_dash=\"solid\", line_width=3))\n\n# Theme-adaptive chrome\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = INK_SOFT\n\np.title.text_font_size = \"28pt\"\np.title.text_color = INK\np.title.text_font_style = \"bold\"\n\np.xaxis.axis_label_text_font_size = \"22pt\"\np.xaxis.axis_label_text_color = INK\np.xaxis.major_label_text_font_size = \"18pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.xaxis.axis_line_color = INK_SOFT\np.xaxis.major_tick_line_color = INK_SOFT\n\np.yaxis.major_label_text_font_size = \"18pt\"\np.yaxis.major_label_text_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\n\np.xgrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.10\np.ygrid.grid_line_color = None\n\n# Save HTML and take PNG screenshot with headless Chrome\noutput_file(f\"plot-{THEME}.html\")\nsave(p, resources=INLINE)\n\nW, H = 4800, 2700\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\n\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(4)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}