honestboard 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jeffery Opoku and David Banahene
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: honestboard
3
+ Version: 0.1.0
4
+ Summary: Statistical checks for AI leaderboards: a selective record test with online certification, rank intervals, a confidence set of the best system and a winner's-curse correction, from per-task results.
5
+ Author: David Banahene
6
+ Author-email: Jeffery Opoku <jeffery.opoku01@utrgv.edu>
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/jopoku16/honestboard
9
+ Project-URL: Repository, https://github.com/jopoku16/honestboard
10
+ Project-URL: Live page, https://jopoku16.github.io/honestboard/
11
+ Project-URL: Issues, https://github.com/jopoku16/honestboard/issues
12
+ Keywords: leaderboard,benchmark,evaluation,selective inference,winner's curse,AI agents,statistics
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Scientific/Engineering
23
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: numpy>=1.22
28
+ Requires-Dist: scipy>=1.9
29
+ Dynamic: license-file
30
+
31
+ # honestboard
32
+
33
+ Honest uncertainty for agent leaderboards. Give it the per-task results of every system on a benchmark and it tells you:
34
+
35
+ - **Is this SOTA claim real?** An exact paired test of a new record against the record it replaced.
36
+ - **What can this benchmark resolve?** The smallest difference it detects with 80% power, and how many tasks a one-point gap would need.
37
+ - **How sure is the ranking?** Rank intervals and the probability each system is the best.
38
+ - **Who could still be the best?** A 95% confidence set of the best system.
39
+ - **What is the winner's score, corrected?** The top score on a leaderboard is biased upward because it was picked for being highest. `honestboard` removes most of that selection bias.
40
+ - **How many record claims survive?** An audit of every record step in a leaderboard's history, with a Holm correction.
41
+ - **Certify records as they arrive.** `certify_chain` runs a sequential rule along the submission history: the exact sign-test p-value conditional on the record event (with a per-date factor for submissions that share a date), at levels set by alpha-spending or by the online fallback rule (family-wise error over the whole chain, any number of records, no independence assumption), by tenure-indexed spending on the certified baseline, or by LORD++ (online FDR; certified records earn back budget), against the leaderboard holder or against the last certified record.
42
+
43
+ It needs per-task outcomes (which tasks each system solved), not just the headline score. A checkout of the SWE-bench `experiments` repository loads with one call (`from_swebench_experiments`); any other leaderboard works once you write a board file (format below).
44
+
45
+ ## Quick start
46
+
47
+ ```python
48
+ from honestboard import Board
49
+
50
+ b = Board.from_json("swebench_verified.json") # board files may carry a "clusters" map (task -> repository)
51
+
52
+ b.compare("20250928_trae_doubao_seed_code", "20251205_sonar-foundation-agent_claude-opus-4-5")
53
+ b.rank_summary(top=10) # 95% rank intervals and top-rank frequency
54
+ b.confidence_set_best() # systems that cannot be ruled out as best
55
+ b.record_audit() # every record step: selective exact test (with the per-date factor), Holm-adjusted
56
+ b.record_lags() # how far back a record is distinguishable
57
+ from honestboard import certify_chain
58
+ certify_chain(b, rule="lord", baseline="certified") # sequential certification in date order
59
+ b.resolution() # minimum detectable difference, tasks needed for 1 and 2 points
60
+ b.winner_adjusted() # selection-corrected score for the leader (iterated bootstrap)
61
+ b.check_claim("some-system-name") # the main facts about one system in one call
62
+ ```
63
+
64
+ Command line:
65
+
66
+ ```
67
+ python -m honestboard report swebench_verified.json
68
+ python -m honestboard check swebench_verified.json 20251205_sonar-foundation-agent_claude-opus-4-5
69
+ python -m honestboard pr-check experiments/evaluation/verified experiments/evaluation/verified/NEW_SUBMISSION --tasks verified_ids.json
70
+ ```
71
+
72
+ `pr-check` reads a checkout of a leaderboard repository and prints a Markdown report on one submission (rank interval, margin over the current record, a paired test of the claim, what the board can resolve). `examples/leaderboard-pr-check.yml` shows how a leaderboard repository can post that report on every pull request.
73
+
74
+ Board file (JSON): `{"tasks": [...ids...], "systems": [{"name": "...", "date": "20251205", "resolved": [...ids...]}], "clusters": {task: group}}`.
75
+ For repeated trials use `"rates": {task: share_passed}` instead of `"resolved"`. A CSV with header `name,date,<task ids>` and 0/1 or rate cells also works. `clusters` is optional; `cluster=True` needs it and raises an error without it.
76
+
77
+ ## Methods
78
+
79
+ | Question | Method |
80
+ |---|---|
81
+ | Is B better than A? | Exact McNemar test (0/1 outcomes) or paired sign-flip permutation test (rates): one-sided, two-sided and the selective p-value `p_selective` (the one-sided tail conditional on B having scored higher, the record test) |
82
+ | Rank uncertainty | Task bootstrap; tied systems share a rank; top-rank frequency counts ties fractionally. `cluster=True` resamples repositories instead of tasks |
83
+ | Confidence set of the best | Paired-bootstrap multiple comparisons with the best over all systems (max studentised difference); coverage is approximate |
84
+ | Record audit | Selective exact paired test of each new record against the previous record (`sided="selective"`, default; `"two"` and `"one"` as sensitivities), Holm-adjusted over the steps. `tie_order` handles same-date submissions best-first (default, fewest steps) or worst-first; under best-first the challenger is the best of its date, so its p-value is multiplied by the size of that date group (`date_correction=True`, default), which the guarantees need |
85
+ | Resolution | Normal approximation from the paired standard deviation between neighbouring top systems; checked by simulation |
86
+ | Winner's score | Iterated plug-in bootstrap bias correction (default). The exact conditional (polyhedral) method is `winner_conditional`; two published baselines are `winner_hybrid` (the hybrid of Andrews, Kitagawa and McCloskey, 2024: conditional, with the truncation set bounded by a simultaneous projection interval, so never unbounded) and `winner_zoom` (the zoom interval of Zrnic and Fithian, 2025: simultaneous over the plausible winners only; corrects the interval, not the estimate) |
87
+ | Record chains over time | `certify_chain`: a selective p-value given the record event, at levels from an alpha-spending series, the online fallback rule (`rule="fallback"`: a certified record passes its level to the next one) or, for `baseline="certified"` (the leader moves only on certification), the tenure-indexed rule (`rule="tenure"`); all three with family-wise error control over the chain proved under the task-wise null, any dependence. LORD++ (online FDR) is calibrated by simulation. See the paper |
88
+
89
+ ## Limits you should know about
90
+
91
+ - It measures **task-sampling luck only**. Developers tuning on a public benchmark and run-to-run randomness of agents are outside the model, so the real overstatement of a winner is larger than the correction removes.
92
+ - Tasks are treated as independent draws. Tasks from the same repository are correlated; `cluster=True` is a rough sensitivity check (with few clusters it is not reliable and the paired tests still ignore clusters).
93
+ - The record test conditions on the record event (the selective p-value); it is exact for a fixed holder under the task-wise null and conservative when the previous record is a running maximum (under a global null it rejects well under 5% of record steps). A record that does not pass is not necessarily worse. It cannot be told apart from the previous record with these tasks. The task-level test treats tasks as independent; where tasks share a repository, `cluster=True` is the cautious count.
94
+ - The winner correction removes most of the bias in simulations but not all of it, much less under a shift to other repositories, and its interval is approximate (below nominal at small sample sizes).
95
+ - The conditional method is exact under normality but gives an unbounded answer when the winner ties or nearly ties the runner-up.
96
+ - The record chain depends on how submissions with the same date are ordered.
97
+
98
+ ## Install and test
99
+
100
+ ```
101
+ pip install honestboard # from PyPI (numpy and scipy only); or, from a checkout of this folder:
102
+ pip install -e .
103
+ python -m pytest tests
104
+ ```
105
+
106
+ MIT licensed. If you use `honestboard`, please cite the paper (see `CITATION.cff`).
@@ -0,0 +1,76 @@
1
+ # honestboard
2
+
3
+ Honest uncertainty for agent leaderboards. Give it the per-task results of every system on a benchmark and it tells you:
4
+
5
+ - **Is this SOTA claim real?** An exact paired test of a new record against the record it replaced.
6
+ - **What can this benchmark resolve?** The smallest difference it detects with 80% power, and how many tasks a one-point gap would need.
7
+ - **How sure is the ranking?** Rank intervals and the probability each system is the best.
8
+ - **Who could still be the best?** A 95% confidence set of the best system.
9
+ - **What is the winner's score, corrected?** The top score on a leaderboard is biased upward because it was picked for being highest. `honestboard` removes most of that selection bias.
10
+ - **How many record claims survive?** An audit of every record step in a leaderboard's history, with a Holm correction.
11
+ - **Certify records as they arrive.** `certify_chain` runs a sequential rule along the submission history: the exact sign-test p-value conditional on the record event (with a per-date factor for submissions that share a date), at levels set by alpha-spending or by the online fallback rule (family-wise error over the whole chain, any number of records, no independence assumption), by tenure-indexed spending on the certified baseline, or by LORD++ (online FDR; certified records earn back budget), against the leaderboard holder or against the last certified record.
12
+
13
+ It needs per-task outcomes (which tasks each system solved), not just the headline score. A checkout of the SWE-bench `experiments` repository loads with one call (`from_swebench_experiments`); any other leaderboard works once you write a board file (format below).
14
+
15
+ ## Quick start
16
+
17
+ ```python
18
+ from honestboard import Board
19
+
20
+ b = Board.from_json("swebench_verified.json") # board files may carry a "clusters" map (task -> repository)
21
+
22
+ b.compare("20250928_trae_doubao_seed_code", "20251205_sonar-foundation-agent_claude-opus-4-5")
23
+ b.rank_summary(top=10) # 95% rank intervals and top-rank frequency
24
+ b.confidence_set_best() # systems that cannot be ruled out as best
25
+ b.record_audit() # every record step: selective exact test (with the per-date factor), Holm-adjusted
26
+ b.record_lags() # how far back a record is distinguishable
27
+ from honestboard import certify_chain
28
+ certify_chain(b, rule="lord", baseline="certified") # sequential certification in date order
29
+ b.resolution() # minimum detectable difference, tasks needed for 1 and 2 points
30
+ b.winner_adjusted() # selection-corrected score for the leader (iterated bootstrap)
31
+ b.check_claim("some-system-name") # the main facts about one system in one call
32
+ ```
33
+
34
+ Command line:
35
+
36
+ ```
37
+ python -m honestboard report swebench_verified.json
38
+ python -m honestboard check swebench_verified.json 20251205_sonar-foundation-agent_claude-opus-4-5
39
+ python -m honestboard pr-check experiments/evaluation/verified experiments/evaluation/verified/NEW_SUBMISSION --tasks verified_ids.json
40
+ ```
41
+
42
+ `pr-check` reads a checkout of a leaderboard repository and prints a Markdown report on one submission (rank interval, margin over the current record, a paired test of the claim, what the board can resolve). `examples/leaderboard-pr-check.yml` shows how a leaderboard repository can post that report on every pull request.
43
+
44
+ Board file (JSON): `{"tasks": [...ids...], "systems": [{"name": "...", "date": "20251205", "resolved": [...ids...]}], "clusters": {task: group}}`.
45
+ For repeated trials use `"rates": {task: share_passed}` instead of `"resolved"`. A CSV with header `name,date,<task ids>` and 0/1 or rate cells also works. `clusters` is optional; `cluster=True` needs it and raises an error without it.
46
+
47
+ ## Methods
48
+
49
+ | Question | Method |
50
+ |---|---|
51
+ | Is B better than A? | Exact McNemar test (0/1 outcomes) or paired sign-flip permutation test (rates): one-sided, two-sided and the selective p-value `p_selective` (the one-sided tail conditional on B having scored higher, the record test) |
52
+ | Rank uncertainty | Task bootstrap; tied systems share a rank; top-rank frequency counts ties fractionally. `cluster=True` resamples repositories instead of tasks |
53
+ | Confidence set of the best | Paired-bootstrap multiple comparisons with the best over all systems (max studentised difference); coverage is approximate |
54
+ | Record audit | Selective exact paired test of each new record against the previous record (`sided="selective"`, default; `"two"` and `"one"` as sensitivities), Holm-adjusted over the steps. `tie_order` handles same-date submissions best-first (default, fewest steps) or worst-first; under best-first the challenger is the best of its date, so its p-value is multiplied by the size of that date group (`date_correction=True`, default), which the guarantees need |
55
+ | Resolution | Normal approximation from the paired standard deviation between neighbouring top systems; checked by simulation |
56
+ | Winner's score | Iterated plug-in bootstrap bias correction (default). The exact conditional (polyhedral) method is `winner_conditional`; two published baselines are `winner_hybrid` (the hybrid of Andrews, Kitagawa and McCloskey, 2024: conditional, with the truncation set bounded by a simultaneous projection interval, so never unbounded) and `winner_zoom` (the zoom interval of Zrnic and Fithian, 2025: simultaneous over the plausible winners only; corrects the interval, not the estimate) |
57
+ | Record chains over time | `certify_chain`: a selective p-value given the record event, at levels from an alpha-spending series, the online fallback rule (`rule="fallback"`: a certified record passes its level to the next one) or, for `baseline="certified"` (the leader moves only on certification), the tenure-indexed rule (`rule="tenure"`); all three with family-wise error control over the chain proved under the task-wise null, any dependence. LORD++ (online FDR) is calibrated by simulation. See the paper |
58
+
59
+ ## Limits you should know about
60
+
61
+ - It measures **task-sampling luck only**. Developers tuning on a public benchmark and run-to-run randomness of agents are outside the model, so the real overstatement of a winner is larger than the correction removes.
62
+ - Tasks are treated as independent draws. Tasks from the same repository are correlated; `cluster=True` is a rough sensitivity check (with few clusters it is not reliable and the paired tests still ignore clusters).
63
+ - The record test conditions on the record event (the selective p-value); it is exact for a fixed holder under the task-wise null and conservative when the previous record is a running maximum (under a global null it rejects well under 5% of record steps). A record that does not pass is not necessarily worse. It cannot be told apart from the previous record with these tasks. The task-level test treats tasks as independent; where tasks share a repository, `cluster=True` is the cautious count.
64
+ - The winner correction removes most of the bias in simulations but not all of it, much less under a shift to other repositories, and its interval is approximate (below nominal at small sample sizes).
65
+ - The conditional method is exact under normality but gives an unbounded answer when the winner ties or nearly ties the runner-up.
66
+ - The record chain depends on how submissions with the same date are ordered.
67
+
68
+ ## Install and test
69
+
70
+ ```
71
+ pip install honestboard # from PyPI (numpy and scipy only); or, from a checkout of this folder:
72
+ pip install -e .
73
+ python -m pytest tests
74
+ ```
75
+
76
+ MIT licensed. If you use `honestboard`, please cite the paper (see `CITATION.cff`).
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "honestboard"
7
+ version = "0.1.0"
8
+ description = "Statistical checks for AI leaderboards: a selective record test with online certification, rank intervals, a confidence set of the best system and a winner's-curse correction, from per-task results."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Jeffery Opoku", email = "jeffery.opoku01@utrgv.edu" },
14
+ { name = "David Banahene" },
15
+ ]
16
+ keywords = ["leaderboard", "benchmark", "evaluation", "selective inference", "winner's curse", "AI agents", "statistics"]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Intended Audience :: Science/Research",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Topic :: Scientific/Engineering",
28
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
29
+ ]
30
+ dependencies = ["numpy>=1.22", "scipy>=1.9"]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/jopoku16/honestboard"
34
+ Repository = "https://github.com/jopoku16/honestboard"
35
+ "Live page" = "https://jopoku16.github.io/honestboard/"
36
+ Issues = "https://github.com/jopoku16/honestboard/issues"
37
+
38
+ [project.scripts]
39
+ honestboard = "honestboard.__main__:main"
40
+
41
+ [tool.setuptools.packages.find]
42
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,9 @@
1
+ """honestboard: honest uncertainty for agent leaderboards."""
2
+ from .chain import certify_chain, fallback_level, selective_sign_pvalue
3
+ from .core import Board
4
+ from .loaders import board_with_submission, from_swebench_experiments
5
+ from .selective import simultaneous_quantile, truncation_limits, winner_adjusted, winner_hybrid, winner_zoom
6
+
7
+ __all__ = ["Board", "board_with_submission", "certify_chain", "fallback_level", "from_swebench_experiments", "selective_sign_pvalue",
8
+ "simultaneous_quantile", "truncation_limits", "winner_adjusted", "winner_hybrid", "winner_zoom"]
9
+ __version__ = "0.1.0"
@@ -0,0 +1,131 @@
1
+ """Command line: python -m honestboard report BOARD.json | check BOARD.json SYSTEM"""
2
+ import argparse
3
+ import sys
4
+
5
+ from .core import Board
6
+
7
+
8
+ def _load(path):
9
+ return Board.from_csv(path) if path.endswith(".csv") else Board.from_json(path)
10
+
11
+
12
+ def _pct(x):
13
+ return f"{100 * x:.1f}%"
14
+
15
+
16
+ def cmd_report(args):
17
+ b = _load(args.board)
18
+ print(f"{b.K} systems, {b.T} tasks\n")
19
+ rs = b.rank_summary(top=args.top, cluster=args.cluster)
20
+ print("Leaders with 95% rank intervals and top-rank frequency:")
21
+ for r in rs:
22
+ print(f" {r['name'][:52]:52s} {_pct(r['score']):>6s} rank [{r['rank_lo']:.0f}, {r['rank_hi']:.0f}] top-rank freq.={r['p_best']:.2f}")
23
+ cs = b.confidence_set_best(cluster=args.cluster)
24
+ print(f"\nConfidence set of the best (95%): {cs['size']} of {b.K} systems cannot be ruled out as best.")
25
+ if b.dates:
26
+ a = b.record_audit()
27
+ print(f"\nRecord claims: {a['n_steps']} steps, {a['n_significant_raw']} beat the previous record at the 5% level "
28
+ f"(selective exact paired test, conditional on the record event), {a['n_significant_holm']} after Holm correction.")
29
+ r = b.resolution()
30
+ print(f"Resolution: this board detects differences of about {100 * r['mdd']:.1f} points (80% power); "
31
+ f"resolving a 1-point gap would take about {r['tasks_for_1pt']:,.0f} tasks.")
32
+ w = b.winner_adjusted(cluster=args.cluster)
33
+ print(f"\nWinner {w['name']}: reported {_pct(w['observed'])}, selection-adjusted estimate {_pct(w['estimate'])}, "
34
+ f"95% interval [{_pct(w['ci'][0])}, {_pct(w['ci'][1])}] (naive [{_pct(w['naive_ci'][0])}, {_pct(w['naive_ci'][1])}]).")
35
+ c = w["conditional"]
36
+ print(f" conditional method: estimate {_pct(c['estimate'])}, interval [{_pct(c['ci'][0])}, {_pct(c['ci'][1])}] "
37
+ f"(unbounded when the winner's margin is about zero)")
38
+ h = b.winner_hybrid(cluster=args.cluster, w=w["winner"])
39
+ z = b.winner_zoom(cluster=args.cluster, w=w["winner"])
40
+ print(f" hybrid (Andrews, Kitagawa and McCloskey): estimate {_pct(h['estimate'])}, interval [{_pct(h['ci'][0])}, {_pct(h['ci'][1])}]"
41
+ + (" (undefined at an exact tie for first)" if h["degenerate"] else ""))
42
+ print(f" zoom interval (Zrnic and Fithian): [{_pct(z['ci'][0])}, {_pct(z['ci'][1])}] over {z['n_plausible']} plausible winners")
43
+
44
+
45
+ def cmd_check(args):
46
+ b = _load(args.board)
47
+ r = b.check_claim(args.system, cluster=args.cluster)
48
+ print(f"{r['system']}: score {_pct(r['score'])}, observed rank {r['rank_observed']}, "
49
+ f"95% rank interval {r['rank_interval_95']}, top-rank freq.={r['p_best']:.2f}")
50
+ print(f"Inside the 95% confidence set of the best: {r['in_confidence_set_of_best']} (set size {r['confidence_set_size']})")
51
+ c = r["vs_best_other"]
52
+ print(f"Paired test, {c['b']} vs {c['a']}: difference {100 * c['diff']:+.1f} pts, one-sided exact p={c['p_one_sided']:.3f}")
53
+ if "winner_adjusted" in r:
54
+ w = r["winner_adjusted"]
55
+ print(f"Selection-adjusted score: {_pct(w['estimate'])}, 95% interval [{_pct(w['ci'][0])}, {_pct(w['ci'][1])}] "
56
+ f"(reported {_pct(w['observed'])})")
57
+
58
+
59
+ def cmd_pr_check(args):
60
+ """Markdown report on one new submission, suitable for a pull-request comment."""
61
+ import json
62
+ import numpy as np
63
+ from .loaders import board_with_submission
64
+ tasks = None
65
+ if args.tasks:
66
+ with open(args.tasks, encoding="utf-8") as f:
67
+ tasks = json.load(f)
68
+ b, k = board_with_submission(args.eval_dir, args.new_dir, tasks=tasks)
69
+ if tasks is None:
70
+ print("_Warning: no --tasks file was given, so tasks that no submission solved are not counted and every score is overstated._\n")
71
+ if b.K < 2:
72
+ print(f"### honestboard check: `{b.names[k]}`\n\n- First submission on this board: score {_pct(b.scores()[k])} on {b.T:,} tasks; nothing to compare with yet.")
73
+ return
74
+ sc = b.scores()
75
+ others = [j for j in np.argsort(-sc, kind="stable") if j != k]
76
+ prev = others[0]
77
+ res = b.resolution()
78
+ rs = {r["name"]: r for r in b.rank_summary(B=4000)}
79
+ me = rs[b.names[k]]
80
+ margin = sc[k] - sc[prev]
81
+ lines = [f"### honestboard check: `{b.names[k]}`", ""]
82
+ lines.append(f"- Score: **{_pct(sc[k])}** on {b.T:,} tasks; observed rank {1 + int((sc > sc[k]).sum())} of {b.K}; "
83
+ f"95% rank interval {me['rank_lo']:.0f} to {me['rank_hi']:.0f}; top-rank frequency {me['p_best']:.2f}.")
84
+ if margin > 1e-12:
85
+ c = b.compare(b.names[prev], b.names[k], B=3000)
86
+ verdict = "beats" if c["p_selective"] <= 0.05 else "cannot be told apart from"
87
+ lines.append(f"- Current record: `{b.names[prev]}` at {_pct(sc[prev])}. This submission is {100 * margin:+.1f} points above it "
88
+ f"(selective exact paired p, conditional on it scoring higher = {c['p_selective']:.3f}), so it **{verdict}** the current record at the 5% level.")
89
+ if abs(100 * margin) < 100 * res["mdd"]:
90
+ lines.append(f"- This board detects differences of about {100 * res['mdd']:.1f} points with 80% power, so a margin of {100 * margin:.1f} points is below its resolution.")
91
+ w = b.winner_adjusted(B=3000)
92
+ lines.append(f"- Selection-adjusted score for the new leader: {_pct(w['estimate'])} (95% interval {_pct(w['ci'][0])} to {_pct(w['ci'][1])}).")
93
+ elif abs(margin) <= 1e-12:
94
+ lines.append(f"- Current record: `{b.names[prev]}` at {_pct(sc[prev])}. This submission ties it exactly and does not set a new record.")
95
+ else:
96
+ c = b.compare(b.names[k], b.names[prev], B=3000)
97
+ lines.append(f"- Current record: `{b.names[prev]}` at {_pct(sc[prev])}. This submission is {100 * -margin:.1f} points below it and does not set a new record "
98
+ f"(one-sided p that the record is better = {c['p_one_sided']:.3f}).")
99
+ lines.append("")
100
+ lines.append("_Task sampling only; single runs; see the honestboard documentation for limits._")
101
+ print("\n".join(lines))
102
+
103
+
104
+ def main(argv=None):
105
+ ap = argparse.ArgumentParser(prog="honestboard")
106
+ sub = ap.add_subparsers(dest="cmd", required=True)
107
+ p = sub.add_parser("pr-check", help="check one new submission against a leaderboard checkout and print Markdown")
108
+ p.add_argument("eval_dir", help="evaluation folder of the leaderboard checkout, for example experiments/evaluation/verified")
109
+ p.add_argument("new_dir", help="the new submission folder")
110
+ p.add_argument("--tasks", help="JSON file with the benchmark's full task id list (recommended)")
111
+ p.set_defaults(fn=cmd_pr_check)
112
+ for name, fn in (("report", cmd_report), ("check", cmd_check)):
113
+ p = sub.add_parser(name)
114
+ p.add_argument("board")
115
+ if name == "report":
116
+ p.add_argument("--top", type=int, default=10)
117
+ else:
118
+ p.add_argument("system")
119
+ p.add_argument("--cluster", action="store_true",
120
+ help="resample task clusters (needs a 'clusters' map in the board file) instead of tasks")
121
+ p.set_defaults(fn=fn)
122
+ args = ap.parse_args(argv)
123
+ try:
124
+ args.fn(args)
125
+ except ValueError as e:
126
+ print(f"error: {e}", file=sys.stderr)
127
+ return 2
128
+
129
+
130
+ if __name__ == "__main__":
131
+ sys.exit(main())
@@ -0,0 +1,244 @@
1
+ """Sequential certification of record claims on a leaderboard.
2
+
3
+ A leaderboard tests a submission only when it scores higher than the current record. Three things make a naive
4
+ test wrong: the record event itself (selection on the sign of the difference), the holder being a running
5
+ maximum, and the unknown, data-dependent number of records that will be tested. This module provides:
6
+
7
+ * `selective_sign_pvalue`: the exact sign-test p-value conditional on the record event,
8
+ P(X >= n_b | X > n/2) with X ~ Bin(n, 1/2), n = n_a + n_b discordant tasks. Under equal ability it is
9
+ super-uniform given that the step is a record (for a fixed holder); a holder that is a running maximum makes
10
+ it conservative.
11
+ * `selective_signflip_pvalue`: the same idea for per-task rates, by Monte Carlo sign flips.
12
+ * `alpha_spending_level`: alpha_i = alpha * 6 / (pi^2 i^2) for the i-th record. The family-wise error over the
13
+ whole chain is at most alpha for ANY number of records and ANY dependence, by the union bound, provided each
14
+ step's p-value is valid given that the step occurred.
15
+ * `lord_level`: LORD++ (Ramdas, Yang, Wainwright and Jordan, 2017), an online false-discovery-rate rule in which
16
+ each certified record earns back testing budget, so late records remain testable. Its guarantee is proved
17
+ for independent p-values; with shared tasks the p-values are dependent, so it is checked by simulation.
18
+ * `certify_chain`: run a rule along a board's submission history, either against the leaderboard holder or
19
+ against the last certified record (a baseline that moves only when a record is certified).
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import numpy as np
24
+ from scipy import stats
25
+
26
+ from . import selective
27
+
28
+
29
+ # ---- per-step p-values ----------------------------------------------
30
+ def selective_sign_pvalue(n_b: int, n_a: int) -> float:
31
+ """P(X >= n_b | X > n/2), X ~ Bin(n, 1/2). Equals 1 unless n_b > n_a."""
32
+ n = int(n_a + n_b)
33
+ if n == 0 or n_b <= n_a:
34
+ return 1.0
35
+ tail = stats.binom.sf(n_b - 1, n, 0.5) # P(X >= n_b)
36
+ cond = stats.binom.sf(n // 2, n, 0.5) # P(X > n/2)
37
+ return float(min(1.0, tail / cond))
38
+
39
+
40
+ SIGNFLIP_DRAWS = 200000
41
+
42
+
43
+ def signflip_pvalues(d, rng=None, draws=SIGNFLIP_DRAWS, chunk=20000):
44
+ """Rates version of the paired tests: one-sided, two-sided and selective p-values for the sum S of randomly
45
+ signed per-task differences d, by Monte Carlo with `draws` sign patterns (generated in chunks to bound memory).
46
+ The selective p-value is P(S >= s_obs | S > 0). With 2e5 patterns about 1e5 fall in {S > 0}, so a p-value near
47
+ 0.001 has a Monte Carlo standard error of about 1e-4. `compare` and `certify_chain` draw the patterns from
48
+ the same seed convention (seed + 7*b + a for the indices of holder a and challenger b), so the audit and the
49
+ certification agree to the last digit. Returns (p_one_sided, p_two_sided, p_selective)."""
50
+ d = np.asarray(d, dtype=float)
51
+ obs = d.sum()
52
+ rng = np.random.default_rng(0) if rng is None else rng
53
+ pos = hit = hit2 = 0
54
+ done = 0
55
+ while done < draws:
56
+ m = min(chunk, draws - done)
57
+ null = rng.choice([-1.0, 1.0], size=(m, d.size)) @ d
58
+ hit += int((null >= obs - 1e-12).sum())
59
+ hit2 += int((np.abs(null) >= abs(obs) - 1e-12).sum())
60
+ pos += int((null > 1e-12).sum())
61
+ done += m
62
+ p1 = (1 + hit) / (1 + draws)
63
+ p2 = (1 + hit2) / (1 + draws)
64
+ ps = 1.0 if (obs <= 1e-12 or pos == 0) else min(1.0, (1 + hit) / (1 + pos))
65
+ return float(p1), float(p2), float(ps)
66
+
67
+
68
+ def selective_signflip_pvalue(d, rng=None, draws=SIGNFLIP_DRAWS) -> float:
69
+ """Rates version of the record test: P(S >= s_obs | S > 0) under random sign flips of the per-task differences d."""
70
+ return signflip_pvalues(d, rng, draws)[2]
71
+
72
+
73
+ def polyhedral_step_pvalue(p_hat, sigma, b, a, earlier, lattice=0.0) -> float:
74
+ """Selective p-value under p_hat ~ N(theta, sigma) for H0: theta_b <= theta_a, conditional on the record
75
+ event {p_b > p_a} and on a having been the holder {p_a >= p_j for every earlier j != a}. `lattice` (1/T for
76
+ pass counts) applies a continuity correction; without it, ties with the closest competitor put the statistic
77
+ on the truncation boundary and the p-value collapses."""
78
+ K = len(p_hat)
79
+ rows, rhs = [], []
80
+ r = np.zeros(K); r[a] = 1.0; r[b] = -1.0
81
+ rows.append(r); rhs.append(0.0) # p_a - p_b <= 0
82
+ for j in earlier:
83
+ if j == a:
84
+ continue
85
+ r = np.zeros(K); r[j] = 1.0; r[a] = -1.0
86
+ rows.append(r); rhs.append(0.0) # p_j - p_a <= 0
87
+ eta = np.zeros(K); eta[b] = 1.0; eta[a] = -1.0
88
+ return selective.polyhedral_pvalue(np.asarray(p_hat, float), sigma, eta, np.array(rows), np.array(rhs), lattice=lattice)
89
+
90
+
91
+ # ---- test levels ----------------------------------------------------
92
+ def alpha_spending_level(i: int, alpha: float) -> float:
93
+ """Level for the i-th record (i = 1, 2, ...); the levels sum to alpha."""
94
+ return alpha * 6.0 / (np.pi ** 2 * i ** 2)
95
+
96
+
97
+ def lord_gamma(j: int, kind: str = "quadratic") -> float:
98
+ """A spending sequence with sum at most 1. 'quadratic': 6 / (pi^2 j^2), which sums to 1 and suits chains of
99
+ tens of records. 'log': the sequence published with LORD++, 0.0722 log(max(j, 2)) / (j e^{sqrt(log j)}), which
100
+ sums to 1 only in the limit and is built for very long streams."""
101
+ if j < 1:
102
+ return 0.0
103
+ if kind == "quadratic":
104
+ return float(6.0 / (np.pi ** 2 * j ** 2))
105
+ if j == 1:
106
+ return float(0.0722 * np.log(2))
107
+ return float(0.0722 * np.log(max(j, 2)) / (j * np.exp(np.sqrt(np.log(j)))))
108
+
109
+
110
+ def lord_level(t: int, alpha: float, w0: float, rejection_times, kind: str = "quadratic") -> float:
111
+ """LORD++ level for the t-th test given the times of earlier rejections (1-indexed test counts)."""
112
+ lvl = lord_gamma(t, kind) * w0
113
+ if rejection_times:
114
+ lvl += (alpha - w0) * lord_gamma(t - rejection_times[0], kind)
115
+ for tau in rejection_times[1:]:
116
+ lvl += alpha * lord_gamma(t - tau, kind)
117
+ return float(lvl)
118
+
119
+
120
+ # ---- the procedure --------------------------------------------------
121
+ def _submission_order(board, tie_order="best_first"):
122
+ if board.dates is None:
123
+ raise ValueError("certification needs dates")
124
+ undated = [n for n, d in zip(board.names, board.dates) if not d]
125
+ if undated:
126
+ raise ValueError(f"{len(undated)} systems have no date (first: {undated[0]})")
127
+ sc = board.scores()
128
+ sign = -1.0 if tie_order == "best_first" else 1.0
129
+ return sorted(range(board.K), key=lambda k: (board.dates[k], sign * sc[k]))
130
+
131
+
132
+ def _step_pvalue(board, a, b, pvalue, sigma, earlier, seed):
133
+ if pvalue == "polyhedral":
134
+ return polyhedral_step_pvalue(board.scores(), sigma, b, a, earlier, lattice=1.0 / board.T)
135
+ ya, yb = board.Y[a], board.Y[b]
136
+ if board.is_binary:
137
+ n_b = int(((yb == 1) & (ya == 0)).sum())
138
+ n_a = int(((ya == 1) & (yb == 0)).sum())
139
+ return selective_sign_pvalue(n_b, n_a)
140
+ return selective_signflip_pvalue(yb.astype(float) - ya.astype(float), np.random.default_rng(seed + 7 * b + a))
141
+
142
+
143
+ def fallback_level(t: int, alpha: float, prev_level: float, prev_certified: bool) -> float:
144
+ """Online fallback (Wiens 2003; Tian and Ramdas 2021): the t-th record is tested at its spending level plus the
145
+ level of the previous record if that record was certified, so a run of certified records accumulates budget.
146
+ Family-wise error over the chain is at most alpha for any dependence, by the same argument as for spending:
147
+ on the event that step t is the first null step certified, every step in the run that feeds it is non-null, and
148
+ the runs feeding different null steps are disjoint, so the levels available to null steps sum to at most alpha."""
149
+ return float(alpha_spending_level(t, alpha) + (prev_level if prev_certified else 0.0))
150
+
151
+
152
+ def tenure_level(alpha: float, n_certified: int, dates_since_holder: int) -> float:
153
+ """Tenure-indexed spending for the certified baseline: alpha * gamma(j) * gamma(m) with j the index of the
154
+ current holder's tenure (one plus the number of certifications so far) and m the number of distinct submission
155
+ dates since the holder's date, gamma(k) = 6/(pi^2 k^2). Both indices are fixed by the submission calendar and
156
+ the earlier history, which is what the chain-wise guarantee needs; several submissions on one date share the
157
+ slot and are handled by the per-date correction. The price is that budget is spent on every date, tested or
158
+ not."""
159
+ return float(alpha * (6.0 / (np.pi ** 2 * n_certified ** 2)) * (6.0 / (np.pi ** 2 * dates_since_holder ** 2)))
160
+
161
+
162
+ def certify_chain(board, alpha=0.05, rule="lord", pvalue="exact", baseline="leader", tie_order="best_first", w0=None, seed=0,
163
+ date_correction=True):
164
+ """Walk the submissions in date order and certify records.
165
+
166
+ rule: 'unadjusted' (level alpha every step), 'holm' (retrospective, over the realised chain),
167
+ 'spending' (alpha * 6 / (pi^2 i^2) for the i-th tested record; family-wise control for any number of
168
+ records), 'fallback' (spending plus the level of the previous record when it was certified; the same
169
+ family-wise guarantee with more power along runs of genuine records), 'tenure' (certified baseline only:
170
+ spending indexed by the holder's tenure and the dates since its date, which carries the chain-wise
171
+ guarantee for that baseline), 'lord' (LORD++ online FDR).
172
+ pvalue: 'exact' (sign test conditional on the record event; sign flips for rates) or 'polyhedral'
173
+ (normal approximation, conditional on the record event and on the holder having been the maximum).
174
+ baseline: 'leader' tests each leaderboard record against the record it replaced; 'certified' tests every
175
+ submission that beats the last certified record against that record (the baseline moves only when
176
+ a record is certified).
177
+ date_correction: under the best-first ordering a tested submission was selected among the submissions of its
178
+ date; the p-value is multiplied by the size of that group (a Bonferroni factor), which the chain-wise
179
+ guarantees need. The worst-first ordering is a bound and is not corrected."""
180
+ if rule not in ("unadjusted", "holm", "spending", "fallback", "tenure", "lord"):
181
+ raise ValueError("rule must be 'unadjusted', 'holm', 'spending', 'fallback', 'tenure' or 'lord'")
182
+ if pvalue not in ("exact", "polyhedral"):
183
+ raise ValueError("pvalue must be 'exact' or 'polyhedral'")
184
+ if baseline not in ("leader", "certified"):
185
+ raise ValueError("baseline must be 'leader' or 'certified'")
186
+ if rule == "tenure" and baseline != "certified":
187
+ raise ValueError("the tenure rule is defined for the certified baseline")
188
+ w0 = alpha / 2 if w0 is None else w0
189
+ order = _submission_order(board, tie_order)
190
+ sc = board.scores()
191
+ sigma = board.sigma() if pvalue == "polyhedral" else None
192
+ correct = bool(date_correction and tie_order == "best_first")
193
+ date_rank = {} # distinct dates in order, for the tenure index
194
+ for k in order:
195
+ date_rank.setdefault(board.dates[k], len(date_rank))
196
+ holder = order[0]
197
+ certified_holder = order[0]
198
+ steps, rejections, t = [], [], 0
199
+ prev_level, prev_cert = 0.0, False
200
+ for pos, k in enumerate(order[1:], start=1):
201
+ ref = holder if baseline == "leader" else certified_holder
202
+ if sc[k] > sc[ref] + 1e-12:
203
+ t += 1
204
+ p_raw = _step_pvalue(board, ref, k, pvalue, sigma, order[:pos], seed)
205
+ g = board.date_group_size(k) if correct else 1
206
+ p = min(1.0, g * p_raw)
207
+ if rule == "unadjusted":
208
+ level = alpha
209
+ elif rule == "spending":
210
+ level = alpha_spending_level(t, alpha)
211
+ elif rule == "fallback":
212
+ level = fallback_level(t, alpha, prev_level, prev_cert)
213
+ elif rule == "tenure":
214
+ level = tenure_level(alpha, len(rejections) + 1, max(1, date_rank[board.dates[k]] - date_rank[board.dates[ref]]))
215
+ elif rule == "lord":
216
+ level = lord_level(t, alpha, w0, rejections)
217
+ else:
218
+ level = None
219
+ cert = (p <= level) if level is not None else None
220
+ prev_level, prev_cert = (level or 0.0), bool(cert)
221
+ if cert:
222
+ rejections.append(t)
223
+ certified_holder = k
224
+ steps.append({"test": t, "position": pos, "new": board.names[k], "reference": board.names[ref],
225
+ "margin": float(sc[k] - sc[ref]), "p": float(p), "p_uncorrected": float(p_raw), "date_group": g,
226
+ "level": level, "certified": cert})
227
+ if sc[k] > sc[holder] + 1e-12:
228
+ holder = k
229
+ if rule == "holm":
230
+ m = len(steps)
231
+ ps = np.array([s["p"] for s in steps]) if m else np.array([])
232
+ rej = np.zeros(m, dtype=bool)
233
+ for rank, idx in enumerate(np.argsort(ps)):
234
+ if ps[idx] <= alpha / (m - rank):
235
+ rej[idx] = True
236
+ else:
237
+ break
238
+ for s, r in zip(steps, rej):
239
+ s["certified"] = bool(r)
240
+ s["level"] = None
241
+ return {"rule": rule, "pvalue": pvalue, "baseline": baseline, "alpha": alpha, "date_correction": correct, "n_tests": len(steps),
242
+ "n_certified": int(sum(1 for s in steps if s["certified"])),
243
+ "certified": [s["new"] for s in steps if s["certified"]], "steps": steps,
244
+ "final_certified_holder": board.names[certified_holder]}