TAI BUI
Back to curriculum
CapstonePython

Leaderboard Aggregation

Per-task scores are the easy part. Ranking models across mixed tasks takes more care, and checking that a gap is real is the step most leaderboards skip. Here you build all three.

TIME: ~90 minPREREQUISITES: Phase 19 Track B foundations, lessons 70, 71, 73

Learning Objectives

  • Aggregate per-task scores across multiple models and multiple tasks into a tidy per-model row.
  • Normalise heterogeneous scores so that pass rates and BLEU values do not over-influence the aggregate.
  • Rank models by mean and by win-rate, and explain when each is the right summary.
  • Compute bootstrap confidence intervals on the mean score per model and on pairwise differences.
  • Explain why pairwise differences use a paired bootstrap, and what a confidence interval that includes zero does and does not change in the ranking.
  • Output the leaderboard as a JSON report and as a markdown table the runner in lesson 75 can paste into a CI comment.

The shape of input

The aggregator takes a list of EvalRun records, one per model and task, each carrying a score:

python
@dataclass
class EvalRun:
    model_id: str
    task_id: str
    metric_name: str
    score: float          # in [0, 1]
    category: str = "general"
    weight: float = 1.0

The runner in lesson 75 emits one record per (model, task) pair. The aggregator doesn't care how a score was produced, but it expects normalisation to have happened already: each score is in [0, 1]. category defaults to "general", and weight defaults to 1.0 but is ignored (see "What this lesson does not do").

Check: The shape of input0/1
1. In the aggregator's input, what does one EvalRun record represent?Define

The output

A leaderboard row is a per-model summary record. Three tables come out:

Mermaid diagram

Each row contains model_id, mean_score, mean_ci_lo, mean_ci_hi, win_rate, tasks_completed, and an optional categories map for per-category mean. pairwise_diffs produces a separate table of model-versus-model differences, and render_json serialises the rows for the JSON report.

Check: The output0/2
1. What fields does the LeaderboardRow output contain, according to this lesson?Define
2. What are render_json and render_markdown each for?Define

Normalisation

Normalisation means putting every task's score on one common scale so no single metric controls the average. If one task scores in [0, 1] and another in [0, 100], the second quietly dominates the mean. The aggregator checks that each input score sits in [0, 1] and refuses the run otherwise. It also raises if the same (model, task) pair appears twice, since a duplicate would silently double-count a task. The fix belongs upstream: the metric should already return a fraction. Lessons 71 to 73 enforce that contract.

Check: Normalisation0/3
1. In this aggregator, what does normalisation mean?Define
2. The aggregator raises on an out-of-range score instead of rescaling it. Why is that the better choice?Explain
3. A runner passes a score of 87.5 from a 0-100 metric, plus two records for the same model and task. What does aggregate do?Apply

Mean and win-rate

Mean score and win-rate answer different questions, so the harness reports both.

Mean score is the average over all of a model's runs. It's the headline number leaderboards report, and it's sensitive to outliers and to task imbalance.

Win-rate is the share of tasks where a model scores highest. On each task, every model whose score is within 1e-12 of the top score (checked with math.isclose) wins. Ties are not split: each tied model gets a full win, so the win-rates on a task can add up to more than 1. Win-rate equals wins divided by the number of tasks where the model has a score. It resists outliers and scale differences, at the cost of throwing away how large each margin was.

python
def _win_rate(model_id, runs_by_task):
    wins, total = 0, 0
    for task_id, runs in runs_by_task.items():
        scores = {r.model_id: r.score for r in runs}
        if model_id not in scores:
            continue
        total += 1
        best = max(scores.values())
        if math.isclose(scores[model_id], best, abs_tol=1e-12):
            wins += 1
    if total == 0:
        return (0.0, 0)
    return (wins / total, total)

It returns the rate and the task count, and the count becomes tasks_completed in the row.

The runner in lesson 75 ranks by mean by default. The win-rate column is in the same table for anyone who prefers that view.

Check: Mean and win-rate0/8
1. What is a model's mean score on the leaderboard?Define
2. What is win-rate?Define
3. What does win-rate give up in exchange for resisting outliers and scale differences?Explain
4. Three models score 0.90, 0.90 and 0.70 on one task. How does the win-rate calculation treat that task?Apply
5. Model a scores 0.9 on t1, 0.2 on t2 and 0.6 on t3. On t1 and t2 it faces model b (0.5 and 0.8), and on t3 it faces model c (0.7). What are a's win_rate and tasks_completed?Apply
6. Model P is rank 1 by mean but has a win-rate of 0.30, while model Q is rank 2 with a win-rate of 0.55. Which explanation fits how the two columns are computed?Analyze
7. Model S shows a win rate of 1.00 on 2 tasks while model L shows 0.60 on 50 tasks. What is the right reading of the two numbers?Analyze
8. Model c ran one task and scored 0.70, while model b ran two tasks and averaged 0.65. The table puts c at rank 1. What explains it?Analyze

Bootstrap confidence intervals

A bootstrap confidence interval is a range for a statistic, built by resampling the observed data with replacement and recomputing the statistic many times. Here the statistic is a model's mean score. bootstrap_mean_ci draws indices into that model's own array of scores with replacement, computes the mean of each draw, repeats B times, and takes the percentile interval at level alpha. Each model is resampled on its own, so the per-model intervals ignore which tasks the models share.

Mermaid diagram

For pairwise comparisons, pairwise_diffs first lines the two models up on the tasks both ran. bootstrap_pairwise_diff then bootstraps the per-task difference score_A - score_B the same way and reports the percentile interval. Read off whether the interval excludes zero. If it does, the difference is significant at level alpha. If it doesn't, significant is False and you should treat the two models as tied. That flag never changes the ranking: rows still sort by mean.

