{"spec_id":"ks-test-comparison","library":"makie","language":"julia","code":"# anyplot.ai\n# ks-test-comparison: Kolmogorov-Smirnov Plot for Distribution Comparison\n# Library: makie 0.22.10 | Julia 1.11.9\n# Quality: 88/100 | Created: 2026-05-29\n\nusing CairoMakie\nusing Colors\nusing Random\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\"\n\nconst IMPRINT_PALETTE = [\n    colorant\"#009E73\",\n    colorant\"#C475FD\",\n    colorant\"#4467A3\",\n    colorant\"#BD8233\",\n    colorant\"#AE3030\",\n    colorant\"#2ABCCD\",\n    colorant\"#954477\",\n    colorant\"#99B314\",\n]\n\n# Data — credit scoring: Good vs Bad customer score distributions\nn_good = 400\nn_bad  = 400\ngood_scores = randn(n_good) .* 60.0 .+ 620.0\nbad_scores  = randn(n_bad)  .* 70.0 .+ 490.0\n\nsorted_good = sort(good_scores)\nsorted_bad  = sort(bad_scores)\n\n# KS statistic (inline — two-sample D and its x-location)\nall_pts = sort(unique(vcat(sorted_good, sorted_bad)))\ndiffs   = abs.(searchsortedlast.(Ref(sorted_good), all_pts) ./ n_good .-\n               searchsortedlast.(Ref(sorted_bad),  all_pts) ./ n_bad)\nks_idx  = argmax(diffs)\nks_d    = diffs[ks_idx]\nks_x    = all_pts[ks_idx]\n\n# KS p-value (asymptotic Kolmogorov distribution, inline)\nn_eff = sqrt((n_good * n_bad) / (n_good + n_bad))\nz     = ks_d * (n_eff + 0.12 + 0.11 / n_eff)\nks_p  = max(0.0, min(1.0, 2.0 * sum((-1)^(k - 1) * exp(-2.0 * k^2 * z^2) for k in 1:100)))\n\n# CDF values at the KS point for the gap segment\ncdf_good_at_ks = searchsortedlast(sorted_good, ks_x) / n_good\ncdf_bad_at_ks  = searchsortedlast(sorted_bad,  ks_x) / n_bad\ny_lo = min(cdf_good_at_ks, cdf_bad_at_ks)\ny_hi = max(cdf_good_at_ks, cdf_bad_at_ks)\n\n# ECDF step function coordinates\nfunction ecdf_step(sv)\n    n  = length(sv)\n    xs = vcat(sv[1] - 5.0,\n              vec(permutedims(hcat(sv, sv))),\n              sv[end] + 5.0)\n    ys = vcat(0.0,\n              vec(permutedims(hcat((0:n-1) ./ n, (1:n) ./ n))),\n              1.0)\n    return xs, ys\nend\n\nxs_good, ys_good = ecdf_step(sorted_good)\nxs_bad,  ys_bad  = ecdf_step(sorted_bad)\n\n# Dense x grid for band fill (evaluates both ECDFs at every data point)\nx_fill      = sort(unique(vcat(sorted_good, sorted_bad)))\ny_good_fill = [searchsortedlast(sorted_good, x) / n_good for x in x_fill]\ny_bad_fill  = [searchsortedlast(sorted_bad,  x) / n_bad  for x in x_fill]\n\n# Title\ntitle_str  = \"ks-test-comparison · julia · makie · anyplot.ai\"\ntitle_size = max(14, round(Int, 20 * min(1.0, 67.0 / length(title_str))))\n\n# Plot\nfig = Figure(\n    size            = (1600, 900),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nax = Axis(\n    fig[1, 1];\n    title             = title_str,\n    titlesize         = title_size,\n    titlecolor        = INK,\n    xlabel            = \"Credit Score\",\n    ylabel            = \"Cumulative Proportion\",\n    xlabelsize        = 14,\n    ylabelsize        = 14,\n    xticklabelsize    = 12,\n    yticklabelsize    = 12,\n    xlabelcolor       = INK,\n    ylabelcolor       = INK,\n    xticklabelcolor   = INK_SOFT,\n    yticklabelcolor   = INK_SOFT,\n    xtickcolor        = INK_SOFT,\n    ytickcolor        = 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.12),\n    ygridcolor        = RGBAf(INK.r, INK.g, INK.b, 0.12),\n    xminorgridvisible = false,\n    yminorgridvisible = false,\n    limits            = (nothing, nothing, -0.02, 1.05),\n)\n\n# Shaded fill between ECDF curves (narrates the divergence region)\nks_red = IMPRINT_PALETTE[5]\nband!(ax, x_fill, min.(y_good_fill, y_bad_fill), max.(y_good_fill, y_bad_fill);\n    color = RGBAf(ks_red.r, ks_red.g, ks_red.b, 0.12))\n\n# ECDFs\nlines!(ax, xs_good, ys_good;\n    color = IMPRINT_PALETTE[1], linewidth = 2.5, label = \"Good Customers\")\nlines!(ax, xs_bad, ys_bad;\n    color = IMPRINT_PALETTE[2], linewidth = 2.5, label = \"Bad Customers\")\n\n# KS statistic: dashed vertical marker + filled gap segment\nvlines!(ax, [ks_x]; color = IMPRINT_PALETTE[5], linewidth = 1.2, linestyle = :dash)\nlines!(ax, [ks_x, ks_x], [y_lo, y_hi];\n    color = IMPRINT_PALETTE[5], linewidth = 4.0)\n\n# Annotation: D value and p-value\nks_p_str = ks_p < 0.001 ? \"p < 0.001\" : \"p = $(round(ks_p; digits = 3))\"\ntext!(ax,\n    \"D = $(round(ks_d; digits = 3))\\n$(ks_p_str)\";\n    position = Point2f(ks_x + 8.0, (y_lo + y_hi) / 2.0),\n    align    = (:left, :center),\n    color    = INK,\n    fontsize = 16,\n)\n\naxislegend(ax;\n    position        = :rb,\n    labelsize       = 12,\n    framevisible    = true,\n    framecolor      = INK_SOFT,\n    backgroundcolor = ELEVATED_BG,\n    labelcolor      = INK,\n)\n\n# Save\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}