Skip to content

Add Voice Arena rating engine (Davidson-BT) - #139

Merged
seribaymadina merged 2 commits into
mainfrom
arena-rating-engine
Jun 19, 2026
Merged

Add Voice Arena rating engine (Davidson-BT)#139
seribaymadina merged 2 commits into
mainfrom
arena-rating-engine

Conversation

@seribaymadina

@seribaymadina seribaymadina commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Rating engine for Voice Arena, now targeting main directly.

It was originally stacked on the schema PR (#119) and merged into feat/voice-arena just after that branch shipped to main — so the math never reached main. This re-lands it on its own.

Pure functions, no DB or I/O: a sequence of A_WIN/B_WIN/TIE battle outcomes in, one rating row per model out (matching arena.leaderboard_snapshots).

  • Davidson Bradley-Terry fit (per-model strength theta + a shared tie parameter nu) by maximum likelihood, with a hand-derived gradient and a small ridge to keep separation finite.
  • Elo conversion (1500 + 400/ln10 * theta).
  • 95% confidence intervals via percentile bootstrap over battles, seeded for reproducibility.
  • Status classifier (preliminary / usable / established) from the CI half-width.

Hardening from review: self-battles are skipped in aggregation, fewer than two bootstrap samples fall back to no-CI/preliminary, and bootstrap_rounds/reg are validated. The disconnected-comparison-graph limitation is documented (adaptive pairing is the intended upstream remedy). Tests cover ranking recovery, CI shrinkage, tie sensitivity, and each hardening fix.

Summary by CodeRabbit

New Features

  • Introduced the Voice Arena rating engine to compute model ratings from head-to-head battle outcomes
  • Model ratings display Elo scores with bootstrap-based confidence intervals for reliability assessment
  • Confidence status classification (preliminary, usable, established) indicates rating stability and maturity
  • Includes per-model win/loss/tie statistics and total votes across all battles

Greptile Summary

This PR introduces the Davidson-BT rating engine for Voice Arena, re-landing it on main after the underlying schema PR (#119) had already shipped. The implementation is pure-function: a sequence of BattleOutcome records in, one ModelRating per model out, with no I/O or DB dependencies.

  • MLE fit (_fit): L-BFGS-B with an analytic gradient over Davidson's extended Bradley-Terry model; an L2 ridge on theta tames complete separation and ensures strict convexity. Bootstrap CIs are computed via percentile resampling over battles, with degenerate resamples (fewer than two samples, or zero-variance pool from fully-separated data) correctly left as None/"preliminary" rather than a spurious narrow interval.
  • Validation & hardening: reg > 0 is enforced; self-battles are filtered in _aggregate before fitting and tally accumulation; non-converged resamples are skipped with a warning; bootstrap_rounds=0 produces clean None CI fields. The test suite covers ranking recovery, CI shrinkage, tie sensitivity, and every hardening path.

Confidence Score: 5/5

Safe to merge — pure functions with no I/O, all prior review concerns addressed, comprehensive test coverage.

The rating math is correct (analytic gradient verified against the Davidson log-likelihood, recenter is consistent between full fit and bootstrap resamples), input validation is tight (reg > 0, bootstrap_rounds ≥ 0), and every hardening case called out in earlier review rounds is now covered: zero-variance bootstrap produces None CI and "preliminary" status, single-sample bootstrap likewise, reg=0 raises ValueError, non-converged resamples are skipped gracefully. The test suite exercises all of these paths deterministically.

No files require special attention; the only file with meaningful logic is rating.py and it is well-covered by the accompanying test file.

Important Files Changed

Filename Overview
runner/src/coval_bench/arena/rating.py Core rating engine: Davidson MLE, bootstrap CI, status classification, and public entry point. Math is correct (gradient derivation verified), all hardening from prior review is in place.
runner/tests/unit/test_arena_rating.py Comprehensive tests covering ranking recovery, CI shrinkage, tie parameter sensitivity, self-battle filtering, zero-variance bootstrap guard, convergence failure paths, and all input validation branches.
runner/src/coval_bench/arena/init.py Simple re-export shim; public API surface matches what is exposed in all.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["compute_ratings(outcomes, bootstrap_rounds, seed, reg)"] --> V{"Validate\nbootstrap_rounds ≥ 0\nreg > 0 finite"}
    V -->|fail| E1["raise ValueError"]
    V -->|ok| AGG["_aggregate(outcomes)\nSkip self-battles\nBuild per-pair win/tie counts"]
    AGG --> N0{"n models == 0?"}
    N0 -->|yes| R0["Return RatingResult(models=[])"]
    N0 -->|no| FIT["_fit(agg, reg)\nL-BFGS-B MLE\nDavidson NLL + L2 ridge\nRecenter theta → mean 0"]
    FIT -->|not converged| E2["raise ConvergenceError"]
    FIT -->|theta, nu| ELO["_elo(theta)\n1500 + 400/ln10 × theta"]
    ELO --> BOOT["_bootstrap(outcomes, models, rounds, reg, rng)\nResample battles → refit → align Elo\nSkip non-converged resamples\nSkip < 2 samples or zero-variance pool"]
    BOOT --> CI["ci_low / ci_high arrays\n(NaN where unavailable)"]
    CI --> TALLY["Per-model win/loss/tie tallies\nvia np.add.at on pair arrays"]
    TALLY --> BUILD["Build ModelRating entries\nNaN CI → None\nhalf_width = (high-low)/2\nclassify_status(half_width)"]
    BUILD --> SORT["Sort by Elo descending"]
    SORT --> OUT["RatingResult(tie_param=nu, models=[...])"]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["compute_ratings(outcomes, bootstrap_rounds, seed, reg)"] --> V{"Validate\nbootstrap_rounds ≥ 0\nreg > 0 finite"}
    V -->|fail| E1["raise ValueError"]
    V -->|ok| AGG["_aggregate(outcomes)\nSkip self-battles\nBuild per-pair win/tie counts"]
    AGG --> N0{"n models == 0?"}
    N0 -->|yes| R0["Return RatingResult(models=[])"]
    N0 -->|no| FIT["_fit(agg, reg)\nL-BFGS-B MLE\nDavidson NLL + L2 ridge\nRecenter theta → mean 0"]
    FIT -->|not converged| E2["raise ConvergenceError"]
    FIT -->|theta, nu| ELO["_elo(theta)\n1500 + 400/ln10 × theta"]
    ELO --> BOOT["_bootstrap(outcomes, models, rounds, reg, rng)\nResample battles → refit → align Elo\nSkip non-converged resamples\nSkip < 2 samples or zero-variance pool"]
    BOOT --> CI["ci_low / ci_high arrays\n(NaN where unavailable)"]
    CI --> TALLY["Per-model win/loss/tie tallies\nvia np.add.at on pair arrays"]
    TALLY --> BUILD["Build ModelRating entries\nNaN CI → None\nhalf_width = (high-low)/2\nclassify_status(half_width)"]
    BUILD --> SORT["Sort by Elo descending"]
    SORT --> OUT["RatingResult(tie_param=nu, models=[...])"]
Loading

Reviews (3): Last reviewed commit: "Treat zero-variance bootstrap as no CI; ..." | Re-trigger Greptile

@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
benchmarks Ready Ready Preview, Comment Jun 19, 2026 5:34pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

A new coval_bench.arena package is introduced, implementing a Davidson (1970) Bradley–Terry-with-ties MLE rating engine. It aggregates raw battle outcomes into per-pair statistics, fits log-strengths and a tie parameter via L-BFGS-B, computes percentile bootstrap confidence intervals, classifies confidence tiers, and exposes a single compute_ratings() leaderboard entry point. A full pytest suite is included.

Changes

Davidson-BT Arena Rating Engine

Layer / File(s) Summary
Data contracts, constants, and package interface
runner/src/coval_bench/arena/rating.py, runner/src/coval_bench/arena/__init__.py
Defines METHODOLOGY_VERSION, Elo constants, tie-parameter bounds, ConvergenceError, and the three exported Pydantic models (BattleOutcome, ModelRating, RatingResult). The __init__.py re-exports all public symbols via __all__.
Sufficient-statistics aggregation and Davidson MLE fitting
runner/src/coval_bench/arena/rating.py
_aggregate reduces a BattleOutcome sequence to per-pair win/tie count arrays and canonical model indices. _fit runs L-BFGS-B with analytic gradient and L2 ridge on centered theta and log-scale bounded nu, raising ConvergenceError on failure. _theta_to_elo provides the display mapping.
Bootstrap CI, status classification, and compute_ratings entry point
runner/src/coval_bench/arena/rating.py
_bootstrap resamples battles, refits per replicate (skipping non-converged), and returns 2.5/97.5 percentile CI arrays. classify_status buckets CI half-widths into preliminary/usable/established. compute_ratings validates inputs, orchestrates aggregation → fit → bootstrap → status assignment, and returns a RatingResult sorted by Elo descending.
Unit tests
runner/tests/unit/test_arena_rating.py
Covers ranking recovery, CI shrinkage, tie-parameter sensitivity, empty input, separation-safe finite Elo, tally correctness, classify_status boundaries, CI well-formedness, disabled/single-round bootstrap, convergence error propagation, bootstrap resilience, self-battle exclusion, and ValueError on invalid parameters.

Sequence Diagram

sequenceDiagram
  participant Caller
  participant compute_ratings
  participant _aggregate
  participant _fit
  participant _bootstrap
  participant classify_status

  Caller->>compute_ratings: outcomes, bootstrap_rounds, seed, reg
  compute_ratings->>compute_ratings: validate bootstrap_rounds ≥ 0, reg > 0 and finite
  compute_ratings->>_aggregate: Sequence[BattleOutcome]
  _aggregate-->>compute_ratings: win_counts, tie_counts, pair_indices, model_list
  compute_ratings->>_fit: win_counts, tie_counts, pair_indices, reg
  _fit-->>compute_ratings: theta (centered), nu → elo[]
  compute_ratings->>_bootstrap: battles, model_list, bootstrap_rounds, seed, reg
  loop each resample
    _bootstrap->>_fit: resampled win/tie counts, reg
    alt converged
      _fit-->>_bootstrap: theta_r, nu_r
    else ConvergenceError
      _bootstrap->>_bootstrap: skip replicate
    end
  end
  _bootstrap-->>compute_ratings: ci_low[], ci_high[] (or NaN)
  compute_ratings->>classify_status: ci_half_width per model
  classify_status-->>compute_ratings: "preliminary" | "usable" | "established"
  compute_ratings-->>Caller: RatingResult sorted by Elo desc
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • coval-ai/benchmarks#134: Implements the same Davidson-BT Voice Arena rating engine across the identical files (arena/__init__.py, arena/rating.py, test_arena_rating.py) with the same compute_ratings, classify_status, and ConvergenceError API surface.

Suggested reviewers

  • coval-cale

Poem

🐇 Hop, hop — the models line up to fight,
Bradley and Terry said "let's do this right."
We fit our thetas with L-BFGS-B flair,
Bootstrap a thousand, and sprinkle in care.
Elo descending, the leaderboard glows —
The fastest rabbit is ranked, and it shows! 🏆

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: adding a Voice Arena rating engine based on the Davidson-Bradley-Terry model, which is the primary feature introduced across the three modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch arena-rating-engine

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coval-cale coval-cale left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LFG!!!!!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
runner/src/coval_bench/arena/__init__.py (1)

5-21: ⚡ Quick win

Consider exporting ConvergenceError for callers who need to handle fit failures.

compute_ratings raises ConvergenceError when the MLE optimizer fails to converge. Callers who want to handle this gracefully would need to import from the submodule (from coval_bench.arena.rating import ConvergenceError) rather than the package root. If this exception is part of the intended public contract, adding it to the re-exports would provide a cleaner API surface.

🔧 Suggested change
 from coval_bench.arena.rating import (
+    ConvergenceError,
     METHODOLOGY_VERSION,
     BattleOutcome,
     ModelRating,
     RatingResult,
     classify_status,
     compute_ratings,
 )

 __all__ = [
+    "ConvergenceError",
     "METHODOLOGY_VERSION",
     "BattleOutcome",
     "ModelRating",
     "RatingResult",
     "classify_status",
     "compute_ratings",
 ]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@runner/src/coval_bench/arena/__init__.py` around lines 5 - 21, The
ConvergenceError exception is raised by the compute_ratings function but is not
currently exported in the __all__ list of the __init__.py file, forcing callers
to import it directly from the submodule. Add ConvergenceError to the import
statement from coval_bench.arena.rating and include it in the __all__ list to
provide a consistent public API surface for exception handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@runner/src/coval_bench/arena/__init__.py`:
- Around line 5-21: The ConvergenceError exception is raised by the
compute_ratings function but is not currently exported in the __all__ list of
the __init__.py file, forcing callers to import it directly from the submodule.
Add ConvergenceError to the import statement from coval_bench.arena.rating and
include it in the __all__ list to provide a consistent public API surface for
exception handling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3fb98057-e7bd-496f-8600-6c1028b4470e

📥 Commits

Reviewing files that changed from the base of the PR and between 53470c7 and 40831c2.

📒 Files selected for processing (3)
  • runner/src/coval_bench/arena/__init__.py
  • runner/src/coval_bench/arena/rating.py
  • runner/tests/unit/test_arena_rating.py

Comment thread runner/src/coval_bench/arena/rating.py
Comment thread runner/src/coval_bench/arena/rating.py Outdated
Comment thread runner/src/coval_bench/arena/rating.py
* Add Voice Arena rating engine (Davidson-BT)

Pure-function rating engine for arena layer #3: Davidson Bradley-Terry MLE
fit (theta + tie parameter nu), Elo conversion, percentile-bootstrap CIs,
and a status classifier. Refuses a non-converged fit and skips non-converged
bootstrap resamples. CI fields are null (not NaN) when unavailable. Unit tests
cover ranking recovery, CI shrinkage with votes, and tie sensitivity.

* Harden arena rating engine inputs and CI

Skip self-battle outcomes in aggregation, treat fewer than two bootstrap
samples as no CI (avoids a false zero-width 'established' interval), and
validate bootstrap_rounds/reg. Document the disconnected-graph limitation and
that adaptive pairing is the intended upstream remedy.

* Add regression tests for rating engine hardening

Cover the self-battle skip, single-sample CI falling back to preliminary, and
the bootstrap_rounds/reg input validation.
A fully-separated dataset refits identically every resample, giving a zero-width
interval that wrongly read as 'established'; skip it so it reads preliminary.
Also require reg > 0, since the ridge the engine relies on for convergence is
absent at reg = 0.
@seribaymadina
seribaymadina added this pull request to the merge queue Jun 19, 2026
Merged via the queue into main with commit 5cea6ae Jun 19, 2026
10 checks passed
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