<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>hypothesis testing | Mike Nguyen</title><link>https://mikenguyen.netlify.app/tag/hypothesis-testing/</link><atom:link href="https://mikenguyen.netlify.app/tag/hypothesis-testing/index.xml" rel="self" type="application/rss+xml"/><description>hypothesis testing</description><generator>Wowchemy (https://wowchemy.com)</generator><language>en-us</language><copyright>© Mike Nguyen 2026</copyright><lastBuildDate>Sun, 25 Oct 2026 00:00:00 +0000</lastBuildDate><image><url>https://mikenguyen.netlify.app/media/social_sharing_image.png</url><title>hypothesis testing</title><link>https://mikenguyen.netlify.app/tag/hypothesis-testing/</link></image><item><title>Are LLM Benchmark Differences Statistically Significant? 25 of 31 Models Could Be First on AIME 2026</title><link>https://mikenguyen.netlify.app/post/are-llm-benchmark-differences-statistically-significant/</link><pubDate>Sun, 25 Oct 2026 00:00:00 +0000</pubDate><guid>https://mikenguyen.netlify.app/post/are-llm-benchmark-differences-statistically-significant/</guid><description>
&lt;p>On 16 October MathArena added Gemini 4 Argon to its September ArXivMath leaderboard, where it
sits at 87.74%, behind GPT-6.1 Sol at
91.51%, a gap of about two problems out of 53. The same day
&lt;a href="https://quantamagazine.org/he-wants-ai-benchmarking-to-be-a-science-not-a-dark-art-20261016">Quanta profiled Moritz Hardt&lt;/a>,
who argues that benchmark scores mislead in absolute terms while rankings between models still
carry real information. So how much information does a ranking carry when the gap is a few
problems?&lt;/p>
&lt;p>&lt;a href="https://matharena.ai/">MathArena&lt;/a> makes that answerable, because it publishes every graded
attempt, which few leaderboards do. For AIME 2026 that is 32 models on 30 problems, two or four
attempts each. The code below downloads them from Hugging Face at the exact revision used here.&lt;/p>
&lt;pre class="r">&lt;code>rev &amp;lt;- &amp;quot;f916cfc3eebc6ffbbf57d70918e79804d740806f&amp;quot;
url &amp;lt;- paste0(&amp;quot;https://huggingface.co/datasets/MathArena/aime_2026_outputs/resolve/&amp;quot;,
rev, &amp;quot;/data/train-00000-of-00001.parquet&amp;quot;)
f &amp;lt;- file.path(tools::R_user_dir(&amp;quot;matharena&amp;quot;, &amp;quot;cache&amp;quot;), &amp;quot;aime_2026_outputs.parquet&amp;quot;)
if (!file.exists(f)) {
dir.create(dirname(f), recursive = TRUE, showWarnings = FALSE)
download.file(url, f, mode = &amp;quot;wb&amp;quot;)
}
raw &amp;lt;- nanoparquet::read_parquet(f, col_select = c(&amp;quot;model_name&amp;quot;, &amp;quot;problem_idx&amp;quot;,
&amp;quot;idx_answer&amp;quot;, &amp;quot;correct&amp;quot;))
d &amp;lt;- data.frame(model = raw$model_name, problem = as.integer(raw$problem_idx),
attempt = raw$idx_answer, correct = as.integer(raw$correct))
# One row per model, one column per problem: the share of attempts correct
A &amp;lt;- tapply(d$correct, list(d$model, d$problem), mean)
B &amp;lt;- A[rowSums(is.na(A)) == 0, ] # models with attempts on all 30
score &amp;lt;- rowMeans(B)&lt;/code>&lt;/pre>
&lt;pre class="r">&lt;code>set.seed(20261020)
ranks &amp;lt;- replicate(4000, {
s &amp;lt;- rowMeans(B[, sample.int(ncol(B), replace = TRUE)])
rank(-s, ties.method = &amp;quot;min&amp;quot;)
})
lo &amp;lt;- apply(ranks, 1, quantile, 0.025, type = 1)
hi &amp;lt;- apply(ranks, 1, quantile, 0.975, type = 1)&lt;/code>&lt;/pre>
&lt;div id="the-result" class="section level2">
&lt;h2>The result&lt;/h2>
&lt;p>Resample the 30 problems with replacement 4,000 times, rescore every model each time, and record
where it lands. 25 of the 31 models with attempts on every problem have a 95%
rank interval that reaches first place.&lt;/p>
&lt;div class="figure">&lt;span style="display:block;" id="fig:rankplot">&lt;/span>
&lt;img src="figs/rankplot-1.png" alt="Each bar is a model's 95% bootstrap rank interval from resampling the 30 AIME 2026 problems. The dot is its rank on the actual 30, with ties sharing the best rank. Blue bars reach first place. The two perfect scores rank first in every resample, so their bars shrink to the dot. Equal scores can get different bars. Every blue model slipped on at most 3 distinct problems and every gray one on at least 4." width="1050" />
&lt;p class="caption">
Figure 1: Each bar is a model’s 95% bootstrap rank interval from resampling the 30 AIME 2026 problems. The dot is its rank on the actual 30, with ties sharing the best rank. Blue bars reach first place. The two perfect scores rank first in every resample, so their bars shrink to the dot. Equal scores can get different bars. Every blue model slipped on at most 3 distinct problems and every gray one on at least 4.
&lt;/p>
&lt;/div>
&lt;p>By this measure only 6 models can be ruled out of first.&lt;/p>
&lt;/div>
&lt;div id="half-the-test-is-blank" class="section level2">
&lt;h2>Half the test is blank&lt;/h2>
&lt;pre class="r">&lt;code>solved_by_all &amp;lt;- colSums(B &amp;lt; 1) == 0
# Share of the variance in model scores that each problem contributes
share &amp;lt;- apply(B, 2, cov, y = score) / (ncol(B) * var(score))
top5 &amp;lt;- sort(share, decreasing = TRUE)[1:5]
data.frame(problem = names(top5), attempts_correct = pct(colMeans(B)[names(top5)]),
share_of_spread = pct(top5))
## problem attempts_correct share_of_spread
## 1 30 61% 26%
## 2 29 85% 18%
## 3 15 20% 14%
## 4 14 93% 11%
## 5 10 86% 10%&lt;/code>&lt;/pre>
&lt;p>16 of the 30 problems were solved by all 31 models on every attempt.
They add the same amount to every score, so they cannot move a ranking. Five problems carry
80% of the spread between models. MathArena now lists AIME 2026 among its
deprecated competitions.&lt;/p>
&lt;/div>
&lt;div id="two-kinds-of-error-bar" class="section level2">
&lt;h2>Two kinds of error bar&lt;/h2>
&lt;p>MathArena prints a 95% interval next to every AIME 2026 score, which most leaderboards do not.
All 31 of the 31 intervals match &lt;code>1.96 * sqrt(p * (1 - p) / attempts)&lt;/code> to the
second decimal. That interval answers one question well: how much would the score move if the
model reran these same 30 problems?&lt;/p>
&lt;p>A launch announcement usually means a broader question: how would the model do on other problems
of this kind? For that one the problem is the unit that was sampled, and a model that misses a
problem tends to miss it on every attempt. Computed over problems, the intervals run a median
1.7 times wider. For DeepSeek-v3.2 (Think), the printed plus or minus
4.19 points becomes plus or minus 7.31.&lt;/p>
&lt;p>The choice changes the verdict. Here is every model below a perfect score compared with
GPT-5.5 (xhigh), first over attempts with Fisher’s exact test, then problem by problem with an
exact sign-flip test on the paired differences.&lt;/p>
&lt;pre class="r">&lt;code>ref &amp;lt;- &amp;quot;GPT-5.5 (xhigh)&amp;quot; # 120 of 120 correct
below &amp;lt;- names(score)[score &amp;lt; 1]
fisher_p &amp;lt;- sapply(below, function(m) {
x &amp;lt;- d$correct[d$model == m]
fisher.test(matrix(c(120, 0, sum(x), sum(1 - x)), 2))$p.value
})
signflip &amp;lt;- function(x) {
x &amp;lt;- x[x != 0]
if (length(x) == 0) return(1)
s &amp;lt;- as.matrix(expand.grid(rep(list(c(-1, 1)), length(x))))
mean(abs(s %*% x) &amp;gt;= abs(sum(x)) - 1e-12)
}
pair_p &amp;lt;- sapply(below, function(m) signflip(B[ref, ] - B[m, ]))
rbind(attempts = c(raw = sum(fisher_p &amp;lt; 0.05), holm = sum(p.adjust(fisher_p, &amp;quot;holm&amp;quot;) &amp;lt; 0.05)),
problems = c(raw = sum(pair_p &amp;lt; 0.05), holm = sum(p.adjust(pair_p, &amp;quot;holm&amp;quot;) &amp;lt; 0.05)))
## raw holm
## attempts 14 7
## problems 2 1&lt;/code>&lt;/pre>
&lt;p>Over attempts, 14 of the 29 models below a perfect score are
significantly behind. Over problems, 2 are, and
1 after a Holm correction for 29
comparisons. The bootstrap rules out 6 and the exact test at most
2, because percentile intervals from 30 resampled problems run narrow. I
would trust the exact test, which makes the field more tied, not less.&lt;/p>
&lt;p>The test also gives a rule you can do in your head. Against a perfect score every paired
difference is positive, so the p-value is &lt;code>2 * 0.5^k&lt;/code>, where &lt;code>k&lt;/code> is the number of distinct
problems a model slipped on. Significance needs &lt;code>k&lt;/code> of at least 6. The median model below
perfect slipped on 2.&lt;/p>
&lt;/div>
&lt;div id="check-coverage-before-averaging" class="section level2">
&lt;h2>Check coverage before averaging&lt;/h2>
&lt;p>One model is missing from the figure. The file has no attempts from Qwen3.5-4B on
6 problems (10, 15, 17, 28, 29, 30), including
4 of the 5 hardest. A plain average of the file ranks it
4th of 32, behind only Claude-Opus-4.8 (max), GPT-5.5 (xhigh), and GPT-5.4 (xhigh). MathArena’s own table
handles this correctly. It estimates the missing problems with an item response model, and on
17 October it ranked the model 29th at 89.66%.&lt;/p>
&lt;/div>
&lt;div id="how-big-a-gap-can-a-benchmark-see" class="section level2">
&lt;h2>How big a gap can a benchmark see&lt;/h2>
&lt;p>The same arithmetic gives a minimum detectable difference for any benchmark. When two models
answer each item once, the paired difference on an item is 1, 0, or minus 1, and its variance is
close to the discordance rate, the share of items where exactly one model is right. At 5%
significance and 80% power the smallest reliable gap is about &lt;code>2.8 * sqrt(discordance / n)&lt;/code>.&lt;/p>
&lt;pre class="r">&lt;code>n &amp;lt;- c(&amp;quot;AIME&amp;quot; = 30, &amp;quot;ArXivMath September&amp;quot; = 53, &amp;quot;GPQA Diamond&amp;quot; = 198,
&amp;quot;SWE-bench Verified&amp;quot; = 500, &amp;quot;Humanity&amp;#39;s Last Exam&amp;quot; = 2500)
data.frame(items = n, disagree_10pct = round(100 * mdd(n, 0.10), 1),
disagree_20pct = round(100 * mdd(n, 0.20), 1))
## items disagree_10pct disagree_20pct
## AIME 30 16.2 22.9
## ArXivMath September 53 12.2 17.2
## GPQA Diamond 198 6.3 8.9
## SWE-bench Verified 500 4.0 5.6
## Humanity&amp;#39;s Last Exam 2500 1.8 2.5&lt;/code>&lt;/pre>
&lt;p>The two right columns are gaps in percentage points. Item counts come from the
&lt;a href="https://arxiv.org/abs/2311.12022">GPQA paper&lt;/a>, the
&lt;a href="https://huggingface.co/datasets/princeton-nlp/SWE-bench_Verified">SWE-bench Verified release&lt;/a>,
and the &lt;a href="https://arxiv.org/abs/2501.14249">Humanity’s Last Exam paper&lt;/a>. This is a normal
approximation for one attempt per item, so read it as an order of magnitude. Even if Argon and
GPT-6.1 Sol disagreed on only one problem in ten, a gap would need about
12 points to show up reliably. The current gap
is 3.77 points. That is a
statement about 53 problems, not about either model.&lt;/p>
&lt;/div>
&lt;div id="the-same-result-in-python" class="section level2">
&lt;h2>The same result in Python&lt;/h2>
&lt;p>Here is the same resampling in numpy, reading the same file with three independent seeds.&lt;/p>
&lt;pre class="python">&lt;code>import numpy as np
import pandas as pd
d = pd.read_parquet(r.f, columns=[&amp;quot;model_name&amp;quot;, &amp;quot;problem_idx&amp;quot;, &amp;quot;correct&amp;quot;])
d[&amp;quot;problem&amp;quot;] = d[&amp;quot;problem_idx&amp;quot;].astype(int)
A = d.pivot_table(index=&amp;quot;model_name&amp;quot;, columns=&amp;quot;problem&amp;quot;, values=&amp;quot;correct&amp;quot;, aggfunc=&amp;quot;mean&amp;quot;)
B = A.dropna().to_numpy()
def reach_first(seed, reps=4000):
rng = np.random.default_rng(seed)
idx = rng.integers(0, B.shape[1], size=(reps, B.shape[1]))
s = B[:, idx].mean(axis=2) # models x reps
rank = 1 + (s[None, :, :] &amp;gt; s[:, None, :]).sum(axis=1)
lo = np.quantile(rank, 0.025, axis=1, method=&amp;quot;inverted_cdf&amp;quot;)
return int((lo == 1).sum())
counts = {seed: reach_first(seed) for seed in (1, 2, 3)}
print(counts, &amp;quot;of&amp;quot;, B.shape[0])
## {1: 25, 2: 25, 3: 25} of 31&lt;/code>&lt;/pre>
&lt;p>Every seed lands on 25.&lt;/p>
&lt;/div>
&lt;div id="what-this-does-not-show" class="section level2">
&lt;h2>What this does not show&lt;/h2>
&lt;ul>
&lt;li>AIME 2026 is saturated. A harder benchmark separates models better. The method transfers, the
tie count does not.&lt;/li>
&lt;li>If these 30 problems are all you care about, only attempt noise is left, and the printed
intervals are the right ones.&lt;/li>
&lt;li>A bootstrap over 30 problems, only 14 of which carry any information,
is coarse. The sign-flip test is exact.&lt;/li>
&lt;li>MathArena marks 22 of the 32 models as released after the
competition was published. This post does not try to account for that.&lt;/li>
&lt;li>One benchmark, one subject. A paper at this year’s ICML workshop on hypothesis testing,
&lt;a href="https://openreview.net/forum?id=ZhVvlyFZP9">The Leaderboard Lottery&lt;/a>, reports that on data
calibrated to the Open LLM Leaderboard, the reported winner was the true best only 0 to 58
percent of the time when prompts were resampled.&lt;/li>
&lt;/ul>
&lt;/div>
&lt;div id="what-to-do-on-monday" class="section level2">
&lt;h2>What to do on Monday&lt;/h2>
&lt;ol style="list-style-type: decimal">
&lt;li>Before reading a gap, ask how many items the benchmark has, and compute
&lt;code>2.8 * sqrt(discordance / n)&lt;/code>.&lt;/li>
&lt;li>Compare models paired on the same items. Two overlapping confidence intervals answer a
different question.&lt;/li>
&lt;li>When the claim is about a kind of task, compute error bars over items, not attempts.&lt;/li>
&lt;li>Check coverage before averaging. A model missing the hard items looks better.&lt;/li>
&lt;li>Report the set of models that could plausibly be first, not a single winner.&lt;/li>
&lt;/ol>
&lt;p>Computing uncertainty over problems rather than attempts is the same move as clustering standard
errors by the unit that was actually sampled, which is covered in
&lt;a href="https://mike-data-analysis.share.connect.posit.cloud/sec-clustered-inference.html">A Guide on Data Analysis&lt;/a>.&lt;/p>
&lt;p>&lt;em>Data: “Model Outputs AIME 2026” by &lt;a href="https://matharena.ai/">MathArena&lt;/a> (Dekoninck et al. 2026,
&lt;a href="https://arxiv.org/abs/2605.00674">arXiv 2605.00674&lt;/a>), from
&lt;a href="https://huggingface.co/datasets/MathArena/aime_2026_outputs">Hugging Face&lt;/a> at revision
&lt;code>f916cfc&lt;/code>, licensed &lt;a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0&lt;/a>. This
post does not redistribute the dataset. The code downloads it from the source and keeps four
columns, and the page shows only statistics computed from them. Leaderboard values quoted from
matharena.ai were read on 17 October 2026, and the Argon and GPT-6.1 Sol scores were unchanged
on 25 October. Thanks to the MathArena
team for publishing raw attempts, which is what makes a check like this possible.&lt;/em>&lt;/p>
&lt;/div></description></item></channel></rss>