approaches/value-policy-learning/oneply-q-prune/analysis/paired_bounds.pyPython67 lines · 3.1 KB
#!/usr/bin/env python3
"""Two-sided percentile-bootstrap bounds for paired per-game deltas.

  paired_bounds.py EVALUATE_JSON [MORE ...] --reference ARM --out FILE

For every other arm on the seeds it shares with the reference: mean score and
lifetime deltas, one-sided 95% lower AND upper percentile-bootstrap bounds
(10,000 resamples, RNG seed 0x6b660001), the detection floor
1.645 sd/sqrt(n), wins/ties/losses, and the ratio of mean logical work per
move.  Pure standard library.
"""
import argparse
import json
import math
import random
import statistics

ap = argparse.ArgumentParser()
ap.add_argument("paths", nargs="+")
ap.add_argument("--reference", required=True)
ap.add_argument("--resamples", type=int, default=10000)
ap.add_argument("--seed", type=lambda s: int(s, 0), default=0x6B660001)
ap.add_argument("--out", required=True)
args = ap.parse_args()

arms = {}
for path in args.paths:
    part = json.load(open(path))
    for a in part["arms"]:
        arms[a["name"]] = a
ref = {r["seedHex"]: r for r in arms[args.reference]["rows"]}


def bounds(deltas, rng):
    n = len(deltas)
    means = []
    for _ in range(args.resamples):
        means.append(sum(deltas[rng.randrange(n)] for _ in range(n)) / n)
    means.sort()
    lo = means[int(0.05 * (args.resamples - 1))]
    hi = means[int(0.95 * (args.resamples - 1))]
    sd = statistics.stdev(deltas) if n > 1 else 0.0
    return {"n": n, "meanDelta": sum(deltas) / n, "sdDelta": sd, "lb95": lo, "ub95": hi,
            "detectionFloor": 1.645 * sd / math.sqrt(n),
            "wins": sum(1 for d in deltas if d > 0), "ties": sum(1 for d in deltas if d == 0),
            "losses": sum(1 for d in deltas if d < 0)}


out = {"reference": args.reference, "resamples": args.resamples, "seed": hex(args.seed), "arms": {}}
for name, arm in arms.items():
    if name == args.reference:
        continue
    rows = [(r, ref[r["seedHex"]]) for r in arm["rows"] if r["seedHex"] in ref]
    rng = random.Random(args.seed)
    score = bounds([a["score"] - b["score"] for a, b in rows], rng)
    rng = random.Random(args.seed)
    moves = bounds([a["moves"] - b["moves"] for a, b in rows], rng)
    work_a = sum(a["logicalWork"] for a, _ in rows) / max(1, sum(a["moves"] for a, _ in rows))
    work_b = sum(b["logicalWork"] for _, b in rows) / max(1, sum(b["moves"] for _, b in rows))
    out["arms"][name] = {"score": score, "moves": moves, "workPerMoveRatio": work_a / work_b if work_b else None,
                         "meanScore": sum(a["score"] for a, _ in rows) / len(rows),
                         "referenceMeanScore": sum(b["score"] for _, b in rows) / len(rows)}
    print("%s vs %s (n=%d): score %+.0f [LB %+.0f, UB %+.0f] floor %.0f W/T/L %d/%d/%d; moves %+.2f [LB %+.2f, UB %+.2f] floor %.2f; work ratio %.3f" % (
        name, args.reference, score["n"], score["meanDelta"], score["lb95"], score["ub95"], score["detectionFloor"],
        score["wins"], score["ties"], score["losses"], moves["meanDelta"], moves["lb95"], moves["ub95"], moves["detectionFloor"],
        out["arms"][name]["workPerMoveRatio"]))
json.dump(out, open(args.out, "w"), indent=2)