ProCreations's picture
Publish validated ICML reproduction
177308a verified
Raw
History Blame Contribute Delete
4.51 kB
"""
Why the oracle-level guarantee of Theorem 2 does or does not show up in the
finite-sample Figure-6 experiment. Sweeps the sample size N and the dimension
d, and separates two candidate causes:
(i) the selected distribution is not linear-Gaussian, so Fisher-z is
misspecified -> tested by generating data DIRECTLY from G^+ (a clean
linear-Gaussian SCM with no selection at all);
(ii) PC simply has too little power at d=20, N=5,000 on the dense
clique-augmented graph.
Run: python3 exp6_sensitivity.py
"""
import json, os, warnings, itertools, time
import numpy as np
warnings.filterwarnings('ignore')
os.environ.setdefault('OMP_NUM_THREADS', '1')
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'outputs')
os.makedirs(OUT, exist_ok=True)
def one(task):
d, N, seed, mode = task
import evosel as E
from evosel import (random_static_dag, clique_augmented, cpdag,
simulate_evolution, sem_params, SEL)
from exp6_synthetic import _edges_from_cl, _score
from causallearn.search.ConstraintBased.PC import pc
rng = np.random.default_rng(1000 * d + seed)
G = random_static_dag(d, rng, avg_deg=2.0)
true = set((i, j) for j in range(d) for i in G.pa[j] if i != SEL)
anS = G.ancestors([SEL]) - {SEL}
gp = clique_augmented(G, d)
dd, uu = cpdag(gp)
orc = _score(dd, uu, true, anS)
if mode == 'evolution':
X, _ = simulate_evolution(G, d, 3, N, rng)
else: # 'gplus': linear-Gaussian SCM on G^+ directly
B = np.zeros((d, d))
for (u, v) in gp.edges():
B[u, v] = rng.uniform(0.5, 2.0) * (1 if rng.random() < .5 else -1)
eps = rng.normal(0, np.sqrt(rng.uniform(1, 4, size=d)), size=(N, d))
X = E._traits(B, eps)
cg = pc(X, alpha=0.05, indep_test='fisherz', show_progress=False)
a, b = _edges_from_cl(cg.G, d)
fin = _score(a, b, true, anS)
skel_true = set(frozenset(e) for e in gp.edges())
skel = set(frozenset(e) for e in a) | b
return {'d': d, 'N': N, 'seed': seed, 'mode': mode,
'anS': len(anS), 'n_edges_Gplus': gp.n_edges(),
'oracle_standard': orc['precision_standard'],
'oracle_ours': orc['precision_ours'],
'oracle_n_oriented': orc['n_oriented'],
'standard': fin['precision_standard'], 'ours': fin['precision_ours'],
'n_oriented': fin['n_oriented'], 'n_adjacencies': fin['n_adjacencies'],
'skeleton_recall_vs_Gplus': len(skel & skel_true) / max(len(skel_true), 1),
'skeleton_precision_vs_Gplus': len(skel & skel_true) / max(len(skel), 1)}
def agg(rows, keys):
out = {}
for r in rows:
k = '_'.join(str(r[x]) for x in keys)
out.setdefault(k, []).append(r)
res = {}
for k, v in out.items():
res[k] = {'n': len(v)}
for f in ('standard', 'ours', 'oracle_standard', 'oracle_ours',
'skeleton_recall_vs_Gplus', 'skeleton_precision_vs_Gplus',
'n_oriented', 'n_adjacencies'):
a = np.array([x[f] for x in v], dtype=float)
a = a[~np.isnan(a)]
res[k][f] = float(a.mean())
res[k]['ours_minus_standard'] = res[k]['ours'] - res[k]['standard']
res[k]['frac_runs_ours_gt_standard'] = float(np.mean(
[x['ours'] > x['standard'] for x in v if not np.isnan(x['ours'])]))
return res
if __name__ == '__main__':
import multiprocessing as mp
tasks = []
for d in (10, 15, 20):
for N in (5000, 20000, 100000, 400000):
for s in range(20):
tasks.append((d, N, s, 'evolution'))
for d in (10, 20):
for N in (5000, 100000, 400000):
for s in range(20):
tasks.append((d, N, s, 'gplus'))
t0 = time.time()
with mp.Pool(processes=min(mp.cpu_count(), 12)) as pool:
rows = pool.map(one, tasks, chunksize=1)
res = {'by_d_N_mode': agg(rows, ['mode', 'd', 'N']),
'runtime_sec': time.time() - t0, 'runs': rows}
json.dump(res, open(os.path.join(OUT, 'sensitivity.json'), 'w'), indent=1)
for k in sorted(res['by_d_N_mode']):
e = res['by_d_N_mode'][k]
print('%-22s std %.3f ours %.3f diff %+.3f win%% %.2f skelrec %.2f n_or %.1f/%.1f' %
(k, e['standard'], e['ours'], e['ours_minus_standard'],
e['frac_runs_ours_gt_standard'], e['skeleton_recall_vs_Gplus'],
e['n_oriented'], e['n_adjacencies']))