Marcin Pawlowski 74ee06047c
Make the model carry-over metric-specific instead of wholesale
§9 claimed that studies predating the countable model "carry over unchanged"
because no difference is resolvable at ρ < 1. That is true of accuracy and false
of the reference rate, and asserting it wholesale is what let the §8.4 capstone
sit at p_ref = 1.000 for weeks after the redesign made it 0.944.

Measured per metric off the committed paired band (δ_max 1–5, U ≥ 1, common
random numbers, so the uncle rule is the only difference):

  D̂/D            0.9994 vs 1.0000   +0.00055   t =   4.9   carries over
  fork rate       0.2256 vs 0.2255   -0.00010   t =  -2.0   carries over
  orphan rate     0.2256 vs 0.2255   -0.00009   t =  -2.2   carries over
  tip agreement   0.9943 vs 0.9950   +0.00070   t =   0.9   carries over
  p_ref           0.9260 vs 0.9998   +0.07381   t = 155.0   RE-MEASURE

p_ref fails in kind, not in degree: the unrestricted rule makes every in-window
orphan referenceable, so its ≈1.0 restates the model rather than measuring the
design. Any pre-countable p_ref is wrong by ~7 points.

Also corrects §6.7(1), which still asserted p_ref ≈ 1 for genuine latency
orphans — §6.8 had been corrected to the measured 0.944 but the earlier claim
was left standing — and drops "no difference is resolvable" from §9, which
contradicted §3.2a's pooled t = 5.7.

Adds scripts/model_carryover.py so the verdict is reproducible rather than
asserted, and a §9 study-table row. deep_ref_share is unaffected (0 by
construction under the countable rule); no other reference-structure metric is
quoted from a pre-countable run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 11:47:51 +02:00

67 lines
3.3 KiB
Python

"""Which metrics carry over from the unrestricted model to the countable one? (§9)
Most of the report's studies predate the countable default. They reproduce exactly under `--old`,
so the question is not whether they are valid runs — it is whether their *numbers* still describe
the countable design. §9 used to answer that wholesale ("no difference is resolvable in the design
regime, so those findings carry over"), and that is how the §8.4 capstone went stale: its `p_ref`
was quoted at 1.000 for weeks after the redesign made the true value 0.944.
The wholesale answer is wrong because the carry-over is metric-specific. Accuracy barely moves
between the models at `rho < 1` — that is the §3.2a finding. But `p_ref` is not a small difference,
it is an artefact: under the unrestricted rule EVERY in-window orphan is referenceable, so
`p_ref ~ 1` is a restatement of the model rather than a measurement of the design.
This prints the per-metric verdict off the committed paired sweep, where both arms share the stake
draw, the peering graph and every lottery outcome, so the uncle rule is the only difference and the
comparison is a paired one. Any metric that fails here must be re-measured rather than carried over.
Run: python scripts/model_carryover.py
"""
from __future__ import annotations
from pathlib import Path
import pandas as pd
HERE = Path(__file__).resolve().parent.parent
RUNS = HERE / "runs"
# The design band: the operating regime the recommendation lives in. U = 0 is excluded because
# with no uncle slots the two models are identical by construction and would dilute the contrast.
METRICS = ["mean_ratio", "fork_rate", "mean_orphan_rate", "agreement_tip", "p_ref"]
CELL = ["blend_delay_max", "max_uncles", "replicate"]
def _load(label: str) -> pd.DataFrame:
"""Equilibrium tail of the newest paired run with this label."""
src = sorted(RUNS.glob(f"*_{label}/results.parquet"))[-1]
df = pd.read_parquet(src)
return df[(df.epoch >= df.epochs.iloc[0] // 2) & (df.max_uncles > 0)]
def main() -> None:
countable, unrestricted = _load("fine-paired-countable"), _load("fine-paired-old")
print("=== countable vs unrestricted ceiling, PAIRED, design band (delta_max 1-5, U >= 1) ===")
print(f"{'metric':>18} {'countable':>11} {'ceiling':>11} {'paired diff':>20} {'t':>7} verdict")
for m in METRICS:
if m not in countable.columns:
continue
a = countable.groupby(CELL)[m].mean()
b = unrestricted.groupby(CELL)[m].mean()
i = a.index.intersection(b.index)
d = b[i] - a[i]
# Paired t over cells. A metric "carries over" if the gap is negligible against the
# +-0.9% per-epoch noise floor of Appendix B, NOT if it merely fails to resolve --- with
# 40 paired replicates even a 0.06% gap resolves, and that one is still immaterial.
t = d.mean() / d.sem() if d.std(ddof=1) > 0 else float("nan")
verdict = "carries over" if abs(d.mean()) < 0.009 else "RE-MEASURE"
print(f"{m:>18} {a[i].mean():11.4f} {b[i].mean():11.4f} "
f"{d.mean():+12.5f}+-{d.sem():.5f} {t:7.1f} {verdict}")
print("\np_ref is the one that fails: the unrestricted model makes every in-window orphan\n"
"referenceable, so its ~1.0 is a property of the model, not of the design.")
if __name__ == "__main__":
main()