Skip to content

Leaderboard: sampling error and McNemar significance groups - #60

Open
ipezygj wants to merge 4 commits into
SWE-bench:masterfrom
ipezygj:leaderboard-significance-columns
Open

Leaderboard: sampling error and McNemar significance groups#60
ipezygj wants to merge 4 commits into
SWE-bench:masterfrom
ipezygj:leaderboard-significance-columns

Conversation

@ipezygj

@ipezygj ipezygj commented Aug 17, 2026

Copy link
Copy Markdown

Adds the two columns proposed in SWE-bench/SWE-bench#621, computed from per-instance results this repo already ships. No new data collection, no change to data/leaderboards.json.

What the columns say

column definition
± SE binomial standard error of that one entry's resolve rate over the instances it was scored on, sqrt(p(1-p)/n) in percentage points. It is the precision of a single number, not the error of a difference. Two entries are scored on the same instances, so whether they differ is a paired question, and overlapping SEs are not the answer to it.
Tie that paired question, answered. Exact two-sided McNemar on the shared instances at α = 0.05. The highest entry publishing per-instance results anchors group 1; every entry the test cannot separate from the anchor joins its group; the highest remaining entry anchors group 2, and so on. Membership is a statement about the comparison with the anchor, not a property of an entry on its own, and not a claim that all members are mutually indistinguishable.

No multiplicity correction is applied to the comparisons against an anchor. A correction makes separation harder and groups larger, so the group sizes here are a lower bound on how much of a board is indistinguishable.

What it shows on the current data

The bash-only view, which is where the site lands by default:

# model % resolved ± SE tie
1 Claude 4.5 Opus (high) 76.80 1.89 1
2 Gemini 3 Flash (high) 75.80 1.92 1
3 MiniMax M2.5 (high) 75.80 1.92 1
4 Claude 4.6 Opus 75.60 1.92 1
5 Claude 4.5 Opus (medium) 74.40 1.95 1
6 Gemini 3 Pro Preview 74.20 1.96 1
7 GPT 5.2 Codex 72.80
8 GLM 5 (high) 72.80 1.99 2

Six entries spanning 2.6 points are one group: the ordering among them is not something these 500 instances decide. Twelve groups over the 40 entries with per-instance results. The 297-instance Multilingual entry is why n is read per entry rather than assumed per board.

Implementation

  • data/leaderboard_statistics.py — stdlib only, no new dependencies. Exact McNemar via math.comb, binomial SE, the grouping rule, and --selftest.
  • build.py — annotates the loaded leaderboards at build time. data/leaderboards.json is untouched, so it stays the record of what was submitted and these numbers stay a function of it, recomputed whenever a submission lands.
  • js/mainResults.js, css/leaderboard.css — two columns, a note under the table stating both definitions and the anchor by name, and widths.

Where it deliberately shows nothing

An entry that publishes no per-instance results gets an em dash, not a blank, and sorts last whichever way the arrow points, so "not published" cannot read as the smallest error or the top group. The columns appear only when the group anchor is the entry at the top of the view; otherwise the number would be anchored on a row far down the page.

That rule is why the full Verified board does not get the columns, and the reason is worth stating plainly: 140 of its 180 entries publish no per-instance results, and the six highest-scoring entries are among them. The board's headline number cannot currently be compared with its runner-up using data this site has. Ingesting the resolved lists from SWE-bench/experiments would light the columns up board-wide; I am happy to do that in a follow-up PR if you want it.

Checks

python data/leaderboard_statistics.py --selftest covers: identical entries are never separated; 40 planted one-directional flips are detected (so the first check is not passing for lack of power); 200 balanced discordant pairs are not called a difference; SE matches sqrt(p(1-p)/n) and is zero at a perfect score; only shared instances are compared; entries without per-instance results get no numbers and do not shift anyone else's group; the anchor is the highest measurable entry and is reported with a count of how many higher entries publish nothing. The p-values were also cross-checked against scipy.stats.binom on the live top pair (both 0.583132 for 29/24 discordant); scipy is not a dependency of the site and is not used at build time.

Verified in the browser in light and dark mode, with the bash-only preset on and off, and with both new columns sorted in both directions.

Note on scope. I dropped the separate n column from the format in the issue: within a board it is nearly constant, so it sits in the SE cell's tooltip and in the JSON rather than taking table width. Say the word and I will make it a column. cc @KeilerHirsch, who offered to review the measurement language.

The board reports a resolve rate per entry and nothing about how precisely it
is measured, so adjacent rows read as ordered even when the data does not order
them. This adds two columns computed from per-instance results the site already
carries:

  ± SE   binomial standard error of that entry's own rate, in percentage
         points, over the instances it was scored on. Stated as the precision
         of one number, not as the error of a difference: entries share an
         instance set, so comparing two of them is a paired question.

  Tie    that paired question, answered. Exact two-sided McNemar at alpha =
         0.05; the highest entry that publishes per-instance results anchors
         group 1 and every entry the test cannot separate from it joins the
         group. Membership is a property of the comparison with the anchor,
         not of an entry alone.

