SabaPivot's picture
download
raw
17.1 kB
"""Build every figure in figs/ from the JSON outputs in outputs/."""
from __future__ import annotations
import json
import os
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt # noqa: E402
import numpy as np # noqa: E402
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OUT = os.path.join(ROOT, "outputs")
FIGS = os.path.join(ROOT, "figs")
os.makedirs(FIGS, exist_ok=True)
ACC = "#1f6f8b"
ACC2 = "#e07a5f"
ACC3 = "#3d5a80"
GREY = "#8d99ae"
plt.rcParams.update(
{
"figure.dpi": 150,
"font.size": 9,
"axes.spines.top": False,
"axes.spines.right": False,
}
)
def load(name):
with open(os.path.join(OUT, name)) as f:
return json.load(f)
# ------------------------------------------------------------------- claim 1
def fig1():
d = load("claim1_pvalue_validity.json")
fig, ax = plt.subplots(1, 3, figsize=(9.5, 3.0))
A = d["A_oracle_uniformity"]
ns = [30, 100, 300]
for k, test in enumerate(("wilcoxon", "sign")):
v = [A[f"n{n}|{test}|perm"]["type_I_at_0.05"] for n in ns]
ax[0].plot(ns, v, "o-", color=[ACC, ACC2][k], label=f"SKO {test} (perm)")
v = [A[f"n{n}|{test}|iid"]["type_I_at_0.05"] for n in ns]
ax[0].plot(
ns,
v,
"s--",
color=[ACC, ACC2][k],
alpha=0.55,
label=f"SKO {test} (iid draws)",
)
ax[0].axhline(0.05, color="k", ls=":", lw=1, label="nominal 0.05")
ax[0].set_xscale("log")
ax[0].set_xlabel("n")
ax[0].set_ylabel("type-I error at $\\alpha=0.05$")
ax[0].set_title("Oracle $\\nu_j,\\rho_j$: null p-values", fontsize=9)
ax[0].set_ylim(0, 0.09)
ax[0].legend(fontsize=6, frameon=False)
B = d["B_split_vs_nosplit"]
names = ["SKO_Wcx_nosplit", "HRT_nosplit", "HRT_split", "HRT_split_oracle_sampler"]
lab = [
"SKO-Wcx\n(no split)",
"HRT\n(no split)",
"HRT\n(50/50 split)",
"HRT split\n(oracle sampler)",
]
v = [B[k]["type_I_at_0.05"] for k in names]
c = [ACC, ACC2, ACC3, GREY]
ax[1].bar(range(4), v, color=c)
ax[1].axhline(0.05, color="k", ls=":", lw=1)
ax[1].set_xticks(range(4))
ax[1].set_xticklabels(lab, fontsize=6)
ax[1].set_ylabel("type-I error at $\\alpha=0.05$")
ax[1].set_title("Estimated $\\hat\\nu,\\hat\\rho$, GB black box", fontsize=9)
for i, x in enumerate(v):
ax[1].text(i, x + 0.02, f"{x:.3f}", ha="center", fontsize=6)
C = d["C_heavy_tailed_elliptical"]
v2 = [
C["SKO_Wcx_oracle_nu"]["type_I_at_0.05"],
C["HRT_split_gaussian_sampler"]["type_I_at_0.05"],
]
ax[2].bar([0, 1], v2, color=[ACC, ACC2])
ax[2].axhline(0.05, color="k", ls=":", lw=1)
ax[2].set_xticks([0, 1])
ax[2].set_xticklabels(
["SKO-Wcx\n(cond. mean only)", "HRT split\n(Gaussian knockoff\nsampler)"],
fontsize=6,
)
ax[2].set_ylabel("type-I error at $\\alpha=0.05$")
ax[2].set_title("Heavy-tailed $t_3$ elliptical design", fontsize=9)
for i, x in enumerate(v2):
ax[2].text(i, x + 0.004, f"{x:.3f}", ha="center", fontsize=6)
fig.tight_layout()
fig.savefig(os.path.join(FIGS, "claim1_pvalue_validity.png"), bbox_inches="tight")
plt.close(fig)
# ------------------------------------------------------------------- claim 2
def fig2():
d = load("claim2_fdr.json")
qs = d["q_grid"]
fig, ax = plt.subplots(1, 2, figsize=(7.5, 3.0))
keys = [
("oracle_gb", "oracle $\\nu,\\rho$ + GB", ACC, "o-"),
("oracle_lasso", "oracle $\\nu,\\rho$ + Lasso", ACC3, "s-"),
("estimated_gb", "estimated $\\hat\\nu,\\hat\\rho$ + GB", ACC2, "^-"),
("estimated_gb_nonlinear", "mis-specified imputer", GREY, "v--"),
]
ax[0].plot([0, 0.55], [0, 0.55], "k:", lw=1, label="nominal $q$")
for k, lab, col, st in keys:
f = [d[k][str(q)]["FDR"] for q in qs]
e = [1.96 * d[k][str(q)]["FDR_se"] for q in qs]
ax[0].errorbar(qs, f, yerr=e, fmt=st, color=col, label=lab, ms=4, lw=1.2)
ax[0].set_xlabel("nominal level $q$")
ax[0].set_ylabel("realised FDR")
ax[0].set_title("Theorem 3.4: FDR$(S_{SKO}) \\leq q$", fontsize=9)
ax[0].legend(fontsize=6, frameon=False)
for k, lab, col, st in keys:
pw = [d[k][str(q)]["power"] for q in qs]
ax[1].plot(qs, pw, st, color=col, label=lab, ms=4, lw=1.2)
ax[1].set_xlabel("nominal level $q$")
ax[1].set_ylabel("power")
ax[1].set_title("Power at the knockoff threshold", fontsize=9)
ax[1].set_ylim(-0.03, 1.05)
fig.tight_layout()
fig.savefig(os.path.join(FIGS, "claim2_fdr.png"), bbox_inches="tight")
plt.close(fig)
# ------------------------------------------------------------------- claim 3
def fig3():
d = load("claim3_stability.json")
fig, ax = plt.subplots(1, 3, figsize=(9.5, 3.0))
ex = d["figure1"]["example"]
imp = np.array(ex["important"], bool)
idx = np.arange(len(imp))
ax[0].scatter(
idx[~imp],
np.array(ex["ridge"])[~imp],
s=14,
color=ACC,
label="Ridge, unimportant",
)
ax[0].scatter(
idx[imp], np.array(ex["ridge"])[imp], s=14, color=ACC2, label="Ridge, important"
)
ax[0].scatter(
idx[~imp],
np.array(ex["lasso"])[~imp],
s=22,
marker="x",
color=ACC,
alpha=0.6,
label="Lasso, unimportant",
)
ax[0].scatter(
idx[imp],
np.array(ex["lasso"])[imp],
s=22,
marker="x",
color=ACC2,
alpha=0.6,
label="Lasso, important",
)
ax[0].set_xlabel("Variable index $j$")
ax[0].set_ylabel(r"$\|\tilde\theta^j-\hat\theta\|_2$")
ax[0].set_title("Figure 1 reproduction ($n$=300, $p$=50)", fontsize=9)
ax[0].legend(fontsize=5.5, frameon=False)
for key, lab, col in (
("rate_ridge_lam1e-3", "Ridge $\\lambda=10^{-3}$", ACC),
("rate_ridge_lam1e-1", "Ridge $\\lambda=10^{-1}$", ACC3),
("rate_lasso_lam1e-2", "Lasso $\\lambda=10^{-2}$", ACC2),
("rate_ridge_heavy_t2", "$t_2$ design (E.2 broken)", GREY),
):
r = d[key]
ns = r["ns"]
q = r["quantiles"]["0.05"]["q_values"]
ax[1].loglog(
ns,
q,
"o-",
color=col,
ms=3.5,
lw=1.2,
label=f"{lab}: {r['quantiles']['0.05']['fitted_exponent']:+.2f}",
)
ref = np.array(d["rate_ridge_lam1e-3"]["ns"], float)
ax[1].loglog(ref, 3.5 * ref**-0.5, "k:", lw=1, label="slope $-1/2$")
ax[1].set_xlabel("n")
ax[1].set_ylabel("$Q_{0.95}$ of $\\|\\tilde\\theta^j-\\hat\\theta\\|_2$")
ax[1].set_title("Rate in $n$ (null $j$)", fontsize=9)
ax[1].legend(fontsize=5.5, frameon=False)
nr = d["nu_rho_coefficients"]
ax[2].loglog(
nr["ns"],
nr["null_q95"],
"o-",
color=ACC,
ms=4,
label=f"null $j$: {nr['null_fitted_exponent']:+.2f}",
)
ax[2].loglog(
nr["ns"],
nr["important_q95"],
"s-",
color=ACC2,
ms=4,
label=f"important $j$: {nr['important_fitted_exponent']:+.2f}",
)
r = np.array(nr["ns"], float)
ax[2].loglog(r, 7.0 * r**-0.5, "k:", lw=1, label="slope $-1/2$")
ax[2].set_xlabel("n")
ax[2].set_ylabel(r"$Q_{0.95}\|\hat\theta_{\hat\rho}-\hat\theta_{\hat\nu}\|_2$")
ax[2].set_title(r"$\hat\nu$ vs $\hat\rho$ coefficients", fontsize=9)
ax[2].legend(fontsize=6, frameon=False)
fig.tight_layout()
fig.savefig(os.path.join(FIGS, "claim3_stability.png"), bbox_inches="tight")
plt.close(fig)
# ------------------------------------------------------------------- claim 4
def fig4():
d = load("claim4_double_robustness.json")
fig, ax = plt.subplots(1, 4, figsize=(12, 2.9))
for k, (kind, lab) in enumerate(
(("rf", "Random Forest"), ("nn", "Neural Network"), ("gb", "Gradient Boosting"))
):
b = np.array(d["figure3"][kind]["blue_sample"])
o = np.array(d["figure3"][kind]["orange_sample"])
lim = np.quantile(np.abs(b), 0.995)
bins = np.linspace(-lim, lim, 61)
ax[k].hist(
b,
bins=bins,
color=ACC,
alpha=0.75,
label=r"$l(\hat m(\tilde X'_1),y)-l(\hat m(\tilde X'_2),y)$",
)
ax[k].hist(
o,
bins=bins,
color=ACC2,
alpha=0.75,
label=r"$l(\hat m(\tilde X'_1),y)-l(\hat m(\tilde X_1),y)$",
)
ax[k].set_title(
f"$\\hat m$ = {lab}\nsd ratio "
f"{d['figure3'][kind]['concentration_ratio_std']:.1f}$\\times$",
fontsize=8,
)
ax[k].set_xlabel("Loss difference value")
if k == 0:
ax[k].set_ylabel("Frequency")
ax[k].legend(fontsize=5.5, frameon=False)
for key, lab, col, st in (
("rate_linear_null", "linear, null $j$", ACC, "o-"),
("rate_linear_important", "linear, important $j$", ACC2, "s-"),
("rate_nn_null", "NN, null $j$", ACC3, "^-"),
):
if key not in d:
continue
r = d[key]
ax[3].loglog(
r["ns"],
r["D_n_q90_abs"],
st,
color=col,
ms=3.5,
lw=1.2,
label=f"{lab}: {r['exponent_D_n_q90_abs']:+.2f}",
)
ref = np.array(d["rate_linear_null"]["ns"], float)
ax[3].loglog(
ref,
ref**-1.0 * d["rate_linear_null"]["D_n_q90_abs"][0] * ref[0],
"k:",
lw=1,
label="slope $-1$",
)
ax[3].loglog(
ref,
ref**-0.5 * d["rate_linear_important"]["D_n_q90_abs"][0] * ref[0] ** 0.5,
"k--",
lw=1,
label="slope $-1/2$",
)
ax[3].set_xlabel("n")
ax[3].set_ylabel("$Q_{0.9}|l(\\hat m(\\tilde X'))-l(\\hat m(\\tilde X))|$")
ax[3].set_title("Compound rate $a_n b_n$", fontsize=8)
ax[3].legend(fontsize=5.5, frameon=False)
fig.tight_layout()
fig.savefig(os.path.join(FIGS, "claim4_double_robustness.png"), bbox_inches="tight")
plt.close(fig)
# ------------------------------------------------------------------- claim 5
def fig5():
d = load("claim5_power.json")
meth = ["LOCO_Wcx", "HRT", "SKO_Wcx", "SKO_Wcx_p5"]
cols = [GREY, ACC3, ACC, ACC2]
panels = [
("figure4_adjacent_gb", "Fig. 4 - adjacent support (GB)"),
("figure5_masked_nn", "Fig. 5 - masked correlation (NN)"),
]
fig, ax = plt.subplots(2, 3, figsize=(9.5, 5.2))
for r, (key, title) in enumerate(panels):
dd = d[key]
for c, met in enumerate(("power", "type_I", "auc")):
v = [dd[f"{m}|{met}"] for m in meth]
e = [1.96 * dd[f"{m}|{met}_se"] for m in meth]
ax[r, c].barh(
range(4), v, xerr=e, color=cols, height=0.65, error_kw={"lw": 0.8}
)
ax[r, c].set_yticks(range(4))
ax[r, c].set_yticklabels(meth, fontsize=7)
ax[r, c].set_xlabel(
{"power": "Power", "type_I": "Type-I Error", "auc": "AUC"}[met],
fontsize=8,
)
if met == "type_I":
ax[r, c].axvline(0.05, color="k", ls=":", lw=1)
for i, x in enumerate(v):
ax[r, c].text(x, i, f" {x:.3f}", va="center", fontsize=6)
if c == 0:
ax[r, c].set_title(title, fontsize=9, loc="left")
fig.tight_layout()
fig.savefig(os.path.join(FIGS, "claim5_power.png"), bbox_inches="tight")
plt.close(fig)
# ------------------------------------------------------------------- claim 6
def fig6():
d = load("claim6_wdbc.json")
kinds = [
("rf", "Random Forest"),
("nn", "Neural Network"),
("gb", "Gradient Boosting"),
]
meth = ["HRT_split", "SKO_Wcx", "SKO_Wcx_p5"]
cols = [ACC3, ACC, ACC2]
fig, ax = plt.subplots(1, 4, figsize=(11.5, 3.0))
for k, (kind, lab) in enumerate(kinds):
v = [d[kind][m]["mean_discoveries"] for m in meth]
e = [d[kind][m]["sd_discoveries"] for m in meth]
t1 = [d[kind][m]["artificial_null_rejection_rate"] for m in meth]
ax[k].barh(range(3), v, xerr=e, color=cols, height=0.6, error_kw={"lw": 0.8})
ax[k].set_yticks(range(3))
ax[k].set_yticklabels(meth, fontsize=7)
ax[k].set_xlabel("Discoveries (of 30 real features)", fontsize=8)
ax[k].set_title(f"$\\hat m$ = {lab}", fontsize=9)
for i, (x, tt) in enumerate(zip(v, t1)):
ax[k].text(x, i, f" {x:.1f} (type-I {tt:.2f})", va="center", fontsize=6)
ax[k].set_xlim(0, max(v) * 1.9)
cm = d["cross_model"]["per_feature_rate_by_model"]
names = [n for n in cm if n != "ARTIFICIAL_NULL"]
M = np.array([cm[n] for n in names])
order = np.argsort(-M.mean(1))
im = ax[3].imshow(M[order], aspect="auto", cmap="Blues", vmin=0, vmax=1)
ax[3].set_xticks(range(3))
ax[3].set_xticklabels(["RF", "NN", "GB"], fontsize=7)
ax[3].set_yticks(range(len(names)))
ax[3].set_yticklabels([names[i][:22] for i in order], fontsize=4.5)
ax[3].set_title("SKO-Wcx-p5 rejection rate\nacross seeds", fontsize=8)
fig.colorbar(im, ax=ax[3], fraction=0.04)
fig.tight_layout()
fig.savefig(os.path.join(FIGS, "claim6_wdbc.png"), bbox_inches="tight")
plt.close(fig)
def fig5_poster():
"""Compact single-row version of the Figure-4 panel for the poster."""
d = load("claim5_power.json")
meth = ["LOCO_Wcx", "HRT", "SKO_Wcx", "SKO_Wcx_p5"]
cols = [GREY, ACC3, ACC, ACC2]
dd = d["figure4_adjacent_gb"]
fig, ax = plt.subplots(1, 3, figsize=(9.5, 2.5))
for c, met in enumerate(("power", "type_I", "auc")):
v = [dd[f"{m}|{met}"] for m in meth]
e = [1.96 * dd[f"{m}|{met}_se"] for m in meth]
ax[c].barh(range(4), v, xerr=e, color=cols, height=.65,
error_kw={"lw": .8})
ax[c].set_yticks(range(4))
ax[c].set_yticklabels(meth, fontsize=7)
ax[c].set_xlabel({"power": "Power", "type_I": "Type-I Error",
"auc": "AUC"}[met], fontsize=8)
if met == "type_I":
ax[c].axvline(0.05, color="k", ls=":", lw=1)
for i, x in enumerate(v):
ax[c].text(x, i, f" {x:.3f}", va="center", fontsize=6)
ax[0].set_title("Fig. 4 - adjacent support, gradient boosting, n=300, p=50, 50 reps",
fontsize=9, loc="left")
fig.tight_layout()
fig.savefig(os.path.join(FIGS, "claim5_power_poster.png"), bbox_inches="tight")
plt.close(fig)
# ------------------------------------------------------------------ claim 5b
def fig5b():
d = load("claim5b_derandomization.json")
c = load("claim5c_derand_validity.json")
perms = d["permutation_grid"]
ns = [50, 75, 100, 150, 300]
fig, ax = plt.subplots(1, 3, figsize=(9.5, 3.0))
cmap = plt.cm.viridis(np.linspace(0.1, 0.85, len(ns)))
for i, n in enumerate(ns):
v = [d[f"n{n}"][f"perm{k}"]["power"] for k in perms]
ax[0].plot(perms, v, "o-", color=cmap[i], ms=4, label=f"n={n}")
t = [d[f"n{n}"][f"perm{k}"]["type_I"] for k in perms]
ax[1].plot(perms, t, "o-", color=cmap[i], ms=4, label=f"n={n}")
ax[0].set_xscale("log")
ax[0].set_xticks(perms)
ax[0].set_xticklabels(perms)
ax[0].set_xlabel("permutations (derandomisation)")
ax[0].set_ylabel("power")
ax[0].set_title("Masked correlation: power gain", fontsize=9)
ax[0].legend(fontsize=6, frameon=False)
ax[1].axhline(0.05, color="k", ls=":", lw=1)
ax[1].set_xscale("log")
ax[1].set_xticks(perms)
ax[1].set_xticklabels(perms)
ax[1].set_xlabel("permutations (derandomisation)")
ax[1].set_ylabel("type-I error at $\\alpha=0.05$")
ax[1].set_title("...bought with type-I inflation", fontsize=9)
for key, lab, col in (
("oracle_n300", "oracle $\\nu=\\rho$, n=300", ACC),
("estimated_n100", "estimated, n=100", ACC2),
("estimated_n300", "estimated, n=300", ACC3),
("estimated_n1000", "estimated, n=1000", GREY),
):
if key not in c:
continue
v = [c[key][f"perm{k}"]["type_I_at_0.05"] for k in perms]
ax[2].plot(perms, v, "o-", color=col, ms=4, label=lab)
ax[2].axhline(0.05, color="k", ls=":", lw=1)
ax[2].set_xscale("log")
ax[2].set_xticks(perms)
ax[2].set_xticklabels(perms)
ax[2].set_xlabel("permutations")
ax[2].set_ylabel("type-I error at $\\alpha=0.05$")
ax[2].set_title("Global null: the inflation is\nan ESTIMATION effect", fontsize=9)
ax[2].legend(fontsize=6, frameon=False)
fig.tight_layout()
fig.savefig(os.path.join(FIGS, "claim5b_derandomization.png"), bbox_inches="tight")
plt.close(fig)
if __name__ == "__main__":
for fn in (fig1, fig2, fig3, fig4, fig5, fig5_poster, fig5b, fig6):
try:
fn()
print("ok", fn.__name__)
except Exception as exc: # noqa: BLE001
print("FAILED", fn.__name__, type(exc).__name__, exc)

Xet Storage Details

Size:
17.1 kB
·
Xet hash:
67c8120b9664422751873e518daa1c74d6a1dbcdaeeb2918b22c12ad8e610716

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.