bunyamineymen's picture
Project 01 code (8e118d5)
668f59a verified
Raw History Blame Contribute Delete
12.7 kB
"""Evaluate the pre-registered hypotheses (prereg.md section 3) and build summary tables + Space data.
Usage: uv run python scripts/report.py [results_dir] (after run_all.py and latency.py)
"""
import itertools
import json
import sys
from pathlib import Path
import numpy as np
import pandas as pd
from gatestats import stats
ROOT = Path(__file__).resolve().parents[1]
RES = Path(sys.argv[1]) if len(sys.argv) > 1 else ROOT / "results"
PRIMARY = dict(truth="population", metric="recall@5")
def pick(df, **kw):
for k, v in kw.items():
df = df[df[k].isin(v) if isinstance(v, (list, tuple)) else np.isclose(df[k], v) if isinstance(v, float) else df[k] == v]
return df
def verdict(rows, need):
return {"supported": sum(r["ok"] for r in rows) >= need, "need": need, "per_unit": rows}
def h1(thr, sel=PRIMARY):
rows = []
for ds, g in pick(thr, **sel, n=100, d=[-0.02, 0.02]).groupby("dataset"):
wrong = int(np.where(g.truth_pass, g.R - g.k_pass, g.k_pass).sum())
lo, hi = stats.wilson_ci(wrong, g.R.sum())
rows.append(dict(dataset=ds, error=wrong / g.R.sum(), lo=float(lo), hi=float(hi), ok=bool(lo >= 0.15)))
return verdict(rows, 2)
def h2(pairs, con, sel=PRIMARY):
rows = []
for ds in sorted(pairs.dataset.unique()):
g1 = pick(pairs, **sel, dataset=ds, B="ctl03", gate="G1", param=0.05, n=100).iloc[0]
c = pick(con, **sel, dataset=ds, B="ctl03", contrast="G1pass-G3pass", param=0.02, n=100).iloc[0]
rate = g1.k_pass / g1.R
rows.append(dict(dataset=ds, g1_pass=rate, diff=c["mean"], lo=c.lo, hi=c.hi, ok=bool(rate >= 0.5 and c.lo > 0)))
return verdict(rows, 2)
def h3(con, sel=PRIMARY):
rows = []
for ds in sorted(con.dataset.unique()):
c = pick(con, **sel, dataset=ds, A="*", param=0.02, n=[50, 100])
rows.append(dict(dataset=ds, diffs={int(r.n): [r["mean"], r.lo, r.hi] for _, r in c.iterrows()},
ok=bool(len(c) == 2 and (c.lo > 0).all())))
return verdict(rows, 2)
def hnsw_table(pq):
h = pq[pq.system.str.startswith("hnsw")].copy()
h["ef"] = h.system.str.extract(r"ef(\d+)").astype(int)
h["seed"] = h.system.str.extract(r"_s(\d+)").astype(int)
means = h.groupby(["dataset", "ef", "seed"])["recall@10"].mean().reset_index()
rows = []
for (ds, ef), g in means.groupby(["dataset", "ef"]):
v = g.sort_values("seed")["recall@10"].to_numpy()
diffs = [abs(x - y) for x, y in itertools.combinations(v, 2)]
q = h[(h.dataset == ds) & (h.ef == ef)].pivot(index="qid", columns="seed", values="recall@10")
rows.append(dict(dataset=ds, ef=int(ef), mean=float(v.mean()), sd=float(v.std(ddof=1)),
p95_abs_diff=float(np.percentile(diffs, 95)), max_abs_diff=float(max(diffs)),
share_queries_changed=float((q.ne(q[0], axis=0)).any(axis=1).mean())))
return pd.DataFrame(rows)
def h4(tab):
rows = []
for ds, g in tab.groupby("dataset"):
p = g.sort_values("ef").p95_abs_diff.to_numpy()
rows.append(dict(dataset=ds, p95_by_ef=p.round(5).tolist(), positive_at_16=bool(p[0] > 0),
non_increasing=bool(np.all(np.diff(p) <= 1e-12)), ok=bool(np.all(np.diff(p) <= 1e-12))))
out = verdict(rows, 2)
out["supported"] = out["supported"] and all(r["positive_at_16"] for r in rows)
return out
def latency_table(lat):
rows = []
for (system, run), g in lat.groupby(["system", "run"]):
x = g.ms.to_numpy()
p50, lo50, hi50 = stats.quantile_bootstrap_ci(x, 0.5, seed=run)
p95, lo95, hi95 = stats.quantile_bootstrap_ci(x, 0.95, seed=run)
rows.append(dict(system=system, run=run, p50=p50, p95=p95, p99=float(np.quantile(x, 0.99)),
rel_w50=(hi50 - lo50) / p50, rel_w95=(hi95 - lo95) / p95))
return pd.DataFrame(rows)
def h5(lt):
rows = []
for system, g in lt.groupby("system"):
ratio = float((g.rel_w95 / g.rel_w50).median())
cv50, cv95 = g.p50.std(ddof=1) / g.p50.mean(), g.p95.std(ddof=1) / g.p95.mean()
rows.append(dict(system=system, width_ratio_median=ratio, cv_p50=float(cv50), cv_p95=float(cv95),
ok=bool(ratio >= 2 and cv95 > cv50)))
return verdict(rows, 4)
def latency_gates(lat, hs=(-0.2, -0.1, -0.05, 0.05, 0.1, 0.2), ns=(50, 100, 200), R=200, B=500, alpha=0.05):
"""Point-estimate P95 gate vs GL (bootstrap upper bound) against the pooled P95 of all runs."""
rng, rows = np.random.default_rng(20261017), []
for system, g in lat.groupby("system"):
true = np.quantile(g.ms, 0.95)
for n in ns:
point, gl = {h: [] for h in hs}, {h: [] for h in hs}
for _, gr in g.groupby("run"):
S = rng.permuted(np.tile(gr.ms.to_numpy(), (R, 1)), axis=1)[:, :n]
est = np.quantile(S, 0.95, axis=1)
idx = rng.integers(0, n, size=(B, n))
ub = np.array([stats.upper_bound(np.quantile(s[idx], 0.95, axis=1), alpha) for s in S])
for h in hs:
point[h].append((est <= true * (1 + h)).mean())
gl[h].append((ub <= true * (1 + h)).mean())
for h in hs:
rows.append(dict(system=system, n=n, sla_rel=h, truth_pass=h > 0,
point_pass=float(np.mean(point[h])), gl_pass=float(np.mean(gl[h]))))
return pd.DataFrame(rows)
def mde_table(pairs):
g = pick(pairs, **PRIMARY, gate="G3", param=0.02, n=100)[["dataset", "family", "A", "B", "delta_true", "sd_d"]]
for n in (50, 100, 200, 300, 500, 1000):
g[f"mde_n{n}"] = stats.mde(g.sd_d, n)
return g
def md(df, digits=3):
return df.round(digits).to_markdown()
def article_tables(thr, pairs, pq, hn, env, secondary, lt=None, lg=None):
"""Markdown tables the article quotes; every number in the article comes from here."""
from scipy.stats import norm
out = []
exact = pq[pq.system.isin(["bm25", "bge", "minilm", "e5"])]
sd = exact.groupby("dataset")["recall@5"].std()
ds = pd.DataFrame({"N": {k: v["queries"] for k, v in env["datasets"].items()},
"docs": {k: v["docs"] for k, v in env["datasets"].items()},
"bge_recall@5": pq[pq.system == "bge"].groupby("dataset")["recall@5"].mean(),
"sd_recall@5": sd})
ds["se_n100"] = ds["sd_recall@5"] / 10 * np.sqrt(1 - 100 / ds.N)
out += ["## A. Veri setleri", md(ds, 4)]
t = pick(thr, truth="population", metric="recall@5")
t = t[t.d.abs() > 1e-9].assign(abs_d=lambda x: x.d.abs().round(2))
tab = t.groupby(["dataset", "n", "abs_d"]).error.mean().unstack("abs_d")
out += ["## B. G1 hata oranı (Recall@5, popülasyon gerçeği, 4 sistem havuzlanmış)", md(tab)]
pred = {d: norm.cdf(-d / ds.se_n100) for d in (0.01, 0.02, 0.03, 0.05)}
comp = pd.DataFrame({f"obs_{d}": tab.xs(100, level="n")[d] for d in pred} | {f"pred_{d}": v for d, v in pred.items()})
out += ["## B2. n=100: gözlenen ve normal yaklaşımla öngörülen G1 hata oranı", md(comp)]
q = pick(pairs, truth="population", metric="recall@5", n=100)
q = q[((q.gate == "G1") & np.isclose(q.param, 0.05)) | ((q.gate != "G1") & np.isclose(q.param, 0.02))].copy()
q["scenario"] = q.B.str.replace(r"hnsw_ef(16|128)_s[1-9]", r"noop_ef\1", regex=True)
q.loc[q.family == "natural", "scenario"] = q.A + " -> " + q.B
g = q.groupby(["dataset", "scenario", "gate"]).agg(pass_rate=("pass_rate", "mean"), delta=("delta_true", "mean"))
wide = g.pass_rate.unstack("gate").join(g.delta.groupby(level=[0, 1]).mean())
out += ["## C. PASS oranları, n=100 (G1: eşik = baseline − 0.05; diğerleri m = 0.02)", md(wide)]
g3 = pick(pairs, truth="population", metric="recall@5", gate="G3", param=0.02).copy()
g3["scenario"] = g3.B.str.replace(r"hnsw_ef(16|128)_s[1-9]", r"noop_ef\1", regex=True)
g3 = g3[g3.scenario.isin(["ctl01", "ctl02", "ctl03", "noop_ef16", "noop_ef128"])]
out += ["## D. G3 PASS oranı golden set boyutuna göre (m = 0.02)",
md(g3.groupby(["dataset", "scenario", "n"]).pass_rate.mean().unstack("n"))]
gap = pick(pairs, truth="population", metric="recall@5", gate="G3").assign(
gap=lambda x: (x.pass_rate - x.analytic_pass).abs())
out += ["## D2. G3: simülasyon ile normal yaklaşım arasındaki mutlak fark",
md(gap.groupby(["dataset", "n"]).gap.agg(["median", "max"]).unstack("n"))]
out += ["## E. HNSW ekleme sırası (Recall@10, 10 seed)", md(hn.set_index(["dataset", "ef"]), 4)]
if lt is not None:
out += ["## F. Latency (FiQA, 2 iş parçacığı, koşu ortalaması; ms)",
md(lt.groupby("system")[["p50", "p95", "p99", "rel_w50", "rel_w95"]].mean(), 3),
"## F2. Koşular arası değişkenlik (CV)",
md(lt.groupby("system").agg(cv_p50=("p50", lambda x: x.std(ddof=1) / x.mean()),
cv_p95=("p95", lambda x: x.std(ddof=1) / x.mean())), 4),
"## F3. Latency gate PASS oranı (SLA = havuzlanmış P95 × (1 + h))",
md(lg.pivot_table(index=["system", "n"], columns="sla_rel", values=["point_pass", "gl_pass"]))]
out += ["## G. İkincil analizler (destek kararı)", md(pd.DataFrame(secondary).T, 0)]
return "\n\n".join(out) + "\n"
def main():
pq = pd.read_parquet(RES / "per_query.parquet")
thr = pd.read_parquet(RES / "threshold.parquet")
pairs = pd.read_parquet(RES / "pairs.parquet")
con = pd.read_parquet(RES / "contrasts.parquet")
thr["error"] = np.where(thr.truth_pass, thr.R - thr.k_pass, thr.k_pass) / thr.R
pairs["pass_rate"] = pairs.k_pass / pairs.R
hn = hnsw_table(pq)
hyp = {"H1": h1(thr), "H2": h2(pairs, con), "H3": h3(con), "H4": h4(hn)}
secondary = {f"{t}/{m}": {k: f(sel=dict(truth=t, metric=m))["supported"] for k, f in
[("H1", lambda sel: h1(thr, sel)), ("H2", lambda sel: h2(pairs, con, sel)),
("H3", lambda sel: h3(con, sel))]}
for t, m in [("population", "ndcg@10"), ("split_half", "recall@5")]}
space = {}
if (RES / "latency.parquet").exists():
lat = pd.read_parquet(RES / "latency.parquet")
lt = latency_table(lat)
hyp["H5"] = h5(lt)
lg = latency_gates(lat)
lt.to_csv(RES / "latency_summary.csv", index=False)
lg.to_csv(RES / "latency_gates.csv", index=False)
space["latency"] = lt.groupby("system")[["p50", "p95", "p99", "rel_w50", "rel_w95"]].mean().reset_index().to_dict("records")
space["latency_gates"] = lg.to_dict("records")
(RES / "hypotheses.json").write_text(json.dumps(hyp, indent=2, default=float))
(RES / "secondary.json").write_text(json.dumps(secondary, indent=2))
mt = mde_table(pairs)
mt.to_csv(RES / "mde.csv", index=False)
hn.to_csv(RES / "hnsw.csv", index=False)
systems = pq.groupby(["dataset", "system"])[["recall@5", "recall@10", "mrr@10", "ndcg@10"]].mean().reset_index()
systems.to_csv(RES / "system_means.csv", index=False)
env = json.loads((RES / "env.json").read_text())
lat_args = (lt, lg) if "H5" in hyp else ()
(RES / "summary.md").write_text(article_tables(thr, pairs, pq, hn, env, secondary, *lat_args), encoding="utf-8")
curves = thr.groupby(["dataset", "truth", "metric", "n", "d"]).apply(
lambda g: np.where(g.truth_pass, g.R - g.k_pass, g.k_pass).sum() / g.R.sum(), include_groups=False)
space.update({
"hypotheses": {k: v["supported"] for k, v in hyp.items()},
"g1_error": curves.rename("error").reset_index().to_dict("records"),
"gates": pairs[(pairs.truth == "population") & ((pairs.gate == "G1") | np.isclose(pairs.param, 0.02))][
["dataset", "truth", "family", "A", "B", "metric", "n", "gate", "param", "delta_true", "sd_d", "pass_rate",
"analytic_pass"]].replace({np.nan: None}).to_dict("records"),
"mde": mt.to_dict("records"),
"hnsw": hn.to_dict("records"),
"systems": systems.to_dict("records"),
})
out = ROOT / "space" / "data"
out.mkdir(parents=True, exist_ok=True)
(out / "summary.json").write_text(json.dumps(space, default=float))
for k, v in hyp.items():
print(k, "SUPPORTED" if v["supported"] else "NOT SUPPORTED")
if __name__ == "__main__":
main()