Nothing is stored: build.py annotates the loaded leaderboards, so
leaderboards.json stays the record of what was submitted and these stay a
function of it. Entries without per-instance results get a dash rather than a
blank, and sort last in both directions, so "not published" cannot read as a
small error or a top group. The columns appear when the group anchor is the
entry at the top of the view, which today means the bash-only view.

On the current data this puts the top six bash-only entries (76.8 down to 74.2)
in one group. leaderboard_statistics.py --selftest covers the test, the SE, the
anchor rule and the empty cases.
Both numbers condition on the single run each entry reports. Nothing here bounds
how much a score would move on a rerun of the same system, so neither column is
the uncertainty of the ranking. Stated in the note a reader sees and in the module
docstring, next to the two claims that were already explicit (SE is not the error
of a difference; the group is a paired result against a named anchor at alpha =
0.05 with no multiplicity correction).

Raised by @KeilerHirsch in SWE-bench/SWE-bench#621: marginal uncertainty, paired
comparison resolution and repeatability are three different things and the wording
should not let one be read as another.
…he board

Both columns are computed from per-instance outcomes while the board's own
number comes from elsewhere, so the two can be checked against each other.
They now are: an entry is annotated only when the rate implied by its
per-instance results agrees with its published rate to within one instance
(100/n percentage points). One instance is the finest step the published rate
can move in, so a smaller gap is rounding or a denominator that differs by one,
and a larger one means the two artifacts disagree about at least one instance.

This is not hypothetical. On the current data two submissions publish
per-instance files that imply 0.00 % and 10.20 % against published rates of
69.60 % and 52.80 %. Before this commit the first of them was rendered with
the most precise-looking cell on the page, an SE of +/- 0.00, because a rate of
exactly zero makes sqrt(p(1-p)/n) vanish; the second was placed in a
significance group computed from a 10.20 % outcome set. Both now show a dash
whose tooltip says which of the two published numbers disagrees with which, and
the build prints them by name so the gap is not only visible on the page.

The anchor and the size of the top group are unchanged on every board.

Three further cases where a formula returns a number that is not a measurement:

- an empty sample raises instead of returning an SE of 0.0;
- a boundary rate (every instance resolved, or none) withholds the SE, since
  the zero is a property of the estimator rather than of the entry, while the
  entry still takes part in the paired comparisons, which remain valid;
- two entries that share no instance are no longer given a p-value of 1.0 and
  put in the same group, which reported absence of paired evidence as absence
  of difference. mcnemar_exact returns None there and the entry anchors its
  own group.

Selftest covers each, including both sides of the one-instance threshold, and
each guard was checked by mutation: loosening the gate by 2x or 10x, tightening
it 10x, returning 0.0 for an empty sample, treating disjoint sets as a tie, and
publishing the boundary zero all fail the selftest.
@ipezygj

ipezygj commented Aug 19, 2026

Copy link
Copy Markdown
Author

Pushed cdf2052, which fixes a defect I found by applying this PR's own standard to itself, plus a data problem in the boards that you may want to know about independently of whether this lands.

Two submissions publish per-instance files that contradict their own board number. Both columns are computed from per-instance outcomes, while the board's rate comes from elsewhere, so the two can be checked against each other. They now are:

board rows published implied by its per_instance_details
mini-SWE-agent + Gemini 3 Pro (Verified) / Gemini 3 Pro (bash-only) 69.60 % 0.00 % — every one of the 500 entries is "resolved": false
mini-SWE-agent + Claude 3.7 Sonnet (20250219) (both boards) 52.80 % 10.20 %

Before this commit my column rendered the first of those as ± 0.00, the most precise-looking cell on the page, because sqrt(p(1-p)/n) vanishes at a rate of exactly zero. The second was placed in a significance group computed from a 10.20 % outcome set. A column added to make uncertainty visible was quietly manufacturing false precision from bad input, which is the thing it exists to stop.

The rule now: an entry is annotated only when the rate implied by its per-instance results agrees with the published rate to within one instance (100/n pp). One instance is the finest step the published rate can move in, so a smaller gap is rounding or a denominator differing by one — mini-SWE-agent + Claude 4 Sonnet is 64.93 % published against 64.80 % implied, which is 324/499 vs 324/500, and it stays in. Anything larger means the two artifacts disagree about at least one instance, and nothing here can say which is right, so the entry gets a dash whose tooltip names both numbers, and the build prints it:

stats: Verified: 38/180 entries with per-instance results, 11 significance groups, 6 in group 1 anchored on Claude 4.5 Opus (6 higher entries publish none)
stats: Verified: Gemini 3 Pro: inconsistent_with_published_rate
stats: Verified: Claude 3.7 Sonnet: inconsistent_with_published_rate

The result the PR is about does not move. The anchor is Claude 4.5 Opus before and after on Verified and bash-only, and group 1 still holds 6 entries. What changes is two rows out of forty and the count of measurable entries, 40 → 38.

