SabaPivot's picture
download
raw
7.19 kB
"""Claim 5 (Figures 4 and 5, Section 5.1).
"On simulated data with adjacent-feature support, Semi-knockoffs maintains
type-I error control while achieving higher power than HRT, and derandomization
with 5 permutations under masked correlation further increases power."
Two settings, both n = 300, p = 50, 50 replicates (the paper's scale):
Figure 4 - ADJACENT SUPPORT. X ~ N(0, Sigma), Sigma_ij = 0.6^|i-j|,
y = beta'X + eps, the first 0.25p coordinates of beta in [1, 2], rest zero,
eps ~ N(0, 1). Pre-trained black box: gradient boosting.
Figure 5 - MASKED CORRELATION. X ~ N(0, Sigma), Sigma_ij = 0.6^|i-j|. One
relevant coordinate l, y = X_l + 0.5 eps1. A correlated NULL variable is
built as X_{l-1} = X_l + 0.5 eps2. Pre-trained black box: neural network.
Methods compared (all at alpha = 0.05 on the p-value scale):
HRT - Tansey et al. (2022), 50/50 train/test split, K = 200 draws
SKO_Wcx - Algorithm 1, single permutation, NO split
SKO_Wcx_p5 - Algorithm 1 Rao-Blackwellised over 5 permutations, NO split
LOCO_Wcx - refit-without-feature baseline with a Wilcoxon test on a
held-out split (a "VIM"-style comparator)
Reported: power, type-I error, AUC - the three panels of Figures 4 and 5.
"""
from __future__ import annotations
import json
import os
import sys
import time
import numpy as np
from joblib import Parallel, delayed
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.linear_model import Ridge
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import cross_val_score
from sklearn.neural_network import MLPRegressor
from scipy import stats
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__))))
from semiknockoffs import ( # noqa: E402
fit_nu_rho,
gen_adjacent,
gen_masked,
hrt_pvalue,
sko_pvalue,
sq_loss,
)
OUT = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "outputs"
)
SEED0 = 20260725
ALPHA = 0.05
def _blank(kind, seed):
if kind == "gb":
return GradientBoostingRegressor(random_state=seed)
if kind == "nn":
return MLPRegressor(random_state=seed)
raise ValueError(kind)
def _fit(kind, X, y, seed):
if kind == "gb":
return GradientBoostingRegressor(random_state=seed).fit(X, y)
if kind == "nn":
# scikit-learn defaults, which reproduce the paper's reported
# R2 = 0.567 (split) / 0.581 (no split) for the masked setting.
return MLPRegressor(random_state=seed).fit(X, y)
raise ValueError(kind)
def _rep(rep, setting, kind, n=300, p=50, K=200):
rng = np.random.default_rng(
SEED0 + 41_000_000 + 1231 * rep + (0 if setting == "adjacent" else 7)
)
if setting == "adjacent":
X, y, beta, Sigma, supp = gen_adjacent(n, p, rng)
else:
X, y, l, supp = gen_masked(n, p, rng)
truth = np.zeros(p, bool)
truth[supp] = True
m_full = _fit(kind, X, y, rep)
r2_full = float(
np.mean(cross_val_score(_blank(kind, rep), X, y, cv=5, scoring="r2"))
) # generalisation R2,
# the closest comparable to the paper's "R2 without split"
idx = rng.permutation(n)
tr, te = idx[: n // 2], idx[n // 2 :]
m_split = _fit(kind, X[tr], y[tr], rep)
r2_split = float(1 - np.mean((m_split.predict(X[te]) - y[te]) ** 2) / np.var(y[te]))
P = {k: np.ones(p) for k in ("HRT", "SKO_Wcx", "SKO_Wcx_p5", "LOCO_Wcx")}
for j in range(p):
cols = [k for k in range(p) if k != j]
nu, rho = fit_nu_rho(X, y, j, alpha=1.0)
P["SKO_Wcx"][j] = sko_pvalue(X, y, j, nu, rho, m_full.predict, sq_loss, rng)
P["SKO_Wcx_p5"][j] = sko_pvalue(
X, y, j, nu, rho, m_full.predict, sq_loss, rng, n_perm=5
)
# HRT: needs the split
rg = Ridge(alpha=1.0).fit(X[np.ix_(tr, cols)], X[tr, j])
res_tr = X[tr, j] - rg.predict(X[np.ix_(tr, cols)])
dfc = np.sqrt(len(tr) / max(1.0, len(tr) - p))
P["HRT"][j] = hrt_pvalue(
X[te],
y[te],
j,
rg.predict(X[np.ix_(te, cols)]),
res_tr * dfc,
m_split.predict,
sq_loss,
rng,
K=K,
)
# LOCO with a Wilcoxon test: refit the model without feature j on the
# train half, compare per-observation losses on the test half.
m_red = _fit(kind, X[np.ix_(tr, cols)], y[tr], rep)
d = sq_loss(m_red.predict(X[np.ix_(te, cols)]), y[te]) - sq_loss(
m_split.predict(X[te]), y[te]
)
nz = d[d != 0]
P["LOCO_Wcx"][j] = (
float(stats.wilcoxon(nz, alternative="greater").pvalue) if len(nz) else 1.0
)
out = {"R2_no_split_cv5": r2_full, "R2_split_heldout": r2_split}
for k, pv in P.items():
out[k] = {
"power": float(np.mean(pv[truth] <= ALPHA)),
"type_I": float(np.mean(pv[~truth] <= ALPHA)),
"auc": float(roc_auc_score(truth.astype(int), -pv)),
}
return out
def run(setting, kind, reps=50, n_jobs=50):
t0 = time.time()
got = Parallel(n_jobs=n_jobs)(delayed(_rep)(r, setting, kind) for r in range(reps))
res = {
"setting": setting,
"model": kind,
"replicates": reps,
"n": 300,
"p": 50,
"alpha": ALPHA,
"R2_no_split_cv5": float(
np.mean([g["R2_no_split_cv5"] for g in got])
),
"R2_split_heldout": float(np.mean([g["R2_split_heldout"] for g in got])),
"seconds": round(time.time() - t0, 1),
}
for k in ("HRT", "SKO_Wcx", "SKO_Wcx_p5", "LOCO_Wcx"):
for met in ("power", "type_I", "auc"):
v = np.array([g[k][met] for g in got])
res[f"{k}|{met}"] = float(np.mean(v))
res[f"{k}|{met}_se"] = float(np.std(v, ddof=1) / np.sqrt(len(v)))
print(
f"[{setting}/{kind}] {k:11s} power={res[k+'|power']:.3f} "
f"typeI={res[k+'|type_I']:.3f} auc={res[k+'|auc']:.3f}",
flush=True,
)
# paired comparisons on the same replicates
for a, b in (("SKO_Wcx", "HRT"), ("SKO_Wcx_p5", "SKO_Wcx"), ("SKO_Wcx_p5", "HRT")):
da = np.array([g[a]["power"] for g in got])
db = np.array([g[b]["power"] for g in got])
d = da - db
t = stats.ttest_rel(da, db)
res[f"paired_power_{a}_minus_{b}"] = {
"mean_diff": float(d.mean()),
"se": float(np.std(d, ddof=1) / np.sqrt(len(d))),
"t_pvalue": float(t.pvalue),
}
print(
f" paired power {a} - {b} = {d.mean():+.4f} " f"(p={t.pvalue:.2e})",
flush=True,
)
return res
if __name__ == "__main__":
os.makedirs(OUT, exist_ok=True)
res = {"seed0": SEED0}
res["figure4_adjacent_gb"] = run("adjacent", "gb")
res["figure5_masked_nn"] = run("masked", "nn")
res["figure4_adjacent_nn"] = run("adjacent", "nn")
res["figure5_masked_gb"] = run("masked", "gb")
with open(os.path.join(OUT, "claim5_power.json"), "w") as f:
json.dump(res, f, indent=2)
print("wrote", os.path.join(OUT, "claim5_power.json"))

Xet Storage Details

Size:
7.19 kB
·
Xet hash:
0835cad33d4a26102c6f44c3dd2db57108a5fc8b0b63e9de1aec8d13e5497813

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