{"spec_id":"diagnostic-regression-panel","library":"makie","language":"julia","code":"# anyplot.ai\n# diagnostic-regression-panel: Regression Diagnostic Panel (Four-Plot Display)\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 88/100 | Created: 2026-09-05\n\nusing CairoMakie\nusing Colors\nusing Random\nusing LinearAlgebra\n\nRandom.seed!(42)\n\n# --- Theme tokens ------------------------------------------------------------\nconst THEME       = get(ENV, \"ANYPLOT_THEME\", \"light\")\nconst PAGE_BG     = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nconst ELEVATED_BG = THEME == \"light\" ? colorant\"#FFFDF6\" : colorant\"#242420\"\nconst INK         = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nconst INK_SOFT    = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\nconst BRAND       = colorant\"#009E73\"  # Imprint position 1 — data points\nconst OCHRE       = colorant\"#BD8233\"  # Imprint position 4 — smoother trend\nconst MATTE_RED   = colorant\"#AE3030\"  # semantic anchor — flagged influential points\nconst AMBER       = colorant\"#DDCC77\"  # semantic anchor — Cook's distance warning contours\n\n# --- Standard normal quantile (inverse CDF), Acklam's rational approximation --\nfunction norm_quantile(p)\n    a = (-3.969683028665376e+01, 2.209460984245205e+02, -2.759285104469687e+02,\n        1.383577518672690e+02, -3.066479806614716e+01, 2.506628277459239e+00)\n    b = (-5.447609879822406e+01, 1.615858368580409e+02, -1.556989798598866e+02,\n        6.680131188771972e+01, -1.328068155288572e+01)\n    c = (-7.784894002430293e-03, -3.223964580411365e-01, -2.400758277161838e+00,\n        -2.549732539343734e+00, 4.374664141464968e+00, 2.938163982698783e+00)\n    d = (7.784695709041462e-03, 3.224671290700398e-01, 2.445134137142996e+00,\n        3.754408661907416e+00)\n    p_low = 0.02425\n    p_high = 1 - p_low\n    if p < p_low\n        q = sqrt(-2 * log(p))\n        return (((((c[1] * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) * q + c[6]) /\n               ((((d[1] * q + d[2]) * q + d[3]) * q + d[4]) * q + 1)\n    elseif p <= p_high\n        q = p - 0.5\n        r = q * q\n        return (((((a[1] * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * r + a[6]) * q /\n               (((((b[1] * r + b[2]) * r + b[3]) * r + b[4]) * r + b[5]) * r + 1)\n    else\n        q = sqrt(-2 * log(1 - p))\n        return -(((((c[1] * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) * q + c[6]) /\n               ((((d[1] * q + d[2]) * q + d[3]) * q + d[4]) * q + 1)\n    end\nend\n\n# --- Gaussian-kernel local regression (LOWESS-style smoother, no package) ----\nfunction lowess_smooth(x, y, x_eval, bandwidth)\n    y_smooth = similar(x_eval)\n    for i in eachindex(x_eval)\n        w = exp.(-((x .- x_eval[i]) ./ bandwidth) .^ 2)\n        wsum = sum(w)\n        y_smooth[i] = wsum > 1e-10 ? sum(w .* y) / wsum : NaN\n    end\n    return y_smooth\nend\n\n# --- Data: a dose-response regression with mild non-linearity, --------------\n# heteroscedastic noise, and a couple of injected influential observations ---\nn = 90\ndose_mg = rand(n) .* 90.0 .+ 5.0                 # drug dose administered, 5-95 mg\ndose_mg[1] = 150.0                               # extrapolated high-dose (high-leverage) point\ndose_mg[2] = 92.0\n\nmu = 14.0 .+ 0.85 .* dose_mg .- 0.0035 .* dose_mg .^ 2  # true response plateaus at high dose\nnoise_scale = 0.9 .+ 0.045 .* dose_mg             # variance grows with dose (heteroscedastic)\nresponse_pct = mu .+ noise_scale .* randn(n)      # percent symptom relief\nresponse_pct[1] += 10.0                           # push the high-leverage point off-trend\nresponse_pct[3] -= 8.5                            # a second, non-leverage but large residual\n\n# Ordinary least squares fit: response_pct ~ 1 + dose_mg\nX = hcat(ones(n), dose_mg)\np = size(X, 2)\nbeta = X \\ response_pct\nfitted = X * beta\nresiduals = response_pct .- fitted\n\nhat_matrix = X * inv(X' * X) * X'\nleverage = diag(hat_matrix)\n\nmse = sum(residuals .^ 2) / (n - p)\nsigma_hat = sqrt(mse)\nstd_residuals = residuals ./ (sigma_hat .* sqrt.(1 .- leverage))\nsqrt_abs_std_resid = sqrt.(abs.(std_residuals))\ncooks_d = (std_residuals .^ 2 .* leverage) ./ (p .* (1 .- leverage))\n\nord = sortperm(std_residuals)\nsorted_std_resid = std_residuals[ord]\nranks = invperm(ord)\nplot_pos = ((1:n) .- 0.5) ./ n\ntheoretical_q = norm_quantile.(plot_pos)\n\ntop_idx = sortperm(cooks_d, rev = true)[1:3]      # 3 most influential observations\n\nfitted_eval = collect(range(minimum(fitted), maximum(fitted), length = 120))\nbw = (maximum(fitted) - minimum(fitted)) / 6\nresid_smooth = lowess_smooth(fitted, residuals, fitted_eval, bw)\nscale_smooth = lowess_smooth(fitted, sqrt_abs_std_resid, fitted_eval, bw)\n\n# --- Figure -------------------------------------------------------------------\ntitle_str = \"diagnostic-regression-panel · julia · makie · anyplot.ai\"\n\nfig = Figure(size = (1200, 1200), fontsize = 14, backgroundcolor = PAGE_BG)\n\nLabel(fig[0, 1:2], title_str; fontsize = 18, color = INK, font = :bold, padding = (0, 0, 6, 0))\n\n# --- Panel 1: Residuals vs Fitted --------------------------------------------\nax1 = Axis(\n    fig[1, 1];\n    title              = \"Residuals vs Fitted\",\n    titlesize          = 15,\n    titlecolor         = INK,\n    xlabel             = \"Fitted Values (% relief)\",\n    ylabel             = \"Residuals\",\n    xlabelsize         = 12,\n    ylabelsize         = 12,\n    xlabelcolor        = INK,\n    ylabelcolor        = INK,\n    xticklabelsize     = 12,\n    yticklabelsize     = 12,\n    xticklabelcolor    = INK_SOFT,\n    yticklabelcolor    = INK_SOFT,\n    xtickcolor         = INK_SOFT,\n    ytickcolor         = INK_SOFT,\n    xticksize          = 0,\n    yticksize          = 0,\n    backgroundcolor    = PAGE_BG,\n    topspinevisible    = false,\n    rightspinevisible  = false,\n    leftspinecolor     = INK_SOFT,\n    bottomspinecolor   = INK_SOFT,\n    xgridvisible       = false,\n    ygridcolor         = RGBAf(INK.r, INK.g, INK.b, 0.12),\n    xminorgridvisible  = false,\n    yminorgridvisible  = false,\n)\nhlines!(ax1, [0.0]; color = INK_SOFT, linewidth = 1.2, linestyle = :dash)\nscatter!(ax1, fitted, residuals;\n    color = (BRAND, 0.75), markersize = 13, strokewidth = 0.8, strokecolor = PAGE_BG)\nlines!(ax1, fitted_eval, resid_smooth; color = OCHRE, linewidth = 2.8)\nscatter!(ax1, fitted[top_idx], residuals[top_idx];\n    color = :transparent, markersize = 17, strokewidth = 1.6, strokecolor = MATTE_RED)\nfor i in top_idx\n    text!(ax1, fitted[i], residuals[i]; text = string(i), fontsize = 11, color = MATTE_RED,\n        align = (:left, :bottom), offset = (6, 4))\nend\n\n# --- Panel 2: Normal Q-Q ------------------------------------------------------\nax2 = Axis(\n    fig[1, 2];\n    title              = \"Normal Q-Q\",\n    titlesize          = 15,\n    titlecolor         = INK,\n    xlabel             = \"Theoretical Quantiles\",\n    ylabel             = \"Standardized Residuals\",\n    xlabelsize         = 12,\n    ylabelsize         = 12,\n    xlabelcolor        = INK,\n    ylabelcolor        = INK,\n    xticklabelsize     = 12,\n    yticklabelsize     = 12,\n    xticklabelcolor    = INK_SOFT,\n    yticklabelcolor    = INK_SOFT,\n    xtickcolor         = INK_SOFT,\n    ytickcolor         = INK_SOFT,\n    xticksize          = 0,\n    yticksize          = 0,\n    backgroundcolor    = PAGE_BG,\n    topspinevisible    = false,\n    rightspinevisible  = false,\n    leftspinecolor     = INK_SOFT,\n    bottomspinecolor   = INK_SOFT,\n    xgridvisible       = false,\n    ygridcolor         = RGBAf(INK.r, INK.g, INK.b, 0.12),\n    xminorgridvisible  = false,\n    yminorgridvisible  = false,\n)\nqq_lo = min(minimum(theoretical_q), minimum(sorted_std_resid))\nqq_hi = max(maximum(theoretical_q), maximum(sorted_std_resid))\nlines!(ax2, [qq_lo, qq_hi], [qq_lo, qq_hi]; color = INK_SOFT, linewidth = 1.2, linestyle = :dash)\nscatter!(ax2, theoretical_q, sorted_std_resid;\n    color = (BRAND, 0.75), markersize = 13, strokewidth = 0.8, strokecolor = PAGE_BG)\nscatter!(ax2, theoretical_q[ranks[top_idx]], std_residuals[top_idx];\n    color = :transparent, markersize = 17, strokewidth = 1.6, strokecolor = MATTE_RED)\nfor i in top_idx\n    text!(ax2, theoretical_q[ranks[i]], std_residuals[i]; text = string(i), fontsize = 11,\n        color = MATTE_RED, align = (:left, :bottom), offset = (6, 4))\nend\n\n# --- Panel 3: Scale-Location ---------------------------------------------------\nax3 = Axis(\n    fig[2, 1];\n    title              = \"Scale-Location\",\n    titlesize          = 15,\n    titlecolor         = INK,\n    xlabel             = \"Fitted Values (% relief)\",\n    ylabel             = \"√|Standardized Residuals|\",\n    xlabelsize         = 12,\n    ylabelsize         = 12,\n    xlabelcolor        = INK,\n    ylabelcolor        = INK,\n    xticklabelsize     = 12,\n    yticklabelsize     = 12,\n    xticklabelcolor    = INK_SOFT,\n    yticklabelcolor    = INK_SOFT,\n    xtickcolor         = INK_SOFT,\n    ytickcolor         = INK_SOFT,\n    xticksize          = 0,\n    yticksize          = 0,\n    backgroundcolor    = PAGE_BG,\n    topspinevisible    = false,\n    rightspinevisible  = false,\n    leftspinecolor     = INK_SOFT,\n    bottomspinecolor   = INK_SOFT,\n    xgridvisible       = false,\n    ygridcolor         = RGBAf(INK.r, INK.g, INK.b, 0.12),\n    xminorgridvisible  = false,\n    yminorgridvisible  = false,\n)\nscatter!(ax3, fitted, sqrt_abs_std_resid;\n    color = (BRAND, 0.75), markersize = 13, strokewidth = 0.8, strokecolor = PAGE_BG)\nlines!(ax3, fitted_eval, scale_smooth; color = OCHRE, linewidth = 2.8)\nscatter!(ax3, fitted[top_idx], sqrt_abs_std_resid[top_idx];\n    color = :transparent, markersize = 17, strokewidth = 1.6, strokecolor = MATTE_RED)\nfor (k, i) in enumerate(top_idx)\n    dy = isodd(k) ? 4 : -14\n    text!(ax3, fitted[i], sqrt_abs_std_resid[i]; text = string(i), fontsize = 11,\n        color = MATTE_RED, align = (:left, :bottom), offset = (6, dy))\nend\n\n# --- Panel 4: Residuals vs Leverage, with Cook's distance contours ------------\nax4 = Axis(\n    fig[2, 2];\n    title              = \"Residuals vs Leverage\",\n    titlesize          = 15,\n    titlecolor         = INK,\n    xlabel             = \"Leverage\",\n    ylabel             = \"Standardized Residuals\",\n    xlabelsize         = 12,\n    ylabelsize         = 12,\n    xlabelcolor        = INK,\n    ylabelcolor        = INK,\n    xticklabelsize     = 12,\n    yticklabelsize     = 12,\n    xticklabelcolor    = INK_SOFT,\n    yticklabelcolor    = INK_SOFT,\n    xtickcolor         = INK_SOFT,\n    ytickcolor         = INK_SOFT,\n    xticksize          = 0,\n    yticksize          = 0,\n    backgroundcolor    = PAGE_BG,\n    topspinevisible    = false,\n    rightspinevisible  = false,\n    leftspinecolor     = INK_SOFT,\n    bottomspinecolor   = INK_SOFT,\n    xgridvisible       = false,\n    ygridcolor         = RGBAf(INK.r, INK.g, INK.b, 0.12),\n    xminorgridvisible  = false,\n    yminorgridvisible  = false,\n)\nlev_max = maximum(leverage) * 1.15\nresid_max = maximum(abs.(std_residuals)) * 1.35\nh_grid = range(lev_max * 0.015, lev_max, length = 200)\nhlines!(ax4, [0.0]; color = INK_SOFT, linewidth = 1.0, linestyle = :dash)\nfor cooks_level in (0.5, 1.0)\n    r_curve = sqrt.(cooks_level .* p .* (1 .- h_grid) ./ h_grid)\n    lines!(ax4, h_grid, r_curve; color = AMBER, linewidth = 1.8, linestyle = :dot)\n    lines!(ax4, h_grid, -r_curve; color = AMBER, linewidth = 1.8, linestyle = :dot)\n    text!(ax4, h_grid[end], r_curve[end]; text = \"D=$(cooks_level)\", fontsize = 10,\n        color = AMBER, align = (:right, :bottom), offset = (-2, 3))\nend\nscatter!(ax4, leverage, std_residuals;\n    color = (BRAND, 0.75), markersize = 13, strokewidth = 0.8, strokecolor = PAGE_BG)\nscatter!(ax4, leverage[top_idx], std_residuals[top_idx];\n    color = :transparent, markersize = 17, strokewidth = 1.6, strokecolor = MATTE_RED)\nfor (k, i) in enumerate(top_idx)\n    dy = isodd(k) ? 4 : -14\n    text!(ax4, leverage[i], std_residuals[i]; text = string(i), fontsize = 11,\n        color = MATTE_RED, align = (:left, :bottom), offset = (6, dy))\nend\nxlims!(ax4, 0.0, lev_max)\nylims!(ax4, -resid_max, resid_max)\n\nrowgap!(fig.layout, 18)\ncolgap!(fig.layout, 18)\n\n# --- Save --------------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}