{"spec_id":"roc-curve","library":"makie","language":"julia","code":"# anyplot.ai\n# roc-curve: ROC Curve with AUC\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\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\"\nIMPRINT_PALETTE = [\n    colorant\"#009E73\",  # 1 — brand green, always first series\n    colorant\"#C475FD\",  # 2 — lavender\n]\n\n# --- Data: two diagnostic biomarkers vs. disease status ----------------------\nn_healthy = 300\nn_disease = 300\n\nscores_healthy_a = randn(n_healthy) .* 1.0 .+ 0.0\nscores_disease_a = randn(n_disease) .* 1.0 .+ 2.2\n\nscores_healthy_b = randn(n_healthy) .* 1.2 .+ 0.0\nscores_disease_b = randn(n_disease) .* 1.2 .+ 1.2\n\n# ROC curve for biomarker A: sweep the combined score axis from high to low,\n# accumulating hits (disease) and misses (healthy) as the threshold falls.\nlabels_a = vcat(ones(Int, n_disease), zeros(Int, n_healthy))\nscores_a = vcat(scores_disease_a, scores_healthy_a)\norder_a = sortperm(scores_a; rev = true)\nsorted_labels_a = labels_a[order_a]\ntpr_a = vcat(0.0, cumsum(sorted_labels_a) ./ n_disease)\nfpr_a = vcat(0.0, cumsum(1 .- sorted_labels_a) ./ n_healthy)\nauc_a = sum(diff(fpr_a) .* (tpr_a[2:end] .+ tpr_a[1:(end - 1)]) ./ 2)\n\n# ROC curve for biomarker B\nlabels_b = vcat(ones(Int, n_disease), zeros(Int, n_healthy))\nscores_b = vcat(scores_disease_b, scores_healthy_b)\norder_b = sortperm(scores_b; rev = true)\nsorted_labels_b = labels_b[order_b]\ntpr_b = vcat(0.0, cumsum(sorted_labels_b) ./ n_disease)\nfpr_b = vcat(0.0, cumsum(1 .- sorted_labels_b) ./ n_healthy)\nauc_b = sum(diff(fpr_b) .* (tpr_b[2:end] .+ tpr_b[1:(end - 1)]) ./ 2)\n\n# Optimal operating point per curve (Youden's J statistic: max(TPR - FPR))\nj_a = tpr_a .- fpr_a\nopt_idx_a = argmax(j_a)\nopt_fpr_a, opt_tpr_a = fpr_a[opt_idx_a], tpr_a[opt_idx_a]\n\nj_b = tpr_b .- fpr_b\nopt_idx_b = argmax(j_b)\nopt_fpr_b, opt_tpr_b = fpr_b[opt_idx_b], tpr_b[opt_idx_b]\n\n# --- Plot ----------------------------------------------------------------\nfig = Figure(\n    size = (1200, 1200),\n    fontsize = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nax = Axis(\n    fig[1, 1];\n    title = \"roc-curve · julia · makie · anyplot.ai\",\n    titlesize = 20,\n    titlecolor = INK,\n    xlabel = \"False Positive Rate\",\n    ylabel = \"True Positive Rate\",\n    xlabelsize = 14,\n    ylabelsize = 14,\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    backgroundcolor = PAGE_BG,\n    aspect = 1,\n    limits = (0, 1, 0, 1),\n    xticks = 0:0.25:1,\n    yticks = 0:0.25:1,\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    ygridcolor = RGBAf(INK.r, INK.g, INK.b, 0.15),\n    xminorgridvisible = false,\n    yminorgridvisible = false,\n)\n\n# Shade the area between the better-performing curve (Biomarker A) and the\n# diagonal to visualize its AUC advantage over random guessing\nband!(ax, fpr_a, fpr_a, tpr_a; color = (IMPRINT_PALETTE[1], 0.09))\n\nlines!(\n    ax, [0.0, 1.0], [0.0, 1.0];\n    color = INK_SOFT, linestyle = :dash, linewidth = 2.0,\n    label = \"Random classifier (AUC = 0.50)\",\n)\nlines!(\n    ax, fpr_a, tpr_a;\n    color = IMPRINT_PALETTE[1], linewidth = 3.5,\n    label = \"Biomarker A (AUC = $(round(auc_a; digits = 2)))\",\n)\nlines!(\n    ax, fpr_b, tpr_b;\n    color = IMPRINT_PALETTE[2], linewidth = 3.5,\n    label = \"Biomarker B (AUC = $(round(auc_b; digits = 2)))\",\n)\n\n# Mark each curve's optimal operating point (Youden's J statistic)\nscatter!(\n    ax, [opt_fpr_a], [opt_tpr_a];\n    color = IMPRINT_PALETTE[1], markersize = 18, strokewidth = 2.0, strokecolor = PAGE_BG,\n)\ntext!(\n    ax, opt_fpr_a, opt_tpr_a;\n    text = \"Optimal J = $(round(j_a[opt_idx_a]; digits = 2))\",\n    color = INK, fontsize = 13, font = :bold, align = (:left, :bottom), offset = (10, 10),\n)\nscatter!(\n    ax, [opt_fpr_b], [opt_tpr_b];\n    color = IMPRINT_PALETTE[2], markersize = 18, strokewidth = 2.0, strokecolor = PAGE_BG,\n)\ntext!(\n    ax, opt_fpr_b, opt_tpr_b;\n    text = \"Optimal J = $(round(j_b[opt_idx_b]; digits = 2))\",\n    color = INK, fontsize = 13, font = :bold, align = (:right, :top), offset = (-10, -10),\n)\n\naxislegend(ax; position = :rb, labelsize = 12, labelcolor = INK_SOFT, framevisible = false)\n\n# --- Save ----------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}