Improvements! And another figure.

This commit is contained in:
2026-07-03 22:15:05 +03:00
parent a078f44838
commit 96d74ef901
4 changed files with 143 additions and 1 deletions
+125
View File
@@ -0,0 +1,125 @@
"""
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)
# Left-side y-axis labels for the intercepts
x_min = x[0]
ax2.text(x_min - 0.15, F0_tau, rf"$1-\alpha$",
fontsize=8, color=C_NEG, va="center", ha="right")
ax2.text(x_min - 0.15, F1_tau, r"$\hat{F}_1(\tau)$" + "\n(loss)",
fontsize=7.5, color=C_POS, va="center", 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}")