The low-level helpers (bootstrap_mean_ci, bootstrap_pairwise_diff) default to B=1000. The public aggregators (aggregate, pairwise_diffs) default to b=500 so the demo and tests stay quick. The default alpha is 0.05, and the bootstrap is plain numpy with no scipy.

Check: Bootstrap confidence intervals0/10
1. How does bootstrap_mean_ci estimate the confidence interval?Define
2. What does "paired" mean in the paired bootstrap used for model comparisons?Define
3. When does the pairwise diff report significant=True?Explain
4. Why is the pairwise bootstrap paired rather than independent?Explain
5. bootstrap_mean_ci defaults to B=1000 resamples, while aggregate defaults to b=500. Why does the public function use fewer?Explain
6. A model scores exactly 0.5 on every one of its tasks. What interval does bootstrap_mean_ci return?Apply
7. Model A scores 0.9 on each of 30 tasks and model B scores 0.1 on each of the same 30. What does bootstrap_pairwise_diff(a, b) return as the mean difference, and is it significant?Apply
8. bootstrap_pairwise_diff is called with paired_a of length 1 and paired_b of length 2. What happens?Apply
9. Four models all ran the same two tasks and you call pairwise_diffs. How many PairwiseDiff rows come back?Apply
10. Model X ranks first by mean and model Y second, but the pairwise CI on X minus Y is [-0.03, +0.05]. What should the report say?Analyze

Categories

A category mean is a model's average score within one group of tasks. Every EvalRun carries a category (default general), and the aggregator reports each one in the row's categories map. It's the math, reasoning, code, safety column on a typical leaderboard, and it shows when a model is good overall but weak at code, which the headline mean hides.

Check: Categories0/3
1. What is a category mean in a leaderboard row?Define
2. Why does the aggregator return per-category means alongside the headline number?Explain
3. A model scores 1.0 on a task in category math and 0.0 on a task in category code. What does its leaderboard row show for mean_score and categories?Apply

Markdown rendering

The leaderboard renders as a markdown table:

text
| Rank | Model | Mean | 95% CI | Win rate | Tasks |
|------|-------|------|--------|----------|-------|
| 1    | gpt   | 0.78 | 0.74-0.82 | 0.62 | 50 |
| 2    | claude| 0.75 | 0.71-0.79 | 0.34 | 50 |
| 3    | random| 0.10 | 0.07-0.13 | 0.04 | 50 |

Rows are sorted by mean score, the CI is rendered to two decimals, and long model ids are truncated to twenty characters. The header hardcodes 95% CI, so if you pass a different alpha the label will be wrong unless you edit it.

Check: Markdown rendering0/2
1. How does this lesson's markdown renderer format the leaderboard table?Define
2. You call aggregate with alpha=0.10 and render the result as markdown. What does the CI column header say?Apply

What this lesson does not do

The scope is narrow. The aggregator doesn't run models and doesn't call the metric layer. It doesn't implement adaptive ECE or other calibration variants (those are lesson 73), and it doesn't implement task weighting. Tasks count equally here. Production leaderboards weight tasks, so the weight field (default 1.0) is left as an open hook that the aggregator ignores. Add weighting in a follow-up if you need it.

Check: What this lesson does not do0/2
1. How does this lesson's aggregator treat a task's weight field?Explain
2. Your team wants math tasks to count double in the leaderboard. Given how this aggregator is built, where does that change belong?Design
Author's Note

My bet is that the pairwise table matters more than the rank column. The leaderboard sorts strictly by mean, so two models can sit at rank 1 and rank 2 while the interval on their difference straddles zero. I'd read the pairwise table before calling a winner, and treat a tied pair as tied.

I'd also keep the [0, 1] check loud. The aggregator raises on an out-of-range score instead of rescaling it, and I prefer that: a quiet rescale would hide a bug in the metric that produced the score.

How to read the code

main.py defines EvalRun, LeaderboardRow, PairwiseDiff, aggregate, pairwise_diffs, bootstrap_mean_ci, bootstrap_pairwise_diff, render_markdown, and render_json. The demo builds a synthetic suite of three models and twelve tasks, aggregates, and prints the leaderboard plus the pairwise diff table. The tests in code/tests/test_leaderboard.py pin the bootstrap, the markdown rendering, the win-rate edge cases, and the empty-input behaviour.

Read main.py top to bottom: the data shapes (EvalRun, LeaderboardRow) first, then the aggregator, the bootstrap, and the rendering. Each function has a focused contract.

Check: How to read the code0/1
1. What do aggregate([]) and bootstrap_mean_ci([]) return?Apply

Going further

A natural next step is a hierarchical bootstrap that respects task families. Math problems are not independent of each other: an arithmetic error pattern can affect ten of them. That's a follow-up. The paired bootstrap on task-by-task differences, which the pairwise comparisons here already use, covers the case where models A and B ran the same hundred tasks. The goal is to get the floor right so the eval reports a number you can defend.

Warm-up0/3
1. You average a model's scores on two tasks, one reported as a fraction from 0 to 1 and the other as a number from 0 to 100. What goes wrong?Warm-up
2. Model A averages 0.80 over five tasks and model B averages 0.79 over the same five. What is the most sensible reading of the 0.01 gap?Warm-up
3. A results spreadsheet accidentally lists one student's score on one exam twice, and you average every row. What happens to that student's average?Warm-up
Check: Going further0/1
1. A suite has 100 tasks, ten of them arithmetic variants that share one error pattern, so they are not independent. Which extension does the lesson point to for the interval?Design

Code