{"spec_id":"shap-waterfall","library":"makie","language":"julia","code":"# anyplot.ai\n# shap-waterfall: SHAP Waterfall Plot for Feature Attribution\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 86/100 | Created: 2026-09-09\n\nusing CairoMakie\nusing Colors\nusing Printf\n\n# --- Theme tokens -----------------------------------------------------------\nTHEME      = get(ENV, \"ANYPLOT_THEME\", \"light\")\nPAGE_BG    = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nINK        = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nINK_SOFT   = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\n\n# Imprint palette semantic anchors: matte red for positive push, blue for negative\nPOSITIVE_COLOR = colorant\"#AE3030\"\nNEGATIVE_COLOR = colorant\"#4467A3\"\n\n# --- Data ---------------------------------------------------------------\n# Credit scoring model explaining a single loan applicant's predicted default\n# probability. Features ranked by descending absolute SHAP magnitude.\nfeatures = [\n    \"Debt-to-Income Ratio\",\n    \"Credit Utilization\",\n    \"Recent Missed Payments\",\n    \"Income Stability\",\n    \"Credit History Length\",\n    \"Age\",\n    \"Number of Open Accounts\",\n    \"Employment Length\",\n    \"Loan-to-Value Ratio\",\n    \"Number of Credit Inquiries\",\n]\nshap_values = [7.2, 5.1, 4.3, -3.8, -3.1, -2.4, 1.8, -1.5, 1.2, 0.9]\n\nbase_value = 18.4\nn = length(features)\n\nends = base_value .+ cumsum(shap_values)\nstarts = ends .- shap_values\nfinal_value = ends[end]\n\n# Position n (top) holds the largest-magnitude feature; position 1 (bottom)\n# holds the smallest, matching the descending-magnitude ranking above.\npositions = collect(n:-1:1)\nbar_colors = [v >= 0 ? POSITIVE_COLOR : NEGATIVE_COLOR for v in shap_values]\n\n# --- Plot -----------------------------------------------------------------\nfig = Figure(\n    resolution      = (1600, 900),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nlo = minimum(vcat(starts, ends, [base_value])) - 5.0\nhi = maximum(vcat(starts, ends, [final_value])) + 5.0\n\nax = Axis(\n    fig[1, 1];\n    title              = \"shap-waterfall · julia · makie · anyplot.ai\",\n    titlesize          = 20,\n    titlecolor         = INK,\n    xlabel             = \"Predicted Default Probability (%)\",\n    xlabelsize         = 14,\n    xlabelcolor        = INK,\n    xticklabelsize     = 12,\n    xticklabelcolor    = INK_SOFT,\n    yticks             = (1:n, reverse(features)),\n    yticklabelsize     = 13,\n    yticklabelcolor    = INK_SOFT,\n    ylabel             = \"Feature\",\n    ylabelsize         = 14,\n    ylabelcolor        = INK,\n    backgroundcolor    = PAGE_BG,\n    topspinevisible    = false,\n    rightspinevisible  = false,\n    leftspinevisible   = false,\n    leftspinecolor     = INK_SOFT,\n    bottomspinecolor   = INK_SOFT,\n    xgridvisible       = true,\n    ygridvisible       = false,\n    xgridcolor         = RGBAf(INK.r, INK.g, INK.b, 0.15),\n    xminorgridvisible  = false,\n    yminorgridvisible  = false,\n)\n\nxlims!(ax, lo, hi)\nylims!(ax, 0.2, n + 1.3)\n\n# Dotted connector lines between consecutive cumulative segments\nfor i in 1:(n - 1)\n    lines!(\n        ax,\n        [ends[i], ends[i]],\n        [positions[i], positions[i + 1]];\n        color = INK_SOFT,\n        linestyle = :dot,\n        linewidth = 1.5,\n    )\nend\n\n# Base value and final prediction reference lines, stopped below the label\n# row (as plain lines! with an explicit y-extent) so the dashed lines never\n# cross through their own annotation text above.\nline_top = n + 0.4\nlines!(ax, [base_value, base_value], [0.2, line_top]; color = INK_SOFT, linestyle = :dash, linewidth = 1.5)\nlines!(ax, [final_value, final_value], [0.2, line_top]; color = INK, linestyle = :dash, linewidth = 1.5)\nscatter!(ax, [base_value], [line_top]; marker = :utriangle, markersize = 10, color = INK_SOFT)\nscatter!(ax, [final_value], [line_top]; marker = :utriangle, markersize = 10, color = INK)\n\ntext!(\n    ax, base_value, n + 0.9;\n    text = @sprintf(\"Base value: %.1f%%\", base_value),\n    color = INK_SOFT, fontsize = 13, align = (:center, :bottom),\n)\ntext!(\n    ax, final_value, n + 0.5;\n    text = @sprintf(\"Prediction: %.1f%%\", final_value),\n    color = INK, fontsize = 13, align = (:center, :bottom), font = :bold,\n)\n\n# Waterfall bars (horizontal, floating from cumulative start to cumulative end)\nbarplot!(\n    ax,\n    positions,\n    ends;\n    fillto = starts,\n    direction = :x,\n    color = bar_colors,\n    width = 0.62,\n    strokewidth = 0,\n)\n\n# Numeric SHAP value labels beside each bar\nfor i in 1:n\n    label = @sprintf(\"%+.1f\", shap_values[i])\n    x_offset = shap_values[i] >= 0 ? ends[i] + 0.5 : ends[i] - 0.5\n    text_align = shap_values[i] >= 0 ? (:left, :center) : (:right, :center)\n    text!(\n        ax, x_offset, positions[i];\n        text = label, color = INK, fontsize = 13, align = text_align,\n    )\nend\n\n# Legend explaining bar color semantics\nlegend_elems = [PolyElement(color = POSITIVE_COLOR), PolyElement(color = NEGATIVE_COLOR)]\nlegend_labels = [\"Increases prediction\", \"Decreases prediction\"]\nLegend(\n    fig[2, 1], legend_elems, legend_labels;\n    orientation = :horizontal, framevisible = false,\n    labelcolor = INK, labelsize = 13, backgroundcolor = PAGE_BG,\n)\n\nrowsize!(fig.layout, 1, Relative(0.92))\n\n# --- Save -------------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}