Three further cases where a formula returns a number that is not a measurement, all fixed in the same commit:

  • an empty sample raises instead of returning an SE of 0.0;
  • a boundary rate withholds the SE, because the zero is a property of the estimator and not of the entry, while the entry still takes part in the paired comparisons, which stay valid;
  • two entries sharing no instance no longer get p = 1.0 and a shared group. That reported absence of paired evidence as absence of difference. mcnemar_exact returns None there and the entry anchors its own group instead. Currently unreachable on Verified, where every measurable entry has the same 500, but Multilingual already mixes 297 and 300.

Selftest covers each case including both sides of the one-instance threshold, and I checked the guards by mutation rather than trusting them: loosening the gate 2x or 10x, tightening it 10x, returning 0.0 for an empty sample, treating disjoint sets as a tie, and publishing the boundary zero each make the selftest fail.

The two inconsistent per-instance files are worth a look on their own, since anything downstream that reads per_instance_details — not just this PR — will read one submission as 0 % resolved. Happy to open a separate issue on the data if that is the better place for it. @KeilerHirsch, the wording that changed is the tooltip on an empty cell: it now says which of the two published numbers disagrees with which, rather than treating "not published" and "published but contradictory" as the same silence.

@KeilerHirsch

Copy link
Copy Markdown

Reviewing this from the measurement side: the new consistency gate is exactly the right direction. Refusing to manufacture precision when the aggregate and per-instance artifacts contradict each other is much better than choosing one silently.

I have one substantive terminology/model objection before I'd call the SE column clean.

The board is a fixed set of instances. On those exact instances, 396/500 is an observed descriptive rate, not itself a sampling estimate with uncertainty. sqrt(p(1-p)/n) becomes a standard error only after declaring an inference model — e.g. treating these items as an iid / exchangeable sample from some target population of tasks.

So I would make the inference target explicit in the column definition:

  • observed rate on this fixed benchmark: exact descriptive quantity;
  • binomial SE: model-based uncertainty for generalisation beyond that fixed item set, under stated sampling/exchangeability assumptions;
  • paired McNemar: comparison on the actually shared fixed items;
  • repeatability uncertainty: still unobservable from the single runs.

That keeps the three uncertainty objects from being collapsed into one "precision" concept.

The contradictory per-instance files are a very good independent data-quality finding and I agree they deserve a separate upstream issue regardless of whether this PR lands.

The resolve rate on a board is an exact descriptive fact: every entry is
scored on the same fixed set of instances, so 396 of 500 is 79.20 % with
nothing left to estimate. sqrt(p(1-p)/n) is a standard error only under a
declared target of inference, and the column never declared one.

The note and the column tooltip now separate four objects that were running
together: the observed proportion on a fixed set (exact, descriptive), the
binomial SE (model-based, generalising to comparable tasks under an
exchangeability assumption), the paired McNemar comparison on shared
instances, and repeatability, which a single run cannot observe.

The exchangeability assumption is stated rather than implied, and stated
against these item sets in particular: they are curated and human-filtered
rather than randomly drawn, which makes the assumption questionable, not
merely unstated.

Wording only. No change to leaderboard_statistics.py, and the computed
output is unchanged: Verified still reports 38 of 180 entries with
per-instance results and 6 in group 1 anchored on Claude 4.5 Opus.
@ipezygj

ipezygj commented Aug 20, 2026

Copy link
Copy Markdown
Author

Pushed a579699, which is the wording change you asked for and nothing else — no change to
leaderboard_statistics.py, and the computed output is identical (Verified: 38 of 180 entries
with per-instance results, 6 in group 1 anchored on Claude 4.5 Opus).

The note and the column tooltip now keep four objects apart instead of letting them run together:

  • the observed proportion on a fixed set, which is an exact descriptive fact — 396 of 500 is
    79.20 %, with nothing left to estimate;
  • the binomial SE, which is a standard error only under a named target of inference, here a wider
    population of comparable tasks of which the item set is treated as an exchangeable sample;
  • the paired McNemar comparison, which is about two entries on shared instances;
  • repeatability, which a single run cannot observe at all.

One thing beyond your list, because leaving it out would have made the column look better than it
is: these item sets are curated and human-filtered rather than randomly drawn, so the
exchangeability assumption is not merely undeclared, it is questionable. The note says that in
those words. I would rather ship a column that argues against itself than one that reads as error
bars on the benchmark result.

The per-instance data problem is now a separate upstream issue, as you suggested:
SWE-bench/experiments#467. It stands whether or not this PR lands, and it is not about statistics
at all — two submissions' own per-instance files disagree with their own declared resolve rate.

@KeilerHirsch

Copy link
Copy Markdown

Yes — this resolves my objection.

The important improvement is not the formula but that the measurement object and the inference claim are now kept separate. Calling 396/500 an exact descriptive fact on the fixed set, while making the binomial SE conditional on an explicit wider-population model, is the distinction I wanted.

Explicitly stating that exchangeability is questionable on a curated/human-filtered benchmark makes the note stronger, not weaker.

And splitting the contradictory per-instance artifacts into experiments#467 was the right move. That's an independent data-integrity issue, not something this statistics PR should quietly absorb.

From the measurement side, I'm happy with this wording. Maintainers' court now. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants