SabaPivot's picture
download
raw
9.92 kB
"""Claim 1 (Theorem 3.3).
"Semi-knockoffs avoids the train-test data split required by prior CIT methods
such as HRT while still yielding valid p-values, via nonparametric paired tests
requiring only conditional expectations nu_j and rho_j rather than exact
knockoff construction."
Four independent tests:
A. ORACLE validity + resampling-scheme audit. Gaussian linear design, nu_j and
rho_j in CLOSED FORM (the exact hypothesis of Theorem 3.3). Null p-values of
the Wilcoxon and sign-test versions collected over many replicates and tested
for uniformity (one-sample KS). Swept over n in {30, 100, 300}.
Boundary probe: Algorithms 1-4 say "sample pi_{j,1}, pi_{j,2} PERMUTATIONS",
while the proof in Appendix E.2 uses i.i.d. uniform indices U_1..U_n. Under
permutations the n paired differences are NOT independent, which the Wilcoxon
signed-rank / sign test formally requires. Both schemes are measured.
B. NO-SPLIT vs SPLIT, everything ESTIMATED (Algorithm 1). Same data, same
gradient-boosting architecture:
- SKO-Wcx, model trained on ALL n samples, tested on those same n samples;
- HRT with NO split (model + sampler fit on all n, test on all n);
- HRT done properly with a 50/50 split (df-corrected residual sampler);
- HRT with a 50/50 split and the EXACT Gaussian conditional sampler.
C. "ONLY CONDITIONAL EXPECTATIONS, NOT EXACT KNOCKOFFS". Heavy-tailed
multivariate-t(3) elliptical design. There the conditional MEAN is still
linear but the conditional distribution is heteroscedastic and non-Gaussian,
so a Gaussian model-X knockoff / sampler is mis-specified. SKO should stay
valid (its exchangeability argument only needs rho = nu under the null);
a Gaussian-sampler HRT should not.
"""
from __future__ import annotations
import json
import os
import sys
import time
import numpy as np
from joblib import Parallel, delayed
from scipy import stats
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.linear_model import Ridge
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from semiknockoffs import ( # noqa: E402
ar1_cov,
fit_nu_rho,
gaussian_nu,
gaussian_rho,
gen_adjacent,
hrt_pvalue,
sko_pvalue,
sq_loss,
)
OUT = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "outputs"
)
SEED0 = 20260725
def _ks(pv):
return stats.kstest(pv, "uniform")
def _pack(pv, secs=None):
ks = _ks(pv)
d = {
"n_pvalues": int(len(pv)),
"ks_stat": float(ks.statistic),
"ks_pvalue": float(ks.pvalue),
"mean_pvalue": float(np.mean(pv)),
"type_I_at_0.05": float(np.mean(pv <= 0.05)),
"type_I_at_0.10": float(np.mean(pv <= 0.10)),
"type_I_at_0.20": float(np.mean(pv <= 0.20)),
}
if secs is not None:
d["seconds"] = round(secs, 1)
return d
# ---------------------------------------------------------------- experiment A
def _oracle_rep(rep, n, p, rho, test, scheme):
tag = {"wilcoxon": 0, "sign": 1}[test] * 2 + {"perm": 0, "iid": 1}[scheme]
rng = np.random.default_rng(SEED0 + 1000 * rep + 137 * tag + 31 * n)
Sigma = ar1_cov(p, rho)
L = np.linalg.cholesky(Sigma)
X = rng.standard_normal((n, p)) @ L.T
beta = np.zeros(p)
beta[0] = 1.5
beta[1] = 1.0 # supp = {0, 1}; every other j is null
sigma = 1.0
y = X @ beta + sigma * rng.standard_normal(n)
# black-box pre-trained model, trained on ALL n samples (no split)
model = GradientBoostingRegressor(random_state=rep).fit(X, y)
out = []
for j in range(2, min(p, 12)): # null coordinates only
nu = gaussian_nu(X, j, Sigma)
rhoj = gaussian_rho(X, y, j, Sigma, beta, sigma)
out.append(
sko_pvalue(
X,
y,
j,
nu,
rhoj,
model.predict,
sq_loss,
rng,
test=test,
scheme=scheme,
)
)
return out
def exp_A(p=50, reps=200, n_jobs=100):
res = {}
for n in (30, 100, 300):
for test in ("wilcoxon", "sign"):
for scheme in ("perm", "iid"):
t0 = time.time()
lists = Parallel(n_jobs=n_jobs)(
delayed(_oracle_rep)(r, n, p, 0.6, test, scheme)
for r in range(reps)
)
key = f"n{n}|{test}|{scheme}"
res[key] = _pack(np.concatenate(lists), time.time() - t0)
print(f"[A] {key:22s} {res[key]}", flush=True)
return res
# ---------------------------------------------------------------- experiment B
def _split_rep(rep, n, p):
rng = np.random.default_rng(SEED0 + 7_000_000 + rep)
X, y, beta, Sigma, supp = gen_adjacent(n, p, rng)
nulls = np.array([j for j in range(p) if beta[j] == 0])[:12]
m_full = GradientBoostingRegressor(random_state=rep).fit(X, y)
idx = rng.permutation(n)
tr, te = idx[: n // 2], idx[n // 2 :]
m_split = GradientBoostingRegressor(random_state=rep).fit(X[tr], y[tr])
r2_split = float(1 - np.mean((m_split.predict(X[te]) - y[te]) ** 2) / np.var(y[te]))
rows = []
for j in nulls:
cols = [k for k in range(p) if k != j]
nu, rhoj = fit_nu_rho(X, y, j, alpha=1.0)
p_sko = sko_pvalue(X, y, j, nu, rhoj, m_full.predict, sq_loss, rng)
# HRT with NO split at all
res_full = X[:, j] - nu
p_hrt_nosplit = hrt_pvalue(
X, y, j, nu, res_full, m_full.predict, sq_loss, rng, K=200
)
# HRT with a proper 50/50 split; residuals df-corrected so the resampled
# conditional variance is unbiased (n_tr - p degrees of freedom).
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))
nu_te = rg.predict(X[np.ix_(te, cols)])
p_hrt_split = hrt_pvalue(
X[te], y[te], j, nu_te, res_tr * dfc, m_split.predict, sq_loss, rng, K=200
)
# HRT with a split AND the exact Gaussian conditional sampler
nu_or = gaussian_nu(X, j, Sigma)[te]
S_mj = Sigma[np.ix_(cols, cols)]
cvar = float(
Sigma[j, j] - Sigma[j, cols] @ np.linalg.solve(S_mj, Sigma[cols, j])
)
pool = np.sqrt(cvar) * rng.standard_normal(4000)
p_hrt_split_or = hrt_pvalue(
X[te], y[te], j, nu_or, pool, m_split.predict, sq_loss, rng, K=200
)
rows.append((p_sko, p_hrt_nosplit, p_hrt_split, p_hrt_split_or))
return rows, r2_split
def exp_B(n=300, p=50, reps=100, n_jobs=100):
t0 = time.time()
got = Parallel(n_jobs=n_jobs)(delayed(_split_rep)(r, n, p) for r in range(reps))
rows = np.array([r for g in got for r in g[0]])
res = {
"R2_with_split_heldout": float(np.mean([g[1] for g in got])),
"n_null_pvalues": int(rows.shape[0]),
"replicates": reps,
"seconds": round(time.time() - t0, 1),
}
for k, nm in enumerate(
["SKO_Wcx_nosplit", "HRT_nosplit", "HRT_split", "HRT_split_oracle_sampler"]
):
res[nm] = _pack(rows[:, k])
print(f"[B] {nm:26s} {res[nm]}", flush=True)
return res
# ---------------------------------------------------------------- experiment C
def _t_rep(rep, n, p, df=3.0):
"""Elliptical multivariate-t(df) design: conditional mean still linear,
conditional law heteroscedastic and non-Gaussian."""
rng = np.random.default_rng(SEED0 + 5_000_000 + rep)
Sigma = ar1_cov(p, 0.6)
L = np.linalg.cholesky(Sigma)
g = rng.standard_normal((n, p)) @ L.T
w = np.sqrt(df / rng.chisquare(df, size=n))[:, None]
X = g * w # multivariate t_df(0, Sigma)
beta = np.zeros(p)
beta[0], beta[1] = 1.5, 1.0
y = X @ beta + rng.standard_normal(n)
m_full = GradientBoostingRegressor(random_state=rep).fit(X, y)
idx = rng.permutation(n)
tr, te = idx[: n // 2], idx[n // 2 :]
m_split = GradientBoostingRegressor(random_state=rep).fit(X[tr], y[tr])
out = []
for j in range(2, 12):
cols = [k for k in range(p) if k != j]
# ORACLE nu for an elliptical law: same linear map as the Gaussian case.
nu = gaussian_nu(X, j, Sigma)
rhoj = nu.copy() # under the null rho == nu exactly
p_sko = sko_pvalue(X, y, j, nu, rhoj, m_full.predict, sq_loss, rng)
# HRT with a split and a mis-specified GAUSSIAN conditional sampler
S_mj = Sigma[np.ix_(cols, cols)]
cvar = float(
Sigma[j, j] - Sigma[j, cols] @ np.linalg.solve(S_mj, Sigma[cols, j])
)
pool = np.sqrt(cvar) * rng.standard_normal(4000)
p_hrt = hrt_pvalue(
X[te], y[te], j, nu[te], pool, m_split.predict, sq_loss, rng, K=200
)
out.append((p_sko, p_hrt))
return out
def exp_C(n=300, p=50, reps=200, n_jobs=100):
t0 = time.time()
got = Parallel(n_jobs=n_jobs)(delayed(_t_rep)(r, n, p) for r in range(reps))
rows = np.array([r for g in got for r in g])
res = {
"design": "multivariate t(3), AR(1) rho=0.6",
"replicates": reps,
"seconds": round(time.time() - t0, 1),
}
for k, nm in enumerate(["SKO_Wcx_oracle_nu", "HRT_split_gaussian_sampler"]):
res[nm] = _pack(rows[:, k])
print(f"[C] {nm:28s} {res[nm]}", flush=True)
return res
if __name__ == "__main__":
os.makedirs(OUT, exist_ok=True)
result = {
"seed0": SEED0,
"A_oracle_uniformity": exp_A(),
"B_split_vs_nosplit": exp_B(),
"C_heavy_tailed_elliptical": exp_C(),
}
with open(os.path.join(OUT, "claim1_pvalue_validity.json"), "w") as f:
json.dump(result, f, indent=2)
print("wrote", os.path.join(OUT, "claim1_pvalue_validity.json"))

Xet Storage Details

Size:
9.92 kB
·
Xet hash:
532558ef475d28ba4c0a50f371d22f6659f29b053e2575510c909458fcfe22b2

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