from __future__ import annotations
import numpy as np
import pandas as pd
from scipy.stats import wilcoxon
N_BOOT = 10000
ALPHA = 0.05
def boot_median_ci(x, n_boot=N_BOOT, alpha=ALPHA, seed=0):
x = np.asarray(x, float)
x = x[np.isfinite(x)]
if len(x) < 3:
return (np.nan, np.nan)
rng = np.random.default_rng(seed)
idx = rng.integers(0, len(x), size=(n_boot, len(x)))
meds = np.median(x[idx], axis=1)
lo, hi = np.percentile(meds, [100 * alpha / 2, 100 * (1 - alpha / 2)])
return float(lo), float(hi)
def boot_paired_median_diff_ci(d, n_boot=N_BOOT, alpha=ALPHA, seed=0):
d = np.asarray(d, float)
d = d[np.isfinite(d)]
if len(d) < 3:
return (np.nan, np.nan)
rng = np.random.default_rng(seed)
idx = rng.integers(0, len(d), size=(n_boot, len(d)))
meds = np.median(d[idx], axis=1)
lo, hi = np.percentile(meds, [100 * alpha / 2, 100 * (1 - alpha / 2)])
return float(lo), float(hi)
def hodges_lehmann(d):
d = np.asarray(d, float)
d = d[np.isfinite(d)]
n = len(d)
if n == 0:
return np.nan
i, j = np.triu_indices(n)
return float(np.median((d[i] + d[j]) / 2.0))
def boot_hl_ci(d, n_boot=2000, alpha=ALPHA, seed=0):
d = np.asarray(d, float)
d = d[np.isfinite(d)]
if len(d) < 3:
return (np.nan, np.nan)
rng = np.random.default_rng(seed)
n = len(d)
i, j = np.triu_indices(n)
vals = np.empty(n_boot)
for b in range(n_boot):
s = d[rng.integers(0, n, n)]
vals[b] = np.median((s[i] + s[j]) / 2.0)
lo, hi = np.percentile(vals, [100 * alpha / 2, 100 * (1 - alpha / 2)])
return float(lo), float(hi)
def rank_biserial(d):
d = np.asarray(d, float)
d = d[np.isfinite(d) & (d != 0)]
if len(d) == 0:
return np.nan
from scipy.stats import rankdata
r = rankdata(np.abs(d))
wp, wn = r[d > 0].sum(), r[d < 0].sum()
tot = wp + wn
return float((wp - wn) / tot) if tot > 0 else np.nan
def holm(pvals):
p = np.asarray(pvals, float)
out = np.full(len(p), np.nan)
ok = np.where(np.isfinite(p))[0]
m = len(ok)
if m == 0:
return out
order = ok[np.argsort(p[ok])]
running = 0.0
for i, j in enumerate(order):
adj = (m - i) * p[j]
running = max(running, adj)
out[j] = min(1.0, running)
return out
def compare(name, err_model, err_base, seed=0, n_boot=N_BOOT):
a = np.asarray(err_model, float)
b = np.asarray(err_base, float)
m = np.isfinite(a) & np.isfinite(b)
a, b = a[m], b[m]
n = len(a)
if n < 3:
return {"comparison": name, "n": n, "median_model": np.nan,
"median_base": np.nan, "median_paired_diff": np.nan,
"p_raw": np.nan, "direction": "not evaluable"}
d = a - b
md = float(np.median(d))
try:
p = float(wilcoxon(a, b, zero_method="wilcox")[1])
except ValueError:
p = 1.0
lo_m, hi_m = boot_median_ci(a, n_boot, seed=seed)
lo_b, hi_b = boot_median_ci(b, n_boot, seed=seed + 1)
lo_d, hi_d = boot_paired_median_diff_ci(d, n_boot, seed=seed + 2)
hl = hodges_lehmann(d)
lo_h, hi_h = boot_hl_ci(d, seed=seed + 3)
ref = hl if np.isfinite(hl) else md
if ref < 0:
direction = "model better"
elif ref > 0:
direction = "model worse"
else:
direction = "tied"
med_a, med_b = float(np.median(a)), float(np.median(b))
naive = "model better" if med_a < med_b else \
("model worse" if med_a > med_b else "tied")
return {
"comparison": name, "n": n,
"median_model": med_a, "model_ci_lo": lo_m, "model_ci_hi": hi_m,
"median_base": med_b, "base_ci_lo": lo_b, "base_ci_hi": hi_b,
"median_paired_diff": md, "diff_ci_lo": lo_d, "diff_ci_hi": hi_d,
"hodges_lehmann": hl, "hl_ci_lo": lo_h, "hl_ci_hi": hi_h,
"wins": int((d < 0).sum()), "losses": int((d > 0).sum()),
"rank_biserial": rank_biserial(d),
"p_raw": p,
"direction": direction,
"naive_direction": naive,
"direction_conflict": direction != naive,
}
class Family:
def __init__(self, label, alpha=ALPHA, n_boot=N_BOOT, seed=0):
self.label, self.alpha, self.n_boot, self.seed = label, alpha, n_boot, seed
self.rows = []
self._final = False
def add(self, name, err_model, err_base):
self.rows.append(compare(name, err_model, err_base,
seed=self.seed + 7 * len(self.rows),
n_boot=self.n_boot))
return self
def add_not_evaluable(self, name, reason):
self.rows.append({"comparison": name, "n": 0, "p_raw": np.nan,
"direction": "not evaluable", "reason": reason,
"median_model": np.nan, "median_base": np.nan,
"median_paired_diff": np.nan})
return self
def finalise(self):
adj = holm([r.get("p_raw", np.nan) for r in self.rows])
for r, pa in zip(self.rows, adj):
r["family"] = self.label
r["p_holm"] = pa
r["significant_raw"] = bool(np.isfinite(r.get("p_raw", np.nan))
and r["p_raw"] < self.alpha)
r["significant_holm"] = bool(np.isfinite(pa) and pa < self.alpha)
r["survives_holm"] = r["significant_raw"] and r["significant_holm"]
r["lost_to_holm"] = r["significant_raw"] and not r["significant_holm"]
self._final = True
return self
def frame(self):
if not self._final:
self.finalise()
return pd.DataFrame(self.rows)
def table(self, width=46):
df = self.frame()
out = [f"### {self.label} (family size {int(np.isfinite(df.p_raw).sum())}"
f", Holm-Bonferroni, alpha={self.alpha})"]
hdr = (f" {'comparison':{width}s} {'n':>4s} {'median [95% CI]':>16s} "
f"{'baseline [95%CI]':>16s} {'HodgesLehmann diff':>19s} "
f"{'w/l':>8s} {'r_rb':>6s} "
f"{'p_raw':>10s} {'p_holm':>10s} verdict")
out.append(hdr)
out.append(" " + "-" * (len(hdr) - 2))
for _, r in df.iterrows():
if r["direction"] == "not evaluable":
out.append(f" {r['comparison']:{width}s} {'--':>4s} "
f"{'NOT EVALUABLE':>16s} {r.get('reason','')}")
continue
med = f"{r.median_model:.4f} [{r.model_ci_lo:.3f},{r.model_ci_hi:.3f}]"
bas = f"{r.median_base:.4f} [{r.base_ci_lo:.3f},{r.base_ci_hi:.3f}]"
dif = f"{r.hodges_lehmann:+.4f} [{r.hl_ci_lo:+.3f},{r.hl_ci_hi:+.3f}]"
wl = f"{int(r.wins)}/{int(r.losses)}"
if r.survives_holm:
verdict = "BETTER (Holm)" if r.direction == "model better" \
else "WORSE (Holm)"
elif r.lost_to_holm:
verdict = f"n.s. after Holm ({r.direction}, raw p<{self.alpha})"
else:
verdict = f"n.s. ({r.direction})"
flag = " [!direction conflict with naive median comparison]" \
if r.get("direction_conflict") else ""
out.append(f" {r['comparison']:{width}s} {int(r.n):4d} {med:>16s} "
f"{bas:>16s} {dif:>19s} {wl:>8s} {r.rank_biserial:+6.3f} "
f"{r.p_raw:10.3g} {r.p_holm:10.3g} {verdict}{flag}")
out.append("")
return "\n".join(out)
if __name__ == "__main__":
import sys, warnings
from pathlib import Path
warnings.filterwarnings("ignore")
D = "/work/project/project/user"
OUT = Path(f"{D}/analysis/out"); OUT.mkdir(parents=True, exist_ok=True)
sys.path.insert(0, f"{D}/analysis")
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.neural_network import MLPRegressor
from sklearn.linear_model import Ridge
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, FunctionTransformer
from sklearn.pipeline import Pipeline
from crossplatform import (load as xload, features as xfeatures,
F_PEAK, INTERSECTION, slog, fac)
NJ = 8
def mk(kind, seed=0):
pre = [("i", SimpleImputer(strategy="median")),
("l", FunctionTransformer(slog, validate=False)),
("s", StandardScaler())]
est = {"rf": RandomForestRegressor(n_estimators=400, min_samples_leaf=2,
random_state=seed, n_jobs=NJ),
"gbq": GradientBoostingRegressor(loss="quantile", alpha=0.5,
n_estimators=300, max_depth=3,
learning_rate=0.05,
random_state=seed),
"mlp": MLPRegressor(hidden_layer_sizes=(32, 16), alpha=1.0,
solver="lbfgs", max_iter=5000,
random_state=seed),
"ridge": Ridge(alpha=10.0)}[kind]
return Pipeline(pre + [("e", est)])
def prep(path, plat, tag):
d = xload(path, plat)
d["dataset"] = tag
return d
a2 = prep(f"{D}/data/runs_expanded.csv", "archer2", "archer2")
cf = prep(f"{D}/data/cirrus_persets.csv", "cirrus", "cirrus-fixed")
cw = prep(f"{D}/data/cirrus_weak.csv", "cirrus", "cirrus-weak")
allf = pd.concat([a2, cf, cw], ignore_index=True)
X = xfeatures(allf)
y = allf.runtime_s.values
fpk = allf.platform.map(F_PEAK).values
t_an = pd.to_numeric(allf.PAPI_TOT_CYC, errors="coerce").values / fpk
eta = t_an / y
ok = (eta > 0) & (eta <= 1.5)
allf = allf[ok].reset_index(drop=True)
X = X[ok].reset_index(drop=True)
y, t_an, eta = y[ok], t_an[ok], eta[ok]
INTER = [c for c in INTERSECTION if c in X.columns]
def loao(mask, cols, kind):
e = np.full(len(y), np.nan)
Xf = X[cols].values
sub = np.where(mask)[0]
for a in sorted(allf.app[mask].unique()):
te = sub[allf.app.values[sub] == a]
tr = sub[allf.app.values[sub] != a]
if len(te) == 0 or len(tr) < 10:
continue
if kind == "const":
p = t_an[te] / (10 ** np.median(np.log10(eta[tr])))
else:
m = mk(kind); m.fit(Xf[tr], np.log10(eta[tr]))
p = t_an[te] / (10 ** m.predict(Xf[te]))
e[te] = fac(p, y[te])
return e
def transfer(tr_mask, te_mask, cols, kind):
e = np.full(len(y), np.nan)
Xf = X[cols].values
tr, te = np.where(tr_mask)[0], np.where(te_mask)[0]
if kind == "const":
p = t_an[te] / (10 ** np.median(np.log10(eta[tr])))
else:
m = mk(kind); m.fit(Xf[tr], np.log10(eta[tr]))
p = t_an[te] / (10 ** m.predict(Xf[te]))
e[te] = fac(p, y[te])
return e
L = []
say = lambda s="": (print(s, flush=True), L.append(s))
say("Corrected statistical reporting for all key comparisons")
say("=" * 100)
say("Direction is the SIGN OF THE HODGES-LEHMANN PSEUDOMEDIAN of the paired")
say("differences (model - baseline), the location estimate the Wilcoxon")
say("signed-rank test actually targets; NOT a comparison of two independent")
say(f"medians. Intervals are percentile bootstrap 95% CIs ({N_BOOT} resamples")
say("for arm medians, 2000 for the O(n^2) Hodges-Lehmann). p_holm is Holm-")
say("Bonferroni adjusted within the family shown in each block heading.")
say("r_rb is the matched-pairs rank-biserial effect size; w/l counts pairs.")
say("Error factor = max(pred/actual, actual/pred); 1.0 is perfect.")
say("")
fams = []
dsets = [("archer2", "ARCHER2 (fixed size, LOAO)"),
("cirrus-fixed", "Cirrus fixed-size (LOAO)"),
("cirrus-weak", "Cirrus weak-scaled (LOAO)")]
for tag, label in dsets:
mask = (allf.dataset == tag).values
base = loao(mask, INTER, "const")
fam = Family(f"{label} vs constant baseline, intersection features")
for kind, nm in [("rf", "Random Forest"), ("gbq", "GBM quantile(0.5)"),
("mlp", "MLP (32,16) alpha=1")]:
fam.add(nm, loao(mask, INTER, kind), base)
fams.append(fam.finalise())
say(fam.table())
GROUPS = {
"config": ["log_ncore", "log_nrank", "log_nthread"],
"analytic": ["log_t_analytic"],
"cheap": ["ipc", "log_instr_per_rank", "flops_per_instr",
"flops_per_cycle"],
}
CUM = [("config only (free)", ["config"]),
("+ analytic cycles (1 counter)", ["config", "analytic"]),
("+ instruction ratios (1 set)", ["config", "analytic", "cheap"]),
("+ all counters (5 sets)", None)]
m_a2 = (allf.dataset == "archer2").values
abl = {}
for lab, grps in CUM:
cols = list(X.columns) if grps is None else \
[c for g in grps for c in GROUPS[g] if c in X.columns]
abl[lab] = loao(m_a2, cols, "rf")
const_a2 = loao(m_a2, INTER, "const")
fam = Family("Counter-group ablation on ARCHER2, RF, vs CONFIG-ONLY")
for lab in [k for k, _ in CUM][1:]:
fam.add(lab, abl[lab], abl["config only (free)"])
fams.append(fam.finalise())
say(fam.table())
fam = Family("Counter-group ablation on ARCHER2, RF, vs CONSTANT baseline")
for lab, _ in CUM:
fam.add(lab, abl[lab], const_a2)
fams.append(fam.finalise())
say(fam.table())
m_cf = (allf.dataset == "cirrus-fixed").values
m_cw = (allf.dataset == "cirrus-weak").values
fam = Family("Cross-platform transfer vs constant, intersection features")
for src, dst, sm, dm in [("ARCHER2", "Cirrus-fixed", m_a2, m_cf),
("Cirrus-fixed", "ARCHER2", m_cf, m_a2),
("ARCHER2", "Cirrus-weak", m_a2, m_cw),
("Cirrus-weak", "ARCHER2", m_cw, m_a2)]:
b = transfer(sm, dm, INTER, "const")
for kind, nm in [("rf", "RF"), ("gbq", "GBQ"), ("mlp", "MLP")]:
fam.add(f"{src} -> {dst}: {nm}", transfer(sm, dm, INTER, kind), b)
fams.append(fam.finalise())
say(fam.table())
big = pd.concat([f.frame() for f in fams], ignore_index=True)
big.to_csv(OUT / "stats_corrected.csv", index=False)
say("=" * 100)
say("WHAT THE CORRECTION COSTS")
say("")
lost = big[big.get("lost_to_holm", False) == True]
say(f" comparisons run : {len(big)}")
say(f" significant at raw alpha=0.05 : {int(big.significant_raw.sum())}")
say(f" still significant after Holm-Bonferroni: {int(big.survives_holm.sum())}")
say(f" LOST to the correction : {len(lost)}")
for _, r in lost.iterrows():
say(f" {r['family']} | {r['comparison']} "
f"raw p={r.p_raw:.4g} -> Holm p={r.p_holm:.4g}")
say("")
conf = big[big.get("direction_conflict", False) == True]
say(f" comparisons where the OLD naive direction label (median(a) vs")
say(f" median(b)) disagrees with the paired direction: {len(conf)}")
for _, r in conf.iterrows():
say(f" {r['family']} | {r['comparison']}: "
f"paired HL={r.hodges_lehmann:+.4f} says '{r['direction']}', "
f"naive median said '{r['naive_direction']}'")
say("")
say(" Comparisons that survive Holm AND favour the model:")
win = big[(big.get("survives_holm", False) == True) &
(big.direction == "model better")]
if len(win) == 0:
say(" none")
for _, r in win.iterrows():
say(f" {r['family']} | {r['comparison']}: "
f"{r.median_model:.4f} vs {r.median_base:.4f}, "
f"HL diff {r.hodges_lehmann:+.4f} "
f"[{r.hl_ci_lo:+.4f},{r.hl_ci_hi:+.4f}], "
f"r_rb={r.rank_biserial:+.3f}, Holm p={r.p_holm:.3g}")
(OUT / "stats_corrected.txt").write_text("\n".join(L) + "\n")
print(f"\nwrote {OUT/'stats_corrected.txt'} and .csv")