"""Offline paired binary-task analysis. Synthetic demo; Python standard library only. CSV columns: task_id,a_pass,b_pass,a_cost,b_cost. One preregistered trial per task. Pass fields must be exactly 0 or 1; costs are USD, finite and nonnegative. Usage: python paired_benchmark_analysis.py [results.csv] [--bootstrap 10000] """ import argparse import csv import json import math import random from pathlib import Path def analyze(rows, bootstrap=10000, seed=20261008): if not rows or bootstrap < 100: raise ValueError("Need nonempty rows and at least 100 bootstrap draws") ids = set() parsed = [] for row in rows: task = row["task_id"] if not task or task in ids: raise ValueError("Missing or duplicate task_id") ids.add(task) passes = [] for key in ("a_pass", "b_pass"): if str(row[key]) not in ("0", "1"): raise ValueError("Pass fields must be 0 or 1") passes.append(int(row[key])) costs = [float(row[key]) for key in ("a_cost", "b_cost")] if any(not math.isfinite(x) or x < 0 for x in costs): raise ValueError("Costs must be finite and nonnegative") parsed.append((*passes, *costs)) n = len(parsed) a = sum(r[0] for r in parsed) b = sum(r[1] for r in parsed) a_only = sum(r[0] == 1 and r[1] == 0 for r in parsed) b_only = sum(r[0] == 0 and r[1] == 1 for r in parsed) discordant = a_only + b_only # Exact two-sided McNemar conditional binomial test; no continuity correction. tail = sum(math.comb(discordant, k) for k in range(min(a_only, b_only) + 1)) p = min(1.0, 2 * tail / (2 ** discordant)) if discordant else 1.0 rng = random.Random(seed) diffs = [r[0] - r[1] for r in parsed] draws = sorted(sum(rng.choice(diffs) for _ in range(n)) / n for _ in range(bootstrap)) def quantile(q): position = (len(draws) - 1) * q lo = math.floor(position) hi = math.ceil(position) return draws[lo] + (draws[hi] - draws[lo]) * (position - lo) a_cost = sum(r[2] for r in parsed) b_cost = sum(r[3] for r in parsed) return {"synthetic_if_demo": True, "tasks": n, "a_resolved": a, "b_resolved": b, "a_rate": a / n, "b_rate": b / n, "a_minus_b_pp": 100 * (a - b) / n, "paired_bootstrap_95pct_pp": [100 * quantile(.025), 100 * quantile(.975)], "a_only": a_only, "b_only": b_only, "exact_mcnemar_p": p, "a_total_usd": a_cost, "b_total_usd": b_cost, "a_usd_per_resolved": a_cost / a if a else None, "b_usd_per_resolved": b_cost / b if b else None, "seed": seed, "bootstrap_draws": bootstrap} def demo(): outcomes = [(1, 1)] * 50 + [(1, 0)] * 20 + [(0, 1)] * 10 + [(0, 0)] * 20 return [{"task_id": f"synthetic-{i}", "a_pass": a, "b_pass": b, "a_cost": 2.0, "b_cost": 1.2} for i, (a, b) in enumerate(outcomes)] def self_test(): result = analyze(demo(), bootstrap=2000) assert result["a_resolved"] == 70 and result["b_resolved"] == 60 assert result["a_minus_b_pp"] == 10 assert abs(result["exact_mcnemar_p"] - 0.09873714670538902) < 1e-12 assert abs(result["a_usd_per_resolved"] - 200 / 70) < 1e-12 assert abs(result["b_usd_per_resolved"] - 2) < 1e-12 assert analyze(demo(), bootstrap=100)["paired_bootstrap_95pct_pp"] == analyze(demo(), bootstrap=100)["paired_bootstrap_95pct_pp"] for bad in ([], [demo()[0], demo()[0]], [{**demo()[0], "a_pass": 2}], [{**demo()[0], "b_cost": "nan"}], [{**demo()[0], "a_cost": -1}]): try: analyze(bad, bootstrap=100) except ValueError: pass else: raise AssertionError("Malformed data accepted") if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("csv", nargs="?", type=Path) parser.add_argument("--bootstrap", type=int, default=10000) parser.add_argument("--self-test", action="store_true") args = parser.parse_args() if args.self_test: self_test() print("Self-test passed") else: if args.csv: with args.csv.open(newline="", encoding="utf-8") as stream: rows = list(csv.DictReader(stream)) else: rows = demo() result = analyze(rows, args.bootstrap) result["synthetic_if_demo"] = args.csv is None print(json.dumps(result, indent=2, allow_nan=False))