{"spec_id":"shap-summary","library":"makie","language":"julia","code":"# anyplot.ai\n# shap-summary: SHAP Summary Plot\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 92/100 | Created: 2026-09-09\n\nusing CairoMakie\nusing Makie\nusing Colors\nusing Random\nusing Statistics\n\nRandom.seed!(42)\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# Continuous feature-value scale — Imprint sequential (green -> blue)\nIMPRINT_SEQ = cgrad([colorant\"#009E73\", colorant\"#4467A3\"])\n\n# --- Data ---------------------------------------------------------------------\n# Synthetic SHAP output for a customer-churn classifier (TreeExplainer-style).\nfeature_names_raw = [\n    \"Contract Type\", \"Tenure (Months)\", \"Monthly Charges\", \"Tech Support Calls\",\n    \"Internet Service\", \"Total Charges\", \"Payment Method\", \"Dependents\",\n    \"Paperless Billing\", \"Senior Citizen\",\n]\nn_features = length(feature_names_raw)\nn_samples  = 220\n\n# Per-feature effect scale (drives mean |SHAP|) and sign of the value-effect\n# correlation (a high feature value can push the prediction up OR down).\neffect_scale = [0.95, 0.85, 0.78, 0.42, 0.38, 0.34, 0.24, 0.20, 0.16, 0.12]\ndirection    = [1, -1, 1, 1, -1, 1, -1, -1, 1, -1]\n\nfeature_values_raw = randn(n_samples, n_features)\nshap_values_raw = similar(feature_values_raw)\nfor j in 1:n_features\n    signal = direction[j] .* feature_values_raw[:, j] .* effect_scale[j]\n    noise = randn(n_samples) .* effect_scale[j] .* 0.35\n    shap_values_raw[:, j] = signal .+ noise\nend\n\n# Sort features by mean absolute SHAP value — most important at the top.\nmean_abs_shap = vec(mean(abs.(shap_values_raw), dims = 1))\norder = sortperm(mean_abs_shap, rev = true)\n\nfeature_names = feature_names_raw[order]\nfeature_values = feature_values_raw[:, order]\nshap_values = shap_values_raw[:, order]\n\n# Per-feature min-max scaling of the raw feature value, used for point color.\ncolor_values = similar(feature_values)\nfor j in 1:n_features\n    col = feature_values[:, j]\n    lo, hi = extrema(col)\n    color_values[:, j] = (col .- lo) ./ (hi - lo)\nend\n\n# Beeswarm-style vertical jitter: bin each feature's SHAP values, then stack\n# same-bin points alternately above/below the row center to avoid overlap.\nn_bins = 24\njitter_width = 0.38\nrow_offsets = similar(shap_values)\nfor j in 1:n_features\n    values = shap_values[:, j]\n    lo, hi = extrema(values)\n    edges = range(lo, hi, length = n_bins + 1)\n    bin_id = Vector{Int}(undef, n_samples)\n    for i in 1:n_samples\n        bin_id[i] = clamp(searchsortedlast(edges, values[i]), 1, n_bins)\n    end\n    counts = zeros(Int, n_bins)\n    offsets = zeros(Float64, n_samples)\n    for i in 1:n_samples\n        b = bin_id[i]\n        k = counts[b]\n        sgn = isodd(k) ? -1.0 : 1.0\n        offsets[i] = sgn * ceil(k / 2)\n        counts[b] += 1\n    end\n    row_offsets[:, j] = offsets ./ max(maximum(counts), 1) .* jitter_width\nend\n\n# Flatten into plotting vectors (column-major: feature 1's samples first).\nxs = vec(shap_values)\ncolors = vec(color_values)\nys = Vector{Float64}(undef, n_samples * n_features)\nfor j in 1:n_features\n    y_base = n_features - j + 1  # rank 1 (most important) sits at the top\n    rng = ((j - 1) * n_samples + 1):(j * n_samples)\n    ys[rng] = fill(Float64(y_base), n_samples) .+ row_offsets[:, j]\nend\n\nmax_abs_shap = maximum(abs.(xs)) * 1.15\n\n# --- Plot ----------------------------------------------------------------------\nfig = Figure(\n    resolution = (1600, 900),\n    fontsize = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nax = Axis(\n    fig[1, 1];\n    title = \"shap-summary · julia · makie · anyplot.ai\",\n    titlesize = 20,\n    titlecolor = INK,\n    xlabel = \"SHAP value (impact on model output)\",\n    xlabelsize = 14,\n    xlabelcolor = INK,\n    xticklabelsize = 12,\n    xticklabelcolor = INK_SOFT,\n    yticklabelsize = 13,\n    yticklabelcolor = INK_SOFT,\n    backgroundcolor = PAGE_BG,\n    topspinevisible = false,\n    rightspinevisible = false,\n    leftspinecolor = INK_SOFT,\n    bottomspinecolor = INK_SOFT,\n    xgridcolor = RGBAf(INK.r, INK.g, INK.b, 0.15),\n    ygridvisible = false,\n    yticks = (1:n_features, reverse(feature_names)),\n)\nxlims!(ax, -max_abs_shap, max_abs_shap)\nylims!(ax, 0.3, n_features + 0.7)\n\n# Subtle accent band behind the top (most important) feature row, drawn\n# before the scatter so it sits underneath the data points.\ntop_row_y = Float64(n_features)\nhspan!(\n    ax, top_row_y - 0.5, top_row_y + 0.5;\n    color = RGBAf(colorant\"#009E73\".r, colorant\"#009E73\".g, colorant\"#009E73\".b, 0.08),\n)\n\nvlines!(ax, 0; color = INK_SOFT, linewidth = 1.5, linestyle = :dash)\n\nscatter!(\n    ax, xs, ys;\n    color = colors,\n    colormap = IMPRINT_SEQ,\n    colorrange = (0, 1),\n    markersize = 7,\n    strokewidth = 0.5,\n    strokecolor = PAGE_BG,\n    alpha = 0.7,\n)\n\nColorbar(\n    fig[1, 2];\n    colormap = IMPRINT_SEQ,\n    limits = (0, 1),\n    label = \"Feature value\",\n    labelcolor = INK,\n    ticks = ([0, 1], [\"Low\", \"High\"]),\n    ticklabelcolor = INK_SOFT,\n    width = 18,\n)\ncolsize!(fig.layout, 2, Relative(0.035))\n\n# --- Save ------------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}