Download analysis/sig_all.py from Ericu950/Stoicheia-code: direct link, hf CLI and curl.
- Browser
- Download file 7.36 kB
-
https://huggingface.co/Ericu950/Stoicheia-code/resolve/main/analysis/sig_all.py
- Command line
-
hf download hf://Ericu950/Stoicheia-code/analysis/sig_all.py
-
curl -L -o sig_all.py https://huggingface.co/Ericu950/Stoicheia-code/resolve/main/analysis/sig_all.py
7.36 kB
| """Paired significance tests for ALL three experiments. | |
| Everything is paired on the unit that was actually replicated -- (fold, seed) for parsing, | |
| fold for documentary, seed for meter -- so each comparison controls for split difficulty | |
| and only the system varies. The headline p-value is an EXACT sign-flip permutation test | |
| (no distributional assumption); note its floor is 2/2^n, so p=0.002 at n=10 is the | |
| smallest attainable value, not a coincidence. d_z is paired Cohen's d. | |
| """ | |
| from __future__ import annotations | |
| import collections, glob, json, math, os, re, sys | |
| import numpy as np | |
| GCB=os.path.expandvars("$STOICHEIA_DATA") | |
| REPO=os.path.expandvars("$STOICHEIA_ROOT") | |
| def _ttest_rel_p(d): | |
| """Two-sided paired t-test p-value without scipy (survival fn of |t| via a | |
| continued-fraction incomplete beta -- keeps this script dependency-free so the | |
| released code needs only numpy).""" | |
| n=len(d); sd=d.std(ddof=1) | |
| if sd==0 or n<2: return float("nan") | |
| t=abs(d.mean())/(sd/np.sqrt(n)); v=n-1 | |
| x=v/(v+t*t) | |
| # regularised incomplete beta I_x(v/2, 1/2) via Lentz continued fraction | |
| a,b=v/2.0,0.5 | |
| lbeta=(math.lgamma(a)+math.lgamma(b)-math.lgamma(a+b)) | |
| def betacf(a,b,x,itmax=300,eps=3e-16): | |
| qab,qap,qam=a+b,a+1.0,a-1.0 | |
| c=1.0; d_=1.0-qab*x/qap | |
| if abs(d_)<1e-30: d_=1e-30 | |
| d_=1.0/d_; h=d_ | |
| for m in range(1,itmax+1): | |
| m2=2*m | |
| aa=m*(b-m)*x/((qam+m2)*(a+m2)) | |
| d_=1.0+aa*d_; c=1.0+aa/c | |
| if abs(d_)<1e-30: d_=1e-30 | |
| if abs(c)<1e-30: c=1e-30 | |
| d_=1.0/d_; h*=d_*c | |
| aa=-(a+m)*(qab+m)*x/((a+m2)*(qap+m2)) | |
| d_=1.0+aa*d_; c=1.0+aa/c | |
| if abs(d_)<1e-30: d_=1e-30 | |
| if abs(c)<1e-30: c=1e-30 | |
| d_=1.0/d_; delta=d_*c; h*=delta | |
| if abs(delta-1.0)<eps: break | |
| return h | |
| if x < (a+1.0)/(a+b+2.0): | |
| ib=np.exp(a*np.log(x)+b*np.log(1-x)-lbeta)*betacf(a,b,x)/a | |
| else: | |
| ib=1.0-np.exp(b*np.log(1-x)+a*np.log(x)-lbeta)*betacf(b,a,1-x)/b | |
| return float(min(1.0, max(0.0, ib))) | |
| def perm_p(diff): | |
| n=len(diff); obs=abs(diff.mean()) | |
| if n<=20: | |
| c=t=0 | |
| for m in range(1<<n): | |
| s=np.array([1 if m>>i&1 else -1 for i in range(n)]) | |
| c+= abs((diff*s).mean())>=obs-1e-12; t+=1 | |
| return c/t | |
| rng=np.random.default_rng(0) | |
| s=rng.choice([-1,1],size=(100000,n)) | |
| return float((np.abs((diff*s).mean(1))>=obs-1e-12).mean()) | |
| def report(title, unit, a_name, a, b_name, b, higher_better=True): | |
| keys=sorted(set(a)&set(b)) | |
| if len(keys)<3: | |
| print(f" {a_name} vs {b_name}: only {len(keys)} paired {unit}s -- not testable yet"); return | |
| x=np.array([a[k] for k in keys]); y=np.array([b[k] for k in keys]) | |
| d=(x-y) if higher_better else (y-x) | |
| p=perm_p(d); dz=d.mean()/(d.std(ddof=1)+1e-12) | |
| t=_ttest_rel_p(d) | |
| print(f" {a_name} vs {b_name}: n={len(keys)} {unit}s mean diff={d.mean():+.4f} " | |
| f"perm p={p:.2e} t p={t:.1e} d_z={dz:.1f}") | |
| def parsing(): | |
| print("\nPARSING (LAS, paired by fold+seed; higher better)") | |
| S=f"{GCB}/parser_data/runs"; g=collections.defaultdict(dict) | |
| for d in sorted(glob.glob(f"{S}/joint_*_f*_s*")): | |
| f=os.path.join(d,"test_scores_greedy.json") | |
| if not os.path.exists(f): continue | |
| j=json.load(open(f)) | |
| if j.get("las") is None: continue | |
| b=os.path.basename(d); m=re.search(r"_(f\d+_s\d+)$",b) | |
| g[re.sub(r"_f\d+_s\d+$","",b)][m.group(1)]=j["las"] | |
| ref=g.get("joint_docclean",{}) | |
| for k in sorted(g): | |
| if k=="joint_docclean": continue | |
| # joint_tune_* are single-fold LR-sweep diagnostics (reported separately as the | |
| # baseline-tuning control), not systems to significance-test. | |
| if k.startswith("joint_tune_"): continue | |
| report("", "run", "ours", ref, k.replace("joint_",""), g[k]) | |
| def documentary(): | |
| print("\nDOCUMENTARY (CER, paired by fold; LOWER better)") | |
| def load(ver): | |
| out={} | |
| for f in glob.glob(f"{REPO}/.scratch/evals/{ver}_t*_iphi_whole_unk.json"): | |
| tv=re.search(r"_(t\dv\d)_",os.path.basename(f)).group(1) | |
| out[tv]=json.load(open(f))["avg"]["CER"] | |
| return out | |
| v3=load("v3"); ri=load("v3_randinit"); v2=load("v2") | |
| if v3 and ri: report("", "fold", "v3", v3, "random-init", ri, higher_better=False) | |
| if v3 and v2: report("", "fold", "v3", v3, "v2 (superseded)", v2, higher_better=False) | |
| if not v3: print(" (v3 evals not written yet)") | |
| def strict_ithaca(): | |
| print("\nSTRICT vs ITHACA (top-1/top-20 exact-match, two-proportion z-test, n=3000 each side)") | |
| print(" (samples are IDENTICAL on both sides -- same frozen file -- so this unpaired") | |
| print(" test is conservative; a paired McNemar test would only be stronger)") | |
| INSC=os.path.expandvars("$STOICHEIA_DATA") | |
| import collections | |
| def merge(pat): | |
| a=collections.defaultdict(lambda: dict(n=0,t1=0.0,t20=0.0)) | |
| for f in glob.glob(pat): | |
| try: d=json.load(open(f)) | |
| except Exception: continue | |
| for r in d.get("per_L",[]): | |
| x=a[r["L"]]; x["n"]+=r["n"]; x["t1"]+=r["top1"]*r["n"]; x["t20"]+=r["top20"]*r["n"] | |
| n=sum(v["n"] for v in a.values()) | |
| return (n, sum(v["t1"] for v in a.values()), sum(v["t20"] for v in a.values())) if n else None | |
| def ztest(k1,n1,k2,n2): | |
| p1,p2=k1/n1,k2/n2; p=(k1+k2)/(n1+n2) | |
| se=math.sqrt(p*(1-p)*(1/n1+1/n2)) | |
| z=(p1-p2)/se | |
| # two-sided normal sf via erfc | |
| pv=math.erfc(abs(z)/math.sqrt(2)) | |
| return z,pv | |
| ith=merge(f"{INSC}/strict_f3/ithaca_shard*.json") | |
| if not ith: print(" (ithaca results missing)"); return | |
| for tag,pat in (("ours old",f"{INSC}/strict_f3/ours_old.json"), | |
| ("ours v3",f"{INSC}/strict_f3/ours_v3_shard*.json"), | |
| ("ours v4",f"{INSC}/strict_f3/ours_v4_shard*.json")): | |
| m=merge(pat) | |
| if not m: continue | |
| for name,ki,ko in (("top-1",1,1),("top-20",2,2)): | |
| z,pv=ztest(m[ki],m[0],ith[ko],ith[0]) | |
| print(f" {tag:<9} vs Ithaca {name:<7}: {m[ki]/m[0]:.4f} vs {ith[ko]/ith[0]:.4f}" | |
| f" z={z:+.2f} p={pv:.2e}") | |
| def meter(): | |
| print("\nMETER (macron balanced acc, paired by seed; higher better)") | |
| rows=collections.defaultdict(dict) | |
| for f in sorted(glob.glob(f"{REPO}/logs/eval-*.out")): | |
| txt=open(f,errors="ignore").read() | |
| if "--norma" not in txt: continue | |
| mm=re.search(r"model=(\S+)",txt); mt=re.search(r"macron test: (\{.*?\})",txt) | |
| if not(mm and mt): continue | |
| run=mm.group(1).split("/runs/")[-1].split("/")[0] | |
| if not run.startswith("meter_mac_v2"): continue | |
| seed=re.search(r"_s(\d)$",run); seed=seed.group(1) if seed else "0" | |
| arm="randinit" if "randinit" in run else "pretrained" | |
| rows[arm][seed]=json.loads(mt.group(1))["bal_acc"] | |
| if rows.get("pretrained") and rows.get("randinit"): | |
| report("", "seed", "pretrained", rows["pretrained"], "random-init", rows["randinit"]) | |
| else: print(" (not enough scored runs yet)") | |
| if __name__=="__main__": | |
| w=sys.argv[1] if len(sys.argv)>1 else "all" | |
| if w in ("all","parsing"): parsing() | |
| if w in ("all","documentary"): documentary(); strict_ithaca() | |
| if w in ("all","meter"): meter() | |
| print() | |