""" Geometric picture of the ROLL TPR@FPR objective. Upper panel: score PDFs for both classes; shaded area under the positive-class PDF to the left of τ equals the ROLL loss F̂₁(τ). Lower panel: CDFs showing how τ is read from F̂₀ and how the loss and TPR are read from F̂₁. Run from the thesis root: nix develop --command python3 content/method/figures/roll_principle.py """ import numpy as np import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt from scipy.stats import norm OUT_DIR = "content/method/figures" np.random.seed(3) # --- Parameters ------------------------------------------------------------ mu0, sig0 = 0.0, 1.00 # negative class N(0, 1) mu1, sig1 = 1.5, 1.00 # positive class N(1.5, 1) — intentional overlap alpha = 0.20 # target FPR # τ = (1-α)-quantile of the negative class → F̂₀(τ) = 1-α → FPR = α tau = norm.ppf(1 - alpha, mu0, sig0) F0_tau = norm.cdf(tau, mu0, sig0) # = 1 - alpha F1_tau = norm.cdf(tau, mu1, sig1) # ROLL loss tpr = 1.0 - F1_tau # --- Sample points for positive class (rug) -------------------------------- rug = np.random.normal(mu1, sig1, 35) # --- Score axis ------------------------------------------------------------ x = np.linspace(-3.6, 5.0, 900) # --- Palette --------------------------------------------------------------- C_NEG = "#4477AA" C_POS = "#CC6633" C_TAU = "#333333" C_LOSS = "#7799BB" # ── Figure ───────────────────────────────────────────────────────────────── fig, (ax1, ax2) = plt.subplots( 2, 1, figsize=(5.5, 4.8), sharex=True, gridspec_kw={"height_ratios": [1.25, 1.0]}, ) fig.subplots_adjust(hspace=0.07) # ══ Top: PDF ══════════════════════════════════════════════════════════════ pdf0 = norm.pdf(x, mu0, sig0) pdf1 = norm.pdf(x, mu1, sig1) ax1.plot(x, pdf0, color=C_NEG, linewidth=1.8, label=r"Negative class ($y=0$)") ax1.plot(x, pdf1, color=C_POS, linewidth=1.8, label=r"Positive class ($y=1$)") ax1.axvline(tau, color=C_TAU, linestyle="--", linewidth=1.3, label=r"Threshold $\tau$") # Shade: area under positive PDF to the left of τ = loss = F̂₁(τ) x_left = x[x <= tau] ax1.fill_between(x_left, norm.pdf(x_left, mu1, sig1), color=C_LOSS, alpha=0.30, label=r"Loss $= \hat{F}_1(\tau)$") # Rug plot rug_y = np.full_like(rug, -0.006) ax1.scatter(rug, rug_y, color=C_POS, s=18, marker="|", zorder=3, alpha=0.65, clip_on=False) ax1.set_ylabel("PDF", fontsize=9) ax1.set_yticks([]) ax1.set_ylim(bottom=-0.025) ax1.legend(fontsize=7.5, loc="upper right", framealpha=0.88) # ══ Bottom: CDF ═══════════════════════════════════════════════════════════ cdf0 = norm.cdf(x, mu0, sig0) cdf1 = norm.cdf(x, mu1, sig1) ax2.plot(x, cdf0, color=C_NEG, linewidth=1.8, label=r"$\hat{F}_0$ (neg.)") ax2.plot(x, cdf1, color=C_POS, linewidth=1.8, label=r"$\hat{F}_1$ (pos.)") ax2.axvline(tau, color=C_TAU, linestyle="--", linewidth=1.3) # Horizontal reference lines ax2.axhline(F0_tau, color=C_NEG, linestyle="-.", linewidth=0.9, alpha=0.7) ax2.axhline(F1_tau, color=C_POS, linestyle="-.", linewidth=0.9, alpha=0.7) # Dots at the intercepts ax2.plot(tau, F0_tau, "o", color=C_NEG, markersize=5, zorder=4) ax2.plot(tau, F1_tau, "o", color=C_POS, markersize=5, zorder=4) # Right-side labels for the intercepts (placed where CDFs have flattened to ~1) x_label_right = x[-1] - 0.2 ax2.text(x_label_right, F0_tau, rf"$1-\alpha$", fontsize=8, color=C_NEG, va="bottom", ha="right") ax2.text(x_label_right, F1_tau, r"$\hat{F}_1(\tau)$" + "\n(loss)", fontsize=7.5, color=C_POS, va="top", ha="right") # TPR annotation: double-headed arrow on the right + label x_bracket = tau + 1.9 ax2.annotate("", xy=(x_bracket, 1.0), xytext=(x_bracket, F1_tau), arrowprops=dict(arrowstyle="<->", color="black", lw=0.8, mutation_scale=10)) ax2.text(x_bracket + 0.12, (1.0 + F1_tau) / 2, f"TPR\n= {tpr*100:.0f}%", fontsize=7.5, va="center", ha="left", color="black") ax2.set_ylabel("CDF", fontsize=9) ax2.set_xlabel(r"Score $f_\theta(\mathbf{x})$", fontsize=9) ax2.set_yticks([0.0, 0.25, 0.50, 0.75, 1.0]) ax2.set_yticklabels(["0", "0.25", "0.5", "0.75", "1"], fontsize=7.5) ax2.set_xlim(x[0] - 0.3, x[-1]) ax2.legend(fontsize=7.5, loc="upper left", framealpha=0.88) for ax in (ax1, ax2): ax.tick_params(labelsize=8) ax.spines[["top", "right"]].set_visible(False) fig.savefig(f"{OUT_DIR}/roll_principle.pdf", bbox_inches="tight") fig.savefig(f"{OUT_DIR}/roll_principle.png", bbox_inches="tight", dpi=180) print(f"Saved to {OUT_DIR}/roll_principle.{{pdf,png}}") print(f"tau={tau:.3f} F0(tau)={F0_tau:.3f} F1(tau)={F1_tau:.3f} TPR={tpr:.3f}")