SabaPivot's picture
download
raw
6.13 kB
"""Claim 2 (Theorem 3.4): FDR(S_SKO) <= q.
Decisive design: a known ground-truth support, MANY replicates, and the realised
FDR (mean FDP over replicates) compared with the nominal q at several q values.
Two regimes:
* ORACLE (Algorithm 3, the exact hypothesis "Given nu_j and rho_j"):
Gaussian design with KNOWN Sigma and beta, so nu_j and rho_j are the exact
closed-form conditional expectations. If Theorem 3.4 is true, FDR <= q must
hold here for EVERY q, at finite n, for ANY model m.
* ESTIMATED (Algorithm 4): nu_j, rho_j are ridge regressions fitted on the
same n samples. Theorem 3.4 does not formally cover this case (Sections
4.2-4.5 argue it approximately holds).
Boundary probes:
* a deliberately mis-specified / nonlinear design where the ridge imputer is
biased (rho_hat != nu_hat under the null) - does estimated SKO break?
* the "BH on Wilcoxon p-values" alternative the paper says should be worse.
"""
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 Lasso
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from semiknockoffs import ( # noqa: E402
ar1_cov,
fdp,
fit_nu_rho,
gaussian_nu,
gaussian_rho,
knockoff_select,
power,
sko_pvalue,
sko_statistic,
sq_loss,
)
OUT = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "outputs"
)
SEED0 = 20260725
QS = [0.05, 0.1, 0.2, 0.3, 0.5]
def _make(rep, n, p, rho, k, tag):
rng = np.random.default_rng(SEED0 + 3_000_000 + 1009 * rep + 7 * tag)
Sigma = ar1_cov(p, rho)
L = np.linalg.cholesky(Sigma)
X = rng.standard_normal((n, p)) @ L.T
beta = np.zeros(p)
supp = np.arange(k)
beta[supp] = rng.uniform(1.0, 2.0, size=k)
sigma = 1.0
y = X @ beta + sigma * rng.standard_normal(n)
return rng, X, y, beta, Sigma, sigma, supp
def _model(kind, X, y, rep):
if kind == "gb":
return GradientBoostingRegressor(random_state=rep).fit(X, y).predict
if kind == "lasso":
return Lasso(alpha=0.05).fit(X, y).predict
raise ValueError(kind)
def _rep_oracle(rep, n, p, k, kind):
rng, X, y, beta, Sigma, sigma, supp = _make(rep, n, p, 0.6, k, 0)
pred = _model(kind, X, y, rep)
W = np.empty(p)
for j in range(p):
nu = gaussian_nu(X, j, Sigma)
rh = gaussian_rho(X, y, j, Sigma, beta, sigma)
W[j] = sko_statistic(X, y, j, nu, rh, pred, sq_loss, rng)
return [
(fdp(knockoff_select(W, q), supp), power(knockoff_select(W, q), supp))
for q in QS
]
def _rep_estimated(rep, n, p, k, kind, alpha=1.0, nonlinear=False):
rng, X, y, beta, Sigma, sigma, supp = _make(rep, n, p, 0.6, k, 1)
if nonlinear:
# break the linear-imputer specification: X^j depends nonlinearly on X^-j
Z = X.copy()
X = np.sign(Z) * np.abs(Z) ** 1.5
y = Z @ beta + sigma * rng.standard_normal(n)
pred = _model(kind, X, y, rep)
W = np.empty(p)
for j in range(p):
nu, rh = fit_nu_rho(X, y, j, alpha=alpha)
W[j] = sko_statistic(X, y, j, nu, rh, pred, sq_loss, rng)
return [
(fdp(knockoff_select(W, q), supp), power(knockoff_select(W, q), supp))
for q in QS
]
def _rep_bh(rep, n, p, k, kind):
"""Wilcoxon p-values + Benjamini-Hochberg, the alternative route."""
rng, X, y, beta, Sigma, sigma, supp = _make(rep, n, p, 0.6, k, 2)
pred = _model(kind, X, y, rep)
pv = np.empty(p)
for j in range(p):
nu = gaussian_nu(X, j, Sigma)
rh = gaussian_rho(X, y, j, Sigma, beta, sigma)
pv[j] = sko_pvalue(X, y, j, nu, rh, pred, sq_loss, rng)
out = []
order = np.argsort(pv)
ranked = pv[order]
for q in QS:
thr = ranked <= q * np.arange(1, p + 1) / p
nsel = np.max(np.where(thr)[0]) + 1 if thr.any() else 0
sel = order[:nsel]
out.append((fdp(sel, supp), power(sel, supp)))
return out
def summarise(rows, label):
arr = np.array(rows) # (reps, len(QS), 2)
res = {}
for i, q in enumerate(QS):
f = arr[:, i, 0]
pw = arr[:, i, 1]
se = float(np.std(f, ddof=1) / np.sqrt(len(f)))
res[str(q)] = {
"FDR": float(np.mean(f)),
"FDR_se": se,
"FDR_upper95": float(np.mean(f) + 1.96 * se),
"power": float(np.nanmean(pw)),
"controlled": bool(np.mean(f) <= q),
}
print(
f"[{label}] q={q:<5} FDR={np.mean(f):.4f} (+-{1.96*se:.4f}) "
f"power={np.nanmean(pw):.3f} ctrl={np.mean(f) <= q}",
flush=True,
)
res["replicates"] = int(arr.shape[0])
return res
def run(fn, reps, n_jobs, label, **kw):
t0 = time.time()
rows = Parallel(n_jobs=n_jobs)(delayed(fn)(r, **kw) for r in range(reps))
out = summarise(rows, label)
out["seconds"] = round(time.time() - t0, 1)
return out
if __name__ == "__main__":
os.makedirs(OUT, exist_ok=True)
res = {"seed0": SEED0, "q_grid": QS, "n": 300, "p": 50, "k_support": 12}
res["oracle_gb"] = run(
_rep_oracle, 500, 100, "oracle-GB", n=300, p=50, k=12, kind="gb"
)
res["oracle_lasso"] = run(
_rep_oracle, 500, 100, "oracle-Lasso", n=300, p=50, k=12, kind="lasso"
)
res["estimated_gb"] = run(
_rep_estimated, 500, 100, "estim-GB", n=300, p=50, k=12, kind="gb"
)
res["estimated_gb_nonlinear"] = run(
_rep_estimated,
300,
100,
"estim-GB-nonlin",
n=300,
p=50,
k=12,
kind="gb",
nonlinear=True,
)
res["oracle_gb_BH"] = run(
_rep_bh, 300, 100, "oracle-GB-BH", n=300, p=50, k=12, kind="gb"
)
res["oracle_gb_global_null"] = run(
_rep_oracle, 300, 100, "oracle-GB-globalnull", n=300, p=50, k=0, kind="gb"
)
with open(os.path.join(OUT, "claim2_fdr.json"), "w") as f:
json.dump(res, f, indent=2)
print("wrote", os.path.join(OUT, "claim2_fdr.json"))

Xet Storage Details

Size:
6.13 kB
·
Xet hash:
ab513299a1adaf4d85ab76014d02f9fcfd63d8b0af2c172a863dfd704d3ae411

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