{"spec_id":"precision-recall","library":"makie","language":"julia","code":"# anyplot.ai\n# precision-recall: Precision-Recall Curve\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 93/100 | Created: 2026-09-05\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 INK      = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nconst INK_SOFT = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\nconst IMPRINT_PALETTE = [\n    colorant\"#009E73\", colorant\"#C475FD\", colorant\"#4467A3\", colorant\"#BD8233\",\n    colorant\"#AE3030\", colorant\"#2ABCCD\", colorant\"#954477\", colorant\"#99B314\",\n]\n\n# --- Data ---------------------------------------------------------------\n# Fraud-detection scenario: rare positive class (fraudulent transactions)\n# scored by a classifier's predicted probability.\nn_transactions = 2000\npositive_rate = 0.06\nis_fraud = Int.(rand(n_transactions) .< positive_rate)\n\n# Simulated classifier scores: fraud cases carry a stronger latent signal,\n# on top of noise that creates realistic overlap with legitimate cases.\nlatent_signal = 2.6 .* is_fraud .+ 1.2 .* randn(n_transactions)\nrisk_score = 1 ./ (1 .+ exp.(-latent_signal))\n\n# --- Precision / recall at every threshold (descending score order) --------\norder = sortperm(risk_score; rev = true)\nsorted_labels = is_fraud[order]\n\nn_positives = sum(is_fraud)\ntrue_positives = cumsum(sorted_labels)\npredicted_positives = collect(1:n_transactions)\n\nprecision = true_positives ./ predicted_positives\nrecall = true_positives ./ n_positives\n\n# Prepend the (recall=0, precision=1) anchor point (standard PR-curve convention)\nrecall_curve = vcat(0.0, recall)\nprecision_curve = vcat(1.0, precision)\n\naverage_precision = sum(\n    (recall_curve[i] - recall_curve[i - 1]) * precision_curve[i]\n    for i in 2:length(recall_curve)\n)\nbaseline = n_positives / n_transactions\n\n# Operating point that maximizes F1 = 2PR/(P+R) — the single most useful\n# threshold for a practitioner, highlighted as a focal point on the curve.\nf1_scores = [p + r > 0 ? 2 * p * r / (p + r) : 0.0 for (p, r) in zip(precision, recall)]\nbest_idx = argmax(f1_scores)\nbest_f1 = f1_scores[best_idx]\nbest_recall = recall[best_idx]\nbest_precision = precision[best_idx]\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              = \"precision-recall · julia · makie · anyplot.ai\",\n    titlesize          = 20,\n    titlecolor         = INK,\n    xlabel             = \"Recall\",\n    ylabel             = \"Precision\",\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    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)\nxlims!(ax, -0.02, 1.02)\nylims!(ax, 0.0, 1.05)\n\n# Iso-F1 reference contours (spec Notes: \"consider showing iso-F1 curves\"),\n# drawn first as light background context so the data curve stays on top.\nf1_levels = (0.2, 0.4, 0.6, 0.8)\nfor (i, f1) in enumerate(f1_levels)\n    r_min = f1 / (2 - f1)\n    r_grid = collect(range(r_min, 1.0; length = 100))\n    p_grid = f1 .* r_grid ./ (2 .* r_grid .- f1)\n    lines!(\n        ax, r_grid, p_grid;\n        color = (INK_SOFT, 0.35),\n        linestyle = :dot,\n        linewidth = 1.0,\n        label = i == 1 ? \"Iso-F1 (0.2 / 0.4 / 0.6 / 0.8)\" : nothing,\n    )\nend\n\n# Light fill under the curve reinforces the Average Precision area visually.\nband!(\n    ax, recall_curve, zeros(length(recall_curve)), precision_curve;\n    color = (IMPRINT_PALETTE[1], 0.10),\n)\n\nstairs!(\n    ax, recall_curve, precision_curve;\n    step = :post,\n    color = IMPRINT_PALETTE[1],\n    linewidth = 3.0,\n    label = \"Precision-recall (AP = $(round(average_precision, digits = 2)))\",\n)\nhlines!(\n    ax, [baseline];\n    color = INK_SOFT,\n    linestyle = :dash,\n    linewidth = 2.0,\n    label = \"Baseline (fraud rate = $(round(100 * baseline, digits = 1))%)\",\n)\n\n# Best-F1 operating point — a halo marker in brand green keeps the data\n# storytelling anchored on the single most actionable threshold.\nscatter!(\n    ax, [best_recall], [best_precision];\n    color = PAGE_BG,\n    strokecolor = IMPRINT_PALETTE[1],\n    strokewidth = 2.5,\n    markersize = 16,\n    marker = :circle,\n    label = \"Best F1 = $(round(best_f1, digits = 2))\",\n)\n\nLegend(\n    fig[1, 2], ax;\n    backgroundcolor = PAGE_BG,\n    labelcolor = INK,\n    framevisible = false,\n)\n\n# --- Save -----------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}