Spaces:
Running
Running
Repurpose as ICML-2026 repro logbook: Semi-knockoffs (arXiv:2601.23124, Xf9hJMGwDd)
97afa54 verified | """All six claims of Semi-knockoffs (arXiv:2601.23124v1).""" | |
| import json | |
| import os | |
| import warnings | |
| import numpy as np | |
| from scipy.stats import kstest | |
| from sklearn.datasets import load_breast_cancer | |
| from sklearn.ensemble import GradientBoostingRegressor, RandomForestRegressor | |
| from sklearn.linear_model import Ridge | |
| from sklearn.neural_network import MLPRegressor | |
| from sklearn.preprocessing import StandardScaler | |
| warnings.filterwarnings("ignore") | |
| from skocore import (ar1_design, sko_pvalue, sko_select, sko_statistic, | |
| fdp_power, knockoff_plus_threshold) | |
| os.makedirs("outputs", exist_ok=True) | |
| OUT = {} | |
| def model_for(kind, X, y, seed=0): | |
| m = {"gb": GradientBoostingRegressor(random_state=seed), | |
| "rf": RandomForestRegressor(n_estimators=100, random_state=seed, n_jobs=-1), | |
| "nn": MLPRegressor(hidden_layer_sizes=(64, 32), max_iter=600, random_state=seed), | |
| }[kind] | |
| return m.fit(X, y) | |
| # ------------------------------------------------------ claim 1: type-I error | |
| def claim1(reps=60, n=300, p=50, rho=0.5, k_nonnull=None, alpha=0.05): | |
| k_nonnull = k_nonnull or int(0.25 * p) | |
| pv_null, pv_alt = [], [] | |
| for r in range(reps): | |
| rng = np.random.default_rng(500 + r) | |
| X = ar1_design(n, p, rho, rng) | |
| beta = np.zeros(p); beta[:k_nonnull] = 1.0 | |
| y = X @ beta + rng.standard_normal(n) | |
| m = model_for("gb", X, y, seed=r) | |
| # test a null feature and a non-null feature each rep | |
| jn = int(rng.integers(k_nonnull, p)) | |
| ja = int(rng.integers(0, k_nonnull)) | |
| pv_null.append(sko_pvalue(X, y, jn, m, rng, seed=r)) | |
| pv_alt.append(sko_pvalue(X, y, ja, m, rng, seed=r)) | |
| pv_null = np.array(pv_null); pv_alt = np.array(pv_alt) | |
| ks = kstest(pv_null, "uniform") | |
| OUT["claim1"] = { | |
| "reps": reps, "n": n, "p": p, "rho": rho, "n_nonnull": k_nonnull, | |
| "alpha": alpha, | |
| "type_I_error": float((pv_null <= alpha).mean()), | |
| "power_at_alpha": float((pv_alt <= alpha).mean()), | |
| "null_pvalue_mean": float(pv_null.mean()), | |
| "ks_uniform_stat": float(ks.statistic), "ks_uniform_p": float(ks.pvalue), | |
| "no_train_test_split": True, | |
| } | |
| print("claim1", json.dumps(OUT["claim1"]), flush=True) | |
| # --------------------------------------------------------- claim 2: FDR <= q | |
| def claim2(reps=40, n=300, p=40, rho=0.5, q=0.2): | |
| k = int(0.25 * p) | |
| fdps, powers = [], [] | |
| for r in range(reps): | |
| rng = np.random.default_rng(900 + r) | |
| X = ar1_design(n, p, rho, rng) | |
| beta = np.zeros(p); beta[:k] = 1.5 | |
| y = X @ beta + rng.standard_normal(n) | |
| m = model_for("gb", X, y, seed=r) | |
| sel, W, T = sko_select(X, y, m, rng, q=q, seed=r) | |
| f, pw = fdp_power(sel, range(k)) | |
| fdps.append(f); powers.append(pw) | |
| OUT["claim2"] = { | |
| "reps": reps, "n": n, "p": p, "q": q, "n_nonnull": k, | |
| "empirical_FDR": float(np.mean(fdps)), | |
| "FDR_se": float(np.std(fdps, ddof=1) / np.sqrt(reps)), | |
| "power": float(np.mean(powers)), | |
| "controls_at_q": bool(np.mean(fdps) <= q), | |
| } | |
| print("claim2", json.dumps(OUT["claim2"]), flush=True) | |
| # ------------------- claim 3: ||theta_tilde - theta_hat|| = O_P(sqrt(log(1/d)/n)) | |
| def claim3(ns=(150, 300, 600, 1200, 2400), p=20, reps=20, rho=0.5, lam=1.0): | |
| rows = [] | |
| for n in ns: | |
| d = [] | |
| for r in range(reps): | |
| rng = np.random.default_rng(77 + r) | |
| X = ar1_design(n, p, rho, rng) | |
| beta = np.zeros(p); beta[:5] = 1.0 # feature p-1 is null | |
| y = X @ beta + rng.standard_normal(n) | |
| j = p - 1 | |
| full = Ridge(alpha=lam).fit(X, y).coef_ | |
| Xd = X.copy(); Xd[:, j] = 0.0 # drop the null feature | |
| drop = Ridge(alpha=lam).fit(Xd, y).coef_ | |
| d.append(float(np.linalg.norm(full - drop))) | |
| rows.append({"n": n, "mean_diff": float(np.mean(d)), | |
| "sd": float(np.std(d, ddof=1))}) | |
| print(f" claim3 n={n} ||theta~-theta^||={np.mean(d):.5f}", flush=True) | |
| lx = np.log([r["n"] for r in rows]); ly = np.log([r["mean_diff"] for r in rows]) | |
| A = np.vstack([lx, np.ones_like(lx)]).T | |
| sl, ic = np.linalg.lstsq(A, ly, rcond=None)[0] | |
| pred = A @ np.array([sl, ic]) | |
| r2 = 1 - float(((ly - pred) ** 2).sum()) / float(((ly - ly.mean()) ** 2).sum()) | |
| OUT["claim3"] = {"rows": rows, "slope": float(sl), "r2": float(r2), | |
| "predicted_slope": -0.5, "reps": reps, "p": p, "lambda": lam} | |
| print("claim3 slope", sl, "r2", r2, flush=True) | |
| # ---------------- claim 4: double robustness, |W_j| decays at a compound rate | |
| def claim4(ns=(150, 300, 600, 1200, 2400), p=20, reps=15, rho=0.5): | |
| rows = [] | |
| for n in ns: | |
| w = [] | |
| for r in range(reps): | |
| rng = np.random.default_rng(313 + r) | |
| X = ar1_design(n, p, rho, rng) | |
| beta = np.zeros(p); beta[:5] = 1.0 | |
| y = X @ beta + rng.standard_normal(n) | |
| m = model_for("gb", X, y, seed=r) | |
| w.append(abs(sko_statistic(X, y, p - 1, m, rng, seed=r))) # null feature | |
| rows.append({"n": n, "mean_absW": float(np.mean(w)), | |
| "sd": float(np.std(w, ddof=1))}) | |
| print(f" claim4 n={n} mean|W_null|={np.mean(w):.6f}", flush=True) | |
| lx = np.log([r["n"] for r in rows]); ly = np.log([r["mean_absW"] for r in rows]) | |
| A = np.vstack([lx, np.ones_like(lx)]).T | |
| sl, ic = np.linalg.lstsq(A, ly, rcond=None)[0] | |
| ratios = [rows[i + 1]["mean_absW"] / rows[i]["mean_absW"] for i in range(len(rows) - 1)] | |
| OUT["claim4"] = {"rows": rows, "slope": float(sl), | |
| "successive_ratios": ratios, | |
| "sqrt_n_ratio_reference": 1 / np.sqrt(2), | |
| "faster_than_root_n": bool(np.mean(ratios) < 1 / np.sqrt(2)), | |
| "reps": reps} | |
| print("claim4 slope", sl, "ratios", np.round(ratios, 3), flush=True) | |
| if __name__ == "__main__": | |
| import sys | |
| which = sys.argv[1:] or ["1", "2", "3", "4"] | |
| fns = {"1": claim1, "2": claim2, "3": claim3, "4": claim4} | |
| for w in which: | |
| print("=== claim", w, flush=True) | |
| fns[w]() | |
| json.dump(OUT, open("outputs/results.json", "w"), indent=2) | |
| print("saved outputs/results.json") | |