Viewing files at commit c10cefe7abe5Commit detailsView latest

Browse files
scripts/benchmark_stats.py 136 lines · 5.2 KB · python
1
#!/usr/bin/env python3
2
"""Compute bootstrap confidence intervals for Oak benchmark JSONL files."""
3
4
from __future__ import annotations
5
6
import argparse
7
import json
8
import math
9
import random
10
import statistics
11
from pathlib import Path
12
from typing import Any, Iterable
13
14
from oakbench.rows import row_returncode
15
from oakbench.stats import percentile_nearest
16
17
18
SUBOP_SUFFIXES = (".add", ".commit")
19
20
# Mirrors oakbench.reporting.TAIL_PERCENTILE_MIN_SAMPLES; kept import-free so
21
# this script stays runnable against result files on machines without the repo.
22
TAIL_MIN_SAMPLES = {"p95": 20, "p99": 100}
23
24
25
def parse_args() -> argparse.Namespace:
26
    parser = argparse.ArgumentParser(description=__doc__)
27
    parser.add_argument("jsonl", nargs="+", type=Path)
28
    parser.add_argument("--metric", default="elapsed_ms")
29
    parser.add_argument("--bootstrap-samples", type=int, default=2000)
30
    parser.add_argument("--confidence", type=float, default=0.95)
31
    parser.add_argument("--seed", type=int, default=20260609)
32
    parser.add_argument("--include-subops", action="store_true")
33
    return parser.parse_args()
34
35
36
def iter_rows(paths: Iterable[Path]) -> Iterable[dict[str, Any]]:
37
    for path in paths:
38
        with path.open() as fh:
39
            for raw_line in fh:
40
                line = raw_line.strip()
41
                if line:
42
                    yield json.loads(line)
43
44
45
def bootstrap_ci(
46
    values: list[float],
47
    statistic: str,
48
    samples: int,
49
    confidence: float,
50
    rng: random.Random,
51
) -> tuple[float, float]:
52
    if len(values) <= 1:
53
        value = values[0] if values else float("nan")
54
        return value, value
55
56
    stats: list[float] = []
57
    n = len(values)
58
    for _ in range(samples):
59
        sample = [values[rng.randrange(n)] for _ in range(n)]
60
        if statistic == "median":
61
            stats.append(float(statistics.median(sample)))
62
        elif statistic == "p90":
63
            stats.append(percentile_nearest(sample, 90.0))
64
        elif statistic == "p95":
65
            stats.append(percentile_nearest(sample, 95.0))
66
        elif statistic == "p99":
67
            stats.append(percentile_nearest(sample, 99.0))
68
        elif statistic == "mean":
69
            stats.append(float(statistics.mean(sample)))
70
        else:
71
            raise ValueError(statistic)
72
73
    stats.sort()
74
    alpha = 1.0 - confidence
75
    low_index = max(0, min(len(stats) - 1, math.floor((alpha / 2.0) * len(stats))))
76
    high_index = max(0, min(len(stats) - 1, math.ceil((1.0 - alpha / 2.0) * len(stats)) - 1))
77
    return stats[low_index], stats[high_index]
78
79
80
def main() -> int:
81
    args = parse_args()
82
    grouped: dict[tuple[str, str, str], list[float]] = {}
83
    for row in iter_rows(args.jsonl):
84
        if row_returncode(row) != 0:
85
            continue
86
        operation = str(row.get("operation") or "")
87
        if not args.include_subops and operation.endswith(SUBOP_SUFFIXES):
88
            continue
89
        try:
90
            value = float(row[args.metric])
91
        except (KeyError, TypeError, ValueError):
92
            continue
93
        key = (str(row.get("subject")), str(row.get("scenario")), operation)
94
        grouped.setdefault(key, []).append(value)
95
96
    rng = random.Random(args.seed)
97
    print(f"# Bootstrap Statistics: `{args.metric}`")
98
    print()
99
    print(f"- Inputs: {', '.join(f'`{path}`' for path in args.jsonl)}")
100
    print(f"- Bootstrap samples: {args.bootstrap_samples}")
101
    print(f"- Confidence: {args.confidence:.3f}")
102
    print()
103
    print(
104
        "| Subject | Scenario / operation | n | median | median CI | p90 | p90 CI | "
105
        "p95 | p99 | max | mean | mean CI |"
106
    )
107
    print("| --- | --- | ---: | ---: | --- | ---: | --- | ---: | ---: | ---: | ---: | --- |")
108
    for (subject, scenario, operation), values in sorted(grouped.items()):
109
        median_value = float(statistics.median(values))
110
        p90_value = percentile_nearest(values, 90.0)
111
        mean_value = float(statistics.mean(values))
112
        median_ci = bootstrap_ci(values, "median", args.bootstrap_samples, args.confidence, rng)
113
        p90_ci = bootstrap_ci(values, "p90", args.bootstrap_samples, args.confidence, rng)
114
        mean_ci = bootstrap_ci(values, "mean", args.bootstrap_samples, args.confidence, rng)
115
        # Tail honesty: a nearest-rank percentile below 1/(1-p) samples is just
116
        # the max relabelled. Blank cells mean "unmeasurable at this n", never 0.
117
        n = len(values)
118
        p95_cell = f"{percentile_nearest(values, 95.0):.3f}" if n >= TAIL_MIN_SAMPLES["p95"] else ""
119
        p99_cell = f"{percentile_nearest(values, 99.0):.3f}" if n >= TAIL_MIN_SAMPLES["p99"] else ""
120
        print(
121
            f"| `{subject}` | `{scenario}/{operation}` | {n} | "
122
            f"{median_value:.3f} | [{median_ci[0]:.3f}, {median_ci[1]:.3f}] | "
123
            f"{p90_value:.3f} | [{p90_ci[0]:.3f}, {p90_ci[1]:.3f}] | "
124
            f"{p95_cell} | {p99_cell} | {max(values):.3f} | "
125
            f"{mean_value:.3f} | [{mean_ci[0]:.3f}, {mean_ci[1]:.3f}] |"
126
        )
127
    print()
128
    print(
129
        f"p95/p99 cells are blank below n={TAIL_MIN_SAMPLES['p95']}/n={TAIL_MIN_SAMPLES['p99']}: "
130
        "at smaller n the nearest-rank tail percentile equals the max, so claiming it would overstate precision."
131
    )
132
    return 0
133
134
135
if __name__ == "__main__":
136
    raise SystemExit(main())