"""Per-seed readouts for the LLM speciation tier (Fig. 5C-D), the source of its SI Table S2 row. Seed 1 ran locally; seeds 2-3 via hpc/llm_speciation_seeds.pbs into ``results/llm_speciation/s{seed}/``. Two pre-registered falsifiers, checked seed by seed: - the conflict cliff: at full conflict (x = 1.0) the merged model's best-convention accuracy on the shared prompts falls below BOTH parents' own-convention accuracy; - the duration null: over the epoch sweep the merged model's mean private-family accuracy does not fall below its value at the shortest training while the parents hold their own families. Usage: python figures/stats_llm_speciation_seeds.py [results/llm_speciation] """ from __future__ import annotations import sys from pathlib import Path import numpy as np import pandas as pd sys.path.insert(0, str(Path(__file__).parent)) from _figlib import load_seed_bundles # noqa: E402 def pick(df: pd.DataFrame, mode: str, model: str, metric: str) -> pd.DataFrame: """Per-seed accuracy against x for one (mode, model, metric).""" sub = df[(df["mode"] == mode) & (df["model"] == model) & (df["metric"] == metric)] return sub.pivot_table(index="x", columns="seed", values="accuracy", aggfunc="mean") def summary(piv: pd.DataFrame) -> pd.DataFrame: n = piv.shape[1] out = piv.copy() out.columns = [f"s{c}" for c in out.columns] out["mean"] = piv.mean(axis=1) out["ci95"] = 1.96 * piv.std(axis=1, ddof=1) / np.sqrt(n) if n > 1 else np.nan return out.round(3) def main(root: str = "results/llm_speciation") -> None: df, _ = load_seed_bundles(root) seeds = sorted(df["seed"].unique()) print(f"seeds: {seeds}\n") print("## Conflict sweep: merged model, best convention on the shared prompts") merge = pick(df, "conflict", "merge_soup", "coherence") print(summary(merge).to_string(), "\n") pa = pick(df, "conflict", "parent_a", "ambig_asc") pb = pick(df, "conflict", "parent_b", "ambig_desc") x1 = merge.index.max() print(f"## Conflict cliff at x = {x1}: merge below both parents? (per seed)") for s in seeds: m, a, b = merge.loc[x1, s], pa.loc[x1, s], pb.loc[x1, s] print(f" seed {s}: merge {m:.3f} parent A {a:.3f} parent B {b:.3f} -> {'cliff' if m < min(a, b) else 'NO cliff'}") print() print("## Duration sweep: merged model, mean private-family accuracy") dur = pick(df, "duration", "merge_soup", "mean_private") print(summary(dur).to_string(), "\n") print("## Duration null: merged accuracy at the longest vs shortest training (per seed)") lo, hi = dur.index.min(), dur.index.max() for s in seeds: d = dur.loc[hi, s] - dur.loc[lo, s] print(f" seed {s}: {dur.loc[lo, s]:.3f} -> {dur.loc[hi, s]:.3f} (Δ {d:+.3f}) -> " f"{'no isolation' if d >= -0.05 else 'DEGRADES'}") for fam, model in (("strings", "parent_a"), ("arith", "parent_b")): p = pick(df, "duration", model, fam) print(f" {model} own-task range over epochs, seed means: {p.mean(axis=1).min():.3f}–{p.mean(axis=1).max():.3f}") if __name__ == "__main__": main(*sys.argv[1:])