ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
# Data files
*.json
*.jsonl
*.csv
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
venv/
ENV/
env/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Generated visualizations
*.png
*.html
!index.html
# Canonical Experiment 7-7 evidence is intentionally versioned. The public
# Arena input remains ignored because it is ~2 GB; manifests bind it by URL,
# byte size, record count, and SHA-256 instead.
!validation/
!validation/**/*.json
!validation/**/*.png
!validation/**/*.html
# OS
.DS_Store
Thumbs.db
# Jupyter
.ipynb_checkpoints/
*.ipynb
# Logs
*.log
+786
View File
@@ -0,0 +1,786 @@
# Elo Rating Leaderboard from Pairwise Comparisons
## English
**Experiment 7-7**: Building Model Leaderboard from Pairwise Comparison Data
This project implements an Elo rating system from scratch to analyze model performance using Chatbot Arena's public voting data. The implementation demonstrates how the Bradley-Terry model extracts relative model capabilities from millions of pairwise comparison votes.
## Overview
The Elo rating system is a method for calculating the relative skill levels of players (or in this case, AI models) in zero-sum games. Originally developed for chess, it has been adapted to rank AI language models based on head-to-head comparisons from user votes.
### Key Features
- **High-performance implementation**: NumPy + Numba JIT + parallel processing for optimal speed
- **Real voting data analysis**: Uses actual Chatbot Arena voting data with millions of pairwise comparisons
- **Win rate prediction**: Calculates expected win probabilities between any two models
- **Historical tracking**: Builds time-series snapshots showing ranking evolution
- **Interactive visualizations**: Multiple visualization types including animated bar chart races
- **Scalable**: Efficiently handles 2GB datasets with hundreds of thousands of matches
## Mathematical Foundation
The Elo system is based on the Bradley-Terry model, which models the probability that model A beats model B as:
```
P(A beats B) = 1 / (1 + 10^((R_B - R_A) / 400))
```
After each match, ratings are updated using:
```
R_A_new = R_A + K * (S_A - E_A)
```
Where:
- `R_A` is the current rating of model A
- `K` is the learning rate (K-factor)
- `S_A` is the actual score (1 for win, 0 for loss, 0.5 for tie)
- `E_A` is the expected score (predicted win probability)
## Requirements
- **Disk Space**: At least 3GB free (2GB for data file, 1GB for processing)
- **RAM**: 4GB+ recommended for full dataset analysis
- **Internet**: Stable connection for ~2GB download
- **Python**: 3.12 for the root `ch6` install
## Installation
```bash
# From the repository root: use the shared Chapter 6 environment
uv sync --locked --python 3.12 --extra ch6
# Activate it before changing directories:
# macOS/Linux:
source .venv/bin/activate
# Windows PowerShell: .\.venv\Scripts\Activate.ps1
# Windows cmd: .venv\Scripts\activate.bat
# pip fallback when uv is not installed:
# python -m pip install -e ".[ch6]"
cd chapter7/elo-leaderboard
# Single-project compatibility path, still supported during migration:
# python -m pip install -r requirements.txt
```
## Testing
```bash
# From the repository root, include the shared test tooling:
uv sync --locked --python 3.12 --extra ch6 --extra dev
# Activate it before changing directories:
source .venv/bin/activate
# pip fallback when uv is not installed:
# python -m pip install -e ".[ch6,dev]"
cd chapter7/elo-leaderboard
python -m pytest tests
```
## Canonical full-data validation
The accepted Experiment 7-7 run uses the complete public 2024-08-14 Arena
snapshot rather than the synthetic quickstart or a small sample. The 2.0 GB
input is not committed to git; the manifest records its official URL, byte
size, 1,799,991-row count, and SHA-256.
```bash
curl -L \
https://storage.googleapis.com/arena_external_data/public/clean_battle_20240814_public.json \
-o /path/to/arena_data.json
python validation/run_experiment.py \
--input /path/to/arena_data.json \
--output-dir validation/runs/exp7-7-arena-20260731-v1 \
--bootstrap-rounds 20
python validation/validate_evidence.py \
validation/runs/exp7-7-arena-20260731-v1 \
--input /path/to/arena_data.json
```
The retained canonical run accepted 1,670,250 anonymous, deduplicated votes
over 129 models. Chronological online Elo (initial 1000, K=4) and the
Bradley-Terry reconstruction reached Spearman 0.787, Kendall 0.606, and 12/20
top-model overlap. This is the expected broad agreement, not score identity:
online Elo is order-dependent while Bradley-Terry fits all comparisons at
once. Seventeen cumulative monthly snapshots drive the retained D3 animation.
Canonical evidence: [`validation/latest.json`](validation/latest.json).
## 命令行工具 / Command-Line Interface (`cli.py`)
`cli.py` 是本实验统一的 argparse 命令行入口(中文 `--help`),把整条流水线拆成子命令:
**对战 (battle) -> 计算评分 (elo) -> 展示排行榜 (leaderboard)**,并提供 `pipeline` 一步到位。
```bash
python cli.py --help # 查看全部子命令
python cli.py battle --help # 查看某个子命令的参数
# 默认离线端到端演示:模拟对战 -> 在线 Elo -> 最终排行榜表格(无需任何数据/API)
python cli.py # 等价于 python cli.py pipeline
```
### 子命令
| 子命令 | 作用 | 关键参数 |
|--------|------|----------|
| `battle` | 生成两两对战结果 | `--source {simulate,arena,llm}``--num-battles``--tie-prob``--seed``--sample``--output` |
| `elo` | 从对战结果计算评分 | `--method {online-elo,bradley-terry}``--k``--bootstrap``--input``--output` |
| `leaderboard` | 渲染最终排行榜表格 | `--input`(对战或评分文件)、`--method``--bootstrap``--top-n` |
| `pipeline` | 一步跑完 对战 -> Elo -> 排行榜 | 上述参数的并集 |
### 三种对战来源(`--source`
- **`simulate`(默认,纯离线)**:从已知的潜在实力分模拟对战。因为真值已知,可用来**校验**恢复出的排行榜排序是否正确;`--tie-prob` 控制平局比例,用于演练平局处理。
- **`arena`(离线)**:加载真实 Chatbot Arena 投票数据(默认 `arena_data.json`,约 2GB),可用 `--sample N` 抽样。
- **`llm`(需 API)**:用 LLM 做配对评判,并内置**位置偏差消除**——每对交换顺序各评一次,两次判决一致才计胜负、否则记为平局(对应书中 6.4 位置偏差讨论)。仅此来源需要 LLM API Key。
**两种评判后端(`--judge-backend {anthropic,openrouter,auto}`,默认 `auto`**
- `anthropic`:官方 `anthropic` SDK,用 `ANTHROPIC_API_KEY`
- `openrouter`OpenAI 兼容 SDK 指向 `https://openrouter.ai/api/v1`,用 `OPENROUTER_API_KEY`。内部 Claude 名字会自动映射为 OpenRouter id`claude-opus-4-8``anthropic/claude-opus-4.8``claude-haiku-4-5``anthropic/claude-haiku-4.5`);已含 `/` 的 id(如 `openai/gpt-5.6-luna`)原样透传。当直连 Anthropic key 缺失或失效时用它兜底。
- `auto`(默认):有 `ANTHROPIC_API_KEY` 走 anthropic,否则回退 openrouter。注意 `auto` 只看 key 是否存在、不校验有效性;若 `ANTHROPIC_API_KEY` 存在但已失效,请显式 `--judge-backend openrouter`
位置偏差消除与 A/B/tie 解析逻辑与后端无关,两条路径完全一致。
### 分步示例
```bash
# 1) 模拟 5000 场对战(含 10% 平局)
python cli.py battle --source simulate --num-battles 5000 --output battles.json
# 2) 用官方 Bradley-Terry MLE + 100 轮 bootstrap 置信区间计算评分
python cli.py elo --input battles.json --method bradley-terry --bootstrap 100
# 3) 展示前 20 名排行榜(也可直接读评分文件)
python cli.py leaderboard --input battles.json --top-n 20
# 用真实 Arena 数据抽样跑(离线)
python cli.py pipeline --source arena --arena-file arena_data.json --sample 50000 --method bradley-terry --bootstrap 100
# LLM 评判对战(需要 API Key)——官方 Anthropic
export ANTHROPIC_API_KEY=your-anthropic-api-key
python cli.py battle --source llm --candidate-models claude-opus-4-8 claude-haiku-4-5
# LLM 评判对战——通过 OpenRouter 兜底(直连 Anthropic key 缺失/失效时)
export OPENROUTER_API_KEY=your-openrouter-api-key
python cli.py battle --source llm --judge-backend openrouter \
--judge-model claude-opus-4-8 \
--candidate-models anthropic/claude-haiku-4.5 openai/gpt-5.6-luna
```
模拟来源会同时打印真值潜在实力,方便和恢复出的排行榜对照;在线 Elo 与 Bradley-Terry 两种方法都应恢复出与真值一致的排名(分值不必精确对齐,见下文说明)。
## Quick Start
The project implements **two ranking methods** following official Chatbot Arena:
### 1. Bradley-Terry Model (Default - Recommended)
```bash
python main.py
# or explicitly:
python main.py bradley-terry
```
**Use this for**: Official leaderboard, stable rankings, production use
**Key features**:
- ✅ Official Chatbot Arena method
- ✅ Uses sklearn LogisticRegression for Maximum Likelihood Estimation
- ✅ Order-independent (processes all matches simultaneously)
- ✅ Includes 95% confidence intervals via bootstrap (100 samples)
- ✅ More stable and reliable rankings
**Processing time**: ~2-3 minutes (including bootstrap)
### 2. Online Elo (K=4)
```bash
python main.py online-elo
```
**Use this for**: Understanding Elo mechanics, educational purposes, faster computation
**Key features**:
- ✅ K-factor = 4 (official value used by Chatbot Arena)
- ✅ Simple sequential rating updates
- ✅ Order-dependent (processes matches chronologically)
- ✅ Faster computation (~30 seconds)
- ⚠️ Less stable, can vary based on match order
### Method Comparison
| Feature | Bradley-Terry | Online Elo |
|---------|--------------|------------|
| **Stability** | High (MLE fit) | Medium (sequential) |
| **Order dependence** | None | High |
| **Confidence intervals** | Yes (bootstrap) | No |
| **Speed** | Slower (~3 min) | Faster (~30 sec) |
| **Official method** | ✅ Yes | For comparison only |
| **Recommended** | ✅ Production | Educational |
### What Both Methods Do
1. Download Chatbot Arena voting data (~2GB, 5-15 minutes depending on connection)
2. Apply official filters:
- Anonymous votes only (blind evaluation)
- Deduplication (removes top 0.1% redundant prompts)
3. Compute model ratings using selected method
4. Calculate predicted win rates between all model pairs
5. Generate visualizations:
- `leaderboard.png` - Top 20 models ranked by rating
- `rating_distribution.png` - Rating histogram and statistics
- `win_rate_matrix.png` - Predicted win rates (top 30 models)
**Note**: The initial data download is ~2GB and may take several minutes. A progress bar shows download status.
### Quick Demo (Synthetic Data)
To quickly understand Elo mechanics without downloading 2GB:
```bash
python quickstart.py
```
This runs a small demo with synthetic matchups between GPT-4, Claude, Llama, and Gemini.
### Benchmark
To compare both methods:
```bash
python benchmark.py
```
This shows performance and accuracy differences between online Elo and Bradley-Terry approaches.
## Project Structure
```
elo-leaderboard/
├── cli.py # Unified argparse CLI (battle / elo / leaderboard / pipeline)
├── battle_simulator.py # Offline synthetic pairwise-battle generator
├── llm_judge.py # LLM-as-judge battles with position-bias mitigation (needs API)
├── main.py # Main analysis script
├── optimized_elo.py # NumPy + Numba Elo rating system
├── parallel_processing.py # Multi-core parallel processing utilities
├── data_loader.py # Data download and preprocessing
├── leaderboard.py # Leaderboard calculation and analysis
├── visualization.py # Static and interactive visualizations
├── animation.py # Animated bar chart race generator
├── benchmark.py # Performance benchmark tool
├── quickstart.py # Quick demo with synthetic data
├── elo_rating.py # Reference implementation (for comparison)
├── tests/ # Unit and regression tests
├── requirements.txt # Python dependencies
└── README.md # This file
```
## Usage Examples
### Building Elo Leaderboard
```python
from optimized_elo import build_leaderboard_optimized
# Build Elo leaderboard from DataFrame
elo = build_leaderboard_optimized(
df, # DataFrame with columns: model_a, model_b, winner
initial_rating=1000.0,
k_factor=32.0,
show_progress=True
)
# Get leaderboard
leaderboard = elo.get_leaderboard()
for rank, (model, rating, matches, wins) in enumerate(leaderboard[:10], 1):
win_rate = wins / matches * 100 if matches > 0 else 0
print(f"{rank}. {model}: {rating:.1f} ({matches} matches, {win_rate:.1f}% win rate)")
```
### Loading and Filtering Data
```python
from data_loader import load_arena_data, filter_data
# Load data
df = load_arena_data("arena_data.json")
# Filter for blind votes only (reduces bias)
df_filtered = filter_data(
df,
anony_only=True, # Only anonymous votes
language="English", # Specific language
min_turn=1 # Minimum conversation turn
)
```
### Building Historical Leaderboards
```python
from data_loader import get_time_slices
from leaderboard import build_historical_leaderboards, get_rating_history
# Create weekly time slices
time_slices = get_time_slices(df, interval='W')
# Build leaderboard for each time point
historical_leaderboards = build_historical_leaderboards(
df, time_slices, initial_rating=1000.0, k_factor=32.0
)
# Get rating history DataFrame
history_df = get_rating_history(historical_leaderboards)
```
### Creating Visualizations
```python
from visualization import (
plot_leaderboard,
plot_win_rate_matrix,
plot_rating_history,
create_interactive_leaderboard
)
# Static leaderboard chart
plot_leaderboard(leaderboard, top_n=20, save_path="leaderboard.png")
# Win rate heatmap
win_rate_df = calculate_win_rate_matrix_from_data(df)
plot_win_rate_matrix(win_rate_df, top_n=15, save_path="matrix.png")
# Rating evolution
plot_rating_history(history_df, models=["gpt-4", "claude-v1"],
save_path="history.png")
# Interactive chart
fig = create_interactive_leaderboard(history_df, top_n=15)
fig.write_html("interactive.html")
```
### Creating Animated Bar Chart Race
```python
from animation import create_simple_animation
# Generate animated HTML
animation_file = create_simple_animation(
history_df,
output_path="animation.html",
top_n=15
)
# Open animation.html in browser to view
```
## Output Files
After running `main.py`, the following files are generated:
### Static Images (PNG)
- `leaderboard.png` - Current top 20 models ranked by Elo rating
- `rating_distribution.png` - Histogram and box plot of rating distribution
- `win_rate_matrix.png` - Heatmap showing pairwise win rates
- `rating_history.png` - Line chart showing rating evolution over time
### Interactive Visualizations (HTML)
- `interactive_rating_evolution.html` - Interactive chart with zoom/pan
- `interactive_rank_evolution.html` - Interactive rank tracking
- `leaderboard_animation.html` - **Animated bar chart race** showing ranking evolution
## Key Parameters
### Elo System Parameters (Online Elo Method)
- **initial_rating** (default: 1000.0): Starting rating for all models
- **k_factor** (default: 4.0): Learning rate controlling update magnitude
- Official Chatbot Arena uses K=4 for stability
- Higher K-factor (e.g., 32): More volatile, faster adaptation to new data
- Lower K-factor (e.g., 4): More stable, less influenced by recent matches
### Bradley-Terry Parameters
- **SCALE** (400): Elo scale parameter - determines rating point interpretation
- **BASE** (10): Base for logistic function - standard for Elo calculations
- **INIT_RATING** (1000): Initial rating for all models
- **bootstrap_rounds** (100): Number of bootstrap samples for confidence intervals
### Time Slice Intervals
For historical analysis, you can adjust the time granularity:
- `'D'` - Daily snapshots
- `'W'` - Weekly snapshots (recommended)
- `'M'` - Monthly snapshots
### Visualization Parameters
- **top_n**: Number of top models to display (10-20 recommended)
- **Animation speed**: Adjustable in the HTML interface (1x to 10x)
## Data Format
The Chatbot Arena data includes the following fields:
- `model_a`: Identifier for first model
- `model_b`: Identifier for second model
- `winner`: Match outcome ('model_a', 'model_b', or 'tie')
- `tstamp`: Unix timestamp of the vote
- `judge`: User who made the vote
- `turn`: Conversation turn number
- `anony`: Whether vote was anonymous/blind
- `language`: Language of the conversation
## Validation
The implementation validates the Elo predictions against empirical win rates:
```python
from leaderboard import compare_win_rates
# Compare predicted vs actual win rates
comparison = compare_win_rates(elo_system, empirical_win_rates)
mean_error = comparison['error'].mean()
print(f"Mean Absolute Error: {mean_error:.4f}")
```
A low MAE (< 0.05) indicates the Elo model fits the data well.
## Analysis Insights
The project helps identify:
1. **Current Rankings**: Which models are currently strongest
2. **Rating Trends**: How model performance evolves over time
3. **Breakthrough Moments**: When new models enter or shake up rankings
4. **Competitive Dynamics**: Which models are closely matched
5. **Long-term Trajectories**: Models in ascent vs. decline
6. **Rating Stability**: Volatility in model performance
## Performance Architecture
The implementation is designed for high performance on large datasets (2GB+).
### Core Optimizations
#### 1. **NumPy + Numba JIT Compilation**
Uses NumPy arrays and Numba's just-in-time compilation:
- **NumPy arrays** for O(1) integer indexing (vs O(n) dictionary lookups)
- **Numba JIT** compiles hot loops to machine code (50-100x speedup)
- **Pre-allocated arrays** eliminate dynamic memory allocation overhead
- **Integer indices** instead of string model names for cache-friendly access
#### 2. **Multi-Core Parallel Processing**
Parallelizes independent operations across all CPU cores:
- **Historical analysis**: Each time slice processed independently
- **Win rate matrices**: Model pairs computed in parallel chunks
- **Data filtering**: DataFrame operations distributed across cores
```python
from parallel_processing import build_historical_leaderboards_parallel
# Automatically uses all available CPU cores
historical_lb = build_historical_leaderboards_parallel(
df, time_slices, n_jobs=-1
)
```
#### 3. **Memory Optimization**
Reduces memory footprint through intelligent data types:
- Downcasts numeric types (int64 → int32, float64 → float32)
- Converts repetitive strings to categorical types
- Achieves 30-50% memory reduction
```python
from parallel_processing import optimize_dataframe
df = optimize_dataframe(df) # Automatic memory optimization
```
### Performance Characteristics
On typical hardware (4-8 core CPU) with the full 2GB dataset:
| Component | Technique | Impact |
|---|---|---|
| Elo Computation | NumPy + Numba JIT | 50-100x faster |
| Historical Analysis | Multi-core parallel | 4-8x faster |
| Win Rate Matrix | Parallel processing | 4-8x faster |
| Memory Usage | Type optimization | 30-50% reduction |
| **Overall** | **Combined** | **~10-15x speedup** |
**Processing time**: 1-2 minutes for full dataset (hundreds of thousands of matches)
## Advanced Usage
### Custom Analysis
The modular design allows for flexible customization:
### Focused Analysis
```python
from optimized_elo import build_leaderboard_optimized
from data_loader import load_arena_data, filter_data
df = load_arena_data("arena_data.json")
# Analyze only recent data
df_recent = filter_data(df, min_date="2024-01-01")
elo_recent = build_leaderboard_optimized(df_recent)
# Analyze specific model family
gpt_models = [m for m in df['model_a'].unique() if 'gpt' in m.lower()]
df_gpt = df[df['model_a'].isin(gpt_models) & df['model_b'].isin(gpt_models)]
elo_gpt = build_leaderboard_optimized(df_gpt)
```
### Export Results
```python
import pandas as pd
# Export leaderboard to CSV
lb_df = pd.DataFrame(leaderboard, columns=['model', 'rating', 'matches', 'wins'])
lb_df.to_csv('leaderboard.csv', index=False)
# Export rating history
history_df.to_csv('rating_history.csv', index=False)
```
## Troubleshooting
### Data Download Issues
If automatic download fails:
1. Manually download from: https://storage.googleapis.com/arena_external_data/public/clean_battle_20240814_public.json
2. Save as `arena_data.json` in the project directory
3. Run `python main.py` again
### Memory Issues
The dataset is large (~2GB, hundreds of thousands of battles). If you encounter memory issues on systems with limited RAM:
```python
from data_loader import load_arena_data, filter_data, get_time_slices
from optimized_elo import build_leaderboard_optimized
# Load and immediately filter to reduce memory usage
df = load_arena_data("arena_data.json")
# Filter to recent data only
df_filtered = filter_data(df, min_date="2024-01-01", anony_only=True)
# Use monthly instead of weekly intervals for historical analysis
time_slices = get_time_slices(df_filtered, interval='M') # vs 'W' for weekly
# Analyze with smaller top_n for visualizations
elo = build_leaderboard_optimized(df_filtered)
```
The built-in memory optimization reduces footprint by 30-50%, but very large analyses may still require 4GB+ RAM.
### Visualization Issues
- Ensure matplotlib, seaborn, and plotly are installed via the root `ch6` extra or the compatibility `requirements.txt` path.
- For HTML animations, use a modern web browser (Chrome, Firefox, Safari, Edge)
- If plots don't display in Jupyter, use `%matplotlib inline` or save to file
## References
- **Chatbot Arena**: https://chat.lmsys.org/
- **Elo Rating System**: https://en.wikipedia.org/wiki/Elo_rating_system
- **Bradley-Terry Model**: https://en.wikipedia.org/wiki/BradleyTerry_model
- **LMSYS Paper**: "Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena"
## Learning Objectives
This experiment demonstrates:
1. **Statistical Modeling**: How pairwise comparisons reveal relative abilities
2. **Online Learning**: Incremental rating updates as new data arrives
3. **Probabilistic Prediction**: Converting rating differences to win probabilities
4. **Data Visualization**: Effective techniques for showing temporal dynamics
5. **Model Evaluation**: Alternative to traditional benchmark approaches
## Technical Details
### Why Elo Computation is Hard to Parallelize
Elo rating computation is **inherently sequential** because each match's rating update depends on the current ratings, which were modified by all previous matches. This is why we can't simply split matches into chunks and process them independently.
However, we can still achieve significant speedups through:
1. **Algorithmic optimization**: NumPy arrays + Numba JIT
2. **Parallelizing independent operations**: Historical analysis, win rate matrices
3. **Memory efficiency**: Better cache utilization
4. **Data structure optimization**: Integer indexing, pre-allocation
### Numba JIT Compilation
The core Elo update loop is compiled to machine code using Numba:
```python
@jit(nopython=True)
def process_elo_updates_vectorized(ratings, model_a_indices, model_b_indices,
outcomes, k_factor, match_counts, win_counts):
for i in range(len(model_a_indices)):
# This loop runs at C speed, not Python speed
# Typical speedup: 50-100x over pure Python
...
```
### Memory Layout
Using NumPy arrays with proper data types:
- `ratings`: float64 array (8 bytes per model)
- `match_counts`: int32 array (4 bytes per model)
- `model_indices`: int32 array (4 bytes per match)
For 500 models and 500K matches: ~10 MB vs ~500 MB for dictionaries.
## Extensions
Potential enhancements:
- Implement Glicko or Glicko-2 rating systems (account for rating uncertainty)
- Add confidence intervals for rating estimates
- Analyze rating by language or task type
- Compare with other ranking methods (e.g., TrueSkill, PageRank)
- Implement time-decay for older matches
- Add statistical significance testing
- Build prediction model for future rankings
- GPU acceleration using CuPy for even larger datasets
- Distributed processing using Dask for multi-machine scaling
## License
This project is part of the AI Agent practical training course materials.
## Contact
For questions or issues, please refer to the course materials or discussion forums.
---
## 中文
该项目围绕**配对比较(pairwise)数据**构建 Elo/Bradley-Terry 排名流程,目标是用公开的模型对战投票数据(重点是 Chatbot Arena)形成可复现的模型排行榜与可视化分析。
### 实验导向背景
Elo 本质上用于“成对对局中的胜率”学习相对能力,最初用于棋类,现被广泛用于语言模型两两对比的排序。
### 关键特性
- 高性能实现:NumPy + Numba JIT + 并行处理。
- 真实数据分析:接入大规模公开投票数据。
- 胜率推断:可预测任意两个模型的胜率。
- 历史追踪:可输出时间序列排行快照。
- 交互可视化:支持静态图与动态动画。
- 可扩展:可承接较大规模比赛集合。
### 数学原理
与 AndroidWorld 风格一致,评分来自 Bradley-Terry
```
P(A 胜过 B) = 1 / (1 + 10^((R_B - R_A) / 400))
```
单步更新:
```
R_A_new = R_A + K * (S_A - E_A)
```
### 安装与运行
```bash
# 在仓库根目录使用统一的第 6 章环境
uv sync --locked --python 3.12 --extra ch6
# 切换目录前先激活环境:
# macOS/Linux
source .venv/bin/activate
# Windows PowerShell.\.venv\Scripts\Activate.ps1
# Windows cmd.venv\Scripts\activate.bat
# 未安装 uv 时可用 pip 兜底:
# python -m pip install -e ".[ch6]"
cd chapter7/elo-leaderboard
# 迁移期间仍支持单项目兼容路径:
# python -m pip install -r requirements.txt
```
### 测试
```bash
# 在仓库根目录安装测试工具:
uv sync --locked --python 3.12 --extra ch6 --extra dev
# 切换目录前先激活环境:
source .venv/bin/activate
# 未安装 uv 时可用 pip 兜底:
# python -m pip install -e ".[ch6,dev]"
cd chapter7/elo-leaderboard
python -m pytest tests
```
### 命令行(`cli.py`
`cli.py` 是统一入口:
- `battle`:生成/采集两两对战
- `elo`:计算评级
- `leaderboard`:出榜
- `pipeline`:端到端一条龙
```bash
python cli.py --help
python cli.py battle --help
python cli.py # 等价于 python cli.py pipeline
```
### 三类对战源
- `simulate`:合成对战(有真值),用于验证是否恢复出正确排序。
- `arena`:离线加载 `arena_data.json`(约 2GB);可用 `--sample` 抽样。
- `llm`:调用 LLM 判断对战,带位置偏差消除;支持 `anthropic``openrouter``auto`
`auto` 会优先使用 Anthropic key,失败时回退 OpenRouter;位置消偏策略与 A/B/tie 判定和后端无关。
### 两种核心评分方法
- Bradley-Terry(推荐):更稳定,适合正式排行。
- Online Elo:更贴近课程里的机制讲解,速度快但对顺序敏感。
### 项目结构
同上方英文学段落中的文件列表。
### 使用示例
核心示例同上英文学:
- `python cli.py battle ...`
- `python cli.py elo ...`
- `python cli.py leaderboard ...`
- `python demo.py` / `python benchmark.py`
### 注意
- `--sample``--pipeline``--top-n` 等参数见命令行帮助。
- 建议先看 CLI 输出再对照 `leaderboard` 与可视化文件确认理解。
+436
View File
@@ -0,0 +1,436 @@
"""
Create animated bar chart race showing leaderboard evolution over time
"""
import pandas as pd
import numpy as np
from typing import List, Tuple
import json
import os
def prepare_animation_data(history_df: pd.DataFrame, top_n: int = 15) -> dict:
"""
Prepare data for D3.js bar chart race animation.
Args:
history_df: DataFrame with columns: date, model, rating, rank
top_n: Number of top models to show at each time point
Returns:
Dictionary with animation data
"""
# Convert date column to Timestamp to support ISO date strings and date objects
if history_df is not None and len(history_df) > 0 and 'date' in history_df.columns:
history_df = history_df.copy()
history_df['date'] = pd.to_datetime(history_df['date'])
# Get all unique dates
dates = sorted(history_df['date'].unique())
if len(dates) == 0:
return {
'frames': [],
'total_frames': 0,
'top_n': top_n,
'start_date': None,
'end_date': None,
}
# For each date, get top N models
frames = []
for date in dates:
date_data = history_df[history_df['date'] == date].nlargest(top_n, 'rating')
frame = {
'date': date.strftime('%Y-%m-%d'),
'timestamp': int(date.timestamp()),
'models': []
}
for rank, row in enumerate(date_data.itertuples(), 1):
frame['models'].append({
'rank': rank,
'name': row.model,
'rating': float(row.rating),
'matches': int(row.matches),
'wins': float(row.wins)
})
frames.append(frame)
animation_data = {
'frames': frames,
'total_frames': len(frames),
'top_n': top_n,
'start_date': dates[0].strftime('%Y-%m-%d'),
'end_date': dates[-1].strftime('%Y-%m-%d')
}
return animation_data
def generate_html_animation(animation_data: dict, output_path: str = "leaderboard_animation.html"):
"""
Generate standalone HTML file with D3.js bar chart race animation.
Args:
animation_data: Dictionary from prepare_animation_data
output_path: Path to save HTML file
"""
html_template = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Model Leaderboard Evolution</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background: #f5f5f5;
}
#container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
text-align: center;
color: #333;
margin-bottom: 10px;
}
#date-display {
text-align: center;
font-size: 24px;
font-weight: bold;
color: #666;
margin-bottom: 20px;
}
#chart {
margin: 20px 0;
}
.bar {
fill: steelblue;
cursor: pointer;
transition: fill 0.3s;
}
.bar:hover {
fill: #4682b4;
}
.bar-label {
font-size: 14px;
fill: white;
font-weight: bold;
}
.bar-value {
font-size: 12px;
fill: #333;
}
.rank-label {
font-size: 18px;
fill: #666;
font-weight: bold;
}
#controls {
text-align: center;
margin-top: 30px;
}
button {
padding: 10px 20px;
margin: 0 5px;
font-size: 16px;
cursor: pointer;
border: none;
border-radius: 5px;
background: #4CAF50;
color: white;
transition: background 0.3s;
}
button:hover {
background: #45a049;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
#progress-bar {
width: 100%;
height: 5px;
background: #e0e0e0;
margin-top: 20px;
border-radius: 3px;
overflow: hidden;
}
#progress {
height: 100%;
background: #4CAF50;
width: 0%;
transition: width 0.5s;
}
#speed-control {
margin-top: 20px;
text-align: center;
}
#speed-slider {
width: 300px;
margin: 0 10px;
}
.info-box {
background: #f9f9f9;
padding: 15px;
border-radius: 5px;
margin-top: 20px;
font-size: 14px;
color: #666;
}
</style>
</head>
<body>
<div id="container">
<h1>🏆 Model Leaderboard Evolution</h1>
<div id="date-display">Loading...</div>
<div id="chart"></div>
<div id="controls">
<button id="play-btn">▶ Play</button>
<button id="pause-btn" disabled>⏸ Pause</button>
<button id="reset-btn">↺ Reset</button>
</div>
<div id="progress-bar">
<div id="progress"></div>
</div>
<div id="speed-control">
<label>Speed: </label>
<input type="range" id="speed-slider" min="1" max="10" value="5">
<span id="speed-value">5x</span>
</div>
<div class="info-box">
<strong>About:</strong> This animation shows the evolution of model rankings based on Elo ratings
calculated from Chatbot Arena voting data. Each frame represents a snapshot in time,
with models ranked by their current Elo rating. Bars show the rating value,
and the animation reveals how models compete and evolve over time.
</div>
</div>
<script>
const data = """ + json.dumps(animation_data, indent=2) + """;
// Configuration
const margin = {top: 20, right: 100, bottom: 40, left: 50};
const width = 1100 - margin.left - margin.right;
const height = 600 - margin.top - margin.bottom;
const barHeight = height / data.top_n - 5;
// Create SVG
const svg = d3.select("#chart")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// Scales
const xScale = d3.scaleLinear()
.domain([0, d3.max(data.frames.flatMap(f => f.models.map(m => m.rating)))])
.range([0, width - 200]);
// Color scale
const colorScale = d3.scaleOrdinal(d3.schemeCategory10);
// Animation state
let currentFrame = 0;
let isPlaying = false;
let animationInterval = null;
let animationSpeed = 500; // milliseconds per frame
// Update speed based on slider
d3.select("#speed-slider").on("input", function() {
const speed = +this.value;
animationSpeed = 1000 / speed;
d3.select("#speed-value").text(`${speed}x`);
if (isPlaying) {
stopAnimation();
startAnimation();
}
});
function updateChart(frameIndex) {
const frame = data.frames[frameIndex];
// Update date display
d3.select("#date-display").text(frame.date);
// Update progress bar
const progress = ((frameIndex + 1) / data.total_frames) * 100;
d3.select("#progress").style("width", `${progress}%`);
// Update max value for scale
const maxRating = d3.max(frame.models, d => d.rating);
xScale.domain([0, maxRating * 1.1]);
// Bind data
const bars = svg.selectAll(".bar-group")
.data(frame.models, d => d.name);
// Remove old bars
bars.exit()
.transition()
.duration(animationSpeed * 0.8)
.style("opacity", 0)
.remove();
// Add new bars
const enter = bars.enter()
.append("g")
.attr("class", "bar-group")
.style("opacity", 0);
enter.append("rect")
.attr("class", "bar")
.attr("height", barHeight);
enter.append("text")
.attr("class", "bar-label")
.attr("x", 10)
.attr("y", barHeight / 2)
.attr("dy", "0.35em");
enter.append("text")
.attr("class", "bar-value")
.attr("y", barHeight / 2)
.attr("dy", "0.35em");
enter.append("text")
.attr("class", "rank-label")
.attr("x", -40)
.attr("y", barHeight / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle");
// Update all bars
const merged = enter.merge(bars);
merged.transition()
.duration(animationSpeed * 0.8)
.style("opacity", 1)
.attr("transform", (d, i) => `translate(0,${i * (barHeight + 5)})`);
merged.select(".bar")
.transition()
.duration(animationSpeed * 0.8)
.attr("width", d => xScale(d.rating))
.attr("fill", d => colorScale(d.name));
merged.select(".bar-label")
.text(d => d.name);
merged.select(".bar-value")
.transition()
.duration(animationSpeed * 0.8)
.attr("x", d => xScale(d.rating) + 10)
.text(d => `${Math.round(d.rating)} (${d.matches} matches)`);
merged.select(".rank-label")
.text(d => `#${d.rank}`);
}
function startAnimation() {
if (currentFrame >= data.total_frames - 1) {
currentFrame = 0;
}
isPlaying = true;
d3.select("#play-btn").property("disabled", true);
d3.select("#pause-btn").property("disabled", false);
animationInterval = setInterval(() => {
updateChart(currentFrame);
currentFrame++;
if (currentFrame >= data.total_frames) {
stopAnimation();
currentFrame = data.total_frames - 1;
}
}, animationSpeed);
}
function stopAnimation() {
isPlaying = false;
d3.select("#play-btn").property("disabled", false);
d3.select("#pause-btn").property("disabled", true);
if (animationInterval) {
clearInterval(animationInterval);
animationInterval = null;
}
}
function resetAnimation() {
stopAnimation();
currentFrame = 0;
updateChart(currentFrame);
d3.select("#progress").style("width", "0%");
}
// Button handlers
d3.select("#play-btn").on("click", startAnimation);
d3.select("#pause-btn").on("click", stopAnimation);
d3.select("#reset-btn").on("click", resetAnimation);
// Initialize with first frame
updateChart(0);
// Auto-play on load
setTimeout(startAnimation, 1000);
</script>
</body>
</html>"""
# Keep generated evidence friendly to `git diff --check` and deterministic
# across editors that otherwise strip indentation-only lines.
html_template = "\n".join(line.rstrip() for line in html_template.splitlines()) + "\n"
# Write to file
with open(output_path, 'w', encoding='utf-8') as f:
f.write(html_template)
print(f"Generated animation HTML at: {output_path}")
print(f"Open the file in a web browser to view the animation.")
def create_simple_animation(history_df: pd.DataFrame, output_path: str = "leaderboard_animation.html", top_n: int = 15):
"""
Convenience function to create animation in one step.
Args:
history_df: DataFrame with rating history
output_path: Path to save HTML file
top_n: Number of top models to show
"""
print("Preparing animation data...")
animation_data = prepare_animation_data(history_df, top_n)
print(f"Generating HTML animation with {animation_data['total_frames']} frames...")
generate_html_animation(animation_data, output_path)
return output_path
@@ -0,0 +1,77 @@
"""
Synthetic pairwise battle generator (offline).
Generates head-to-head "battle" outcomes from a set of known latent skill
scores, so the whole battles -> Elo -> leaderboard pipeline can be demonstrated
end-to-end without downloading the 2GB Chatbot Arena dataset or calling any API.
Because the ground-truth skills are known, the recovered Elo leaderboard can be
checked against them: the ranking should match, which validates the
implementation. Ties are produced with a configurable probability to exercise
the tie-handling paths in both the online Elo and Bradley-Terry code.
"""
import random
from typing import Dict, List, Optional
# Default roster with plausible latent skills (in Elo points). The exact numbers
# are only used to *generate* battles; the experiment then tries to recover them.
DEFAULT_TRUE_SKILLS: Dict[str, float] = {
"gpt-4": 1250.0,
"claude-3-opus": 1225.0,
"gemini-1.5-pro": 1180.0,
"llama-3-70b": 1120.0,
"mixtral-8x7b": 1075.0,
"gpt-3.5-turbo": 1035.0,
"llama-2-13b": 980.0,
"vicuna-13b": 935.0,
}
def expected_score(rating_a: float, rating_b: float,
base: float = 10.0, scale: float = 400.0) -> float:
"""Bradley-Terry / Elo win probability of A against B."""
return 1.0 / (1.0 + base ** ((rating_b - rating_a) / scale))
def simulate_battles(true_skills: Dict[str, float],
num_battles: int,
tie_prob: float = 0.1,
seed: Optional[int] = None) -> List[dict]:
"""
Simulate `num_battles` random pairwise battles.
For each battle two distinct models are drawn uniformly at random. With
probability `tie_prob` the outcome is a tie; otherwise the winner is sampled
according to the Bradley-Terry win probability implied by the latent skills
(so upsets happen, but stronger models win more often).
Args:
true_skills: Mapping of model name -> latent skill (Elo points).
num_battles: Number of battles to generate.
tie_prob: Probability that a battle ends in a tie.
seed: Optional RNG seed for reproducibility.
Returns:
List of dicts with keys 'model_a', 'model_b', 'winner'
(winner in {'model_a', 'model_b', 'tie'}), matching the Chatbot Arena
schema consumed by the Elo / Bradley-Terry code.
"""
if len(true_skills) < 2:
raise ValueError("Need at least 2 models to simulate battles")
rng = random.Random(seed)
models = list(true_skills.keys())
battles: List[dict] = []
for _ in range(num_battles):
model_a, model_b = rng.sample(models, 2)
if rng.random() < tie_prob:
winner = "tie"
elif rng.random() < expected_score(true_skills[model_a], true_skills[model_b]):
winner = "model_a"
else:
winner = "model_b"
battles.append({"model_a": model_a, "model_b": model_b, "winner": winner})
return battles
+144
View File
@@ -0,0 +1,144 @@
"""
Benchmark script to compare performance of different Elo implementations
"""
import time
import pandas as pd
import numpy as np
from elo_rating import EloRatingSystem
from optimized_elo import build_leaderboard_optimized
from data_loader import load_arena_data, filter_data
def benchmark_basic_elo(df: pd.DataFrame) -> float:
"""Benchmark the basic Elo implementation."""
print("\n" + "="*80)
print("Benchmarking Basic Elo Implementation (Python dict)")
print("="*80)
start_time = time.time()
elo = EloRatingSystem(initial_rating=1000.0, k_factor=32.0)
for _, row in df.iterrows():
elo.update_ratings(row['model_a'], row['model_b'], row['winner'])
end_time = time.time()
elapsed = end_time - start_time
leaderboard = elo.get_leaderboard()
print(f"✓ Processed {len(df)} matches in {elapsed:.2f} seconds")
print(f" Speed: {len(df)/elapsed:.0f} matches/second")
print(f" Top 3 models: {[m[0] for m in leaderboard[:3]]}")
return elapsed
def benchmark_optimized_elo(df: pd.DataFrame) -> float:
"""Benchmark the NumPy + Numba optimized implementation."""
print("\n" + "="*80)
print("Benchmarking Optimized Elo Implementation (NumPy + Numba JIT)")
print("="*80)
start_time = time.time()
elo = build_leaderboard_optimized(
df,
initial_rating=1000.0,
k_factor=32.0,
show_progress=False
)
end_time = time.time()
elapsed = end_time - start_time
leaderboard = elo.get_leaderboard()
print(f"✓ Processed {len(df)} matches in {elapsed:.2f} seconds")
print(f" Speed: {len(df)/elapsed:.0f} matches/second")
print(f" Top 3 models: {[m[0] for m in leaderboard[:3]]}")
return elapsed
def main():
"""Run benchmark comparison."""
print("="*80)
print("ELO RATING COMPUTATION BENCHMARK")
print("="*80)
print("\nThis benchmark compares the performance of different Elo implementations")
print("on Chatbot Arena voting data.\n")
# Load data
print("Loading data...")
try:
df = load_arena_data("arena_data.json")
except FileNotFoundError:
print("Error: arena_data.json not found. Please run main.py first to download the data.")
return
# Filter for blind votes
print("Filtering data...")
df_filtered = filter_data(df, anony_only=True, min_turn=1)
# Use a subset for quick benchmarking (can change to full dataset)
sample_size = 50000
if len(df_filtered) > sample_size:
print(f"\nUsing a sample of {sample_size} matches for benchmarking")
print("(To benchmark on full dataset, set sample_size = len(df_filtered))")
df_sample = df_filtered.head(sample_size).copy()
else:
df_sample = df_filtered.copy()
print(f"\nBenchmark dataset: {len(df_sample)} matches")
print(f"Unique models: {len(set(df_sample['model_a'].unique()) | set(df_sample['model_b'].unique()))}")
# Warm up Numba JIT (first run compiles the functions)
print("\n" + "-"*80)
print("Warming up Numba JIT compiler (first run)...")
print("-"*80)
df_tiny = df_sample.head(1000)
build_leaderboard_optimized(df_tiny, show_progress=False)
print("✓ JIT compilation complete")
# Run benchmarks
time_basic = benchmark_basic_elo(df_sample)
time_optimized = benchmark_optimized_elo(df_sample)
# Results summary
print("\n" + "="*80)
print("BENCHMARK RESULTS")
print("="*80)
speedup = time_basic / time_optimized if time_optimized > 0 else 0
print(f"\nBasic Implementation: {time_basic:8.2f} seconds")
print(f"Optimized Implementation: {time_optimized:8.2f} seconds")
print(f"\nSpeedup: {speedup:.1f}x faster")
pct_reduction = (1 - time_optimized / time_basic) * 100 if time_basic > 0 else 0.0
print(f"Time saved: {time_basic - time_optimized:.2f} seconds ({pct_reduction:.1f}% reduction)")
# Extrapolate to full dataset
if len(df_sample) > 0 and len(df_sample) < len(df_filtered):
full_time_basic = time_basic * (len(df_filtered) / len(df_sample))
full_time_optimized = time_optimized * (len(df_filtered) / len(df_sample))
print(f"\nExtrapolated times for full dataset ({len(df_filtered)} matches):")
print(f" Basic: ~{full_time_basic/60:.1f} minutes")
print(f" Optimized: ~{full_time_optimized/60:.1f} minutes")
print(f" Time saved: ~{(full_time_basic - full_time_optimized)/60:.1f} minutes")
print("\n" + "="*80)
print("\nOptimization Techniques Applied:")
print(" • NumPy arrays instead of Python dicts (O(1) integer indexing)")
print(" • Numba JIT compilation (compiles hot loops to machine code)")
print(" • Pre-allocated arrays (no dynamic memory allocation)")
print(" • Integer model indices (no string lookups)")
print(" • Vectorized operations where possible")
print("\nFor the full optimized pipeline with parallel processing, run main_optimized.py")
print("="*80 + "\n")
if __name__ == "__main__":
main()
+238
View File
@@ -0,0 +1,238 @@
"""
Bradley-Terry Model Implementation
Official Chatbot Arena leaderboard calculation method
"""
import math
import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
def compute_mle_elo(df: pd.DataFrame,
SCALE: int = 400,
BASE: int = 10,
INIT_RATING: int = 1000,
calibration_model: str | None = None,
calibration_rating: int | None = None) -> pd.Series:
"""
Compute Elo ratings using Bradley-Terry model with Maximum Likelihood Estimation.
This is the official method used by Chatbot Arena for their leaderboard.
It uses sklearn's LogisticRegression to fit a Bradley-Terry model.
Args:
df: DataFrame with columns 'model_a', 'model_b', 'winner'
SCALE: Elo scale parameter (default 400)
BASE: Base for logistic function (default 10)
INIT_RATING: Initial rating (default 1000)
calibration_model: Model name to calibrate ratings to
calibration_rating: Target rating for calibration model
Returns:
Series of Elo ratings indexed by model name
"""
# Empty battle frame (e.g. --num-battles 0 or fully filtered input) is valid.
if df is None or len(df) == 0:
return pd.Series(dtype=float)
models = sorted({m for m in (set(df["model_a"]) | set(df["model_b"])) if pd.notna(m)})
if len(models) <= 1:
res_dict = {}
for m in models:
if calibration_model == m and calibration_rating is not None:
res_dict[m] = float(calibration_rating)
else:
res_dict[m] = float(INIT_RATING)
return pd.Series(res_dict, index=pd.Index(models), dtype=float)
# Create pivot tables for wins
ptbl_a_win = pd.pivot_table(
df[df["winner"] == "model_a"],
index="model_a",
columns="model_b",
aggfunc="size",
fill_value=0,
observed=False
)
# Handle ties (including "tie (bothbad)"). Symmetrize only after aligning to
# the full model square; (pivot + pivot.T) on a one-sided A×B pivot zeroes
# every cell via pandas index/column alignment.
if sum(df["winner"].isin(["tie", "tie (bothbad)"])) == 0:
ptbl_tie = pd.DataFrame(0, index=ptbl_a_win.index, columns=ptbl_a_win.columns)
else:
ptbl_tie = pd.pivot_table(
df[df["winner"].isin(["tie", "tie (bothbad)"])],
index="model_a",
columns="model_b",
aggfunc="size",
fill_value=0,
observed=False
)
ptbl_b_win = pd.pivot_table(
df[df["winner"] == "model_b"],
index="model_a",
columns="model_b",
aggfunc="size",
fill_value=0,
observed=False
)
# Align pivots on the full model universe (small samples otherwise leave NaNs).
models = sorted({m for m in (set(df["model_a"]) | set(df["model_b"])) if pd.notna(m)})
ptbl_a_win = ptbl_a_win.reindex(index=models, columns=models, fill_value=0)
ptbl_b_win = ptbl_b_win.reindex(index=models, columns=models, fill_value=0)
ptbl_tie = ptbl_tie.reindex(index=models, columns=models, fill_value=0)
ptbl_tie = ptbl_tie + ptbl_tie.T
# Compute win matrix (A wins * 2 + B wins * 2 + ties)
ptbl_win = (ptbl_a_win * 2 + ptbl_b_win.T * 2 + ptbl_tie).fillna(0)
# Map models to indices
models = pd.Series(np.arange(len(ptbl_win.index)), index=ptbl_win.index)
p = len(models)
X = np.zeros([p * (p - 1) * 2, p])
Y = np.zeros(p * (p - 1) * 2)
cur_row = 0
sample_weights = []
for m_a in ptbl_win.index:
for m_b in ptbl_win.columns:
if m_a == m_b:
continue
# Skip if nan
if math.isnan(ptbl_win.loc[m_a, m_b]) or math.isnan(ptbl_win.loc[m_b, m_a]):
continue
X[cur_row, models[m_a]] = +math.log(BASE)
X[cur_row, models[m_b]] = -math.log(BASE)
Y[cur_row] = 1.0
sample_weights.append(ptbl_win.loc[m_a, m_b])
X[cur_row + 1, models[m_a]] = math.log(BASE)
X[cur_row + 1, models[m_b]] = -math.log(BASE)
Y[cur_row + 1] = 0.0
sample_weights.append(ptbl_win.loc[m_b, m_a])
cur_row += 2
X = X[:cur_row]
Y = Y[:cur_row]
# Fit logistic regression
lr = LogisticRegression(fit_intercept=False, penalty=None, tol=1e-6)
lr.fit(X, Y, sample_weight=sample_weights)
# Convert to Elo scores
elo_scores = SCALE * lr.coef_[0] + INIT_RATING
# Calibrate to reference model if provided
if calibration_model and calibration_model in models.index:
target_rating = INIT_RATING if calibration_rating is None else calibration_rating
elo_scores += target_rating - elo_scores[models[calibration_model]]
return pd.Series(elo_scores, index=models.index).sort_values(ascending=False)
def predict_win_rate(elo_ratings: dict[str, float],
SCALE: int = 400,
BASE: int = 10) -> pd.DataFrame:
"""
Predict win rates between all model pairs using Elo ratings.
Args:
elo_ratings: Dictionary of model names to Elo ratings
SCALE: Elo scale parameter
BASE: Base for logistic function
Returns:
DataFrame with predicted win rates (row vs column)
"""
from collections import defaultdict
names = sorted(elo_ratings)
wins = defaultdict(lambda: defaultdict(lambda: 0))
for a in names:
for b in names:
ea = 1 / (1 + BASE ** ((elo_ratings[b] - elo_ratings[a]) / SCALE))
wins[a][b] = ea
wins[b][a] = 1 - ea
data = {
# np.nan, not np.NAN: the upper-case aliases were removed in NumPy 2.0
# and requirements.txt allows numpy>=1.24 (i.e. 2.x).
a: [wins[a][b] if a != b else np.nan for b in names]
for a in names
}
df = pd.DataFrame(data, index=names)
df.index.name = "model_a"
df.columns.name = "model_b"
return df.T
def get_bootstrap_result(battles: pd.DataFrame,
func_compute_elo,
num_round: int = 100,
random_seed: int = 0) -> pd.DataFrame:
"""
Compute bootstrap confidence intervals for Elo ratings.
Args:
battles: DataFrame with battle data
func_compute_elo: Function to compute Elo ratings
num_round: Number of bootstrap rounds
Returns:
DataFrame with ratings from each bootstrap round
"""
from tqdm import tqdm
rows = []
for i in tqdm(range(num_round), desc="Bootstrap sampling"):
rows.append(
func_compute_elo(
battles.sample(frac=1.0, replace=True, random_state=random_seed + i)
)
)
df = pd.DataFrame(rows)
return df[df.median().sort_values(ascending=False).index]
def compute_bradley_terry_leaderboard(df: pd.DataFrame,
bootstrap_rounds: int = 0) -> pd.DataFrame:
"""
Compute leaderboard using Bradley-Terry model (official Chatbot Arena method).
Args:
df: DataFrame with columns 'model_a', 'model_b', 'winner'
bootstrap_rounds: Number of bootstrap rounds for confidence intervals (0 = no bootstrap)
Returns:
DataFrame with model ratings (and confidence intervals if bootstrap > 0)
"""
print("Computing Bradley-Terry model ratings...")
# Compute MLE Elo ratings
elo_ratings = compute_mle_elo(df)
if bootstrap_rounds > 0:
print(f"Computing {bootstrap_rounds} bootstrap samples for confidence intervals...")
bootstrap_df = get_bootstrap_result(df, compute_mle_elo, bootstrap_rounds)
# Compute confidence intervals
result = pd.DataFrame({
'rating': bootstrap_df.quantile(0.5),
'lower_ci': bootstrap_df.quantile(0.025),
'upper_ci': bootstrap_df.quantile(0.975)
}).sort_values('rating', ascending=False)
else:
result = pd.DataFrame({
'rating': elo_ratings
}).sort_values('rating', ascending=False)
result.index.name = 'model'
return result.reset_index()
+343
View File
@@ -0,0 +1,343 @@
#!/usr/bin/env python3
"""
实验 7-7:从配对比较数据构建模型排行榜 —— 命令行入口
统一的 argparse 命令行工具,把整个流程拆成三个子命令:
battle 运行两两对战,生成对战结果(模拟 / Chatbot Arena 真实数据 / LLM 评判)
elo 从对战结果计算 Elo 或 Bradley-Terry 评分
leaderboard 把对战结果或评分渲染成最终排行榜表格
pipeline 一步跑完 对战 -> Elo -> 排行榜(默认离线可复现)
其中 battle 的 simulate/arena 来源与 elo、leaderboard、pipeline 均为纯离线计算,
无需任何 API;只有 --source llmLLM 评判对战)需要 LLM API Key:优先用官方
AnthropicANTHROPIC_API_KEY),若无则自动回退到 OpenRouterOPENROUTER_API_KEY),
也可用 --judge-backend openrouter 强制走 OpenRouterdirect key 失效时)。
示例:
# 离线一条龙:模拟对战 -> Elo -> 排行榜
python cli.py pipeline
# 分步运行
python cli.py battle --source simulate --num-battles 5000 --output battles.json
python cli.py elo --input battles.json --method bradley-terry --bootstrap 100
python cli.py leaderboard --input battles.json --top-n 20
"""
import argparse
import json
import os
import sys
import warnings
from typing import List, Optional
import pandas as pd
# Bradley-Terry 的 LogisticRegression 在新版 sklearn 会对 penalty=None 抛
# FutureWarningbootstrap 会重复上百次,这里静音以保持排行榜输出整洁。
warnings.filterwarnings("ignore", category=FutureWarning, module="sklearn")
from battle_simulator import DEFAULT_TRUE_SKILLS, simulate_battles
from elo_rating import EloRatingSystem
# --------------------------------------------------------------------------- #
# 通用辅助函数
# --------------------------------------------------------------------------- #
def _load_battles(path: str) -> pd.DataFrame:
"""从 JSON 文件加载对战结果,返回带 model_a/model_b/winner 列的 DataFrame。"""
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
df = pd.DataFrame(data)
# `[]` from --num-battles 0 has no columns; treat as empty battle frame.
if len(df) == 0:
return pd.DataFrame(columns=["model_a", "model_b", "winner"])
required = {"model_a", "model_b", "winner"}
if not required.issubset(df.columns):
raise ValueError(
f"对战文件 {path} 缺少必要字段 {required},实际字段:{list(df.columns)}"
)
return df
def _save_json(obj, path: str) -> None:
with open(path, "w", encoding="utf-8") as f:
json.dump(obj, f, ensure_ascii=False, indent=2)
def _battle_stats(df: pd.DataFrame) -> dict:
"""统计每个模型的对战场数与胜场(平局按 0.5 计)。"""
matches: dict = {}
wins: dict = {}
for model_a, model_b, winner in zip(df["model_a"], df["model_b"], df["winner"]):
matches[model_a] = matches.get(model_a, 0) + 1
matches[model_b] = matches.get(model_b, 0) + 1
if winner == "model_a":
wins[model_a] = wins.get(model_a, 0) + 1.0
elif winner == "model_b":
wins[model_b] = wins.get(model_b, 0) + 1.0
else: # tie / tie (bothbad)
wins[model_a] = wins.get(model_a, 0) + 0.5
wins[model_b] = wins.get(model_b, 0) + 0.5
return {"matches": matches, "wins": wins}
def _compute_online_elo(df: pd.DataFrame, k: float, init_rating: float) -> pd.DataFrame:
"""在线增量 Elo(按记录顺序处理),返回带 model/rating 列的 DataFrame。"""
elo = EloRatingSystem(initial_rating=init_rating, k_factor=k)
for model_a, model_b, winner in zip(df["model_a"], df["model_b"], df["winner"]):
elo.update_ratings(model_a, model_b, winner)
rows = [(m, r) for m, r, *_ in elo.get_leaderboard()]
return pd.DataFrame(rows, columns=["model", "rating"])
def _compute_bradley_terry(df: pd.DataFrame, bootstrap: int) -> pd.DataFrame:
"""Bradley-Terry MLE 评分(可选 bootstrap 置信区间)。"""
# 延迟导入:Bradley-Terry 依赖 scikit-learn,仅在需要时加载。
from bradley_terry import compute_bradley_terry_leaderboard
return compute_bradley_terry_leaderboard(df, bootstrap_rounds=bootstrap)
def _compute_ratings(df: pd.DataFrame, method: str, k: float,
init_rating: float, bootstrap: int) -> pd.DataFrame:
if method == "bradley-terry":
return _compute_bradley_terry(df, bootstrap)
return _compute_online_elo(df, k, init_rating)
def _print_leaderboard(ratings: pd.DataFrame, df: Optional[pd.DataFrame],
top_n: int, title: str) -> None:
"""打印最终排行榜表格。若评分含置信区间则展示 95% CI 列。"""
has_ci = {"lower_ci", "upper_ci"}.issubset(ratings.columns)
stats = _battle_stats(df) if df is not None else {"matches": {}, "wins": {}}
ratings = ratings.sort_values("rating", ascending=False).reset_index(drop=True)
print("=" * 78)
print(title)
print("=" * 78)
if has_ci:
header = f"{'排名':<6}{'模型':<24}{'Elo':>8} {'95% 置信区间':<20}{'场数':>7}{'胜率':>9}"
else:
header = f"{'排名':<6}{'模型':<24}{'Elo':>8} {'场数':>7}{'胜率':>9}"
print(header)
print("-" * 78)
for idx, row in ratings.head(top_n).iterrows():
model = str(row["model"])
n = stats["matches"].get(model, 0)
w = stats["wins"].get(model, 0.0)
win_rate = (w / n * 100.0) if n else 0.0
if has_ci:
ci = f"[{row['lower_ci']:.0f}, {row['upper_ci']:.0f}]"
print(f"{idx + 1:<6}{model:<24}{row['rating']:>8.1f} "
f"{ci:<20}{n:>7}{win_rate:>8.1f}%")
else:
print(f"{idx + 1:<6}{model:<24}{row['rating']:>8.1f} "
f"{n:>7}{win_rate:>8.1f}%")
print("-" * 78)
print(f"{len(ratings)} 个模型,"
f"评分范围 {ratings['rating'].min():.1f} ~ {ratings['rating'].max():.1f}")
if has_ci:
avg_ci = (ratings["upper_ci"] - ratings["lower_ci"]).mean()
print(f"平均 95% 置信区间宽度:{avg_ci:.1f}")
print()
# --------------------------------------------------------------------------- #
# 子命令实现
# --------------------------------------------------------------------------- #
def _make_battles(args) -> List[dict]:
if args.source == "simulate":
skills = DEFAULT_TRUE_SKILLS
if args.models:
# 用户指定模型名时,围绕 1000 分等距分配潜在实力。
n = len(args.models)
skills = {m: 1000.0 + (n - 1 - 2 * i) * 40.0 for i, m in enumerate(args.models)}
print(f"模拟 {args.num_battles} 场对战({len(skills)} 个模型,"
f"平局概率 {args.tie_prob},随机种子 {args.seed}...")
battles = simulate_battles(skills, args.num_battles,
tie_prob=args.tie_prob, seed=args.seed)
print("真实潜在实力(用于事后对照):")
for m, s in sorted(skills.items(), key=lambda kv: -kv[1]):
print(f" {m:<24}{s:>8.1f}")
return battles
if args.source == "arena":
from data_loader import load_arena_data, filter_data
from parallel_processing import optimize_dataframe
if not os.path.exists(args.arena_file):
print(f"错误:找不到 Chatbot Arena 数据文件 {args.arena_file}", file=sys.stderr)
print("可从以下地址下载并保存为该文件名:", file=sys.stderr)
print("https://storage.googleapis.com/arena_external_data/public/"
"clean_battle_20240814_public.json", file=sys.stderr)
sys.exit(1)
df = load_arena_data(args.arena_file)
df = optimize_dataframe(df)
df = filter_data(df, anony_only=True, use_dedup=True, min_turn=1)
if args.sample and args.sample < len(df):
df = df.sample(n=args.sample, random_state=args.seed).reset_index(drop=True)
print(f"采样 {args.sample} 场对战。")
return df[["model_a", "model_b", "winner"]].to_dict("records")
# source == "llm"
from llm_judge import run_llm_battles
print("运行 LLM 评判对战(顺序交换以消除位置偏差)...")
return run_llm_battles(
candidate_models=args.candidate_models,
judge_model=args.judge_model,
backend=args.judge_backend,
)
def cmd_battle(args) -> None:
battles = _make_battles(args)
_save_json(battles, args.output)
print(f"\n已生成 {len(battles)} 场对战,写入 {args.output}")
def cmd_elo(args) -> None:
df = _load_battles(args.input)
print(f"{args.input} 加载 {len(df)} 场对战,方法:{args.method}")
ratings = _compute_ratings(df, args.method, args.k, args.init_rating, args.bootstrap)
_print_leaderboard(ratings, df, top_n=args.top_n,
title=f"Elo 评分({args.method}")
if args.output:
_save_json(ratings.to_dict("records"), args.output)
print(f"评分已写入 {args.output}")
def cmd_leaderboard(args) -> None:
with open(args.input, "r", encoding="utf-8") as f:
data = json.load(f)
sample = data[0] if isinstance(data, list) and data else {}
if "rating" in sample: # 输入已是评分文件,直接展示。
ratings = pd.DataFrame(data)
_print_leaderboard(ratings, None, top_n=args.top_n, title="模型排行榜")
return
# 否则视为对战文件:先计算评分再展示。
df = _load_battles(args.input)
print(f"{args.input} 加载 {len(df)} 场对战,方法:{args.method}")
ratings = _compute_ratings(df, args.method, args.k, args.init_rating, args.bootstrap)
_print_leaderboard(ratings, df, top_n=args.top_n, title="模型排行榜")
def cmd_pipeline(args) -> None:
print("=" * 78)
print("实验 7-7:对战 -> Elo -> 排行榜(端到端)")
print("=" * 78)
battles = _make_battles(args)
if args.output:
_save_json(battles, args.output)
print(f"对战结果写入 {args.output}")
df = pd.DataFrame(battles)
print(f"\n{args.method} 方法从 {len(df)} 场对战计算评分...")
ratings = _compute_ratings(df, args.method, args.k, args.init_rating, args.bootstrap)
_print_leaderboard(ratings, df, top_n=args.top_n, title="最终排行榜")
# --------------------------------------------------------------------------- #
# 参数解析
# --------------------------------------------------------------------------- #
def _add_source_args(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--source", choices=["simulate", "arena", "llm"],
default="simulate",
help="对战来源:simulate=离线模拟(默认)arena=真实 Chatbot Arena 数据,"
"llm=LLM 评判(需 API)")
parser.add_argument("--models", nargs="+", default=None,
help="simulate:自定义模型名列表(默认使用内置 8 个模型)")
parser.add_argument("--num-battles", type=int, default=3000,
help="simulate:模拟对战场数(默认 3000)")
parser.add_argument("--tie-prob", type=float, default=0.1,
help="simulate:平局概率(默认 0.1")
parser.add_argument("--seed", type=int, default=42,
help="随机种子(默认 42")
parser.add_argument("--arena-file", default="arena_data.json",
help="arenaChatbot Arena 数据文件路径(默认 arena_data.json")
parser.add_argument("--sample", type=int, default=0,
help="arena:随机采样 N 场对战,0 表示全部(默认 0)")
parser.add_argument("--candidate-models", nargs="+", default=None,
help="llm:参与对战的候选模型(默认 Claude 系列)")
parser.add_argument("--judge-model", default="claude-opus-4-8",
help="llm:评判模型(默认 claude-opus-4-8")
parser.add_argument("--judge-backend", choices=["anthropic", "openrouter", "auto"],
default="auto",
help="llm:评判后端。auto=有 ANTHROPIC_API_KEY 用官方 Anthropic"
"否则回退到 OpenRouterOPENROUTER_API_KEY);"
"openrouter=强制走 OpenRouterdirect key 失效时用)")
def _add_rating_args(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--method", choices=["online-elo", "bradley-terry"],
default="online-elo",
help="评分方法:online-elo=在线增量 Elo(默认)"
"bradley-terry=官方 MLE 拟合")
parser.add_argument("--k", type=float, default=4.0,
help="online-eloK 因子/学习率(默认 4.0,官方取值)")
parser.add_argument("--init-rating", type=float, default=1000.0,
help="初始评分(默认 1000")
parser.add_argument("--bootstrap", type=int, default=0,
help="bradley-terrybootstrap 轮数以估计 95%% 置信区间(默认 0=不估计)")
parser.add_argument("--top-n", type=int, default=20,
help="排行榜展示的模型数量(默认 20")
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="cli.py",
description="实验 7-7:从配对比较数据构建模型排行榜(对战 -> Elo -> 排行榜)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
sub = parser.add_subparsers(dest="command", metavar="{battle,elo,leaderboard,pipeline}")
# battle
p_battle = sub.add_parser("battle", help="运行两两对战,生成对战结果")
_add_source_args(p_battle)
p_battle.add_argument("--output", default="battles.json",
help="对战结果输出文件(默认 battles.json")
p_battle.set_defaults(func=cmd_battle)
# elo
p_elo = sub.add_parser("elo", help="从对战结果计算 Elo / Bradley-Terry 评分")
p_elo.add_argument("--input", default="battles.json",
help="对战结果输入文件(默认 battles.json")
_add_rating_args(p_elo)
p_elo.add_argument("--output", default=None,
help="把评分写入 JSON 文件(可选)")
p_elo.set_defaults(func=cmd_elo)
# leaderboard
p_lb = sub.add_parser("leaderboard", help="显示最终排行榜表格")
p_lb.add_argument("--input", default="battles.json",
help="对战结果或评分输入文件(默认 battles.json")
_add_rating_args(p_lb)
p_lb.set_defaults(func=cmd_leaderboard)
# pipeline
p_pipe = sub.add_parser("pipeline", help="一步跑完 对战 -> Elo -> 排行榜(默认离线)")
_add_source_args(p_pipe)
_add_rating_args(p_pipe)
p_pipe.add_argument("--output", default=None,
help="把对战结果写入 JSON 文件(可选)")
p_pipe.set_defaults(func=cmd_pipeline)
return parser
def main(argv: Optional[List[str]] = None) -> None:
parser = build_parser()
# 无子命令时默认运行离线端到端演示,保留开箱即用体验。
args = parser.parse_args(argv if argv is not None else (sys.argv[1:] or ["pipeline"]))
try:
args.func(args)
except (RuntimeError, FileNotFoundError, ValueError) as exc:
print(f"错误:{exc}", file=sys.stderr)
sys.exit(1)
except Exception as exc: # 例如无效 ANTHROPIC_API_KEY 触发的 anthropic.AuthenticationError
print(f"错误:{type(exc).__name__}: {exc}", file=sys.stderr)
print("(若为 LLM 评审路径,请检查对应 provider 的 API key 是否有效)", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
+223
View File
@@ -0,0 +1,223 @@
"""
Data loading and preprocessing for Chatbot Arena voting data
"""
import pandas as pd
import requests
import os
from typing import Optional
from tqdm import tqdm
def download_arena_data(output_path: str = "arena_data.json", force_download: bool = False) -> str:
"""
Download Chatbot Arena voting data via HTTPS.
Args:
output_path: Path to save downloaded file
force_download: If True, re-download even if file exists
Returns:
Path to downloaded file
"""
if os.path.exists(output_path) and not force_download:
print(f"Data file already exists at {output_path}")
file_size = os.path.getsize(output_path) / (1024 * 1024)
print(f"File size: {file_size:.2f} MB")
return output_path
print("Downloading Chatbot Arena voting data...")
url = "https://storage.googleapis.com/arena_external_data/public/clean_battle_20240814_public.json"
try:
# Stream download with progress bar
response = requests.get(url, stream=True)
response.raise_for_status()
# Get total file size
total_size = int(response.headers.get('content-length', 0))
# Download with progress bar
with open(output_path, 'wb') as f, tqdm(
desc=output_path,
total=total_size,
unit='B',
unit_scale=True,
unit_divisor=1024,
) as pbar:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
pbar.update(len(chunk))
file_size = os.path.getsize(output_path) / (1024 * 1024)
print(f"\nDownloaded data to {output_path} ({file_size:.2f} MB)")
return output_path
except Exception as e:
print(f"Error downloading data: {e}")
print("Please ensure you have internet connection and the URL is accessible.")
raise
def load_arena_data(filepath: str) -> pd.DataFrame:
"""
Load and preprocess Chatbot Arena voting data.
Expected columns:
- model_a: Identifier for first model
- model_b: Identifier for second model
- winner: Which model won ('model_a', 'model_b', or 'tie')
- tstamp: Unix timestamp of the vote
- judge: User who made the vote
- turn: Conversation turn
- anony: Whether vote was anonymous (blind)
- language: Language of the conversation
Args:
filepath: Path to data file
Returns:
Preprocessed DataFrame sorted by timestamp
"""
print(f"Loading data from {filepath}...")
print("Note: This is a large file (~2GB), loading may take 1-2 minutes...")
# Try different file formats
if filepath.endswith('.json'):
df = pd.read_json(filepath)
elif filepath.endswith('.jsonl'):
df = pd.read_json(filepath, lines=True)
elif filepath.endswith('.csv'):
df = pd.read_csv(filepath)
else:
# Try JSON by default
try:
df = pd.read_json(filepath)
except (ValueError, KeyError):
df = pd.read_json(filepath, lines=True)
print(f"Loaded {len(df)} records")
print(f"Columns: {df.columns.tolist()}")
# Sort by timestamp
if 'tstamp' in df.columns:
df = df.sort_values('tstamp', ascending=True).reset_index(drop=True)
print(f"Data spans from {pd.to_datetime(df['tstamp'].min(), unit='s')} to {pd.to_datetime(df['tstamp'].max(), unit='s')}")
# Basic statistics
if 'winner' in df.columns:
print(f"\nOutcome distribution:")
print(df['winner'].value_counts())
if 'model_a' in df.columns and 'model_b' in df.columns:
all_models = set(df['model_a'].unique()) | set(df['model_b'].unique())
print(f"\nTotal unique models: {len(all_models)}")
print(f"Top 10 models by appearance:")
model_counts = pd.concat([df['model_a'], df['model_b']]).value_counts().head(10)
print(model_counts)
return df
def filter_data(df: pd.DataFrame,
min_date: Optional[str] = None,
max_date: Optional[str] = None,
anony_only: bool = True,
language: Optional[str] = None,
min_turn: int = 1,
use_dedup: bool = True) -> pd.DataFrame:
"""
Filter voting data based on various criteria (following official Chatbot Arena method).
Args:
df: Input DataFrame
min_date: Minimum date (YYYY-MM-DD format)
max_date: Maximum date (YYYY-MM-DD format)
anony_only: If True, only include anonymous (blind) votes
language: If specified, filter by language
min_turn: Minimum conversation turn
use_dedup: If True, apply deduplication filter (official Arena method)
Returns:
Filtered DataFrame
"""
filtered = df.copy()
print(f"Before filtering: {len(filtered)} records")
# Filter by anonymous votes only (official method)
if anony_only and 'anony' in filtered.columns:
filtered = filtered[filtered['anony'] == True]
print(f" After anony filter: {len(filtered)} records")
# Apply deduplication (official method removes top 0.1% redundant prompts)
if use_dedup and 'dedup_tag' in filtered.columns:
try:
filtered = filtered[filtered["dedup_tag"].apply(lambda x: x.get("sampled", False) if isinstance(x, dict) else False)]
print(f" After dedup filter: {len(filtered)} records")
except Exception as e:
print(f" Warning: Could not apply dedup filter: {e}")
# Filter by date
if 'tstamp' in filtered.columns:
if min_date:
min_timestamp = pd.to_datetime(min_date).timestamp()
filtered = filtered[filtered['tstamp'] >= min_timestamp]
if max_date:
max_timestamp = pd.to_datetime(max_date).timestamp()
filtered = filtered[filtered['tstamp'] <= max_timestamp]
# Filter by language
if language and 'language' in filtered.columns:
filtered = filtered[filtered['language'] == language]
# Filter by turn
if 'turn' in filtered.columns:
filtered = filtered[filtered['turn'] >= min_turn]
pct = (len(filtered) / len(df) * 100) if len(df) else 0.0
print(f"After filtering: {len(filtered)} records ({pct:.1f}% of original)")
return filtered.reset_index(drop=True)
def get_time_slices(df: pd.DataFrame, interval: str = 'W') -> list:
"""
Split data into time slices for historical analysis.
Args:
df: Input DataFrame with 'tstamp' column
interval: Pandas frequency string ('D' for daily, 'W' for weekly, 'M' for monthly)
Returns:
List of (end_date, dataframe_slice) tuples
"""
if 'tstamp' not in df.columns:
raise ValueError("DataFrame must have 'tstamp' column")
if len(df) == 0:
print(f"Created 0 time slices with interval '{interval}'")
return []
df['datetime'] = pd.to_datetime(df['tstamp'], unit='s')
min_date = df['datetime'].min()
max_date = df['datetime'].max()
# Pandas 2.2+ removed 'M' (month-end); keep the documented monthly alias.
freq = "ME" if interval == "M" else interval
# Generate date ranges
date_ranges = pd.date_range(start=min_date, end=max_date, freq=freq)
slices = []
for end_date in date_ranges:
slice_df = df[df['datetime'] <= end_date].copy()
if len(slice_df) > 0:
slices.append((end_date, slice_df))
# Empty date_ranges when span < interval; also cover trailing gap to max_date.
if len(date_ranges) == 0 or date_ranges[-1] < max_date:
slices.append((max_date, df.copy()))
print(f"Created {len(slices)} time slices with interval '{interval}'")
return slices
+169
View File
@@ -0,0 +1,169 @@
"""
Elo Rating System Implementation
Based on Bradley-Terry model for pairwise comparison
"""
import numpy as np
from typing import Dict, Tuple, Optional
class EloRatingSystem:
"""
Implementation of Elo rating system for model comparison.
The Elo system updates ratings based on pairwise comparison outcomes,
where the rating difference between two models determines expected win probability.
"""
def __init__(self, initial_rating: float = 1000.0, k_factor: float = 4.0):
"""
Initialize Elo rating system.
Args:
initial_rating: Starting rating for all models
k_factor: Learning rate controlling magnitude of rating updates
"""
self.initial_rating = initial_rating
self.k_factor = k_factor
self.ratings: Dict[str, float] = {}
self.match_counts: Dict[str, int] = {}
self.win_counts: Dict[str, float] = {} # ties add 0.5, so this is float
def get_rating(self, model: str) -> float:
"""Get current rating for a model, initializing if necessary."""
if model not in self.ratings:
self.ratings[model] = self.initial_rating
self.match_counts[model] = 0
self.win_counts[model] = 0
return self.ratings[model]
def expected_score(self, rating_a: float, rating_b: float) -> float:
"""
Calculate expected win probability for model A against model B.
Uses logistic function: P(A wins) = 1 / (1 + 10^((R_B - R_A)/400))
Args:
rating_a: Rating of model A
rating_b: Rating of model B
Returns:
Expected probability that A wins (between 0 and 1)
"""
return 1.0 / (1.0 + 10.0 ** ((rating_b - rating_a) / 400.0))
def update_ratings(self, model_a: str, model_b: str, outcome: str) -> Tuple[float, float]:
"""
Update ratings after a match between two models.
Args:
model_a: Identifier for first model
model_b: Identifier for second model
outcome: Match result ('model_a', 'model_b', or 'tie')
Returns:
Tuple of (new_rating_a, new_rating_b)
"""
# Get current ratings
rating_a = self.get_rating(model_a)
rating_b = self.get_rating(model_b)
# Calculate expected scores
expected_a = self.expected_score(rating_a, rating_b)
expected_b = 1.0 - expected_a
# Determine actual scores
if outcome == 'model_a':
score_a, score_b = 1.0, 0.0
self.win_counts[model_a] = self.win_counts.get(model_a, 0) + 1
elif outcome == 'model_b':
score_a, score_b = 0.0, 1.0
self.win_counts[model_b] = self.win_counts.get(model_b, 0) + 1
else: # tie
score_a, score_b = 0.5, 0.5
# A tie counts as half a win for each side, keeping win_counts (and
# the win-rate derived from it) consistent with the 0.5-per-tie
# convention used elsewhere (leaderboard win-rate matrix, CLI stats).
self.win_counts[model_a] = self.win_counts.get(model_a, 0) + 0.5
self.win_counts[model_b] = self.win_counts.get(model_b, 0) + 0.5
# Update ratings using Elo formula
new_rating_a = rating_a + self.k_factor * (score_a - expected_a)
new_rating_b = rating_b + self.k_factor * (score_b - expected_b)
# Store updated ratings
self.ratings[model_a] = new_rating_a
self.ratings[model_b] = new_rating_b
# Update match counts
self.match_counts[model_a] = self.match_counts.get(model_a, 0) + 1
self.match_counts[model_b] = self.match_counts.get(model_b, 0) + 1
return new_rating_a, new_rating_b
def get_leaderboard(self) -> list:
"""
Get current leaderboard sorted by rating.
Returns:
List of tuples (model, rating, matches, wins) sorted by rating descending
"""
leaderboard = []
for model in self.ratings:
leaderboard.append((
model,
self.ratings[model],
self.match_counts.get(model, 0),
self.win_counts.get(model, 0)
))
# Sort by rating descending
leaderboard.sort(key=lambda x: x[1], reverse=True)
return leaderboard
def calculate_win_probability(self, model_a: str, model_b: str) -> float:
"""
Calculate win probability of model_a against model_b based on current ratings.
Args:
model_a: First model identifier
model_b: Second model identifier
Returns:
Probability that model_a wins (between 0 and 1)
"""
rating_a = self.get_rating(model_a)
rating_b = self.get_rating(model_b)
return self.expected_score(rating_a, rating_b)
def get_win_rate_matrix(self) -> Dict[Tuple[str, str], float]:
"""
Calculate pairwise win probability matrix for all models.
Returns:
Dictionary mapping (model_a, model_b) to win probability of model_a
"""
models = sorted(self.ratings.keys())
matrix = {}
for model_a in models:
for model_b in models:
if model_a != model_b:
prob = self.calculate_win_probability(model_a, model_b)
matrix[(model_a, model_b)] = prob
return matrix
def reset(self):
"""Reset all ratings to initial values."""
self.ratings.clear()
self.match_counts.clear()
self.win_counts.clear()
def copy(self) -> 'EloRatingSystem':
"""Create a deep copy of the current rating system."""
new_system = EloRatingSystem(self.initial_rating, self.k_factor)
new_system.ratings = self.ratings.copy()
new_system.match_counts = self.match_counts.copy()
new_system.win_counts = self.win_counts.copy()
return new_system
+24
View File
@@ -0,0 +1,24 @@
# 实验 7-7 环境变量示例
#
# 只有 `--source llm`LLM 评判对战)需要 API Key
# simulate / arena / elo / leaderboard / pipeline 均为纯离线,无需任何 Key。
#
# 用法:把本文件复制为 .env 并填入真实 Key,或直接 `export` 到 shell。
# --- 评判后端 1:官方 Anthropic(默认优先)---
# 有此 Key 时 --judge-backend auto 会走官方 Anthropic SDK。
ANTHROPIC_API_KEY=your-anthropic-api-key
# --- 评判后端 2OpenRouter 兜底 ---
# 当 ANTHROPIC_API_KEY 缺失或失效时使用;用 OpenAI 兼容 SDK 指向 OpenRouter。
# 内部 Claude 名字会自动映射为 OpenRouter id
# claude-opus-4-8 -> anthropic/claude-opus-4.8
# claude-haiku-4-5 -> anthropic/claude-haiku-4.5
# claude-sonnet-4-6 -> anthropic/claude-sonnet-4.6
# 已含 '/' 的 id(如 openai/gpt-5.6-luna)原样透传。
#
# 强制走 OpenRouter
# python cli.py battle --source llm --judge-backend openrouter \
# --judge-model claude-opus-4-8 \
# --candidate-models anthropic/claude-haiku-4.5 openai/gpt-5.6-luna
OPENROUTER_API_KEY=your-openrouter-api-key
+243
View File
@@ -0,0 +1,243 @@
"""
Leaderboard calculation and analysis
"""
import pandas as pd
import numpy as np
from typing import Dict, List, Tuple
from tqdm import tqdm
from elo_rating import EloRatingSystem
def build_leaderboard(df: pd.DataFrame,
initial_rating: float = 1000.0,
k_factor: float = 32.0,
show_progress: bool = True) -> EloRatingSystem:
"""
Build Elo leaderboard from voting data.
Args:
df: DataFrame with columns 'model_a', 'model_b', 'winner'
initial_rating: Starting rating for all models
k_factor: Elo learning rate
show_progress: Whether to show progress bar
Returns:
EloRatingSystem with final ratings
"""
elo = EloRatingSystem(initial_rating=initial_rating, k_factor=k_factor)
iterator = tqdm(df.iterrows(), total=len(df), desc="Processing matches") if show_progress else df.iterrows()
for idx, row in iterator:
model_a = row['model_a']
model_b = row['model_b']
winner = row['winner']
elo.update_ratings(model_a, model_b, winner)
return elo
def calculate_win_rate_matrix_from_data(df: pd.DataFrame) -> pd.DataFrame:
"""
Calculate empirical win rate matrix directly from vote data.
Args:
df: DataFrame with columns 'model_a', 'model_b', 'winner'
Returns:
DataFrame with win rates (rows beat columns)
"""
# Get all unique models
all_models = sorted(set(df['model_a'].unique()) | set(df['model_b'].unique()))
# Initialize counts
wins = {model: {opponent: 0 for opponent in all_models} for model in all_models}
total = {model: {opponent: 0 for opponent in all_models} for model in all_models}
# Count wins and totals
for _, row in df.iterrows():
model_a = row['model_a']
model_b = row['model_b']
winner = row['winner']
total[model_a][model_b] += 1
total[model_b][model_a] += 1
if winner == 'model_a':
wins[model_a][model_b] += 1
elif winner == 'model_b':
wins[model_b][model_a] += 1
else: # tie
wins[model_a][model_b] += 0.5
wins[model_b][model_a] += 0.5
# Calculate win rates
win_rates = {}
for model in all_models:
win_rates[model] = {}
for opponent in all_models:
if model == opponent:
win_rates[model][opponent] = 0.5
elif total[model][opponent] > 0:
win_rates[model][opponent] = wins[model][opponent] / total[model][opponent]
else:
win_rates[model][opponent] = np.nan
# Convert to DataFrame
win_rate_df = pd.DataFrame(win_rates).T
win_rate_df = win_rate_df[all_models] # Ensure consistent ordering
return win_rate_df
def compare_win_rates(elo_system: EloRatingSystem, empirical_df: pd.DataFrame) -> pd.DataFrame:
"""
Compare predicted win rates from Elo with empirical win rates.
Args:
elo_system: Trained Elo rating system
empirical_df: DataFrame with empirical win rates
Returns:
DataFrame with comparison statistics
"""
models = empirical_df.index.tolist()
comparisons = []
for model_a in models:
for model_b in models:
if model_a != model_b:
empirical = empirical_df.loc[model_a, model_b]
if not np.isnan(empirical):
predicted = elo_system.calculate_win_probability(model_a, model_b)
error = abs(predicted - empirical)
comparisons.append({
'model_a': model_a,
'model_b': model_b,
'empirical': empirical,
'predicted': predicted,
'error': error
})
cols = ['model_a', 'model_b', 'empirical', 'predicted', 'error']
if not comparisons:
return pd.DataFrame(columns=cols)
comparison_df = pd.DataFrame(comparisons, columns=cols)
return comparison_df
def build_historical_leaderboards(df: pd.DataFrame,
time_slices: List[Tuple],
initial_rating: float = 1000.0,
k_factor: float = 32.0) -> List[Tuple]:
"""
Build leaderboard snapshots at different time points.
Args:
df: Full voting DataFrame
time_slices: List of (end_date, slice_df) tuples from get_time_slices
initial_rating: Starting rating
k_factor: Elo learning rate
Returns:
List of (date, leaderboard_data) tuples
"""
historical_leaderboards = []
for end_date, slice_df in tqdm(time_slices, desc="Building historical leaderboards"):
elo = build_leaderboard(slice_df, initial_rating, k_factor, show_progress=False)
leaderboard = elo.get_leaderboard()
# Convert to DataFrame for easier handling
lb_df = pd.DataFrame(leaderboard, columns=['model', 'rating', 'matches', 'wins'])
lb_df['date'] = end_date
lb_df['rank'] = range(1, len(lb_df) + 1)
historical_leaderboards.append((end_date, lb_df))
return historical_leaderboards
def get_rating_history(historical_leaderboards: List[Tuple]) -> pd.DataFrame:
"""
Extract rating history for all models over time.
Args:
historical_leaderboards: List of (date, leaderboard_df) tuples
Returns:
DataFrame with columns: date, model, rating, rank
"""
all_data = []
for date, lb_df in historical_leaderboards:
for _, row in lb_df.iterrows():
all_data.append({
'date': date,
'model': row['model'],
'rating': row['rating'],
'rank': row['rank'],
'matches': row['matches'],
'wins': row['wins']
})
cols = ['date', 'model', 'rating', 'rank', 'matches', 'wins']
if not all_data:
return pd.DataFrame(columns=cols)
history_df = pd.DataFrame(all_data)
return history_df
def analyze_rating_changes(history_df: pd.DataFrame, top_n: int = 20) -> pd.DataFrame:
"""
Analyze rating changes over time for top models.
Args:
history_df: DataFrame from get_rating_history
top_n: Number of top models to analyze
Returns:
DataFrame with change statistics
"""
stats_cols = [
'model', 'final_rating', 'initial_rating', 'rating_change',
'max_rating', 'min_rating', 'volatility', 'total_matches',
]
if history_df is None or len(history_df) == 0:
return pd.DataFrame(columns=stats_cols)
# Get final ratings
final_date = history_df['date'].max()
final_ratings = history_df[history_df['date'] == final_date].nlargest(top_n, 'rating')
top_models = final_ratings['model'].tolist()
# Calculate statistics for each model
stats = []
for model in top_models:
model_data = history_df[history_df['model'] == model].sort_values('date')
if len(model_data) > 0:
initial_rating = model_data.iloc[0]['rating']
final_rating = model_data.iloc[-1]['rating']
max_rating = model_data['rating'].max()
min_rating = model_data['rating'].min()
rating_change = final_rating - initial_rating
volatility = model_data['rating'].std()
stats.append({
'model': model,
'final_rating': final_rating,
'initial_rating': initial_rating,
'rating_change': rating_change,
'max_rating': max_rating,
'min_rating': min_rating,
'volatility': volatility,
'total_matches': model_data.iloc[-1]['matches']
})
if not stats:
return pd.DataFrame(columns=stats_cols)
stats_df = pd.DataFrame(stats).sort_values('final_rating', ascending=False)
return stats_df
+271
View File
@@ -0,0 +1,271 @@
"""
LLM-as-judge pairwise battles with position-bias mitigation.
This is the only battle source that needs network access; `simulate` and
`arena` run fully offline.
Two backends are supported, selected automatically or via ``backend=``:
* ``anthropic`` the official ``anthropic`` SDK, using ``ANTHROPIC_API_KEY``
(the default when that key is present).
* ``openrouter`` the OpenAI-compatible ``openai`` SDK pointed at
``https://openrouter.ai/api/v1`` with ``OPENROUTER_API_KEY``. Internal
Claude ids (e.g. ``claude-opus-4-8``) are mapped to their OpenRouter ids
(``anthropic/claude-opus-4.8``); ids that already contain a ``/`` such as
``openai/gpt-5.6-luna`` are passed through untouched. This lets the judge run
when a direct Anthropic key is missing or invalid.
The two backends are interchangeable: the position-bias swap-and-agree logic and
the A/B/tie response parsing are identical regardless of which one is used.
The book (实验 7-7, 位置偏差 discussion) notes that an LLM judge systematically
favours whichever answer appears in a fixed slot (usually the first). The
standard mitigation, implemented here, is to judge each pair twice with the
answers swapped and only record a winner when both judgements agree; a
disagreement is counted as a tie. This cancels the position bias instead of
letting it leak into the ratings.
The resulting battle list uses the same {'model_a', 'model_b', 'winner'} schema
as the simulated and Chatbot Arena data, so it feeds straight into the Elo /
Bradley-Terry pipeline.
"""
import os
from typing import Dict, List, Optional
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass
# Default candidate roster and judge (Claude models). Kept small because every
# battle costs several API calls (two responses + two swapped judgements).
DEFAULT_CANDIDATE_MODELS = ["claude-opus-4-8", "claude-haiku-4-5"]
DEFAULT_JUDGE_MODEL = "claude-opus-4-8"
DEFAULT_PROMPTS = [
"用一句话解释什么是 Transformer 的自注意力机制。",
"Write a haiku about distributed systems.",
"给出快速排序的时间复杂度,并简要说明最坏情况。",
]
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
# Map internal Claude ids -> OpenRouter model ids. Any id already containing a
# '/' (e.g. 'openai/gpt-5.6-luna') is treated as a native OpenRouter id and used
# verbatim; unknown ids are also passed through unchanged.
_OPENROUTER_MODEL_MAP = {
"claude-opus-4-8": "anthropic/claude-opus-4.8",
"claude-opus-4-1": "anthropic/claude-opus-4.1",
"claude-sonnet-4-6": "anthropic/claude-sonnet-4.6",
"claude-sonnet-4-5": "anthropic/claude-sonnet-4.5",
"claude-haiku-4-5": "anthropic/claude-haiku-4.5",
}
_JUDGE_SYSTEM = (
"你是一个严格的评委。用户会给你一个问题和两个候选回答(回答 A 和回答 B)。"
"请只根据回答质量判断哪个更好,忽略它们出现的顺序。"
"只输出一个词:A、B 或 tie。"
)
def _to_openrouter_model(model: str) -> str:
"""Translate an internal model id into an OpenRouter model id."""
if "/" in model: # already a native OpenRouter id
return model
return _OPENROUTER_MODEL_MAP.get(model, model)
class JudgeClient:
"""
Thin adapter over either the Anthropic SDK or the OpenAI-compatible
OpenRouter endpoint, exposing a single ``chat()`` method so the rest of the
module is backend-agnostic.
"""
def __init__(self, backend: str, impl):
self.backend = backend
self.impl = impl
def chat(self, model: str, user: str, max_tokens: int,
system: Optional[str] = None) -> str:
"""Send a single-turn chat and return the assistant's text reply."""
if self.backend == "anthropic":
kwargs = {
"model": model,
"max_tokens": max_tokens,
"messages": [{"role": "user", "content": user}],
}
if system is not None:
kwargs["system"] = system
response = self.impl.messages.create(**kwargs)
return "".join(
block.text for block in response.content if block.type == "text"
).strip()
# openrouter (OpenAI-compatible chat.completions)
messages = []
if system is not None:
messages.append({"role": "system", "content": system})
messages.append({"role": "user", "content": user})
response = self.impl.chat.completions.create(
model=_to_openrouter_model(model),
max_tokens=max_tokens,
messages=messages,
)
return (response.choices[0].message.content or "").strip()
def _resolve_backend(backend: str = "auto") -> str:
"""
Resolve the effective backend.
``auto`` -> ``anthropic`` if ANTHROPIC_API_KEY is set, else ``openrouter``
if OPENROUTER_API_KEY is set. Raises if neither key is available.
"""
if backend not in ("anthropic", "openrouter", "auto"):
raise ValueError(
f"Unknown judge backend {backend!r}; expected 'anthropic', "
"'openrouter' or 'auto'."
)
if backend != "auto":
return backend
if os.environ.get("ANTHROPIC_API_KEY"):
return "anthropic"
if os.environ.get("OPENROUTER_API_KEY"):
return "openrouter"
raise RuntimeError(
"No LLM-judge credentials found. Set ANTHROPIC_API_KEY (direct Anthropic) "
"or OPENROUTER_API_KEY (OpenRouter fallback); or use --source simulate / "
"--source arena to run the experiment fully offline."
)
def _get_client(backend: str = "auto") -> JudgeClient:
"""Create a JudgeClient for the resolved backend, with clear errors."""
backend = _resolve_backend(backend)
if backend == "anthropic":
try:
import anthropic
except ImportError as exc: # pragma: no cover - depends on environment
raise RuntimeError(
"The 'anthropic' package is required for the anthropic judge "
"backend. Install it with: pip install anthropic"
) from exc
if not os.environ.get("ANTHROPIC_API_KEY"):
raise RuntimeError(
"ANTHROPIC_API_KEY is not set. Set it, or use "
"--judge-backend openrouter with OPENROUTER_API_KEY, or run "
"--source simulate / --source arena fully offline."
)
return JudgeClient("anthropic", anthropic.Anthropic())
# backend == "openrouter"
try:
import openai
except ImportError as exc: # pragma: no cover - depends on environment
raise RuntimeError(
"The 'openai' package is required for the openrouter judge backend. "
"Install it with: pip install openai"
) from exc
if not os.environ.get("OPENROUTER_API_KEY"):
raise RuntimeError(
"OPENROUTER_API_KEY is not set. Set it, or use --judge-backend "
"anthropic with ANTHROPIC_API_KEY, or run --source simulate / "
"--source arena fully offline."
)
return JudgeClient(
"openrouter",
openai.OpenAI(
base_url=OPENROUTER_BASE_URL,
api_key=os.environ["OPENROUTER_API_KEY"],
),
)
def generate_response(client: JudgeClient, model: str, prompt: str,
max_tokens: int = 1024) -> str:
"""Generate a single model answer for a prompt."""
return client.chat(model, prompt, max_tokens=max_tokens)
def _judge_once(client: JudgeClient, judge_model: str, prompt: str,
answer_first: str, answer_second: str) -> str:
"""Ask the judge which slot is better; returns 'first', 'second' or 'tie'."""
user = (
f"问题:\n{prompt}\n\n"
f"回答 A\n{answer_first}\n\n"
f"回答 B\n{answer_second}\n\n"
"哪个回答更好?只输出 A、B 或 tie。"
)
verdict = client.chat(judge_model, user, max_tokens=8, system=_JUDGE_SYSTEM).lower()
if verdict.startswith("a"):
return "first"
if verdict.startswith("b"):
return "second"
return "tie"
def judge_pair(client: JudgeClient, judge_model: str, prompt: str,
answer_a: str, answer_b: str) -> str:
"""
Judge a pair with position-bias mitigation (swap order, tie on disagreement).
Returns 'model_a', 'model_b', or 'tie'.
"""
# First pass: A in slot 1, B in slot 2.
first_pass = _judge_once(client, judge_model, prompt, answer_a, answer_b)
# Second pass: swap the slots so B is now in slot 1.
second_pass = _judge_once(client, judge_model, prompt, answer_b, answer_a)
# Translate both judgements into "which real model won", then require
# agreement. Slot 1 in the first pass is A; slot 1 in the second pass is B.
winner_first = {"first": "model_a", "second": "model_b", "tie": "tie"}[first_pass]
winner_second = {"first": "model_b", "second": "model_a", "tie": "tie"}[second_pass]
if winner_first == winner_second:
return winner_first
return "tie" # inconsistent under swap -> position bias, count as tie
def run_llm_battles(candidate_models: Optional[List[str]] = None,
prompts: Optional[List[str]] = None,
judge_model: str = DEFAULT_JUDGE_MODEL,
backend: str = "auto") -> List[dict]:
"""
Run LLM-judged battles between every model pair over every prompt.
Args:
candidate_models: Models to compare (default: DEFAULT_CANDIDATE_MODELS).
prompts: Prompts to battle on (default: DEFAULT_PROMPTS).
judge_model: Model used as the judge.
backend: 'anthropic', 'openrouter', or 'auto' (anthropic if
ANTHROPIC_API_KEY else openrouter).
Returns:
List of battle dicts ({'model_a', 'model_b', 'winner'}).
"""
candidate_models = candidate_models or DEFAULT_CANDIDATE_MODELS
prompts = prompts or DEFAULT_PROMPTS
if len(candidate_models) < 2:
raise ValueError("Need at least 2 candidate models for LLM-judge battles")
client = _get_client(backend)
battles: List[dict] = []
for prompt in prompts:
# Cache each model's answer per prompt so it is generated only once.
answers: Dict[str, str] = {
model: generate_response(client, model, prompt) for model in candidate_models
}
for i, model_a in enumerate(candidate_models):
for model_b in candidate_models[i + 1:]:
winner = judge_pair(
client, judge_model, prompt, answers[model_a], answers[model_b]
)
battles.append(
{"model_a": model_a, "model_b": model_b, "winner": winner}
)
return battles
+242
View File
@@ -0,0 +1,242 @@
"""
Main script for Model Leaderboard Calculation
Experiment 7-7: Building Model Leaderboard from Pairwise Comparison Data
Supports two methods (following official Chatbot Arena):
1. Online Elo (K=4) - Simple but order-dependent
2. Bradley-Terry MLE - Official leaderboard method (more stable)
"""
import os
import sys
import pandas as pd
from bradley_terry import (
compute_bradley_terry_leaderboard,
predict_win_rate,
)
from data_loader import download_arena_data, filter_data, load_arena_data
from elo_rating import EloRatingSystem
from parallel_processing import optimize_dataframe
from visualization import plot_leaderboard, plot_rating_distribution, plot_win_rate_matrix
def compute_online_elo_leaderboard(df: pd.DataFrame) -> pd.DataFrame:
"""
Compute leaderboard using online Elo updates (K=4, official value).
This method updates ratings sequentially as matches are processed.
It's simpler but can be unstable and order-dependent.
Args:
df: DataFrame with columns 'model_a', 'model_b', 'winner'
Returns:
DataFrame with model ratings
"""
from tqdm import tqdm
print("Computing online Elo ratings (K=4)...")
elo = EloRatingSystem(initial_rating=1000.0, k_factor=4.0)
# Process matches sequentially
for _, row in tqdm(df.iterrows(), total=len(df), desc="Processing matches"):
elo.update_ratings(row['model_a'], row['model_b'], row['winner'])
# Get leaderboard
leaderboard = elo.get_leaderboard()
# Convert to DataFrame
result = pd.DataFrame(leaderboard, columns=['model', 'rating', 'matches', 'wins'])
return result
def main(method: str = 'bradley-terry'):
"""
Run model leaderboard calculation.
Args:
method: 'bradley-terry' (default, official) or 'online-elo' (simple)
"""
print("="*80)
print("Experiment 7-7: Building Model Leaderboard from Pairwise Comparisons")
if method == 'bradley-terry':
print("Method: Bradley-Terry Model with MLE (Official Chatbot Arena)")
else:
print("Method: Online Elo Updates (K=4)")
print("="*80)
print()
# Step 1: Download and load data
print("Step 1: Loading Chatbot Arena voting data...")
print("-" * 80)
data_file = "arena_data.json"
try:
# Download data if not exists
if not os.path.exists(data_file):
data_file = download_arena_data(data_file)
# Load data
df = load_arena_data(data_file)
# Optimize memory usage
df = optimize_dataframe(df)
except Exception as e: # noqa: BLE001 - surface loader/provider diagnostics to CLI users
print(f"Error loading data: {e}")
print("\nNote: If the data download fails, you can manually download the file from:")
print("https://storage.googleapis.com/arena_external_data/public/clean_battle_20240814_public.json")
print("and save it as 'arena_data.json' in the current directory.")
return
print()
# Step 2: Filter data (official Chatbot Arena method)
print("Step 2: Filtering data (following official Arena method)...")
print("-" * 80)
df_filtered = filter_data(
df,
anony_only=True, # Only anonymous/blind votes
use_dedup=True, # Apply deduplication (removes top 0.1% redundant prompts)
min_turn=1
)
print()
# Step 3: Compute ratings using selected method
print(f"Step 3: Computing ratings using {method} method...")
print("-" * 80)
if method == 'bradley-terry':
print("Note: Bradley-Terry model uses sklearn LogisticRegression for MLE.")
print("This is the official Chatbot Arena method - more stable than online Elo.")
print()
# Compute ratings with bootstrap for confidence intervals
leaderboard_df = compute_bradley_terry_leaderboard(df_filtered, bootstrap_rounds=100)
print("\nTop 20 models by Bradley-Terry rating:")
print("-" * 80)
print(f"{'Rank':<6}{'Model':<35}{'Rating':<10}{'95% CI':<20}")
print("-" * 80)
for idx, row in leaderboard_df.head(20).iterrows():
if 'lower_ci' in row and 'upper_ci' in row:
ci_str = f"[{row['lower_ci']:.1f}, {row['upper_ci']:.1f}]"
else:
ci_str = "N/A"
print(f"{idx+1:<6}{row['model']:<35}{row['rating']:7.1f} {ci_str:<20}")
else: # online-elo
print("Note: Online Elo uses K=4 (official value) for stable ratings.")
print("Processes matches sequentially - simpler but can be order-dependent.")
print()
# Compute online Elo ratings
leaderboard_df = compute_online_elo_leaderboard(df_filtered)
print("\nTop 20 models by Online Elo rating:")
print("-" * 80)
print(f"{'Rank':<6}{'Model':<35}{'Rating':<10}{'Matches':<10}{'Win Rate':<10}")
print("-" * 80)
for idx, row in leaderboard_df.head(20).iterrows():
win_rate = row['wins'] / row['matches'] * 100 if row['matches'] > 0 else 0
print(f"{idx+1:<6}{row['model']:<35}{row['rating']:7.1f} {row['matches']:<10}{win_rate:6.1f}%")
print()
# Step 4: Predict win rates using Bradley-Terry model
print("Step 4: Calculating predicted win rates...")
print("-" * 80)
# Get ratings as dictionary
ratings_dict = dict(zip(leaderboard_df['model'], leaderboard_df['rating']))
# Predict win rates
predicted_win_rates = predict_win_rate(ratings_dict)
print(f"Calculated predicted win rates for {len(ratings_dict)} models")
print()
# Step 5: Create visualizations
print("Step 5: Creating visualizations...")
print("-" * 80)
# Convert leaderboard_df to format expected by visualization functions
leaderboard_tuples = [(row['model'], row['rating'], 0, 0) for _, row in leaderboard_df.iterrows()]
plot_leaderboard(leaderboard_tuples, top_n=20, save_path="leaderboard.png")
plot_rating_distribution(leaderboard_tuples, save_path="rating_distribution.png")
# Plot win rate matrix
top_30_models = leaderboard_df.head(30)['model'].tolist()
plot_win_rate_matrix(predicted_win_rates.loc[top_30_models, top_30_models],
top_n=30, save_path="win_rate_matrix.png")
print()
# Summary
print("="*80)
print("Analysis complete!")
print("="*80)
print("\nGenerated files:")
print(" - leaderboard.png : Top 20 models by Bradley-Terry rating")
print(" - rating_distribution.png : Distribution of ratings")
print(" - win_rate_matrix.png : Predicted win rate matrix (top 30 models)")
print()
# Method summary
print("Method:")
print("-" * 80)
if method == 'bradley-terry':
print(" ✓ Bradley-Terry model with Maximum Likelihood Estimation")
print(" ✓ sklearn LogisticRegression for stable rating computation")
print(" ✓ Bootstrap confidence intervals (100 samples)")
else:
print(" ✓ Online Elo with K=4 (official value)")
print(" ✓ Sequential match processing")
print(" ✓ Simple but order-dependent")
print(" ✓ Deduplication filter (removes top 0.1% redundant prompts)")
print(" ✓ Anonymous votes only (blind evaluation)")
print()
# Key insights
print("Key Insights:")
print("-" * 80)
print(f" • Total models analyzed: {len(leaderboard_df)}")
print(f" • Total battles: {len(df_filtered):,}")
print(f" • Rating range: {leaderboard_df['rating'].min():.1f} - {leaderboard_df['rating'].max():.1f}")
print(f" • Top model: {leaderboard_df.iloc[0]['model']} ({leaderboard_df.iloc[0]['rating']:.1f})")
if 'lower_ci' in leaderboard_df.columns:
avg_ci_width = (leaderboard_df['upper_ci'] - leaderboard_df['lower_ci']).mean()
print(f" • Average confidence interval width: {avg_ci_width:.1f} rating points")
print()
print("This implementation matches the official Chatbot Arena leaderboard calculation!")
print("Source: https://colab.research.google.com/drive/1KdwokPjirkTmpO_P1WByFNFiqxWQquwH")
print()
if __name__ == "__main__":
# Allow method selection via command line argument
import sys
method = 'bradley-terry' # Default to official method
if len(sys.argv) > 1:
if sys.argv[1] in ['bradley-terry', 'bt', 'mle']:
method = 'bradley-terry'
elif sys.argv[1] in ['online-elo', 'elo', 'online']:
method = 'online-elo'
else:
print(f"Unknown method: {sys.argv[1]}")
print("Usage: python main.py [bradley-terry|online-elo]")
print(" bradley-terry (default): Official Arena method, more stable")
print(" online-elo: Simple Elo updates with K=4")
sys.exit(1)
main(method)
+303
View File
@@ -0,0 +1,303 @@
"""
Optimized Elo rating system using NumPy vectorization and Numba JIT
"""
import numpy as np
import pandas as pd
from typing import Dict, Tuple, List
from numba import jit
from tqdm import tqdm
@jit(nopython=True)
def expected_score_fast(rating_a: float, rating_b: float) -> float:
"""
Fast expected score calculation using Numba JIT.
Args:
rating_a: Rating of model A
rating_b: Rating of model B
Returns:
Expected probability that A wins
"""
return 1.0 / (1.0 + 10.0 ** ((rating_b - rating_a) / 400.0))
@jit(nopython=True)
def process_elo_updates_vectorized(ratings: np.ndarray,
model_a_indices: np.ndarray,
model_b_indices: np.ndarray,
outcomes: np.ndarray,
k_factor: float,
match_counts: np.ndarray,
win_counts: np.ndarray) -> np.ndarray:
"""
Process Elo updates using vectorized NumPy operations with Numba JIT.
This is the core hot loop optimized with Numba for maximum performance.
Args:
ratings: Array of current ratings for all models
model_a_indices: Indices of model A for each match
model_b_indices: Indices of model B for each match
outcomes: Match outcomes (1.0 = A wins, 0.0 = B wins, 0.5 = tie)
k_factor: Elo K-factor
match_counts: Array to track match counts per model
win_counts: Array to track win counts per model
Returns:
Updated ratings array
"""
n_matches = len(model_a_indices)
for i in range(n_matches):
idx_a = model_a_indices[i]
idx_b = model_b_indices[i]
outcome = outcomes[i]
# Get current ratings
rating_a = ratings[idx_a]
rating_b = ratings[idx_b]
# Calculate expected scores
expected_a = 1.0 / (1.0 + 10.0 ** ((rating_b - rating_a) / 400.0))
expected_b = 1.0 - expected_a
# Update ratings
ratings[idx_a] += k_factor * (outcome - expected_a)
ratings[idx_b] += k_factor * ((1.0 - outcome) - expected_b)
# Update counts
match_counts[idx_a] += 1
match_counts[idx_b] += 1
win_counts[idx_a] += outcome
win_counts[idx_b] += (1.0 - outcome)
return ratings
@jit(nopython=True)
def calculate_expected_scores_vectorized(ratings_a: np.ndarray,
ratings_b: np.ndarray) -> np.ndarray:
"""
Vectorized calculation of expected scores for multiple matches.
Args:
ratings_a: Array of ratings for model A
ratings_b: Array of ratings for model B
Returns:
Array of expected scores for model A
"""
return 1.0 / (1.0 + np.power(10.0, (ratings_b - ratings_a) / 400.0))
class NumpyEloRatingSystem:
"""
Highly optimized Elo rating system using NumPy arrays and Numba JIT.
Optimizations:
- NumPy arrays for O(1) indexing instead of dictionary lookups
- Numba JIT compilation of hot loops
- Pre-allocated arrays to avoid memory reallocation
- Integer indexing for models instead of string lookups
"""
def __init__(self, initial_rating: float = 1000.0, k_factor: float = 4.0):
"""Initialize NumPy-based Elo system."""
self.initial_rating = initial_rating
self.k_factor = k_factor
# Model name to index mapping
self.model_to_idx: Dict[str, int] = {}
self.idx_to_model: Dict[int, str] = {}
# NumPy arrays for fast access
self.ratings: np.ndarray = None
self.match_counts: np.ndarray = None
self.win_counts: np.ndarray = None
self.n_models = 0
def _prepare_data(self, df: pd.DataFrame):
"""
Prepare NumPy arrays from DataFrame for fast processing.
Args:
df: DataFrame with columns 'model_a', 'model_b', 'winner'
"""
print("Preparing data structures...")
# Get all unique models
all_models = sorted(set(df['model_a'].unique()) | set(df['model_b'].unique()))
self.n_models = len(all_models)
print(f"Found {self.n_models} unique models")
# Create model mappings
for idx, model in enumerate(all_models):
self.model_to_idx[model] = idx
self.idx_to_model[idx] = model
# Initialize arrays
self.ratings = np.full(self.n_models, self.initial_rating, dtype=np.float64)
self.match_counts = np.zeros(self.n_models, dtype=np.int32)
self.win_counts = np.zeros(self.n_models, dtype=np.float64)
# Convert DataFrame columns to NumPy arrays with integer indices
print("Converting model names to indices...")
model_a_indices = df['model_a'].map(self.model_to_idx).values.astype(np.int32)
model_b_indices = df['model_b'].map(self.model_to_idx).values.astype(np.int32)
# Convert outcomes to numeric (1.0 for A wins, 0.0 for B wins, 0.5 for tie)
print("Converting outcomes to numeric...")
# 'tie (bothbad)' is a real Arena outcome; unmapped values become NaN and
# would silently poison every rating they touch, so fall back to a tie
# (same as EloRatingSystem.update_ratings).
outcome_map = {'model_a': 1.0, 'model_b': 0.0,
'tie': 0.5, 'tie (bothbad)': 0.5}
outcomes = df['winner'].map(outcome_map).fillna(0.5).values.astype(np.float64)
return model_a_indices, model_b_indices, outcomes
def process_matches_vectorized(self, df: pd.DataFrame, show_progress: bool = True):
"""
Process all matches using vectorized NumPy operations and Numba JIT.
This is the fastest way to compute Elo ratings for large datasets.
Args:
df: DataFrame with columns 'model_a', 'model_b', 'winner'
show_progress: Whether to show progress bar
"""
# Prepare data
model_a_indices, model_b_indices, outcomes = self._prepare_data(df)
print(f"\nProcessing {len(df)} matches with NumPy + Numba JIT...")
# Process all matches using JIT-compiled function
# This is where the magic happens - Numba compiles this to machine code
if show_progress:
# Process in chunks to show progress
chunk_size = 50000
n_chunks = (len(model_a_indices) + chunk_size - 1) // chunk_size
for i in tqdm(range(n_chunks), desc="Processing matches"):
start_idx = i * chunk_size
end_idx = min((i + 1) * chunk_size, len(model_a_indices))
self.ratings = process_elo_updates_vectorized(
self.ratings,
model_a_indices[start_idx:end_idx],
model_b_indices[start_idx:end_idx],
outcomes[start_idx:end_idx],
self.k_factor,
self.match_counts,
self.win_counts
)
else:
self.ratings = process_elo_updates_vectorized(
self.ratings,
model_a_indices,
model_b_indices,
outcomes,
self.k_factor,
self.match_counts,
self.win_counts
)
print("✓ Processing complete!")
def get_leaderboard(self) -> List[Tuple]:
"""
Get sorted leaderboard using NumPy's fast sorting.
Returns:
List of tuples (model, rating, matches, wins)
"""
# Use NumPy's argsort for fast sorting
sorted_indices = np.argsort(-self.ratings) # Negative for descending order
leaderboard = []
for idx in sorted_indices:
model = self.idx_to_model[idx]
rating = float(self.ratings[idx])
matches = int(self.match_counts[idx])
wins = float(self.win_counts[idx])
leaderboard.append((model, rating, matches, wins))
return leaderboard
def calculate_win_probability(self, model_a: str, model_b: str) -> float:
"""
Calculate win probability using fast NumPy operations.
Args:
model_a: First model identifier
model_b: Second model identifier
Returns:
Probability that model_a wins
"""
if model_a not in self.model_to_idx or model_b not in self.model_to_idx:
return 0.5
idx_a = self.model_to_idx[model_a]
idx_b = self.model_to_idx[model_b]
rating_a = self.ratings[idx_a]
rating_b = self.ratings[idx_b]
return expected_score_fast(rating_a, rating_b)
def get_win_rate_matrix(self) -> Dict[Tuple[str, str], float]:
"""
Calculate pairwise win probability matrix using vectorized operations.
Returns:
Dictionary mapping (model_a, model_b) to win probability
"""
matrix = {}
# Vectorized calculation for all pairs
for i in range(self.n_models):
model_a = self.idx_to_model[i]
# Calculate win probabilities against all other models at once
ratings_a = np.full(self.n_models, self.ratings[i])
win_probs = calculate_expected_scores_vectorized(ratings_a, self.ratings)
for j in range(self.n_models):
if i != j:
model_b = self.idx_to_model[j]
matrix[(model_a, model_b)] = float(win_probs[j])
return matrix
def build_leaderboard_optimized(df: pd.DataFrame,
initial_rating: float = 1000.0,
k_factor: float = 4.0,
show_progress: bool = True) -> NumpyEloRatingSystem:
"""
Build Elo leaderboard using highly optimized NumPy + Numba algorithm.
This implementation is significantly faster than the basic version:
- Uses NumPy arrays for O(1) indexing
- Numba JIT compilation for hot loops
- Pre-allocated arrays to avoid memory overhead
- Integer-based model indexing instead of string lookups
Args:
df: DataFrame with match data (columns: model_a, model_b, winner)
initial_rating: Starting rating for all models
k_factor: Elo learning rate (K-factor)
show_progress: Whether to display progress bar
Returns:
NumpyEloRatingSystem with final ratings
"""
elo = NumpyEloRatingSystem(initial_rating=initial_rating, k_factor=k_factor)
elo.process_matches_vectorized(df, show_progress=show_progress)
return elo
@@ -0,0 +1,322 @@
"""
Parallel processing utilities for Elo rating computation
"""
import pandas as pd
import numpy as np
from multiprocessing import Pool, cpu_count
from functools import partial
from typing import List, Tuple
from tqdm import tqdm
from elo_rating import EloRatingSystem
def process_time_slice(args: Tuple) -> Tuple:
"""
Process a single time slice to build leaderboard.
Args:
args: Tuple of (end_date, slice_df, initial_rating, k_factor)
Returns:
Tuple of (end_date, leaderboard_data)
"""
end_date, slice_df, initial_rating, k_factor = args
# Build Elo system for this time slice
elo = EloRatingSystem(initial_rating=initial_rating, k_factor=k_factor)
# Process all matches in this slice
for _, row in slice_df.iterrows():
elo.update_ratings(row['model_a'], row['model_b'], row['winner'])
# Get leaderboard
leaderboard = elo.get_leaderboard()
# Convert to list of dicts for easier handling
lb_data = []
for rank, (model, rating, matches, wins) in enumerate(leaderboard, 1):
lb_data.append({
'model': model,
'rating': rating,
'matches': matches,
'wins': wins,
'rank': rank,
'date': end_date
})
return (end_date, lb_data)
def build_historical_leaderboards_parallel(df: pd.DataFrame,
time_slices: List[Tuple],
initial_rating: float = 1000.0,
k_factor: float = 32.0,
n_jobs: int = -1) -> List[Tuple]:
"""
Build historical leaderboards using parallel processing.
Args:
df: Full voting DataFrame
time_slices: List of (end_date, slice_df) tuples
initial_rating: Starting rating
k_factor: Elo learning rate
n_jobs: Number of parallel jobs (-1 for all cores)
Returns:
List of (date, leaderboard_data) tuples
"""
if n_jobs == -1:
n_jobs = cpu_count()
print(f"Building historical leaderboards using {n_jobs} cores...")
# Prepare arguments for parallel processing
args_list = [
(end_date, slice_df, initial_rating, k_factor)
for end_date, slice_df in time_slices
]
# Process in parallel
with Pool(processes=n_jobs) as pool:
results = list(tqdm(
pool.imap(process_time_slice, args_list),
total=len(args_list),
desc="Processing time slices"
))
# Convert results to expected format
historical_leaderboards = []
for end_date, lb_data in results:
lb_df = pd.DataFrame(lb_data)
historical_leaderboards.append((end_date, lb_df))
# Sort by date
historical_leaderboards.sort(key=lambda x: x[0])
return historical_leaderboards
def calculate_pairwise_win_rates_chunk(args: Tuple) -> List[dict]:
"""
Calculate win rates for a chunk of model pairs.
Args:
args: Tuple of (model_pairs, df)
Returns:
List of win rate dictionaries
"""
model_pairs, df = args
results = []
for model_a, model_b in model_pairs:
# Filter matches between these two models
matches = df[
((df['model_a'] == model_a) & (df['model_b'] == model_b)) |
((df['model_a'] == model_b) & (df['model_b'] == model_a))
]
if len(matches) == 0:
continue
wins_a = 0
total = len(matches)
for _, row in matches.iterrows():
# Arena data has four winner values; any non-win outcome
# ('tie' and 'tie (bothbad)') is worth 0.5, matching the
# serial calculate_win_rate_matrix_from_data.
if row['model_a'] == model_a:
if row['winner'] == 'model_a':
wins_a += 1
elif row['winner'] != 'model_b':
wins_a += 0.5
else: # model_a is model_b in the row
if row['winner'] == 'model_b':
wins_a += 1
elif row['winner'] != 'model_a':
wins_a += 0.5
win_rate = wins_a / total if total > 0 else 0.5
results.append({
'model_a': model_a,
'model_b': model_b,
'win_rate': win_rate,
'total_matches': total
})
return results
def calculate_win_rate_matrix_parallel(df: pd.DataFrame,
models: List[str] = None,
n_jobs: int = -1) -> pd.DataFrame:
"""
Calculate win rate matrix using parallel processing.
Args:
df: DataFrame with match data
models: List of models to include (if None, use all)
n_jobs: Number of parallel jobs
Returns:
DataFrame with win rates
"""
if n_jobs == -1:
n_jobs = cpu_count()
if models is None:
models = sorted(set(df['model_a'].unique()) | set(df['model_b'].unique()))
print(f"Calculating win rate matrix for {len(models)} models using {n_jobs} cores...")
# Generate all model pairs
model_pairs = [(m1, m2) for i, m1 in enumerate(models) for m2 in models[i+1:]]
# Split pairs into chunks for parallel processing
chunk_size = max(1, len(model_pairs) // (n_jobs * 4))
chunks = [model_pairs[i:i+chunk_size] for i in range(0, len(model_pairs), chunk_size)]
# Prepare arguments
args_list = [(chunk, df) for chunk in chunks]
# Process in parallel
with Pool(processes=n_jobs) as pool:
results_chunks = list(tqdm(
pool.imap(calculate_pairwise_win_rates_chunk, args_list),
total=len(args_list),
desc="Calculating win rates"
))
# Flatten results
all_results = [item for chunk in results_chunks for item in chunk]
# Build matrix. Pairs with no data stay NaN (the serial version's
# convention) — 0.5 would misreport "no data" as an even record;
# the diagonal is 0.5 by definition.
win_rates = {model: {opponent: (0.5 if opponent == model else np.nan)
for opponent in models} for model in models}
for result in all_results:
model_a = result['model_a']
model_b = result['model_b']
win_rate = result['win_rate']
win_rates[model_a][model_b] = win_rate
win_rates[model_b][model_a] = 1.0 - win_rate
# Convert to DataFrame
win_rate_df = pd.DataFrame(win_rates).T
win_rate_df = win_rate_df[models]
return win_rate_df
def filter_data_parallel(df: pd.DataFrame,
filters: dict,
n_jobs: int = -1) -> pd.DataFrame:
"""
Filter large DataFrame using parallel processing.
Args:
df: Input DataFrame
filters: Dictionary of filter conditions
n_jobs: Number of parallel jobs
Returns:
Filtered DataFrame
"""
if n_jobs == -1:
n_jobs = min(cpu_count(), 4) # Cap at 4 for filtering
if len(df) == 0:
return df.copy()
n_jobs = max(1, min(n_jobs, len(df)))
# Split DataFrame into chunks
chunk_size = max(1, len(df) // n_jobs)
chunks = [df.iloc[i:i+chunk_size] for i in range(0, len(df), chunk_size)]
def apply_filters(chunk):
filtered = chunk.copy()
# Apply each filter
if 'anony_only' in filters and filters['anony_only'] and 'anony' in filtered.columns:
filtered = filtered[filtered['anony'] == True]
if 'language' in filters and filters['language'] and 'language' in filtered.columns:
filtered = filtered[filtered['language'] == filters['language']]
if 'min_turn' in filters and 'turn' in filtered.columns:
filtered = filtered[filtered['turn'] >= filters['min_turn']]
if 'min_date' in filters and 'tstamp' in filtered.columns:
min_timestamp = pd.to_datetime(filters['min_date']).timestamp()
filtered = filtered[filtered['tstamp'] >= min_timestamp]
if 'max_date' in filters and 'tstamp' in filtered.columns:
max_timestamp = pd.to_datetime(filters['max_date']).timestamp()
filtered = filtered[filtered['tstamp'] <= max_timestamp]
return filtered
# Process chunks in parallel
with Pool(processes=n_jobs) as pool:
filtered_chunks = pool.map(apply_filters, chunks)
# Combine results
result = pd.concat(filtered_chunks, ignore_index=True)
return result
def optimize_dataframe(df: pd.DataFrame) -> pd.DataFrame:
"""
Optimize DataFrame memory usage by downcasting numeric types.
Args:
df: Input DataFrame
Returns:
Optimized DataFrame
"""
print("Optimizing DataFrame memory usage...")
initial_memory = df.memory_usage(deep=True).sum() / 1024**2
# Optimize numeric columns
for col in df.columns:
col_type = df[col].dtype
if col_type == 'int64':
df[col] = pd.to_numeric(df[col], downcast='integer')
elif col_type == 'float64':
df[col] = pd.to_numeric(df[col], downcast='float')
# Convert string columns to category if they have few unique values
for col in df.select_dtypes(include=['object']).columns:
try:
# Check if column contains hashable types (not dict, list, etc.)
# Try to get unique values - will fail if unhashable
num_unique = df[col].nunique()
num_total = len(df[col])
if num_total == 0:
continue
# If less than 50% unique values, convert to category
if num_unique / num_total < 0.5:
df[col] = df[col].astype('category')
except (TypeError, AttributeError):
# Column contains unhashable types (dicts, lists), skip optimization
print(f" Skipping column '{col}' (contains complex data types)")
continue
final_memory = df.memory_usage(deep=True).sum() / 1024**2
reduction = 0.0 if initial_memory == 0 else (1 - final_memory / initial_memory) * 100
print(f"Memory usage reduced from {initial_memory:.2f} MB to {final_memory:.2f} MB ({reduction:.1f}% reduction)")
return df
+80
View File
@@ -0,0 +1,80 @@
"""
Quick start demo - minimal example to get started quickly
"""
from elo_rating import EloRatingSystem
def demo_basic_elo():
"""Demonstrate basic Elo rating calculation with synthetic data."""
print("="*60)
print("Quick Start: Elo Rating System Demo")
print("="*60)
print()
# Initialize Elo system
elo = EloRatingSystem(initial_rating=1000.0, k_factor=32.0)
# Simulate some matches
matches = [
("GPT-4", "Claude-v1", "GPT-4"),
("GPT-4", "Llama-2", "GPT-4"),
("Claude-v1", "Llama-2", "Claude-v1"),
("GPT-4", "Claude-v1", "tie"),
("Llama-2", "Gemini", "Gemini"),
("GPT-4", "Gemini", "GPT-4"),
("Claude-v1", "Gemini", "Claude-v1"),
("GPT-4", "Llama-2", "GPT-4"),
("Claude-v1", "Llama-2", "Claude-v1"),
("Gemini", "Llama-2", "Gemini"),
]
print("Processing matches:")
print("-" * 60)
for i, (model_a, model_b, winner) in enumerate(matches, 1):
old_rating_a = elo.get_rating(model_a)
old_rating_b = elo.get_rating(model_b)
# update_ratings expects 'model_a' / 'model_b' / 'tie', not the
# winning model's name (anything unrecognized is scored as a tie).
outcome = ("model_a" if winner == model_a
else "model_b" if winner == model_b else "tie")
new_rating_a, new_rating_b = elo.update_ratings(model_a, model_b, outcome)
print(f"Match {i}: {model_a} vs {model_b} -> {winner} wins")
print(f" {model_a}: {old_rating_a:.1f}{new_rating_a:.1f} ({new_rating_a-old_rating_a:+.1f})")
print(f" {model_b}: {old_rating_b:.1f}{new_rating_b:.1f} ({new_rating_b-old_rating_b:+.1f})")
print()
# Show final leaderboard
print("=" * 60)
print("Final Leaderboard:")
print("=" * 60)
leaderboard = elo.get_leaderboard()
for rank, (model, rating, matches, wins) in enumerate(leaderboard, 1):
win_rate = (wins / matches * 100) if matches > 0 else 0
print(f"{rank}. {model:15s} - Rating: {rating:7.1f} | "
f"Matches: {matches:2d} | Wins: {wins:4.1f} | Win Rate: {win_rate:5.1f}%")
print()
# Show win probability predictions
print("=" * 60)
print("Win Probability Predictions:")
print("=" * 60)
models = [m[0] for m in leaderboard]
for i, model_a in enumerate(models):
for model_b in models[i+1:]:
prob = elo.calculate_win_probability(model_a, model_b)
print(f"{model_a} vs {model_b}: {prob*100:.1f}% - {(1-prob)*100:.1f}%")
print()
print("=" * 60)
print("Demo complete! Check main.py for full analysis with real data.")
print("=" * 60)
if __name__ == "__main__":
demo_basic_elo()
@@ -0,0 +1,9 @@
pandas>=2.0.0
numpy>=1.24.0
matplotlib>=3.7.0
seaborn>=0.12.0
requests>=2.31.0
plotly>=5.14.0
tqdm>=4.65.0
scikit-learn>=1.3.0
python-dotenv>=1.0.0
@@ -0,0 +1,10 @@
"""Helpers for direct execution of tests moved under tests/."""
from pathlib import Path
import sys
def bootstrap_experiment_root() -> None:
experiment_root = Path(__file__).resolve().parents[1]
if str(experiment_root) not in sys.path:
sys.path.insert(0, str(experiment_root))
@@ -0,0 +1,9 @@
"""Test import bootstrap for the elo-leaderboard experiment."""
from pathlib import Path
import sys
EXPERIMENT_ROOT = Path(__file__).resolve().parents[1]
if str(EXPERIMENT_ROOT) not in sys.path:
sys.path.insert(0, str(EXPERIMENT_ROOT))
@@ -0,0 +1,39 @@
"""Empty rating history must not crash analyze_rating_changes / get_rating_history."""
import pandas as pd
from animation import prepare_animation_data
from leaderboard import (
analyze_rating_changes,
build_historical_leaderboards,
get_rating_history,
)
def test_get_rating_history_empty_keeps_columns():
hist = build_historical_leaderboards(
pd.DataFrame(columns=["model_a", "model_b", "winner"]),
[(pd.Timestamp("2020-01-01"), pd.DataFrame(columns=["model_a", "model_b", "winner"]))],
)
rh = get_rating_history(hist)
assert list(rh.columns) == ["date", "model", "rating", "rank", "matches", "wins"]
assert len(rh) == 0
def test_analyze_empty_history_returns_empty_frame():
empty = pd.DataFrame(columns=["date", "model", "rating", "rank", "matches", "wins"])
stats = analyze_rating_changes(empty)
assert len(stats) == 0
assert "model" in stats.columns
def test_analyze_after_empty_historical_leaderboards():
hist = build_historical_leaderboards(
pd.DataFrame(columns=["model_a", "model_b", "winner"]),
[(pd.Timestamp("2020-01-01"), pd.DataFrame(columns=["model_a", "model_b", "winner"]))],
)
rh = get_rating_history(hist)
stats = analyze_rating_changes(rh)
assert len(stats) == 0
anim = prepare_animation_data(rh)
assert anim["frames"] == []
assert anim["total_frames"] == 0
@@ -0,0 +1,11 @@
"""Regression: prepare_animation_data must tolerate empty history."""
import pandas as pd
from animation import prepare_animation_data
def test_empty_history_returns_empty_frames():
df = pd.DataFrame(columns=["date", "model", "rating", "rank", "matches", "wins"])
data = prepare_animation_data(df)
assert data["frames"] == []
assert data["total_frames"] == 0
assert data["start_date"] is None
@@ -0,0 +1,35 @@
"""prepare_animation_data must keep fractional wins from Elo ties."""
import pandas as pd
from animation import prepare_animation_data
def test_tie_half_wins_are_not_truncated():
history = pd.DataFrame(
{
"date": pd.to_datetime(["2024-01-07", "2024-01-07"]),
"model": ["A", "B"],
"rating": [1000.0, 1000.0],
"rank": [1, 2],
"matches": [1, 1],
"wins": [0.5, 0.5],
}
)
data = prepare_animation_data(history, top_n=2)
wins = {m["name"]: m["wins"] for m in data["frames"][0]["models"]}
assert wins["A"] == 0.5
assert wins["B"] == 0.5
def test_whole_wins_still_serialize():
history = pd.DataFrame(
{
"date": pd.to_datetime(["2024-01-07"]),
"model": ["A"],
"rating": [1010.0],
"rank": [1],
"matches": [2],
"wins": [2.0],
}
)
data = prepare_animation_data(history, top_n=1)
assert data["frames"][0]["models"][0]["wins"] == 2.0
@@ -0,0 +1,23 @@
"""
Test suite locking out ZeroDivisionError in benchmark summary print logic
when time_basic is 0.0 or df_sample is empty.
"""
def test_benchmark_pct_reduction_zero_division():
"""
Ensure zero time_basic does not raise ZeroDivisionError during benchmark calculation.
"""
time_basic = 0.0
time_optimized = 0.0
pct_reduction = (1 - time_optimized / time_basic) * 100 if time_basic > 0 else 0.0
assert pct_reduction == 0.0
def test_benchmark_extrapolation_zero_sample():
"""
Ensure empty df_sample does not raise ZeroDivisionError during extrapolation check.
"""
df_sample = []
df_filtered = [1, 2, 3]
should_extrapolate = len(df_sample) > 0 and len(df_sample) < len(df_filtered)
assert not should_extrapolate
@@ -0,0 +1,16 @@
import pandas as pd
from bradley_terry import compute_mle_elo, get_bootstrap_result
def test_bootstrap_is_reproducible():
battles = pd.DataFrame(
[
{"model_a": "a", "model_b": "b", "winner": "model_a"},
{"model_a": "a", "model_b": "b", "winner": "model_b"},
{"model_a": "a", "model_b": "b", "winner": "tie"},
{"model_a": "b", "model_b": "a", "winner": "model_a"},
]
)
first = get_bootstrap_result(battles, compute_mle_elo, num_round=3)
second = get_bootstrap_result(battles, compute_mle_elo, num_round=3)
pd.testing.assert_frame_equal(first, second)
@@ -0,0 +1,11 @@
"""Regression: compute_mle_elo must work on small Arena-shaped battle sets."""
import pandas as pd
from battle_simulator import simulate_battles
from bradley_terry import compute_mle_elo
def test_small_two_model_sample():
df = pd.DataFrame(simulate_battles({"gpt-4": 1200.0, "llama-3": 1000.0}, 10, seed=1))
ratings = compute_mle_elo(df)
assert len(ratings) == 2
assert set(ratings.index) == {"gpt-4", "llama-3"}
@@ -0,0 +1,41 @@
"""Ties must contribute to Bradley-Terry weights (not be zeroed by pivot+T)."""
import pandas as pd
from bradley_terry import compute_mle_elo
def test_all_ties_rates_models_instead_of_sample_weight_error():
df = pd.DataFrame(
[
{"model_a": "A", "model_b": "B", "winner": "tie"},
{"model_a": "A", "model_b": "C", "winner": "tie (bothbad)"},
{"model_a": "B", "model_b": "C", "winner": "tie"},
]
)
ratings = compute_mle_elo(df)
assert set(ratings.index) == {"A", "B", "C"}
# Pure ties -> equal latent skills under BT.
assert abs(float(ratings["A"]) - float(ratings["B"])) < 1e-6
assert abs(float(ratings["A"]) - float(ratings["C"])) < 1e-6
def test_ties_change_ratings_versus_wins_only():
wins_only = pd.DataFrame(
[
{"model_a": "A", "model_b": "B", "winner": "model_a"},
{"model_a": "B", "model_b": "C", "winner": "model_a"},
]
)
with_ties = pd.concat(
[
wins_only,
pd.DataFrame(
[{"model_a": "A", "model_b": "C", "winner": "tie"}] * 8
),
],
ignore_index=True,
)
r1 = compute_mle_elo(wins_only)
r2 = compute_mle_elo(with_ties)
# Extra AC ties pull A and C together relative to the wins-only fit.
assert abs(float(r2["A"]) - float(r2["C"])) < abs(float(r1["A"]) - float(r1["C"]))
@@ -0,0 +1,32 @@
"""Regression test for prepare_animation_data with string or date objects in history_df."""
import pandas as pd
from animation import prepare_animation_data
def test_prepare_animation_data_string_date():
"""prepare_animation_data must handle string dates without raising AttributeError."""
history = pd.DataFrame([
{
"date": "2024-08-01",
"model": "model_a",
"rating": 1050.0,
"rank": 1,
"matches": 10,
"wins": 7.0,
},
{
"date": "2024-08-01",
"model": "model_b",
"rating": 950.0,
"rank": 2,
"matches": 10,
"wins": 3.0,
},
])
data = prepare_animation_data(history, top_n=2)
assert data["total_frames"] == 1
assert data["start_date"] == "2024-08-01"
assert data["end_date"] == "2024-08-01"
assert len(data["frames"]) == 1
assert data["frames"][0]["date"] == "2024-08-01"
assert data["frames"][0]["timestamp"] == 1722470400
@@ -0,0 +1,17 @@
"""Regression test for compare_win_rates when comparisons list is empty."""
import numpy as np
import pandas as pd
from elo_rating import EloRatingSystem
from leaderboard import compare_win_rates
def test_compare_win_rates_empty_has_required_columns():
"""compare_win_rates must return a DataFrame with required columns when no valid comparisons exist."""
elo = EloRatingSystem()
empirical_df = pd.DataFrame(np.nan, index=["model_a", "model_b"], columns=["model_a", "model_b"])
df_comp = compare_win_rates(elo, empirical_df)
assert list(df_comp.columns) == ["model_a", "model_b", "empirical", "predicted", "error"]
assert len(df_comp) == 0
# Accessing columns on empty result must not raise KeyError
assert "error" in df_comp
assert df_comp["error"].empty
+170
View File
@@ -0,0 +1,170 @@
"""
Unit tests for Elo rating system
"""
import math
import pytest
from _bootstrap import bootstrap_experiment_root
bootstrap_experiment_root()
from elo_rating import EloRatingSystem
def test_initial_rating():
"""Test that models start with initial rating."""
elo = EloRatingSystem(initial_rating=1000.0)
assert elo.get_rating("model_a") == 1000.0
assert elo.get_rating("model_b") == 1000.0
def test_expected_score():
"""Test expected score calculation."""
elo = EloRatingSystem()
# Equal ratings should give 50% probability
assert elo.expected_score(1000, 1000) == 0.5
# Higher rated player should have > 50% probability
assert elo.expected_score(1200, 1000) > 0.5
assert elo.expected_score(1000, 1200) < 0.5
# 400 point difference should give ~91% probability
prob = elo.expected_score(1400, 1000)
assert 0.90 < prob < 0.92
def test_rating_update_win():
"""Test rating update when model_a wins."""
elo = EloRatingSystem(initial_rating=1000.0, k_factor=32.0)
new_a, new_b = elo.update_ratings("model_a", "model_b", "model_a")
# Winner should gain rating, loser should lose rating
assert new_a > 1000.0
assert new_b < 1000.0
# Total rating should be conserved (zero-sum)
assert abs((new_a + new_b) - 2000.0) < 0.01
def test_rating_update_tie():
"""Test rating update for a tie."""
elo = EloRatingSystem(initial_rating=1000.0, k_factor=32.0)
new_a, new_b = elo.update_ratings("model_a", "model_b", "tie")
# With equal ratings, tie should not change ratings much
assert abs(new_a - 1000.0) < 0.01
assert abs(new_b - 1000.0) < 0.01
def test_upset_gives_larger_change():
"""Test that unexpected results cause larger rating changes."""
elo = EloRatingSystem(initial_rating=1000.0, k_factor=32.0)
# Give model_a higher rating
elo.ratings["model_a"] = 1200.0
elo.ratings["model_b"] = 1000.0
# If weaker model wins (upset), changes should be larger
new_a_upset, new_b_upset = elo.update_ratings("model_a", "model_b", "model_b")
# Reset
elo.ratings["model_a"] = 1200.0
elo.ratings["model_b"] = 1000.0
# If stronger model wins (expected), changes should be smaller
new_a_expected, new_b_expected = elo.update_ratings("model_a", "model_b", "model_a")
# Upset should cause larger change
change_upset = abs(new_a_upset - 1200.0)
change_expected = abs(new_a_expected - 1200.0)
assert change_upset > change_expected
def test_leaderboard_sorting():
"""Test that leaderboard is sorted by rating."""
elo = EloRatingSystem(initial_rating=1000.0, k_factor=32.0)
# Create some matches to differentiate ratings
elo.update_ratings("model_a", "model_b", "model_a")
elo.update_ratings("model_a", "model_c", "model_a")
elo.update_ratings("model_b", "model_c", "model_b")
leaderboard = elo.get_leaderboard()
# Check descending order
for i in range(len(leaderboard) - 1):
assert leaderboard[i][1] >= leaderboard[i+1][1]
# model_a should be first (won all matches)
assert leaderboard[0][0] == "model_a"
def test_win_probability_symmetry():
"""Test that win probabilities sum to 1."""
elo = EloRatingSystem()
elo.ratings["model_a"] = 1200.0
elo.ratings["model_b"] = 1000.0
prob_a = elo.calculate_win_probability("model_a", "model_b")
prob_b = elo.calculate_win_probability("model_b", "model_a")
# Should sum to 1
assert abs(prob_a + prob_b - 1.0) < 0.001
def test_match_counting():
"""Test that match and win counts are tracked correctly."""
elo = EloRatingSystem(initial_rating=1000.0, k_factor=32.0)
elo.update_ratings("model_a", "model_b", "model_a") # model_a wins
elo.update_ratings("model_a", "model_c", "model_b") # model_a loses (2nd slot wins)
elo.update_ratings("model_a", "model_b", "tie") # tie -> 0.5 each
# model_a played 3 matches
assert elo.match_counts["model_a"] == 3
# model_a won 1 match and tied 1 (1.5 total)
assert elo.win_counts["model_a"] == 1.5
# model_b played 2 matches
assert elo.match_counts["model_b"] == 2
def test_copy():
"""Test that copy creates independent instance."""
elo1 = EloRatingSystem(initial_rating=1000.0, k_factor=32.0)
elo1.update_ratings("model_a", "model_b", "model_a")
elo2 = elo1.copy()
# Modify elo2
elo2.update_ratings("model_a", "model_b", "model_b")
# elo1 should be unchanged
assert elo1.ratings["model_a"] != elo2.ratings["model_a"]
def test_reset():
"""Test that reset clears all data."""
elo = EloRatingSystem(initial_rating=1000.0, k_factor=32.0)
elo.update_ratings("model_a", "model_b", "model_a")
elo.update_ratings("model_a", "model_c", "model_a")
assert len(elo.ratings) > 0
elo.reset()
assert len(elo.ratings) == 0
assert len(elo.match_counts) == 0
assert len(elo.win_counts) == 0
if __name__ == "__main__":
# Run tests
pytest.main([__file__, "-v"])
@@ -0,0 +1,21 @@
"""Regression: filter_data_parallel must tolerate n_jobs > len(df)."""
from unittest.mock import MagicMock, patch
import pandas as pd
from parallel_processing import filter_data_parallel
def test_n_jobs_larger_than_rows():
df = pd.DataFrame({"anony": [True, False, True], "turn": [1, 2, 1]})
def map_inline(fn, chunks):
return [fn(c) for c in chunks]
pool = MagicMock()
pool.__enter__.return_value.map.side_effect = map_inline
pool.__exit__.return_value = False
with patch("parallel_processing.Pool", return_value=pool):
out = filter_data_parallel(df, {"anony_only": True}, n_jobs=8)
assert len(out) == 2
@@ -0,0 +1,37 @@
"""
Regression test for filter_data on empty input (实验 7-7 排行榜).
An empty arena data file (e.g. a failed/truncated download saved as `[]`) used to
crash with ZeroDivisionError at the "After filtering" percentage print.
"""
import pandas as pd
import pytest
from _bootstrap import bootstrap_experiment_root
bootstrap_experiment_root()
from data_loader import filter_data
def test_filter_data_tolerates_empty_dataframe():
"""Empty input no longer raises ZeroDivisionError; returns an empty DataFrame."""
empty = pd.DataFrame({"model_a": [], "model_b": [], "winner": []})
result = filter_data(empty)
assert len(result) == 0
def test_filter_data_normal_case_unchanged():
"""Non-empty input still filters and reports normally."""
df = pd.DataFrame({
"model_a": ["a", "b", "a"],
"model_b": ["b", "a", "c"],
"winner": ["model_a", "model_b", "tie"],
"anony": [True, True, False],
})
result = filter_data(df, anony_only=True, use_dedup=False)
assert len(result) == 2 # 非匿名的一条被过滤
if __name__ == "__main__":
pytest.main([__file__, "-v"])
@@ -0,0 +1,34 @@
"""Empty battles JSON array [] must load as an empty battle frame."""
import json
from pathlib import Path
import cli
def test_load_battles_empty_json_array(tmp_path):
path = tmp_path / "battles.json"
path.write_text("[]", encoding="utf-8")
df = cli._load_battles(str(path))
assert list(df.columns) == ["model_a", "model_b", "winner"]
assert len(df) == 0
def test_load_battles_nonempty_still_requires_columns(tmp_path):
path = tmp_path / "bad.json"
path.write_text(json.dumps([{"x": 1}]), encoding="utf-8")
try:
cli._load_battles(str(path))
assert False, "expected ValueError"
except ValueError as e:
assert "model_a" in str(e)
def test_load_battles_normal(tmp_path):
path = tmp_path / "ok.json"
path.write_text(
json.dumps([{"model_a": "A", "model_b": "B", "winner": "model_a"}]),
encoding="utf-8",
)
df = cli._load_battles(str(path))
assert len(df) == 1
assert df.iloc[0]["winner"] == "model_a"
@@ -0,0 +1,13 @@
"""Regression: optimize_dataframe must tolerate empty object columns."""
import pandas as pd
from parallel_processing import optimize_dataframe
def test_optimize_empty_object_columns():
df = pd.DataFrame({
"model_a": pd.Series([], dtype=object),
"model_b": pd.Series([], dtype=object),
"winner": pd.Series([], dtype=object),
})
out = optimize_dataframe(df)
assert len(out) == 0
@@ -0,0 +1,49 @@
"""
Regression test for 'tie (bothbad)' handling in optimized_elo (实验 7-7 排行榜).
Chatbot Arena battle data has four outcomes; 'tie (bothbad)' was missing from
the outcome map, so Series.map produced NaN. NaN then propagated through the
rating updates and spread to every model that later faced an affected one,
leaving the whole leaderboard NaN.
"""
import numpy as np
import pandas as pd
from optimized_elo import NumpyEloRatingSystem
def test_tie_bothbad_does_not_produce_nan_outcomes():
"""'tie (bothbad)' maps to a tie instead of NaN."""
df = pd.DataFrame({
"model_a": ["a", "a"],
"model_b": ["b", "b"],
"winner": ["model_a", "tie (bothbad)"],
})
_, _, outcomes = NumpyEloRatingSystem()._prepare_data(df)
assert not np.isnan(outcomes).any()
assert outcomes[1] == 0.5
def test_tie_bothbad_does_not_poison_the_leaderboard():
"""One 'tie (bothbad)' battle used to NaN every rating, including model c."""
df = pd.DataFrame({
"model_a": ["a", "a", "b"],
"model_b": ["b", "b", "c"],
"winner": ["model_a", "tie (bothbad)", "model_a"],
})
system = NumpyEloRatingSystem()
system.process_matches_vectorized(df, show_progress=False)
ratings = [rating for _, rating, _, _ in system.get_leaderboard()]
assert len(ratings) == 3
assert not any(np.isnan(r) for r in ratings)
def test_unknown_outcome_falls_back_to_tie():
"""An unrecognized label degrades to a tie rather than NaN."""
df = pd.DataFrame({
"model_a": ["a"],
"model_b": ["b"],
"winner": ["something_new"],
})
_, _, outcomes = NumpyEloRatingSystem()._prepare_data(df)
assert outcomes[0] == 0.5
@@ -0,0 +1,9 @@
"""Regression: documented interval='M' must work on modern pandas."""
import pandas as pd
from data_loader import get_time_slices
def test_monthly_interval_alias():
df = pd.DataFrame({"tstamp": [1_700_000_000, 1_710_000_000]})
slices = get_time_slices(df, interval="M")
assert len(slices) >= 1
@@ -0,0 +1,55 @@
"""
Regression: get_time_slices must not IndexError when the tstamp span is
shorter than the requested interval (default weekly).
Chatbot Arena samples, same-second dumps, and single-row demos all produce an
empty pd.date_range for freq='W'; the old code then crashed on date_ranges[-1].
"""
import pandas as pd
from _bootstrap import bootstrap_experiment_root
bootstrap_experiment_root()
from data_loader import get_time_slices
def test_identical_timestamps_return_one_slice():
"""Two battles at the same unix second (weekly interval) -> one slice."""
ts = 1_700_000_000
df = pd.DataFrame({
"tstamp": [ts, ts],
"model_a": ["a", "c"],
"model_b": ["b", "d"],
"winner": ["model_a", "model_b"],
})
slices = get_time_slices(df, interval="W")
assert len(slices) == 1
end_date, slice_df = slices[0]
assert len(slice_df) == 2
assert end_date == pd.to_datetime(ts, unit="s")
def test_empty_dataframe_returns_empty_list():
"""Empty input returns [] instead of NaT ValueError."""
df = pd.DataFrame({"tstamp": pd.Series(dtype="float64")})
assert get_time_slices(df, interval="W") == []
def test_multi_week_span_still_produces_buckets():
"""A span covering multiple weeks still yields intermediate buckets."""
# ~3 weeks apart
df = pd.DataFrame({
"tstamp": [1_700_000_000, 1_700_000_000 + 21 * 86400],
"model_a": ["a", "c"],
"model_b": ["b", "d"],
"winner": ["model_a", "model_b"],
})
slices = get_time_slices(df, interval="W")
assert len(slices) >= 2
assert all(len(s[1]) > 0 for s in slices)
if __name__ == "__main__":
import pytest
pytest.main([__file__, "-v"])
@@ -0,0 +1,20 @@
import json
from pathlib import Path
def test_canonical_manifest_is_hash_complete():
run_dir = Path(__file__).resolve().parents[1] / "validation" / "runs" / "exp7-7-arena-20260731-v1"
manifest_path = run_dir / "manifest.json"
assert manifest_path.exists()
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
assert manifest["experiment"] == "7-7"
assert manifest["official_complete"] is True
assert all(manifest["gates"].values())
assert set(manifest["artifacts"]) >= {
"summary.json",
"online_elo.json",
"bradley_terry.json",
"win_rate_matrix.json",
"rating_history.json",
"leaderboard_animation.html",
}
@@ -0,0 +1,32 @@
"""Empty battle DataFrame must not crash Bradley-Terry LogisticRegression."""
import pandas as pd
from bradley_terry import compute_bradley_terry_leaderboard, compute_mle_elo
def test_compute_mle_elo_empty_battles():
df = pd.DataFrame(columns=["model_a", "model_b", "winner"])
ratings = compute_mle_elo(df)
assert isinstance(ratings, pd.Series)
assert len(ratings) == 0
def test_compute_bradley_terry_leaderboard_empty():
df = pd.DataFrame(columns=["model_a", "model_b", "winner"])
board = compute_bradley_terry_leaderboard(df)
assert isinstance(board, pd.DataFrame)
assert len(board) == 0
def test_nonempty_still_rates():
df = pd.DataFrame(
[
{"model_a": "A", "model_b": "B", "winner": "model_a"},
{"model_a": "A", "model_b": "B", "winner": "model_a"},
{"model_a": "B", "model_b": "C", "winner": "model_b"},
{"model_a": "A", "model_b": "C", "winner": "model_a"},
]
)
ratings = compute_mle_elo(df)
assert set(ratings.index) >= {"A", "B", "C"}
assert ratings["A"] > ratings["C"]
@@ -0,0 +1,7 @@
{
"experiment": "7-7",
"manifest_sha256": "b67f8b15a088e694ae356322e0ccd676b54ed79a0dc1c787ca1960ef670ba2e8",
"official_complete": true,
"run": "runs/exp7-7-arena-20260731-v1",
"status": "passed"
}
@@ -0,0 +1,389 @@
"""Run the complete, evidence-producing Experiment 7-7 campaign.
The public Arena file is deliberately not copied into git. A canonical run
binds the exact input by URL, size, record count, and SHA-256, then retains all
derived tables, visualizations, the D3 history animation, and a manifest that
hashes every output and the source used to create it.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import platform
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
os.environ.setdefault("MPLBACKEND", "Agg")
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from scipy.stats import kendalltau, spearmanr
HERE = Path(__file__).resolve().parent
PROJECT = HERE.parent
sys.path.insert(0, str(PROJECT))
from animation import create_simple_animation
from bradley_terry import compute_bradley_terry_leaderboard
from optimized_elo import (
NumpyEloRatingSystem,
process_elo_updates_vectorized,
)
DATASET_URL = (
"https://storage.googleapis.com/arena_external_data/public/"
"clean_battle_20240814_public.json"
)
REQUIRED_COLUMNS = ["model_a", "model_b", "winner", "tstamp", "anony", "turn"]
ALLOWED_OUTCOMES = {"model_a", "model_b", "tie", "tie (bothbad)"}
def sha256_file(path: Path, chunk_size: int = 8 * 1024 * 1024) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
while chunk := handle.read(chunk_size):
digest.update(chunk)
return digest.hexdigest()
def write_json(path: Path, value: Any) -> None:
path.write_text(
json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
def json_records(frame: pd.DataFrame) -> list[dict[str, Any]]:
return json.loads(frame.to_json(orient="records", date_format="iso"))
def load_and_filter(path: Path, max_records: int) -> tuple[pd.DataFrame, dict[str, Any]]:
started = time.perf_counter()
raw = pd.read_json(path)
missing = sorted(set(REQUIRED_COLUMNS) - set(raw.columns))
if missing:
raise ValueError(f"Arena input is missing columns: {missing}")
source_records = len(raw)
frame = raw[REQUIRED_COLUMNS + (["dedup_tag"] if "dedup_tag" in raw else [])].copy()
del raw
frame = frame[frame["anony"].eq(True) & frame["turn"].ge(1)]
if "dedup_tag" in frame:
sampled = frame["dedup_tag"].map(
lambda value: bool(value.get("sampled", False)) if isinstance(value, dict) else False
)
frame = frame[sampled]
frame = frame[frame["winner"].isin(ALLOWED_OUTCOMES)]
frame = frame.sort_values("tstamp", kind="stable").reset_index(drop=True)
if max_records:
frame = frame.head(max_records).copy()
if frame.empty:
raise ValueError("Arena filtering produced no accepted blind votes")
metadata = {
"source_records": source_records,
"accepted_records": len(frame),
"model_count": len(set(frame["model_a"]) | set(frame["model_b"])),
"start_utc": datetime.fromtimestamp(float(frame["tstamp"].min()), timezone.utc).isoformat(),
"end_utc": datetime.fromtimestamp(float(frame["tstamp"].max()), timezone.utc).isoformat(),
"outcomes": {str(k): int(v) for k, v in frame["winner"].value_counts().items()},
"load_filter_seconds": round(time.perf_counter() - started, 3),
"bounded_test_run": bool(max_records),
}
return frame, metadata
def online_elo_and_history(
frame: pd.DataFrame,
) -> tuple[pd.DataFrame, pd.DataFrame, NumpyEloRatingSystem, float]:
started = time.perf_counter()
system = NumpyEloRatingSystem(initial_rating=1000.0, k_factor=4.0)
model_a, model_b, outcomes = system._prepare_data(frame)
months = (
pd.to_datetime(frame["tstamp"], unit="s", utc=True)
.dt.tz_localize(None)
.dt.to_period("M")
)
boundaries = np.flatnonzero(months.to_numpy()[1:] != months.to_numpy()[:-1]) + 1
boundaries = np.append(boundaries, len(frame))
start = 0
history_rows: list[dict[str, Any]] = []
for stop in boundaries:
process_elo_updates_vectorized(
system.ratings,
model_a[start:stop],
model_b[start:stop],
outcomes[start:stop],
system.k_factor,
system.match_counts,
system.win_counts,
)
snapshot = system.get_leaderboard()
date = pd.to_datetime(float(frame.iloc[stop - 1]["tstamp"]), unit="s", utc=True)
for rank, (model, rating, matches, wins) in enumerate(snapshot, 1):
history_rows.append(
{
"date": date.tz_localize(None),
"model": model,
"rating": rating,
"rank": rank,
"matches": matches,
"wins": wins,
}
)
start = int(stop)
leaderboard = pd.DataFrame(
system.get_leaderboard(), columns=["model", "rating", "matches", "wins"]
)
leaderboard.insert(0, "rank", range(1, len(leaderboard) + 1))
history = pd.DataFrame(history_rows)
return leaderboard, history, system, round(time.perf_counter() - started, 3)
def rank_comparison(online: pd.DataFrame, official_method: pd.DataFrame) -> dict[str, Any]:
online_rank = online.set_index("model")["rank"]
official = official_method.sort_values("rating", ascending=False).reset_index(drop=True)
official["rank"] = np.arange(1, len(official) + 1)
official_rank = official.set_index("model")["rank"]
common = sorted(set(online_rank.index) & set(official_rank.index))
rho = spearmanr(online_rank.loc[common], official_rank.loc[common]).statistic
tau = kendalltau(online_rank.loc[common], official_rank.loc[common]).statistic
online_top = online.nsmallest(20, "rank")["model"].tolist()
official_top = official.nsmallest(20, "rank")["model"].tolist()
return {
"comparison_target": "Bradley-Terry MLE reconstruction used by Chatbot Arena",
"claim_boundary": (
"This is a same-snapshot reconstruction of the official method, not a scrape of "
"the mutable live leaderboard. Scores need not match the live service."
),
"common_models": len(common),
"spearman_rank_correlation": round(float(rho), 6),
"kendall_rank_correlation": round(float(tau), 6),
"top_20_overlap": len(set(online_top) & set(official_top)),
"online_top_20": online_top,
"official_method_top_20": official_top,
}
def empirical_matrix(frame: pd.DataFrame, models: list[str]) -> pd.DataFrame:
subset = frame[frame["model_a"].isin(models) & frame["model_b"].isin(models)].copy()
rows: list[tuple[str, str, float]] = []
for a, b, winner in subset[["model_a", "model_b", "winner"]].itertuples(index=False):
score = 1.0 if winner == "model_a" else 0.0 if winner == "model_b" else 0.5
rows.append((a, b, score))
rows.append((b, a, 1.0 - score))
scored = pd.DataFrame(rows, columns=["model", "opponent", "score"])
matrix = scored.pivot_table(index="model", columns="opponent", values="score", aggfunc="mean")
matrix = matrix.reindex(index=models, columns=models)
np.fill_diagonal(matrix.values, 0.5)
return matrix
def plot_artifacts(
out: Path,
online: pd.DataFrame,
history: pd.DataFrame,
empirical: pd.DataFrame,
) -> None:
top = online.head(20).sort_values("rating")
fig, ax = plt.subplots(figsize=(11, 8))
ax.barh(top["model"], top["rating"], color="#3b82f6")
ax.set_title("Experiment 7-7: Online Elo leaderboard")
ax.set_xlabel("Elo rating (K=4, chronological)")
fig.tight_layout()
fig.savefig(out / "leaderboard.png", dpi=180)
plt.close(fig)
fig, ax = plt.subplots(figsize=(13, 11))
sns.heatmap(empirical, cmap="RdYlGn", center=0.5, vmin=0, vmax=1, ax=ax)
ax.set_title("Empirical pairwise win rate — final online-Elo top 20")
fig.tight_layout()
fig.savefig(out / "win_rate_matrix.png", dpi=180)
plt.close(fig)
top_models = online.head(10)["model"].tolist()
fig, ax = plt.subplots(figsize=(13, 7))
for model in top_models:
values = history[history["model"].eq(model)].sort_values("date")
ax.plot(values["date"], values["rating"], label=model, linewidth=1.8)
ax.set_title("Monthly online-Elo evolution — final top 10")
ax.set_ylabel("Elo rating")
ax.legend(fontsize=7, ncol=2)
fig.autofmt_xdate()
fig.tight_layout()
fig.savefig(out / "rating_history.png", dpi=180)
plt.close(fig)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--input", type=Path, required=True, help="Downloaded public Arena JSON")
parser.add_argument("--output-dir", type=Path, required=True)
parser.add_argument("--bootstrap-rounds", type=int, default=20)
parser.add_argument("--max-records", type=int, default=0, help="Noncanonical bounded test only")
return parser.parse_args()
def main() -> None:
args = parse_args()
args.output_dir.mkdir(parents=True, exist_ok=True)
input_path = args.input.resolve()
if not input_path.is_file():
raise SystemExit(f"Arena input not found: {input_path}")
run_started = time.perf_counter()
input_hash = sha256_file(input_path)
frame, dataset = load_and_filter(input_path, args.max_records)
online, history, online_system, online_seconds = online_elo_and_history(frame)
bt_started = time.perf_counter()
official_method = compute_bradley_terry_leaderboard(
frame[["model_a", "model_b", "winner"]],
bootstrap_rounds=args.bootstrap_rounds,
)
bt_seconds = round(time.perf_counter() - bt_started, 3)
official_method = official_method.sort_values("rating", ascending=False).reset_index(drop=True)
official_method.insert(0, "rank", range(1, len(official_method) + 1))
comparison = rank_comparison(online, official_method)
top_models = online.head(20)["model"].tolist()
empirical = empirical_matrix(frame, top_models)
predicted = pd.DataFrame(
{
opponent: {
model: online_system.calculate_win_probability(model, opponent)
for model in top_models
}
for opponent in top_models
}
).reindex(index=top_models, columns=top_models)
write_json(args.output_dir / "online_elo.json", json_records(online))
write_json(args.output_dir / "bradley_terry.json", json_records(official_method))
write_json(
args.output_dir / "win_rate_matrix.json",
{
"models": top_models,
"empirical": empirical.where(pd.notna(empirical), None).to_dict(orient="index"),
"online_elo_predicted": predicted.to_dict(orient="index"),
},
)
write_json(args.output_dir / "rating_history.json", json_records(history))
plot_artifacts(args.output_dir, online, history, empirical)
create_simple_animation(history, str(args.output_dir / "leaderboard_animation.html"), top_n=15)
gates = {
"official_public_arena_snapshot_hashed": not args.max_records,
"millions_of_blind_votes_loaded": dataset["source_records"] >= 1_000_000,
"chronological_online_elo_k4_completed": len(online) == dataset["model_count"],
"bradley_terry_official_method_completed": len(official_method) == dataset["model_count"],
"online_vs_official_method_rank_agreement_observed": (
comparison["spearman_rank_correlation"] >= 0.70
and comparison["top_20_overlap"] >= 10
),
"pairwise_empirical_and_predicted_matrix_saved": len(empirical) == 20,
"monthly_history_saved": history["date"].nunique() >= 2,
"d3_animation_saved": (args.output_dir / "leaderboard_animation.html").is_file(),
"static_visualizations_saved": all(
(args.output_dir / name).is_file()
for name in ["leaderboard.png", "win_rate_matrix.png", "rating_history.png"]
),
}
accepted = all(gates.values())
summary = {
"schema_version": 1,
"experiment": "7-7",
"status": "passed" if accepted else "noncanonical_test",
"official_complete": accepted,
"generated_at_utc": datetime.now(timezone.utc).isoformat(),
"dataset": {
"url": DATASET_URL,
"path_recorded_as": input_path.name,
"bytes": input_path.stat().st_size,
"sha256": input_hash,
**dataset,
},
"protocol": {
"online_elo": "initial=1000, K=4, stable chronological order",
"official_method": "Bradley-Terry maximum-likelihood reconstruction",
"history_interval": "monthly cumulative snapshots",
"bootstrap_rounds": args.bootstrap_rounds,
"bootstrap_random_seed": 0,
},
"results": {
"online_top_20": json_records(online.head(20)),
"official_method_top_20": json_records(official_method.head(20)),
"rank_comparison": comparison,
},
"timing_seconds": {
"online_and_history": online_seconds,
"bradley_terry": bt_seconds,
"total": round(time.perf_counter() - run_started, 3),
},
"gates": gates,
}
write_json(args.output_dir / "summary.json", summary)
artifact_names = [
"online_elo.json",
"bradley_terry.json",
"win_rate_matrix.json",
"rating_history.json",
"leaderboard.png",
"win_rate_matrix.png",
"rating_history.png",
"leaderboard_animation.html",
"summary.json",
]
source_names = [
"animation.py",
"bradley_terry.py",
"optimized_elo.py",
"validation/run_experiment.py",
"validation/validate_evidence.py",
]
manifest = {
"schema_version": 1,
"experiment": "7-7",
"status": summary["status"],
"official_complete": accepted,
"input": {
"url": DATASET_URL,
"filename": input_path.name,
"bytes": input_path.stat().st_size,
"sha256": input_hash,
},
"artifacts": {
name: {"bytes": (args.output_dir / name).stat().st_size, "sha256": sha256_file(args.output_dir / name)}
for name in artifact_names
},
"sources": {
name: sha256_file(PROJECT / name)
for name in source_names
},
"runtime": {
"python": sys.version.split()[0],
"platform": platform.platform(),
"pandas": pd.__version__,
"numpy": np.__version__,
},
"gates": gates,
}
write_json(args.output_dir / "manifest.json", manifest)
print(json.dumps({"status": manifest["status"], "output": str(args.output_dir)}, indent=2))
if not accepted:
raise SystemExit(2)
if __name__ == "__main__":
main()
@@ -0,0 +1,905 @@
[
{
"lower_ci": 1199.4356201692,
"model": "chatgpt-4o-latest",
"rank": 1,
"rating": 1202.8722324768,
"upper_ci": 1207.3805726125
},
{
"lower_ci": 1182.767114327,
"model": "gemini-1.5-pro-exp-0801",
"rank": 2,
"rating": 1187.3727762743,
"upper_ci": 1191.7564879767
},
{
"lower_ci": 1173.1270556478,
"model": "gpt-4o-2024-05-13",
"rank": 3,
"rating": 1174.5475812185,
"upper_ci": 1177.0403252386
},
{
"lower_ci": 1161.1978031325,
"model": "gpt-4o-mini-2024-07-18",
"rank": 4,
"rating": 1162.9712796813,
"upper_ci": 1166.568334134
},
{
"lower_ci": 1156.0224735749,
"model": "claude-3-5-sonnet-20240620",
"rank": 5,
"rating": 1159.6692177384,
"upper_ci": 1161.8458744744
},
{
"lower_ci": 1153.6447817793,
"model": "gemini-advanced-0514",
"rank": 6,
"rating": 1155.6305094344,
"upper_ci": 1157.1030226933
},
{
"lower_ci": 1150.4614200682,
"model": "llama-3.1-405b-instruct",
"rank": 7,
"rating": 1153.3950109278,
"upper_ci": 1157.0848375454
},
{
"lower_ci": 1146.348878889,
"model": "gpt-4o-2024-08-06",
"rank": 8,
"rating": 1150.7428051987,
"upper_ci": 1156.3245456986
},
{
"lower_ci": 1146.7772301944,
"model": "gemini-1.5-pro-api-0514",
"rank": 9,
"rating": 1149.1265569601,
"upper_ci": 1151.5010539806
},
{
"lower_ci": 1143.6682527667,
"model": "gemini-1.5-pro-api-0409-preview",
"rank": 10,
"rating": 1146.1756037638,
"upper_ci": 1147.8895040945
},
{
"lower_ci": 1142.3990049162,
"model": "gpt-4-turbo-2024-04-09",
"rank": 11,
"rating": 1144.9187340632,
"upper_ci": 1146.8362809167
},
{
"lower_ci": 1138.1303324947,
"model": "gpt-4-1106-preview",
"rank": 12,
"rating": 1139.5004505155,
"upper_ci": 1140.97644635
},
{
"lower_ci": 1136.3852091186,
"model": "mistral-large-2407",
"rank": 13,
"rating": 1139.3566231011,
"upper_ci": 1143.8960727747
},
{
"lower_ci": 1135.0624903381,
"model": "athene-70b-0725",
"rank": 14,
"rating": 1138.2016223593,
"upper_ci": 1141.397916447
},
{
"lower_ci": 1135.0278139027,
"model": "claude-3-opus-20240229",
"rank": 15,
"rating": 1136.5739464324,
"upper_ci": 1137.5663039742
},
{
"lower_ci": 1131.7219814044,
"model": "llama-3.1-70b-instruct",
"rank": 16,
"rating": 1134.8942150397,
"upper_ci": 1139.3028524197
},
{
"lower_ci": 1132.1800230236,
"model": "gpt-4-0125-preview",
"rank": 17,
"rating": 1133.974390087,
"upper_ci": 1135.7552442263
},
{
"lower_ci": 1127.3300289872,
"model": "yi-large-preview",
"rank": 18,
"rating": 1128.1692229165,
"upper_ci": 1131.371743138
},
{
"lower_ci": 1111.850158929,
"model": "reka-core-20240722",
"rank": 19,
"rating": 1117.1464671073,
"upper_ci": 1121.9537941313
},
{
"lower_ci": 1114.0244965884,
"model": "gemini-1.5-flash-api-0514",
"rank": 20,
"rating": 1116.6161359294,
"upper_ci": 1118.5223962538
},
{
"lower_ci": 1103.6704090106,
"model": "deepseek-v2-api-0628",
"rank": 21,
"rating": 1107.2808888587,
"upper_ci": 1112.0417779712
},
{
"lower_ci": 1103.9432951297,
"model": "gemma-2-27b-it",
"rank": 22,
"rating": 1105.9132519337,
"upper_ci": 1108.0293102426
},
{
"lower_ci": 1099.3459906048,
"model": "deepseek-coder-v2-0724",
"rank": 23,
"rating": 1105.3954927337,
"upper_ci": 1113.0353361228
},
{
"lower_ci": 1099.9821845459,
"model": "yi-large",
"rank": 24,
"rating": 1102.4018397329,
"upper_ci": 1104.8525831946
},
{
"lower_ci": 1094.5198632757,
"model": "nemotron-4-340b-instruct",
"rank": 25,
"rating": 1098.0765768899,
"upper_ci": 1100.3082488796
},
{
"lower_ci": 1092.3225621341,
"model": "bard-jan-24-gemini-pro",
"rank": 26,
"rating": 1096.589845604,
"upper_ci": 1104.5333605578
},
{
"lower_ci": 1089.9500461084,
"model": "glm-4-0520",
"rank": 27,
"rating": 1095.5341569364,
"upper_ci": 1100.1005750187
},
{
"lower_ci": 1093.6731280724,
"model": "llama-3-70b-instruct",
"rank": 28,
"rating": 1094.9281802329,
"upper_ci": 1095.7283536945
},
{
"lower_ci": 1088.0897564085,
"model": "claude-3-sonnet-20240229",
"rank": 29,
"rating": 1090.1434308802,
"upper_ci": 1091.5131310821
},
{
"lower_ci": 1086.224736458,
"model": "reka-core-20240501",
"rank": 30,
"rating": 1088.4321613308,
"upper_ci": 1090.272779249
},
{
"lower_ci": 1082.4844014079,
"model": "reka-flash-20240722",
"rank": 31,
"rating": 1088.2114217704,
"upper_ci": 1094.1409244278
},
{
"lower_ci": 1076.2505646464,
"model": "command-r-plus",
"rank": 32,
"rating": 1078.6392855764,
"upper_ci": 1080.5241685262
},
{
"lower_ci": 1072.978332193,
"model": "gemma-2-9b-it",
"rank": 33,
"rating": 1076.1200490762,
"upper_ci": 1078.284506667
},
{
"lower_ci": 1073.5327980749,
"model": "qwen2-72b-instruct",
"rank": 34,
"rating": 1075.9397511888,
"upper_ci": 1077.9102798056
},
{
"lower_ci": 1073.0022184434,
"model": "gpt-4-0314",
"rank": 35,
"rating": 1074.4688060969,
"upper_ci": 1077.3841437021
},
{
"lower_ci": 1069.6849684012,
"model": "qwen-max-0428",
"rank": 36,
"rating": 1072.288830872,
"upper_ci": 1075.2788486953
},
{
"lower_ci": 1067.4064473159,
"model": "glm-4-0116",
"rank": 37,
"rating": 1072.1854778301,
"upper_ci": 1078.0278828583
},
{
"lower_ci": 1065.2785446605,
"model": "claude-3-haiku-20240307",
"rank": 38,
"rating": 1067.2876085746,
"upper_ci": 1068.5180263057
},
{
"lower_ci": 1063.4499078695,
"model": "deepseek-coder-v2",
"rank": 39,
"rating": 1066.5634197273,
"upper_ci": 1071.0372811761
},
{
"lower_ci": 1052.4060997176,
"model": "llama-3.1-8b-instruct",
"rank": 40,
"rating": 1057.2617029177,
"upper_ci": 1062.4851344029
},
{
"lower_ci": 1050.6516231153,
"model": "reka-flash-preview-20240611",
"rank": 41,
"rating": 1053.4286082128,
"upper_ci": 1056.9047978837
},
{
"lower_ci": 1049.878634752,
"model": "gpt-4-0613",
"rank": 42,
"rating": 1051.2288727429,
"upper_ci": 1053.3859970522
},
{
"lower_ci": 1047.658821981,
"model": "qwen1.5-110b-chat",
"rank": 43,
"rating": 1050.1472801797,
"upper_ci": 1054.1870464263
},
{
"lower_ci": 1043.4416581432,
"model": "yi-1.5-34b-chat",
"rank": 44,
"rating": 1046.5798307425,
"upper_ci": 1048.4870877901
},
{
"lower_ci": 1043.9717227179,
"model": "mistral-large-2402",
"rank": 45,
"rating": 1045.8549746499,
"upper_ci": 1048.6558688014
},
{
"lower_ci": 1039.8435107486,
"model": "reka-flash-21b-20240226-online",
"rank": 46,
"rating": 1044.4541273285,
"upper_ci": 1047.5722059799
},
{
"lower_ci": 1038.3457593513,
"model": "llama-3-8b-instruct",
"rank": 47,
"rating": 1040.8943517232,
"upper_ci": 1042.1727089946
},
{
"lower_ci": 1034.3532315209,
"model": "command-r",
"rank": 48,
"rating": 1037.5954245649,
"upper_ci": 1039.801976874
},
{
"lower_ci": 1033.8210405932,
"model": "claude-1",
"rank": 49,
"rating": 1037.2264989242,
"upper_ci": 1039.8445566211
},
{
"lower_ci": 1032.4181357042,
"model": "reka-flash-21b-20240226",
"rank": 50,
"rating": 1036.2083492306,
"upper_ci": 1038.9143450059
},
{
"lower_ci": 1033.5592570692,
"model": "mistral-medium",
"rank": 51,
"rating": 1035.9830773208,
"upper_ci": 1038.0506828269
},
{
"lower_ci": 1033.4716682328,
"model": "mixtral-8x22b-instruct-v0.1",
"rank": 52,
"rating": 1035.6187089038,
"upper_ci": 1037.3385699205
},
{
"lower_ci": 1032.3748800585,
"model": "qwen1.5-72b-chat",
"rank": 53,
"rating": 1035.5734020095,
"upper_ci": 1037.8584457516
},
{
"lower_ci": 1016.6859168483,
"model": "claude-2.0",
"rank": 54,
"rating": 1020.4032512576,
"upper_ci": 1023.0900087939
},
{
"lower_ci": 1016.4818650296,
"model": "gemini-pro-dev-api",
"rank": 55,
"rating": 1018.8216738996,
"upper_ci": 1022.0575603351
},
{
"lower_ci": 1013.9644553596,
"model": "gemma-2-2b-it",
"rank": 56,
"rating": 1018.7496539694,
"upper_ci": 1022.6257555787
},
{
"lower_ci": 1009.8189712109,
"model": "zephyr-orpo-141b-A35b-v0.1",
"rank": 57,
"rating": 1017.4768858149,
"upper_ci": 1023.4231756618
},
{
"lower_ci": 1011.7444717478,
"model": "qwen1.5-32b-chat",
"rank": 58,
"rating": 1014.2192047782,
"upper_ci": 1017.4848181067
},
{
"lower_ci": 1009.527805507,
"model": "mistral-next",
"rank": 59,
"rating": 1013.8190347615,
"upper_ci": 1016.756252395
},
{
"lower_ci": 1008.0043265533,
"model": "phi-3-medium-4k-instruct",
"rank": 60,
"rating": 1011.6064259691,
"upper_ci": 1014.8977711664
},
{
"lower_ci": 1003.3664596337,
"model": "claude-2.1",
"rank": 61,
"rating": 1006.9894951694,
"upper_ci": 1008.7578243389
},
{
"lower_ci": 1003.5743630599,
"model": "starling-lm-7b-beta",
"rank": 62,
"rating": 1006.9783218473,
"upper_ci": 1009.6647795891
},
{
"lower_ci": 1003.3560004715,
"model": "gpt-3.5-turbo-0613",
"rank": 63,
"rating": 1004.9730864576,
"upper_ci": 1008.7646121872
},
{
"lower_ci": 1000.3011942689,
"model": "mixtral-8x7b-instruct-v0.1",
"rank": 64,
"rating": 1002.12545233,
"upper_ci": 1004.4555563028
},
{
"lower_ci": 996.8355186436,
"model": "yi-34b-chat",
"rank": 65,
"rating": 1000.1263930743,
"upper_ci": 1002.9767969377
},
{
"lower_ci": 996.5513388826,
"model": "claude-instant-1",
"rank": 66,
"rating": 999.4536204497,
"upper_ci": 1003.0420312018
},
{
"lower_ci": 991.5815494729,
"model": "gemini-pro",
"rank": 67,
"rating": 998.3909699055,
"upper_ci": 1005.6665144072
},
{
"lower_ci": 994.5272838271,
"model": "qwen1.5-14b-chat",
"rank": 68,
"rating": 997.6714948516,
"upper_ci": 1000.3276398407
},
{
"lower_ci": 988.7882364736,
"model": "gpt-3.5-turbo-0314",
"rank": 69,
"rating": 997.4790766208,
"upper_ci": 1005.2920277339
},
{
"lower_ci": 989.2933547328,
"model": "wizardlm-70b",
"rank": 70,
"rating": 995.5069328559,
"upper_ci": 1000.0203225061
},
{
"lower_ci": 992.3677866278,
"model": "gpt-3.5-turbo-0125",
"rank": 71,
"rating": 994.221194786,
"upper_ci": 996.0922832004
},
{
"lower_ci": 989.517752069,
"model": "dbrx-instruct-preview",
"rank": 72,
"rating": 991.6558051925,
"upper_ci": 994.3414735308
},
{
"lower_ci": 988.3935167102,
"model": "phi-3-small-8k-instruct",
"rank": 73,
"rating": 990.3926872181,
"upper_ci": 993.4532484928
},
{
"lower_ci": 980.2003929739,
"model": "tulu-2-dpo-70b",
"rank": 74,
"rating": 986.8619167507,
"upper_ci": 995.6082960114
},
{
"lower_ci": 979.2393563337,
"model": "llama-2-70b-chat",
"rank": 75,
"rating": 982.0382072414,
"upper_ci": 983.1882888775
},
{
"lower_ci": 976.2082709482,
"model": "openchat-3.5-0106",
"rank": 76,
"rating": 980.6486425506,
"upper_ci": 984.7448454329
},
{
"lower_ci": 976.21123294,
"model": "vicuna-33b",
"rank": 77,
"rating": 978.8419077515,
"upper_ci": 983.0972163635
},
{
"lower_ci": 976.387496134,
"model": "snowflake-arctic-instruct",
"rank": 78,
"rating": 978.4458039182,
"upper_ci": 981.5060581663
},
{
"lower_ci": 969.5806601345,
"model": "starling-lm-7b-alpha",
"rank": 79,
"rating": 976.025808172,
"upper_ci": 980.086762811
},
{
"lower_ci": 965.8992419629,
"model": "nous-hermes-2-mixtral-8x7b-dpo",
"rank": 80,
"rating": 975.4699172429,
"upper_ci": 981.307231701
},
{
"lower_ci": 969.0894947313,
"model": "gemma-1.1-7b-it",
"rank": 81,
"rating": 971.847848073,
"upper_ci": 975.4703266199
},
{
"lower_ci": 966.6172052307,
"model": "llama2-70b-steerlm-chat",
"rank": 82,
"rating": 971.6537111318,
"upper_ci": 979.8268878882
},
{
"lower_ci": 961.617441776,
"model": "pplx-70b-online",
"rank": 83,
"rating": 967.4367908935,
"upper_ci": 972.8126406202
},
{
"lower_ci": 959.5692952959,
"model": "openchat-3.5",
"rank": 84,
"rating": 966.0510063396,
"upper_ci": 971.8466170437
},
{
"lower_ci": 956.7287406383,
"model": "deepseek-llm-67b-chat",
"rank": 85,
"rating": 965.1147523375,
"upper_ci": 971.4330625559
},
{
"lower_ci": 956.3934469962,
"model": "openhermes-2.5-mistral-7b",
"rank": 86,
"rating": 963.454411119,
"upper_ci": 968.2145476373
},
{
"lower_ci": 957.6795493747,
"model": "mistral-7b-instruct-v0.2",
"rank": 87,
"rating": 961.195768793,
"upper_ci": 964.3273838331
},
{
"lower_ci": 955.9914563912,
"model": "qwen1.5-7b-chat",
"rank": 88,
"rating": 959.7566395303,
"upper_ci": 963.8434392791
},
{
"lower_ci": 953.2396686903,
"model": "phi-3-mini-4k-instruct-june-2024",
"rank": 89,
"rating": 958.8907631692,
"upper_ci": 964.0199135738
},
{
"lower_ci": 953.2564762147,
"model": "gpt-3.5-turbo-1106",
"rank": 90,
"rating": 955.8557887224,
"upper_ci": 959.2854635955
},
{
"lower_ci": 951.2318268117,
"model": "phi-3-mini-4k-instruct",
"rank": 91,
"rating": 955.5964299136,
"upper_ci": 958.4767635868
},
{
"lower_ci": 939.9598426545,
"model": "dolphin-2.2.1-mistral-7b",
"rank": 92,
"rating": 952.2051167375,
"upper_ci": 962.2676381777
},
{
"lower_ci": 945.2431328588,
"model": "solar-10.7b-instruct-v1.0",
"rank": 93,
"rating": 951.1224233106,
"upper_ci": 958.6524028214
},
{
"lower_ci": 948.6812080723,
"model": "llama-2-13b-chat",
"rank": 94,
"rating": 951.0465511917,
"upper_ci": 953.7276982632
},
{
"lower_ci": 940.2487365654,
"model": "wizardlm-13b",
"rank": 95,
"rating": 945.6170139554,
"upper_ci": 952.3153173674
},
{
"lower_ci": 937.3497020159,
"model": "zephyr-7b-beta",
"rank": 96,
"rating": 941.7856240916,
"upper_ci": 945.7193614882
},
{
"lower_ci": 925.3514960917,
"model": "mpt-30b-chat",
"rank": 97,
"rating": 933.4637447927,
"upper_ci": 942.1936901157
},
{
"lower_ci": 928.7552755121,
"model": "pplx-7b-online",
"rank": 98,
"rating": 932.9740226673,
"upper_ci": 940.2890736975
},
{
"lower_ci": 924.8240945839,
"model": "codellama-34b-instruct",
"rank": 99,
"rating": 931.25075507,
"upper_ci": 936.6721012017
},
{
"lower_ci": 919.9794338309,
"model": "zephyr-7b-alpha",
"rank": 100,
"rating": 930.8024303718,
"upper_ci": 940.3904190626
},
{
"lower_ci": 927.6228696967,
"model": "vicuna-13b",
"rank": 101,
"rating": 930.129806663,
"upper_ci": 933.8873904378
},
{
"lower_ci": 916.3079081804,
"model": "codellama-70b-instruct",
"rank": 102,
"rating": 929.7441814853,
"upper_ci": 939.4638589466
},
{
"lower_ci": 919.0275736817,
"model": "gemma-7b-it",
"rank": 103,
"rating": 925.6063141152,
"upper_ci": 931.2321796245
},
{
"lower_ci": 921.4335696267,
"model": "llama-2-7b-chat",
"rank": 104,
"rating": 925.1226871516,
"upper_ci": 927.4211774328
},
{
"lower_ci": 919.914948524,
"model": "phi-3-mini-128k-instruct",
"rank": 105,
"rating": 925.0941128748,
"upper_ci": 928.953409783
},
{
"lower_ci": 917.0649637172,
"model": "qwen-14b-chat",
"rank": 106,
"rating": 923.0565707958,
"upper_ci": 927.5147584562
},
{
"lower_ci": 910.6316722798,
"model": "falcon-180b-chat",
"rank": 107,
"rating": 922.8642075646,
"upper_ci": 938.170426621
},
{
"lower_ci": 912.8204058608,
"model": "guanaco-33b",
"rank": 108,
"rating": 921.0201740806,
"upper_ci": 929.2566479601
},
{
"lower_ci": 903.2450471973,
"model": "gemma-1.1-2b-it",
"rank": 109,
"rating": 908.7494526802,
"upper_ci": 913.4741035764
},
{
"lower_ci": 899.9846491667,
"model": "stripedhyena-nous-7b",
"rank": 110,
"rating": 905.1534113535,
"upper_ci": 912.9382997405
},
{
"lower_ci": 899.9839053334,
"model": "olmo-7b-instruct",
"rank": 111,
"rating": 903.629872537,
"upper_ci": 908.2570306538
},
{
"lower_ci": 890.8398870858,
"model": "mistral-7b-instruct",
"rank": 112,
"rating": 897.2720855909,
"upper_ci": 902.9292885859
},
{
"lower_ci": 887.0047037055,
"model": "vicuna-7b",
"rank": 113,
"rating": 893.825818387,
"upper_ci": 899.7593423485
},
{
"lower_ci": 887.0036385995,
"model": "palm-2",
"rank": 114,
"rating": 891.3072880052,
"upper_ci": 897.1963496269
},
{
"lower_ci": 872.1578125039,
"model": "gemma-2b-it",
"rank": 115,
"rating": 880.4830688196,
"upper_ci": 884.0952002573
},
{
"lower_ci": 868.4676861275,
"model": "qwen1.5-4b-chat",
"rank": 116,
"rating": 877.0664166393,
"upper_ci": 883.3511645246
},
{
"lower_ci": 846.7609834499,
"model": "koala-13b",
"rank": 117,
"rating": 852.5741703315,
"upper_ci": 859.5370102373
},
{
"lower_ci": 833.5366986653,
"model": "chatglm3-6b",
"rank": 118,
"rating": 843.5467457427,
"upper_ci": 853.6979083485
},
{
"lower_ci": 808.672593832,
"model": "gpt4all-13b-snoozy",
"rank": 119,
"rating": 821.0385947815,
"upper_ci": 835.6716204673
},
{
"lower_ci": 804.1197078456,
"model": "chatglm2-6b",
"rank": 120,
"rating": 815.3737962537,
"upper_ci": 821.0889058237
},
{
"lower_ci": 805.0790145038,
"model": "mpt-7b-chat",
"rank": 121,
"rating": 813.8798502443,
"upper_ci": 823.374846381
},
{
"lower_ci": 803.3055604626,
"model": "RWKV-4-Raven-14B",
"rank": 122,
"rating": 810.0683748343,
"upper_ci": 819.7203248755
},
{
"lower_ci": 781.8369679949,
"model": "alpaca-13b",
"rank": 123,
"rating": 789.0910768147,
"upper_ci": 797.3012759671
},
{
"lower_ci": 778.7113024193,
"model": "oasst-pythia-12b",
"rank": 124,
"rating": 782.6327874047,
"upper_ci": 789.0013974908
},
{
"lower_ci": 760.5962982346,
"model": "chatglm-6b",
"rank": 125,
"rating": 767.7699631784,
"upper_ci": 776.2247501319
},
{
"lower_ci": 749.711207841,
"model": "fastchat-t5-3b",
"rank": 126,
"rating": 756.8214872887,
"upper_ci": 762.6617679978
},
{
"lower_ci": 723.1589751426,
"model": "stablelm-tuned-alpha-7b",
"rank": 127,
"rating": 727.6986691281,
"upper_ci": 737.7996673488
},
{
"lower_ci": 701.8523006848,
"model": "dolly-v2-12b",
"rank": 128,
"rating": 710.1023148692,
"upper_ci": 717.2721178625
},
{
"lower_ci": 679.2645194103,
"model": "llama-13b",
"rank": 129,
"rating": 689.1631346499,
"upper_ci": 694.5590720343
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

@@ -0,0 +1,74 @@
{
"artifacts": {
"bradley_terry.json": {
"bytes": 19827,
"sha256": "4a861b80c69132fdb3e3d1cfdc9d962444d664623b956595e67798d141ee1c6b"
},
"leaderboard.png": {
"bytes": 118255,
"sha256": "386889f573198987400fef7c7803e0779b95e2fa35cb274d04071fed842c6499"
},
"leaderboard_animation.html": {
"bytes": 54322,
"sha256": "afb9b0426605dcc5accf9e1ea5067dd885ad9a9226884cc341b3430404ca433a"
},
"online_elo.json": {
"bytes": 16885,
"sha256": "41ec9a46bb37457a385d0c4b4036ff8ae3d1affcbae8b9b00689fef494f9f9a6"
},
"rating_history.json": {
"bytes": 354362,
"sha256": "3a5e3193cbbe50bf1de3a9e5d9702f2b594f449300d195674ed0da2a76f72574"
},
"rating_history.png": {
"bytes": 193995,
"sha256": "cb676a82b38a8876bc1346e58f72b6a9d72c8fa5e405a9d22610f6948382fba4"
},
"summary.json": {
"bytes": 10441,
"sha256": "6258914ebdb6be7686985e637903c929ed14535d8572a4b9d42b1256d5cdf261"
},
"win_rate_matrix.json": {
"bytes": 37040,
"sha256": "a6ac9580a8d0290d53adf16458ac777d72e214d0771cd0e72d1b2d8a4d86abd2"
},
"win_rate_matrix.png": {
"bytes": 230883,
"sha256": "db375436646096bfafdad458b53aba54ac156432cf9e8bb7458a2f733c9ae690"
}
},
"experiment": "7-7",
"gates": {
"bradley_terry_official_method_completed": true,
"chronological_online_elo_k4_completed": true,
"d3_animation_saved": true,
"millions_of_blind_votes_loaded": true,
"monthly_history_saved": true,
"official_public_arena_snapshot_hashed": true,
"online_vs_official_method_rank_agreement_observed": true,
"pairwise_empirical_and_predicted_matrix_saved": true,
"static_visualizations_saved": true
},
"input": {
"bytes": 2131976365,
"filename": "arena_data.json",
"sha256": "747c1c937dfa941d5a455a1fd70e2879d29642058da39b97996b977233b9bd1b",
"url": "https://storage.googleapis.com/arena_external_data/public/clean_battle_20240814_public.json"
},
"official_complete": true,
"runtime": {
"numpy": "1.26.4",
"pandas": "2.3.3",
"platform": "macOS-26.3-arm64-arm-64bit",
"python": "3.12.11"
},
"schema_version": 1,
"sources": {
"animation.py": "6839a3173a6a9ee8855901ca3c781fd91de13219b52ce6b285778ab89d926376",
"bradley_terry.py": "030c716284017b14cff204ecd7036020c05554ae0eacf71af5da5c0963848b54",
"optimized_elo.py": "f56b5b4aaba9cda11006bf01742be99fc07ee44ceeed5bd2dee5322a8ce08a57",
"validation/run_experiment.py": "6c668081c4fbf04f9dbc84411a2a40391d7b00c76a90336542cc6a88623e991c",
"validation/validate_evidence.py": "36fe448c94608cfc6a299f21224870357d109d999ba3391ed7d7c71fc1817e62"
},
"status": "passed"
}
@@ -0,0 +1,905 @@
[
{
"matches": 55654,
"model": "gemini-1.5-pro-api-0409-preview",
"rank": 1,
"rating": 1159.8203895968,
"wins": 33738.5
},
{
"matches": 20071,
"model": "gemini-1.5-pro-exp-0801",
"rank": 2,
"rating": 1127.8877886511,
"wins": 12139.5
},
{
"matches": 14514,
"model": "chatgpt-4o-latest",
"rank": 3,
"rating": 1126.6935165964,
"wins": 8999.0
},
{
"matches": 5655,
"model": "gpt-3.5-turbo-0314",
"rank": 4,
"rating": 1103.9751202512,
"wins": 3789.0
},
{
"matches": 11827,
"model": "bard-jan-24-gemini-pro",
"rank": 5,
"rating": 1094.2118678517,
"wins": 7075.5
},
{
"matches": 21172,
"model": "claude-1",
"rank": 6,
"rating": 1092.7309868534,
"wins": 12081.5
},
{
"matches": 52155,
"model": "gemini-advanced-0514",
"rank": 7,
"rating": 1087.5408038371,
"wins": 30523.0
},
{
"matches": 13604,
"model": "llama-3.1-70b-instruct",
"rank": 8,
"rating": 1087.3529845908,
"wins": 7058.5
},
{
"matches": 77509,
"model": "gpt-4o-2024-05-13",
"rank": 9,
"rating": 1086.7242770205,
"wins": 46951.0
},
{
"matches": 9761,
"model": "gpt-4o-2024-08-06",
"rank": 10,
"rating": 1085.601246973,
"wins": 5323.0
},
{
"matches": 19370,
"model": "gpt-4o-mini-2024-07-18",
"rank": 11,
"rating": 1079.187026245,
"wins": 11082.0
},
{
"matches": 47703,
"model": "claude-3-5-sonnet-20240620",
"rank": 12,
"rating": 1079.0096726066,
"wins": 27107.5
},
{
"matches": 155944,
"model": "claude-3-opus-20240229",
"rank": 13,
"rating": 1074.9078256001,
"wins": 91432.5
},
{
"matches": 11476,
"model": "athene-70b-0725",
"rank": 14,
"rating": 1073.7607622933,
"wins": 6098.0
},
{
"matches": 18790,
"model": "gemini-pro-dev-api",
"rank": 15,
"rating": 1072.3072263884,
"wins": 8728.5
},
{
"matches": 12774,
"model": "claude-2.0",
"rank": 16,
"rating": 1069.530762176,
"wins": 6814.5
},
{
"matches": 10239,
"model": "glm-4-0520",
"rank": 17,
"rating": 1065.2967580553,
"wins": 5154.5
},
{
"matches": 20669,
"model": "nemotron-4-340b-instruct",
"rank": 18,
"rating": 1063.9797181947,
"wins": 10567.5
},
{
"matches": 51739,
"model": "yi-large-preview",
"rank": 19,
"rating": 1060.8561460851,
"wins": 28793.0
},
{
"matches": 39631,
"model": "llama-2-70b-chat",
"rank": 20,
"rating": 1060.6350445555,
"wins": 18272.0
},
{
"matches": 6974,
"model": "reka-core-20240722",
"rank": 21,
"rating": 1059.9327626435,
"wins": 3490.0
},
{
"matches": 69418,
"model": "gemini-1.5-pro-api-0514",
"rank": 22,
"rating": 1059.8892020659,
"wins": 39302.5
},
{
"matches": 6565,
"model": "gemini-pro",
"rank": 23,
"rating": 1059.2020149016,
"wins": 3028.0
},
{
"matches": 18897,
"model": "llama-3.1-405b-instruct",
"rank": 24,
"rating": 1058.4003562569,
"wins": 10361.5
},
{
"matches": 11517,
"model": "mistral-large-2407",
"rank": 25,
"rating": 1058.1277726399,
"wins": 6038.5
},
{
"matches": 84505,
"model": "gpt-4-turbo-2024-04-09",
"rank": 26,
"rating": 1054.9014485987,
"wins": 48844.5
},
{
"matches": 92488,
"model": "gpt-4-1106-preview",
"rank": 27,
"rating": 1054.2069148591,
"wins": 57496.5
},
{
"matches": 38959,
"model": "gpt-3.5-turbo-0613",
"rank": 28,
"rating": 1052.6707187439,
"wins": 19308.0
},
{
"matches": 85867,
"model": "gpt-4-0125-preview",
"rank": 29,
"rating": 1050.1218586343,
"wins": 50802.5
},
{
"matches": 7579,
"model": "glm-4-0116",
"rank": 30,
"rating": 1049.1894798578,
"wins": 3566.0
},
{
"matches": 4858,
"model": "zephyr-orpo-141b-A35b-v0.1",
"rank": 31,
"rating": 1045.2216087838,
"wins": 2116.5
},
{
"matches": 25748,
"model": "qwen-max-0428",
"rank": 32,
"rating": 1044.8335252342,
"wins": 12269.5
},
{
"matches": 20633,
"model": "claude-instant-1",
"rank": 33,
"rating": 1044.7562986355,
"wins": 11178.0
},
{
"matches": 16672,
"model": "yi-large",
"rank": 34,
"rating": 1044.7374546104,
"wins": 8533.5
},
{
"matches": 3997,
"model": "deepseek-coder-v2-0724",
"rank": 35,
"rating": 1043.8935002553,
"wins": 1883.5
},
{
"matches": 16165,
"model": "deepseek-v2-api-0628",
"rank": 36,
"rating": 1042.4852039574,
"wins": 7994.5
},
{
"matches": 56129,
"model": "gemini-1.5-flash-api-0514",
"rank": 37,
"rating": 1041.3766839839,
"wins": 29778.0
},
{
"matches": 161827,
"model": "llama-3-70b-instruct",
"rank": 38,
"rating": 1040.8719399645,
"wins": 84977.0
},
{
"matches": 80911,
"model": "command-r-plus",
"rank": 39,
"rating": 1034.8822379837,
"wins": 40967.0
},
{
"matches": 55979,
"model": "gpt-4-0314",
"rank": 40,
"rating": 1033.6412751608,
"wins": 31098.0
},
{
"matches": 37702,
"model": "claude-2.1",
"rank": 41,
"rating": 1032.8143823524,
"wins": 16848.0
},
{
"matches": 8391,
"model": "wizardlm-70b",
"rank": 42,
"rating": 1032.5111769139,
"wins": 4325.0
},
{
"matches": 27838,
"model": "gemma-2-27b-it",
"rank": 43,
"rating": 1030.7976774989,
"wins": 13981.5
},
{
"matches": 1714,
"model": "dolphin-2.2.1-mistral-7b",
"rank": 44,
"rating": 1029.9575607586,
"wins": 738.5
},
{
"matches": 3003,
"model": "guanaco-33b",
"rank": 45,
"rating": 1027.2582838587,
"wins": 1552.0
},
{
"matches": 3843,
"model": "nous-hermes-2-mixtral-8x7b-dpo",
"rank": 46,
"rating": 1025.2145335894,
"wins": 1697.5
},
{
"matches": 7192,
"model": "wizardlm-13b",
"rank": 47,
"rating": 1024.793903105,
"wins": 3690.0
},
{
"matches": 2650,
"model": "mpt-30b-chat",
"rank": 48,
"rating": 1024.0563241095,
"wins": 1351.5
},
{
"matches": 27508,
"model": "qwen1.5-110b-chat",
"rank": 49,
"rating": 1023.7761579902,
"wins": 12612.0
},
{
"matches": 113106,
"model": "claude-3-sonnet-20240229",
"rank": 50,
"rating": 1023.6338810846,
"wins": 60284.5
},
{
"matches": 12377,
"model": "mistral-next",
"rank": 51,
"rating": 1021.9755255724,
"wins": 5952.5
},
{
"matches": 15801,
"model": "deepseek-coder-v2",
"rank": 52,
"rating": 1021.6778455715,
"wins": 7243.0
},
{
"matches": 16056,
"model": "reka-flash-21b-20240226-online",
"rank": 53,
"rating": 1020.8238893282,
"wins": 7554.5
},
{
"matches": 16667,
"model": "starling-lm-7b-beta",
"rank": 54,
"rating": 1020.1246920766,
"wins": 7626.0
},
{
"matches": 3636,
"model": "llama2-70b-steerlm-chat",
"rank": 55,
"rating": 1018.5882277478,
"wins": 1587.5
},
{
"matches": 35555,
"model": "mistral-medium",
"rank": 56,
"rating": 1018.0043978294,
"wins": 17731.5
},
{
"matches": 19740,
"model": "llama-2-13b-chat",
"rank": 57,
"rating": 1016.6652779795,
"wins": 8935.5
},
{
"matches": 6660,
"model": "tulu-2-dpo-70b",
"rank": 58,
"rating": 1014.6804751782,
"wins": 3231.0
},
{
"matches": 62688,
"model": "reka-core-20240501",
"rank": 59,
"rating": 1013.8222528866,
"wins": 31198.0
},
{
"matches": 89617,
"model": "gpt-4-0613",
"rank": 60,
"rating": 1013.3276942754,
"wins": 43878.0
},
{
"matches": 4981,
"model": "deepseek-llm-67b-chat",
"rank": 61,
"rating": 1011.6783077357,
"wins": 2017.0
},
{
"matches": 4291,
"model": "solar-10.7b-instruct-v1.0",
"rank": 62,
"rating": 1011.6055213958,
"wins": 1814.0
},
{
"matches": 12987,
"model": "openchat-3.5-0106",
"rank": 63,
"rating": 1009.5145969972,
"wins": 5974.0
},
{
"matches": 7156,
"model": "reka-flash-20240722",
"rank": 64,
"rating": 1003.5048766039,
"wins": 3290.0
},
{
"matches": 25248,
"model": "gemma-2-9b-it",
"rank": 65,
"rating": 1003.026164166,
"wins": 11661.5
},
{
"matches": 12671,
"model": "llama-3.1-8b-instruct",
"rank": 66,
"rating": 1002.2631876345,
"wins": 5121.0
},
{
"matches": 8117,
"model": "openchat-3.5",
"rank": 67,
"rating": 1001.939993772,
"wins": 3783.0
},
{
"matches": 6338,
"model": "pplx-7b-online",
"rank": 68,
"rating": 1001.6927968751,
"wins": 2572.0
},
{
"matches": 40643,
"model": "qwen1.5-72b-chat",
"rank": 69,
"rating": 1000.9425509065,
"wins": 19941.0
},
{
"matches": 1815,
"model": "zephyr-7b-alpha",
"rank": 70,
"rating": 999.8845726251,
"wins": 855.0
},
{
"matches": 109888,
"model": "claude-3-haiku-20240307",
"rank": 71,
"rating": 999.7072031054,
"wins": 55074.0
},
{
"matches": 10422,
"model": "starling-lm-7b-alpha",
"rank": 72,
"rating": 998.0936577805,
"wins": 4817.0
},
{
"matches": 25792,
"model": "reka-flash-21b-20240226",
"rank": 73,
"rating": 992.8107482264,
"wins": 11891.0
},
{
"matches": 64938,
"model": "mistral-large-2402",
"rank": 74,
"rating": 992.5035561283,
"wins": 30754.5
},
{
"matches": 17026,
"model": "gpt-3.5-turbo-1106",
"rank": 75,
"rating": 990.5828502496,
"wins": 7026.0
},
{
"matches": 4864,
"model": "qwen1.5-7b-chat",
"rank": 76,
"rating": 990.5290268684,
"wins": 1903.5
},
{
"matches": 20477,
"model": "reka-flash-preview-20240611",
"rank": 77,
"rating": 990.4289024537,
"wins": 9003.5
},
{
"matches": 25156,
"model": "yi-1.5-34b-chat",
"rank": 78,
"rating": 988.8079557694,
"wins": 11066.0
},
{
"matches": 5091,
"model": "openhermes-2.5-mistral-7b",
"rank": 79,
"rating": 988.507956639,
"wins": 2379.0
},
{
"matches": 7508,
"model": "codellama-34b-instruct",
"rank": 80,
"rating": 986.9963372601,
"wins": 3192.5
},
{
"matches": 18666,
"model": "qwen1.5-14b-chat",
"rank": 81,
"rating": 985.79676379,
"wins": 8265.0
},
{
"matches": 15946,
"model": "yi-34b-chat",
"rank": 82,
"rating": 984.9789753527,
"wins": 7423.5
},
{
"matches": 6890,
"model": "pplx-70b-online",
"rank": 83,
"rating": 984.0082457269,
"wins": 3118.0
},
{
"matches": 34410,
"model": "qwen2-72b-instruct",
"rank": 84,
"rating": 983.7228839439,
"wins": 16578.5
},
{
"matches": 33736,
"model": "dbrx-instruct-preview",
"rank": 85,
"rating": 983.0655232047,
"wins": 14350.5
},
{
"matches": 107100,
"model": "llama-3-8b-instruct",
"rank": 86,
"rating": 978.2506897907,
"wins": 49020.5
},
{
"matches": 1325,
"model": "falcon-180b-chat",
"rank": 87,
"rating": 976.0053410687,
"wins": 580.0
},
{
"matches": 8745,
"model": "palm-2",
"rank": 88,
"rating": 975.4724123669,
"wins": 3851.5
},
{
"matches": 5070,
"model": "qwen-14b-chat",
"rank": 89,
"rating": 974.4393986925,
"wins": 2262.5
},
{
"matches": 5270,
"model": "stripedhyena-nous-7b",
"rank": 90,
"rating": 972.5214538963,
"wins": 2001.0
},
{
"matches": 22783,
"model": "qwen1.5-32b-chat",
"rank": 91,
"rating": 970.5573357551,
"wins": 10038.5
},
{
"matches": 56373,
"model": "command-r",
"rank": 92,
"rating": 968.3429828775,
"wins": 25830.5
},
{
"matches": 9178,
"model": "gemma-7b-it",
"rank": 93,
"rating": 966.2526965022,
"wins": 3447.5
},
{
"matches": 11330,
"model": "zephyr-7b-beta",
"rank": 94,
"rating": 966.1969243012,
"wins": 5268.0
},
{
"matches": 50256,
"model": "mixtral-8x22b-instruct-v0.1",
"rank": 95,
"rating": 964.4141739849,
"wins": 22236.0
},
{
"matches": 7022,
"model": "vicuna-7b",
"rank": 96,
"rating": 963.6362226151,
"wins": 3187.5
},
{
"matches": 34197,
"model": "snowflake-arctic-instruct",
"rank": 97,
"rating": 961.9084215198,
"wins": 13326.5
},
{
"matches": 22938,
"model": "vicuna-33b",
"rank": 98,
"rating": 959.3823284501,
"wins": 10924.5
},
{
"matches": 10509,
"model": "gemma-2-2b-it",
"rank": 99,
"rating": 959.2713799268,
"wins": 3731.5
},
{
"matches": 7038,
"model": "koala-13b",
"rank": 100,
"rating": 954.395027749,
"wins": 3380.0
},
{
"matches": 68914,
"model": "gpt-3.5-turbo-0125",
"rank": 101,
"rating": 951.9181205239,
"wins": 29165.5
},
{
"matches": 9145,
"model": "mistral-7b-instruct",
"rank": 102,
"rating": 948.891187234,
"wins": 3685.5
},
{
"matches": 14559,
"model": "llama-2-7b-chat",
"rank": 103,
"rating": 948.4212635676,
"wins": 6212.0
},
{
"matches": 20074,
"model": "mistral-7b-instruct-v0.2",
"rank": 104,
"rating": 945.8420359878,
"wins": 8282.5
},
{
"matches": 25095,
"model": "gemma-1.1-7b-it",
"rank": 105,
"rating": 941.8141470663,
"wins": 9687.0
},
{
"matches": 1787,
"model": "gpt4all-13b-snoozy",
"rank": 106,
"rating": 938.6958755482,
"wins": 680.0
},
{
"matches": 18505,
"model": "phi-3-small-8k-instruct",
"rank": 107,
"rating": 930.6944270029,
"wins": 7100.0
},
{
"matches": 6496,
"model": "olmo-7b-instruct",
"rank": 108,
"rating": 928.8306135323,
"wins": 2157.0
},
{
"matches": 21129,
"model": "phi-3-mini-4k-instruct",
"rank": 109,
"rating": 928.4371869598,
"wins": 7094.0
},
{
"matches": 21617,
"model": "phi-3-mini-128k-instruct",
"rank": 110,
"rating": 928.3336295806,
"wins": 7094.5
},
{
"matches": 4938,
"model": "RWKV-4-Raven-14B",
"rank": 111,
"rating": 927.486785137,
"wins": 1965.5
},
{
"matches": 19794,
"model": "vicuna-13b",
"rank": 112,
"rating": 927.1632498042,
"wins": 9553.5
},
{
"matches": 1193,
"model": "codellama-70b-instruct",
"rank": 113,
"rating": 926.7141337337,
"wins": 411.5
},
{
"matches": 4018,
"model": "mpt-7b-chat",
"rank": 114,
"rating": 922.0268289601,
"wins": 1573.5
},
{
"matches": 76064,
"model": "mixtral-8x7b-instruct-v0.1",
"rank": 115,
"rating": 920.8542542685,
"wins": 33437.0
},
{
"matches": 22427,
"model": "phi-3-medium-4k-instruct",
"rank": 116,
"rating": 919.7045738753,
"wins": 8854.0
},
{
"matches": 4921,
"model": "gemma-2b-it",
"rank": 117,
"rating": 918.581510353,
"wins": 1513.0
},
{
"matches": 10484,
"model": "phi-3-mini-4k-instruct-june-2024",
"rank": 118,
"rating": 913.3885931532,
"wins": 3097.5
},
{
"matches": 7811,
"model": "qwen1.5-4b-chat",
"rank": 119,
"rating": 910.8121423587,
"wins": 2364.5
},
{
"matches": 4995,
"model": "chatglm-6b",
"rank": 120,
"rating": 908.8611775605,
"wins": 1834.5
},
{
"matches": 5874,
"model": "alpaca-13b",
"rank": 121,
"rating": 904.2619553966,
"wins": 2330.0
},
{
"matches": 11363,
"model": "gemma-1.1-2b-it",
"rank": 122,
"rating": 892.2955737397,
"wins": 3282.0
},
{
"matches": 2707,
"model": "chatglm2-6b",
"rank": 123,
"rating": 888.2580854124,
"wins": 808.0
},
{
"matches": 3334,
"model": "stablelm-tuned-alpha-7b",
"rank": 124,
"rating": 882.7115402984,
"wins": 1141.5
},
{
"matches": 4763,
"model": "chatglm3-6b",
"rank": 125,
"rating": 867.498613261,
"wins": 1454.0
},
{
"matches": 6382,
"model": "oasst-pythia-12b",
"rank": 126,
"rating": 865.3424632797,
"wins": 2425.0
},
{
"matches": 2443,
"model": "llama-13b",
"rank": 127,
"rating": 852.9506118392,
"wins": 745.5
},
{
"matches": 4304,
"model": "fastchat-t5-3b",
"rank": 128,
"rating": 847.3254654092,
"wins": 1465.0
},
{
"matches": 3484,
"model": "dolly-v2-12b",
"rank": 129,
"rating": 834.3876685486,
"wins": 1099.5
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

@@ -0,0 +1,387 @@
{
"dataset": {
"accepted_records": 1670250,
"bounded_test_run": false,
"bytes": 2131976365,
"end_utc": "2024-08-14T19:52:53.409200+00:00",
"load_filter_seconds": 15.133,
"model_count": 129,
"outcomes": {
"model_a": 543065,
"model_b": 550810,
"tie": 281121,
"tie (bothbad)": 295254
},
"path_recorded_as": "arena_data.json",
"sha256": "747c1c937dfa941d5a455a1fd70e2879d29642058da39b97996b977233b9bd1b",
"source_records": 1799991,
"start_utc": "2023-04-24T15:53:11.132200+00:00",
"url": "https://storage.googleapis.com/arena_external_data/public/clean_battle_20240814_public.json"
},
"experiment": "7-7",
"gates": {
"bradley_terry_official_method_completed": true,
"chronological_online_elo_k4_completed": true,
"d3_animation_saved": true,
"millions_of_blind_votes_loaded": true,
"monthly_history_saved": true,
"official_public_arena_snapshot_hashed": true,
"online_vs_official_method_rank_agreement_observed": true,
"pairwise_empirical_and_predicted_matrix_saved": true,
"static_visualizations_saved": true
},
"generated_at_utc": "2026-07-31T09:59:17.815099+00:00",
"official_complete": true,
"protocol": {
"bootstrap_random_seed": 0,
"bootstrap_rounds": 20,
"history_interval": "monthly cumulative snapshots",
"official_method": "Bradley-Terry maximum-likelihood reconstruction",
"online_elo": "initial=1000, K=4, stable chronological order"
},
"results": {
"official_method_top_20": [
{
"lower_ci": 1199.4356201692,
"model": "chatgpt-4o-latest",
"rank": 1,
"rating": 1202.8722324768,
"upper_ci": 1207.3805726125
},
{
"lower_ci": 1182.767114327,
"model": "gemini-1.5-pro-exp-0801",
"rank": 2,
"rating": 1187.3727762743,
"upper_ci": 1191.7564879767
},
{
"lower_ci": 1173.1270556478,
"model": "gpt-4o-2024-05-13",
"rank": 3,
"rating": 1174.5475812185,
"upper_ci": 1177.0403252386
},
{
"lower_ci": 1161.1978031325,
"model": "gpt-4o-mini-2024-07-18",
"rank": 4,
"rating": 1162.9712796813,
"upper_ci": 1166.568334134
},
{
"lower_ci": 1156.0224735749,
"model": "claude-3-5-sonnet-20240620",
"rank": 5,
"rating": 1159.6692177384,
"upper_ci": 1161.8458744744
},
{
"lower_ci": 1153.6447817793,
"model": "gemini-advanced-0514",
"rank": 6,
"rating": 1155.6305094344,
"upper_ci": 1157.1030226933
},
{
"lower_ci": 1150.4614200682,
"model": "llama-3.1-405b-instruct",
"rank": 7,
"rating": 1153.3950109278,
"upper_ci": 1157.0848375454
},
{
"lower_ci": 1146.348878889,
"model": "gpt-4o-2024-08-06",
"rank": 8,
"rating": 1150.7428051987,
"upper_ci": 1156.3245456986
},
{
"lower_ci": 1146.7772301944,
"model": "gemini-1.5-pro-api-0514",
"rank": 9,
"rating": 1149.1265569601,
"upper_ci": 1151.5010539806
},
{
"lower_ci": 1143.6682527667,
"model": "gemini-1.5-pro-api-0409-preview",
"rank": 10,
"rating": 1146.1756037638,
"upper_ci": 1147.8895040945
},
{
"lower_ci": 1142.3990049162,
"model": "gpt-4-turbo-2024-04-09",
"rank": 11,
"rating": 1144.9187340632,
"upper_ci": 1146.8362809167
},
{
"lower_ci": 1138.1303324947,
"model": "gpt-4-1106-preview",
"rank": 12,
"rating": 1139.5004505155,
"upper_ci": 1140.97644635
},
{
"lower_ci": 1136.3852091186,
"model": "mistral-large-2407",
"rank": 13,
"rating": 1139.3566231011,
"upper_ci": 1143.8960727747
},
{
"lower_ci": 1135.0624903381,
"model": "athene-70b-0725",
"rank": 14,
"rating": 1138.2016223593,
"upper_ci": 1141.397916447
},
{
"lower_ci": 1135.0278139027,
"model": "claude-3-opus-20240229",
"rank": 15,
"rating": 1136.5739464324,
"upper_ci": 1137.5663039742
},
{
"lower_ci": 1131.7219814044,
"model": "llama-3.1-70b-instruct",
"rank": 16,
"rating": 1134.8942150397,
"upper_ci": 1139.3028524197
},
{
"lower_ci": 1132.1800230236,
"model": "gpt-4-0125-preview",
"rank": 17,
"rating": 1133.974390087,
"upper_ci": 1135.7552442263
},
{
"lower_ci": 1127.3300289872,
"model": "yi-large-preview",
"rank": 18,
"rating": 1128.1692229165,
"upper_ci": 1131.371743138
},
{
"lower_ci": 1111.850158929,
"model": "reka-core-20240722",
"rank": 19,
"rating": 1117.1464671073,
"upper_ci": 1121.9537941313
},
{
"lower_ci": 1114.0244965884,
"model": "gemini-1.5-flash-api-0514",
"rank": 20,
"rating": 1116.6161359294,
"upper_ci": 1118.5223962538
}
],
"online_top_20": [
{
"matches": 55654,
"model": "gemini-1.5-pro-api-0409-preview",
"rank": 1,
"rating": 1159.8203895968,
"wins": 33738.5
},
{
"matches": 20071,
"model": "gemini-1.5-pro-exp-0801",
"rank": 2,
"rating": 1127.8877886511,
"wins": 12139.5
},
{
"matches": 14514,
"model": "chatgpt-4o-latest",
"rank": 3,
"rating": 1126.6935165964,
"wins": 8999.0
},
{
"matches": 5655,
"model": "gpt-3.5-turbo-0314",
"rank": 4,
"rating": 1103.9751202512,
"wins": 3789.0
},
{
"matches": 11827,
"model": "bard-jan-24-gemini-pro",
"rank": 5,
"rating": 1094.2118678517,
"wins": 7075.5
},
{
"matches": 21172,
"model": "claude-1",
"rank": 6,
"rating": 1092.7309868534,
"wins": 12081.5
},
{
"matches": 52155,
"model": "gemini-advanced-0514",
"rank": 7,
"rating": 1087.5408038371,
"wins": 30523.0
},
{
"matches": 13604,
"model": "llama-3.1-70b-instruct",
"rank": 8,
"rating": 1087.3529845908,
"wins": 7058.5
},
{
"matches": 77509,
"model": "gpt-4o-2024-05-13",
"rank": 9,
"rating": 1086.7242770205,
"wins": 46951.0
},
{
"matches": 9761,
"model": "gpt-4o-2024-08-06",
"rank": 10,
"rating": 1085.601246973,
"wins": 5323.0
},
{
"matches": 19370,
"model": "gpt-4o-mini-2024-07-18",
"rank": 11,
"rating": 1079.187026245,
"wins": 11082.0
},
{
"matches": 47703,
"model": "claude-3-5-sonnet-20240620",
"rank": 12,
"rating": 1079.0096726066,
"wins": 27107.5
},
{
"matches": 155944,
"model": "claude-3-opus-20240229",
"rank": 13,
"rating": 1074.9078256001,
"wins": 91432.5
},
{
"matches": 11476,
"model": "athene-70b-0725",
"rank": 14,
"rating": 1073.7607622933,
"wins": 6098.0
},
{
"matches": 18790,
"model": "gemini-pro-dev-api",
"rank": 15,
"rating": 1072.3072263884,
"wins": 8728.5
},
{
"matches": 12774,
"model": "claude-2.0",
"rank": 16,
"rating": 1069.530762176,
"wins": 6814.5
},
{
"matches": 10239,
"model": "glm-4-0520",
"rank": 17,
"rating": 1065.2967580553,
"wins": 5154.5
},
{
"matches": 20669,
"model": "nemotron-4-340b-instruct",
"rank": 18,
"rating": 1063.9797181947,
"wins": 10567.5
},
{
"matches": 51739,
"model": "yi-large-preview",
"rank": 19,
"rating": 1060.8561460851,
"wins": 28793.0
},
{
"matches": 39631,
"model": "llama-2-70b-chat",
"rank": 20,
"rating": 1060.6350445555,
"wins": 18272.0
}
],
"rank_comparison": {
"claim_boundary": "This is a same-snapshot reconstruction of the official method, not a scrape of the mutable live leaderboard. Scores need not match the live service.",
"common_models": 129,
"comparison_target": "Bradley-Terry MLE reconstruction used by Chatbot Arena",
"kendall_rank_correlation": 0.606347,
"official_method_top_20": [
"chatgpt-4o-latest",
"gemini-1.5-pro-exp-0801",
"gpt-4o-2024-05-13",
"gpt-4o-mini-2024-07-18",
"claude-3-5-sonnet-20240620",
"gemini-advanced-0514",
"llama-3.1-405b-instruct",
"gpt-4o-2024-08-06",
"gemini-1.5-pro-api-0514",
"gemini-1.5-pro-api-0409-preview",
"gpt-4-turbo-2024-04-09",
"gpt-4-1106-preview",
"mistral-large-2407",
"athene-70b-0725",
"claude-3-opus-20240229",
"llama-3.1-70b-instruct",
"gpt-4-0125-preview",
"yi-large-preview",
"reka-core-20240722",
"gemini-1.5-flash-api-0514"
],
"online_top_20": [
"gemini-1.5-pro-api-0409-preview",
"gemini-1.5-pro-exp-0801",
"chatgpt-4o-latest",
"gpt-3.5-turbo-0314",
"bard-jan-24-gemini-pro",
"claude-1",
"gemini-advanced-0514",
"llama-3.1-70b-instruct",
"gpt-4o-2024-05-13",
"gpt-4o-2024-08-06",
"gpt-4o-mini-2024-07-18",
"claude-3-5-sonnet-20240620",
"claude-3-opus-20240229",
"athene-70b-0725",
"gemini-pro-dev-api",
"claude-2.0",
"glm-4-0520",
"nemotron-4-340b-instruct",
"yi-large-preview",
"llama-2-70b-chat"
],
"spearman_rank_correlation": 0.786717,
"top_20_overlap": 12
}
},
"schema_version": 1,
"status": "passed",
"timing_seconds": {
"bradley_terry": 71.472,
"online_and_history": 8.301,
"total": 97.248
}
}
@@ -0,0 +1,908 @@
{
"empirical": {
"athene-70b-0725": {
"athene-70b-0725": 0.5,
"bard-jan-24-gemini-pro": NaN,
"chatgpt-4o-latest": 0.41589648798521256,
"claude-1": NaN,
"claude-2.0": NaN,
"claude-3-5-sonnet-20240620": 0.49077181208053694,
"claude-3-opus-20240229": 0.5027472527472527,
"gemini-1.5-pro-api-0409-preview": NaN,
"gemini-1.5-pro-exp-0801": 0.43097014925373134,
"gemini-advanced-0514": 0.48249027237354086,
"gemini-pro-dev-api": NaN,
"glm-4-0520": NaN,
"gpt-3.5-turbo-0314": NaN,
"gpt-4o-2024-05-13": 0.4431239388794567,
"gpt-4o-2024-08-06": 0.505249343832021,
"gpt-4o-mini-2024-07-18": 0.4753787878787879,
"llama-2-70b-chat": NaN,
"llama-3.1-70b-instruct": 0.5317460317460317,
"nemotron-4-340b-instruct": NaN,
"yi-large-preview": 0.5294117647058824
},
"bard-jan-24-gemini-pro": {
"athene-70b-0725": NaN,
"bard-jan-24-gemini-pro": 0.5,
"chatgpt-4o-latest": NaN,
"claude-1": 0.572289156626506,
"claude-2.0": 0.6696428571428571,
"claude-3-5-sonnet-20240620": NaN,
"claude-3-opus-20240229": 0.3,
"gemini-1.5-pro-api-0409-preview": NaN,
"gemini-1.5-pro-exp-0801": NaN,
"gemini-advanced-0514": NaN,
"gemini-pro-dev-api": 0.6332794830371568,
"glm-4-0520": NaN,
"gpt-3.5-turbo-0314": NaN,
"gpt-4o-2024-05-13": NaN,
"gpt-4o-2024-08-06": NaN,
"gpt-4o-mini-2024-07-18": NaN,
"llama-2-70b-chat": 0.6492537313432836,
"llama-3.1-70b-instruct": NaN,
"nemotron-4-340b-instruct": NaN,
"yi-large-preview": NaN
},
"chatgpt-4o-latest": {
"athene-70b-0725": 0.5841035120147874,
"bard-jan-24-gemini-pro": NaN,
"chatgpt-4o-latest": 0.5,
"claude-1": NaN,
"claude-2.0": NaN,
"claude-3-5-sonnet-20240620": 0.5534351145038168,
"claude-3-opus-20240229": 0.5915750915750916,
"gemini-1.5-pro-api-0409-preview": NaN,
"gemini-1.5-pro-exp-0801": 0.5329736211031175,
"gemini-advanced-0514": NaN,
"gemini-pro-dev-api": NaN,
"glm-4-0520": NaN,
"gpt-3.5-turbo-0314": NaN,
"gpt-4o-2024-05-13": 0.5442092154420921,
"gpt-4o-2024-08-06": 0.5164835164835165,
"gpt-4o-mini-2024-07-18": 0.6010733452593918,
"llama-2-70b-chat": NaN,
"llama-3.1-70b-instruct": 0.5793838862559242,
"nemotron-4-340b-instruct": NaN,
"yi-large-preview": NaN
},
"claude-1": {
"athene-70b-0725": NaN,
"bard-jan-24-gemini-pro": 0.42771084337349397,
"chatgpt-4o-latest": NaN,
"claude-1": 0.5,
"claude-2.0": 0.5460750853242321,
"claude-3-5-sonnet-20240620": NaN,
"claude-3-opus-20240229": 0.34452296819787986,
"gemini-1.5-pro-api-0409-preview": NaN,
"gemini-1.5-pro-exp-0801": NaN,
"gemini-advanced-0514": NaN,
"gemini-pro-dev-api": 0.5342465753424658,
"glm-4-0520": NaN,
"gpt-3.5-turbo-0314": 0.5548780487804879,
"gpt-4o-2024-05-13": NaN,
"gpt-4o-2024-08-06": NaN,
"gpt-4o-mini-2024-07-18": NaN,
"llama-2-70b-chat": 0.604,
"llama-3.1-70b-instruct": NaN,
"nemotron-4-340b-instruct": NaN,
"yi-large-preview": NaN
},
"claude-2.0": {
"athene-70b-0725": NaN,
"bard-jan-24-gemini-pro": 0.33035714285714285,
"chatgpt-4o-latest": NaN,
"claude-1": 0.4539249146757679,
"claude-2.0": 0.5,
"claude-3-5-sonnet-20240620": NaN,
"claude-3-opus-20240229": NaN,
"gemini-1.5-pro-api-0409-preview": NaN,
"gemini-1.5-pro-exp-0801": NaN,
"gemini-advanced-0514": NaN,
"gemini-pro-dev-api": 0.5569620253164557,
"glm-4-0520": NaN,
"gpt-3.5-turbo-0314": NaN,
"gpt-4o-2024-05-13": NaN,
"gpt-4o-2024-08-06": NaN,
"gpt-4o-mini-2024-07-18": NaN,
"llama-2-70b-chat": 0.5866834170854272,
"llama-3.1-70b-instruct": NaN,
"nemotron-4-340b-instruct": NaN,
"yi-large-preview": NaN
},
"claude-3-5-sonnet-20240620": {
"athene-70b-0725": 0.5092281879194631,
"bard-jan-24-gemini-pro": NaN,
"chatgpt-4o-latest": 0.44656488549618323,
"claude-1": NaN,
"claude-2.0": NaN,
"claude-3-5-sonnet-20240620": 0.5,
"claude-3-opus-20240229": 0.5447761194029851,
"gemini-1.5-pro-api-0409-preview": NaN,
"gemini-1.5-pro-exp-0801": 0.4537037037037037,
"gemini-advanced-0514": 0.49312714776632305,
"gemini-pro-dev-api": NaN,
"glm-4-0520": 0.5800756620428752,
"gpt-3.5-turbo-0314": NaN,
"gpt-4o-2024-05-13": 0.47875730217737655,
"gpt-4o-2024-08-06": 0.5189873417721519,
"gpt-4o-mini-2024-07-18": 0.47484909456740443,
"llama-2-70b-chat": NaN,
"llama-3.1-70b-instruct": 0.538961038961039,
"nemotron-4-340b-instruct": 0.589835728952772,
"yi-large-preview": 0.5205761316872428
},
"claude-3-opus-20240229": {
"athene-70b-0725": 0.49725274725274726,
"bard-jan-24-gemini-pro": 0.7,
"chatgpt-4o-latest": 0.4084249084249084,
"claude-1": 0.6554770318021201,
"claude-2.0": NaN,
"claude-3-5-sonnet-20240620": 0.4552238805970149,
"claude-3-opus-20240229": 0.5,
"gemini-1.5-pro-api-0409-preview": 0.4815616343490305,
"gemini-1.5-pro-exp-0801": 0.4305157593123209,
"gemini-advanced-0514": 0.46121231155778897,
"gemini-pro-dev-api": 0.6489533011272142,
"glm-4-0520": 0.5424311926605505,
"gpt-3.5-turbo-0314": NaN,
"gpt-4o-2024-05-13": 0.4481694338016177,
"gpt-4o-2024-08-06": 0.4369627507163324,
"gpt-4o-mini-2024-07-18": 0.4437386569872958,
"llama-2-70b-chat": 0.6890951276102089,
"llama-3.1-70b-instruct": 0.4813519813519814,
"nemotron-4-340b-instruct": 0.5431309904153354,
"yi-large-preview": 0.5238190552441954
},
"gemini-1.5-pro-api-0409-preview": {
"athene-70b-0725": NaN,
"bard-jan-24-gemini-pro": NaN,
"chatgpt-4o-latest": NaN,
"claude-1": NaN,
"claude-2.0": NaN,
"claude-3-5-sonnet-20240620": NaN,
"claude-3-opus-20240229": 0.5184383656509696,
"gemini-1.5-pro-api-0409-preview": 0.5,
"gemini-1.5-pro-exp-0801": NaN,
"gemini-advanced-0514": NaN,
"gemini-pro-dev-api": NaN,
"glm-4-0520": NaN,
"gpt-3.5-turbo-0314": NaN,
"gpt-4o-2024-05-13": NaN,
"gpt-4o-2024-08-06": NaN,
"gpt-4o-mini-2024-07-18": NaN,
"llama-2-70b-chat": 0.6875,
"llama-3.1-70b-instruct": NaN,
"nemotron-4-340b-instruct": NaN,
"yi-large-preview": NaN
},
"gemini-1.5-pro-exp-0801": {
"athene-70b-0725": 0.5690298507462687,
"bard-jan-24-gemini-pro": NaN,
"chatgpt-4o-latest": 0.4670263788968825,
"claude-1": NaN,
"claude-2.0": NaN,
"claude-3-5-sonnet-20240620": 0.5462962962962963,
"claude-3-opus-20240229": 0.5694842406876791,
"gemini-1.5-pro-api-0409-preview": NaN,
"gemini-1.5-pro-exp-0801": 0.5,
"gemini-advanced-0514": 0.5263157894736842,
"gemini-pro-dev-api": NaN,
"glm-4-0520": NaN,
"gpt-3.5-turbo-0314": NaN,
"gpt-4o-2024-05-13": 0.5229445506692161,
"gpt-4o-2024-08-06": 0.5241071428571429,
"gpt-4o-mini-2024-07-18": 0.529559748427673,
"llama-2-70b-chat": NaN,
"llama-3.1-70b-instruct": 0.5773092369477911,
"nemotron-4-340b-instruct": NaN,
"yi-large-preview": 0.5862068965517241
},
"gemini-advanced-0514": {
"athene-70b-0725": 0.5175097276264592,
"bard-jan-24-gemini-pro": NaN,
"chatgpt-4o-latest": NaN,
"claude-1": NaN,
"claude-2.0": NaN,
"claude-3-5-sonnet-20240620": 0.506872852233677,
"claude-3-opus-20240229": 0.538787688442211,
"gemini-1.5-pro-api-0409-preview": NaN,
"gemini-1.5-pro-exp-0801": 0.47368421052631576,
"gemini-advanced-0514": 0.5,
"gemini-pro-dev-api": NaN,
"glm-4-0520": 0.5773381294964028,
"gpt-3.5-turbo-0314": NaN,
"gpt-4o-2024-05-13": 0.4901780233271946,
"gpt-4o-2024-08-06": NaN,
"gpt-4o-mini-2024-07-18": 0.4604072398190045,
"llama-2-70b-chat": 0.6145833333333334,
"llama-3.1-70b-instruct": 0.5423076923076923,
"nemotron-4-340b-instruct": 0.5807407407407408,
"yi-large-preview": 0.5602636534839924
},
"gemini-pro-dev-api": {
"athene-70b-0725": NaN,
"bard-jan-24-gemini-pro": 0.3667205169628433,
"chatgpt-4o-latest": NaN,
"claude-1": 0.4657534246575342,
"claude-2.0": 0.4430379746835443,
"claude-3-5-sonnet-20240620": NaN,
"claude-3-opus-20240229": 0.35104669887278583,
"gemini-1.5-pro-api-0409-preview": NaN,
"gemini-1.5-pro-exp-0801": NaN,
"gemini-advanced-0514": NaN,
"gemini-pro-dev-api": 0.5,
"glm-4-0520": NaN,
"gpt-3.5-turbo-0314": NaN,
"gpt-4o-2024-05-13": NaN,
"gpt-4o-2024-08-06": NaN,
"gpt-4o-mini-2024-07-18": NaN,
"llama-2-70b-chat": 0.5535714285714286,
"llama-3.1-70b-instruct": NaN,
"nemotron-4-340b-instruct": NaN,
"yi-large-preview": NaN
},
"glm-4-0520": {
"athene-70b-0725": NaN,
"bard-jan-24-gemini-pro": NaN,
"chatgpt-4o-latest": NaN,
"claude-1": NaN,
"claude-2.0": NaN,
"claude-3-5-sonnet-20240620": 0.41992433795712486,
"claude-3-opus-20240229": 0.45756880733944955,
"gemini-1.5-pro-api-0409-preview": NaN,
"gemini-1.5-pro-exp-0801": NaN,
"gemini-advanced-0514": 0.4226618705035971,
"gemini-pro-dev-api": NaN,
"glm-4-0520": 0.5,
"gpt-3.5-turbo-0314": NaN,
"gpt-4o-2024-05-13": 0.3613861386138614,
"gpt-4o-2024-08-06": NaN,
"gpt-4o-mini-2024-07-18": NaN,
"llama-2-70b-chat": NaN,
"llama-3.1-70b-instruct": NaN,
"nemotron-4-340b-instruct": 0.5087025316455697,
"yi-large-preview": 0.448943661971831
},
"gpt-3.5-turbo-0314": {
"athene-70b-0725": NaN,
"bard-jan-24-gemini-pro": NaN,
"chatgpt-4o-latest": NaN,
"claude-1": 0.4451219512195122,
"claude-2.0": NaN,
"claude-3-5-sonnet-20240620": NaN,
"claude-3-opus-20240229": NaN,
"gemini-1.5-pro-api-0409-preview": NaN,
"gemini-1.5-pro-exp-0801": NaN,
"gemini-advanced-0514": NaN,
"gemini-pro-dev-api": NaN,
"glm-4-0520": NaN,
"gpt-3.5-turbo-0314": 0.5,
"gpt-4o-2024-05-13": NaN,
"gpt-4o-2024-08-06": NaN,
"gpt-4o-mini-2024-07-18": NaN,
"llama-2-70b-chat": NaN,
"llama-3.1-70b-instruct": NaN,
"nemotron-4-340b-instruct": NaN,
"yi-large-preview": NaN
},
"gpt-4o-2024-05-13": {
"athene-70b-0725": 0.5568760611205433,
"bard-jan-24-gemini-pro": NaN,
"chatgpt-4o-latest": 0.45579078455790784,
"claude-1": NaN,
"claude-2.0": NaN,
"claude-3-5-sonnet-20240620": 0.5212426978226234,
"claude-3-opus-20240229": 0.5518305661983823,
"gemini-1.5-pro-api-0409-preview": NaN,
"gemini-1.5-pro-exp-0801": 0.4770554493307839,
"gemini-advanced-0514": 0.5098219766728054,
"gemini-pro-dev-api": NaN,
"glm-4-0520": 0.6386138613861386,
"gpt-3.5-turbo-0314": NaN,
"gpt-4o-2024-05-13": 0.5,
"gpt-4o-2024-08-06": 0.544973544973545,
"gpt-4o-mini-2024-07-18": 0.5163472378804961,
"llama-2-70b-chat": 0.5607476635514018,
"llama-3.1-70b-instruct": 0.5650224215246636,
"nemotron-4-340b-instruct": 0.5968559837728195,
"yi-large-preview": 0.5756445047489823
},
"gpt-4o-2024-08-06": {
"athene-70b-0725": 0.494750656167979,
"bard-jan-24-gemini-pro": NaN,
"chatgpt-4o-latest": 0.4835164835164835,
"claude-1": NaN,
"claude-2.0": NaN,
"claude-3-5-sonnet-20240620": 0.4810126582278481,
"claude-3-opus-20240229": 0.5630372492836676,
"gemini-1.5-pro-api-0409-preview": NaN,
"gemini-1.5-pro-exp-0801": 0.4758928571428571,
"gemini-advanced-0514": NaN,
"gemini-pro-dev-api": NaN,
"glm-4-0520": NaN,
"gpt-3.5-turbo-0314": NaN,
"gpt-4o-2024-05-13": 0.455026455026455,
"gpt-4o-2024-08-06": 0.5,
"gpt-4o-mini-2024-07-18": 0.5052910052910053,
"llama-2-70b-chat": NaN,
"llama-3.1-70b-instruct": 0.5273037542662116,
"nemotron-4-340b-instruct": NaN,
"yi-large-preview": NaN
},
"gpt-4o-mini-2024-07-18": {
"athene-70b-0725": 0.5246212121212122,
"bard-jan-24-gemini-pro": NaN,
"chatgpt-4o-latest": 0.39892665474060823,
"claude-1": NaN,
"claude-2.0": NaN,
"claude-3-5-sonnet-20240620": 0.5251509054325956,
"claude-3-opus-20240229": 0.5562613430127041,
"gemini-1.5-pro-api-0409-preview": NaN,
"gemini-1.5-pro-exp-0801": 0.47044025157232705,
"gemini-advanced-0514": 0.5395927601809954,
"gemini-pro-dev-api": NaN,
"glm-4-0520": NaN,
"gpt-3.5-turbo-0314": NaN,
"gpt-4o-2024-05-13": 0.4836527621195039,
"gpt-4o-2024-08-06": 0.4947089947089947,
"gpt-4o-mini-2024-07-18": 0.5,
"llama-2-70b-chat": NaN,
"llama-3.1-70b-instruct": 0.5584756898817346,
"nemotron-4-340b-instruct": NaN,
"yi-large-preview": 0.553125
},
"llama-2-70b-chat": {
"athene-70b-0725": NaN,
"bard-jan-24-gemini-pro": 0.35074626865671643,
"chatgpt-4o-latest": NaN,
"claude-1": 0.396,
"claude-2.0": 0.41331658291457285,
"claude-3-5-sonnet-20240620": NaN,
"claude-3-opus-20240229": 0.3109048723897912,
"gemini-1.5-pro-api-0409-preview": 0.3125,
"gemini-1.5-pro-exp-0801": NaN,
"gemini-advanced-0514": 0.3854166666666667,
"gemini-pro-dev-api": 0.44642857142857145,
"glm-4-0520": NaN,
"gpt-3.5-turbo-0314": NaN,
"gpt-4o-2024-05-13": 0.4392523364485981,
"gpt-4o-2024-08-06": NaN,
"gpt-4o-mini-2024-07-18": NaN,
"llama-2-70b-chat": 0.5,
"llama-3.1-70b-instruct": NaN,
"nemotron-4-340b-instruct": NaN,
"yi-large-preview": 0.42142857142857143
},
"llama-3.1-70b-instruct": {
"athene-70b-0725": 0.46825396825396826,
"bard-jan-24-gemini-pro": NaN,
"chatgpt-4o-latest": 0.4206161137440758,
"claude-1": NaN,
"claude-2.0": NaN,
"claude-3-5-sonnet-20240620": 0.461038961038961,
"claude-3-opus-20240229": 0.5186480186480187,
"gemini-1.5-pro-api-0409-preview": NaN,
"gemini-1.5-pro-exp-0801": 0.4226907630522088,
"gemini-advanced-0514": 0.4576923076923077,
"gemini-pro-dev-api": NaN,
"glm-4-0520": NaN,
"gpt-3.5-turbo-0314": NaN,
"gpt-4o-2024-05-13": 0.4349775784753363,
"gpt-4o-2024-08-06": 0.4726962457337884,
"gpt-4o-mini-2024-07-18": 0.44152431011826543,
"llama-2-70b-chat": NaN,
"llama-3.1-70b-instruct": 0.5,
"nemotron-4-340b-instruct": NaN,
"yi-large-preview": 0.58
},
"nemotron-4-340b-instruct": {
"athene-70b-0725": NaN,
"bard-jan-24-gemini-pro": NaN,
"chatgpt-4o-latest": NaN,
"claude-1": NaN,
"claude-2.0": NaN,
"claude-3-5-sonnet-20240620": 0.4101642710472279,
"claude-3-opus-20240229": 0.45686900958466453,
"gemini-1.5-pro-api-0409-preview": NaN,
"gemini-1.5-pro-exp-0801": NaN,
"gemini-advanced-0514": 0.4192592592592593,
"gemini-pro-dev-api": NaN,
"glm-4-0520": 0.4912974683544304,
"gpt-3.5-turbo-0314": NaN,
"gpt-4o-2024-05-13": 0.40314401622718055,
"gpt-4o-2024-08-06": NaN,
"gpt-4o-mini-2024-07-18": NaN,
"llama-2-70b-chat": NaN,
"llama-3.1-70b-instruct": NaN,
"nemotron-4-340b-instruct": 0.5,
"yi-large-preview": 0.44568690095846647
},
"yi-large-preview": {
"athene-70b-0725": 0.47058823529411764,
"bard-jan-24-gemini-pro": NaN,
"chatgpt-4o-latest": NaN,
"claude-1": NaN,
"claude-2.0": NaN,
"claude-3-5-sonnet-20240620": 0.4794238683127572,
"claude-3-opus-20240229": 0.47618094475580464,
"gemini-1.5-pro-api-0409-preview": NaN,
"gemini-1.5-pro-exp-0801": 0.41379310344827586,
"gemini-advanced-0514": 0.4397363465160075,
"gemini-pro-dev-api": NaN,
"glm-4-0520": 0.551056338028169,
"gpt-3.5-turbo-0314": NaN,
"gpt-4o-2024-05-13": 0.4243554952510176,
"gpt-4o-2024-08-06": NaN,
"gpt-4o-mini-2024-07-18": 0.446875,
"llama-2-70b-chat": 0.5785714285714286,
"llama-3.1-70b-instruct": 0.42,
"nemotron-4-340b-instruct": 0.5543130990415336,
"yi-large-preview": 0.5
}
},
"models": [
"gemini-1.5-pro-api-0409-preview",
"gemini-1.5-pro-exp-0801",
"chatgpt-4o-latest",
"gpt-3.5-turbo-0314",
"bard-jan-24-gemini-pro",
"claude-1",
"gemini-advanced-0514",
"llama-3.1-70b-instruct",
"gpt-4o-2024-05-13",
"gpt-4o-2024-08-06",
"gpt-4o-mini-2024-07-18",
"claude-3-5-sonnet-20240620",
"claude-3-opus-20240229",
"athene-70b-0725",
"gemini-pro-dev-api",
"claude-2.0",
"glm-4-0520",
"nemotron-4-340b-instruct",
"yi-large-preview",
"llama-2-70b-chat"
],
"online_elo_predicted": {
"athene-70b-0725": {
"athene-70b-0725": 0.5,
"bard-jan-24-gemini-pro": 0.4706024381703526,
"chatgpt-4o-latest": 0.4244076072370654,
"claude-1": 0.4727267496028405,
"claude-2.0": 0.5060871587475949,
"claude-3-5-sonnet-20240620": 0.4924467854859755,
"claude-3-opus-20240229": 0.4983492492034077,
"gemini-1.5-pro-api-0409-preview": 0.3786225197416955,
"gemini-1.5-pro-exp-0801": 0.4227290744565508,
"gemini-advanced-0514": 0.48017931822260296,
"gemini-pro-dev-api": 0.5020917941129024,
"glm-4-0520": 0.5121782721655728,
"gpt-3.5-turbo-0314": 0.45662732701186726,
"gpt-4o-2024-05-13": 0.4813526553622729,
"gpt-4o-2024-08-06": 0.482966766553394,
"gpt-4o-mini-2024-07-18": 0.492191613318456,
"llama-2-70b-chat": 0.5188804447839834,
"llama-3.1-70b-instruct": 0.4804491928529648,
"nemotron-4-340b-instruct": 0.5140723365206418,
"yi-large-preview": 0.5185627002158419
},
"bard-jan-24-gemini-pro": {
"athene-70b-0725": 0.5293975618296474,
"bard-jan-24-gemini-pro": 0.5,
"chatgpt-4o-latest": 0.45339086380344346,
"claude-1": 0.5021311461637057,
"claude-2.0": 0.5354593391190725,
"claude-3-5-sonnet-20240620": 0.5218637663710245,
"claude-3-opus-20240229": 0.5277521980731233,
"gemini-1.5-pro-api-0409-preview": 0.4066882610443156,
"gemini-1.5-pro-exp-0801": 0.45168765548015544,
"gemini-advanced-0514": 0.5095992532128728,
"gemini-pro-dev-api": 0.5314816122659721,
"glm-4-0520": 0.54151638062692,
"gpt-3.5-turbo-0314": 0.48595324754769137,
"gpt-4o-2024-05-13": 0.5107738414883239,
"gpt-4o-2024-08-06": 0.5123891431197954,
"gpt-4o-mini-2024-07-18": 0.5216090163334262,
"llama-2-70b-chat": 0.5481710593389454,
"llama-3.1-70b-instruct": 0.5098694443797749,
"nemotron-4-340b-instruct": 0.5433980845222826,
"yi-large-preview": 0.5478558027577041
},
"chatgpt-4o-latest": {
"athene-70b-0725": 0.5755923927629346,
"bard-jan-24-gemini-pro": 0.5466091361965566,
"chatgpt-4o-latest": 0.5,
"claude-1": 0.5487209243907352,
"claude-2.0": 0.5815294906471586,
"claude-3-5-sonnet-20240620": 0.5681949260650095,
"claude-3-opus-20240229": 0.5739785673924995,
"gemini-1.5-pro-api-0409-preview": 0.45247054063006786,
"gemini-1.5-pro-exp-0801": 0.49828131112534013,
"gemini-advanced-0514": 0.5561079756808931,
"gemini-pro-dev-api": 0.5776350830826076,
"glm-4-0520": 0.587448649362509,
"gpt-3.5-turbo-0314": 0.5326478829339771,
"gpt-4o-2024-05-13": 0.5572679471226406,
"gpt-4o-2024-08-06": 0.5588623197754942,
"gpt-4o-mini-2024-07-18": 0.5679444241258642,
"llama-2-70b-chat": 0.5939365657179976,
"llama-3.1-70b-instruct": 0.5563748494378427,
"nemotron-4-340b-instruct": 0.5892848182429368,
"yi-large-preview": 0.5936295693344523
},
"claude-1": {
"athene-70b-0725": 0.5272732503971594,
"bard-jan-24-gemini-pro": 0.4978688538362943,
"chatgpt-4o-latest": 0.45127907560926495,
"claude-1": 0.5,
"claude-2.0": 0.5333382703189705,
"claude-3-5-sonnet-20240620": 0.5197362986493318,
"claude-3-opus-20240229": 0.5256271146687194,
"gemini-1.5-pro-api-0409-preview": 0.4046329739918908,
"gemini-1.5-pro-exp-0801": 0.44957727554595445,
"gemini-advanced-0514": 0.5074687182117393,
"gemini-pro-dev-api": 0.5293583449328568,
"glm-4-0520": 0.539399178222487,
"gpt-3.5-turbo-0314": 0.483824038337802,
"gpt-4o-2024-05-13": 0.5086434891644779,
"gpt-4o-2024-08-06": 0.5102590804411641,
"gpt-4o-mini-2024-07-18": 0.519481458808123,
"llama-2-70b-chat": 0.5460588266923004,
"llama-3.1-70b-instruct": 0.5077389493164274,
"nemotron-4-340b-instruct": 0.541282210739082,
"yi-large-preview": 0.5457433176189294
},
"claude-2.0": {
"athene-70b-0725": 0.4939128412524052,
"bard-jan-24-gemini-pro": 0.46454066088092755,
"chatgpt-4o-latest": 0.41847050935284147,
"claude-1": 0.46666172968102954,
"claude-2.0": 0.5,
"claude-3-5-sonnet-20240620": 0.4863621348844711,
"claude-3-opus-20240229": 0.4922624014572012,
"gemini-1.5-pro-api-0409-preview": 0.3729109568972296,
"gemini-1.5-pro-exp-0801": 0.41679845455453196,
"gemini-advanced-0514": 0.474104656737174,
"gemini-pro-dev-api": 0.4960044318617072,
"glm-4-0520": 0.5060929201168735,
"gpt-3.5-turbo-0314": 0.4505923459773481,
"gpt-4o-2024-05-13": 0.4752767219072891,
"gpt-4o-2024-08-06": 0.4768891926892078,
"gpt-4o-mini-2024-07-18": 0.4861070959392006,
"llama-2-70b-chat": 0.5127991699819673,
"llama-3.1-70b-instruct": 0.47437423287938824,
"nemotron-4-340b-instruct": 0.5079879147696204,
"yi-large-preview": 0.512481182668407
},
"claude-3-5-sonnet-20240620": {
"athene-70b-0725": 0.5075532145140246,
"bard-jan-24-gemini-pro": 0.4781362336289755,
"chatgpt-4o-latest": 0.4318050739349906,
"claude-1": 0.4802637013506682,
"claude-2.0": 0.5136378651155289,
"claude-3-5-sonnet-20240620": 0.5,
"claude-3-opus-20240229": 0.505902758110997,
"gemini-1.5-pro-api-0409-preview": 0.38575678604471353,
"gemini-1.5-pro-exp-0801": 0.4301191468425039,
"gemini-advanced-0514": 0.4877251820914822,
"gemini-pro-dev-api": 0.5096443991097894,
"glm-4-0520": 0.519724229338601,
"gpt-3.5-turbo-0314": 0.46413354167370846,
"gpt-4o-2024-05-13": 0.48889961603567494,
"gpt-4o-2024-08-06": 0.4905150999228697,
"gpt-4o-mini-2024-07-18": 0.49974476761967085,
"llama-2-70b-chat": 0.526418589284047,
"llama-3.1-70b-instruct": 0.487995316371302,
"nemotron-4-340b-instruct": 0.5216163605038188,
"yi-large-preview": 0.5261012762929619
},
"claude-3-opus-20240229": {
"athene-70b-0725": 0.5016507507965924,
"bard-jan-24-gemini-pro": 0.47224780192687665,
"chatgpt-4o-latest": 0.4260214326075004,
"claude-1": 0.47437288533128064,
"claude-2.0": 0.5077375985427988,
"claude-3-5-sonnet-20240620": 0.49409724188900295,
"claude-3-opus-20240229": 0.5,
"gemini-1.5-pro-api-0409-preview": 0.3801772378799626,
"gemini-1.5-pro-exp-0801": 0.4243412226190692,
"gemini-advanced-0514": 0.4818276906995902,
"gemini-pro-dev-api": 0.5037424932177174,
"glm-4-0520": 0.513827911016007,
"gpt-3.5-turbo-0314": 0.4582661256484433,
"gpt-4o-2024-05-13": 0.4830013131364567,
"gpt-4o-2024-08-06": 0.48461578707994823,
"gpt-4o-mini-2024-07-18": 0.4938420466183484,
"llama-2-70b-chat": 0.520528636323993,
"llama-3.1-70b-instruct": 0.48209763256058513,
"nemotron-4-340b-instruct": 0.5157216264686881,
"yi-large-preview": 0.5202109737620978
},
"gemini-1.5-pro-api-0409-preview": {
"athene-70b-0725": 0.6213774802583045,
"bard-jan-24-gemini-pro": 0.5933117389556843,
"chatgpt-4o-latest": 0.5475294593699321,
"claude-1": 0.5953670260081093,
"claude-2.0": 0.6270890431027705,
"claude-3-5-sonnet-20240620": 0.6142432139552865,
"claude-3-opus-20240229": 0.6198227621200374,
"gemini-1.5-pro-api-0409-preview": 0.5,
"gemini-1.5-pro-exp-0801": 0.5458257442135583,
"gemini-advanced-0514": 0.6025435895577902,
"gemini-pro-dev-api": 0.6233440078256695,
"glm-4-0520": 0.6327707226950309,
"gpt-3.5-turbo-0314": 0.579682755828864,
"gpt-4o-2024-05-13": 0.6036686971453942,
"gpt-4o-2024-08-06": 0.6052143488362124,
"gpt-4o-mini-2024-07-18": 0.6140012780307695,
"llama-2-70b-chat": 0.6389839091289585,
"llama-3.1-70b-instruct": 0.6028024856995498,
"nemotron-4-340b-instruct": 0.6345306684098833,
"yi-large-preview": 0.6386902518991052
},
"gemini-1.5-pro-exp-0801": {
"athene-70b-0725": 0.5772709255434492,
"bard-jan-24-gemini-pro": 0.5483123445198447,
"chatgpt-4o-latest": 0.5017186888746599,
"claude-1": 0.5504227244540455,
"claude-2.0": 0.5832015454454681,
"claude-3-5-sonnet-20240620": 0.569880853157496,
"claude-3-opus-20240229": 0.5756587773809309,
"gemini-1.5-pro-api-0409-preview": 0.4541742557864416,
"gemini-1.5-pro-exp-0801": 0.5,
"gemini-advanced-0514": 0.557804367756876,
"gemini-pro-dev-api": 0.5793114416789945,
"glm-4-0520": 0.5891137641041534,
"gpt-3.5-turbo-0314": 0.5343588600926158,
"gpt-4o-2024-05-13": 0.5589634219132797,
"gpt-4o-2024-08-06": 0.5605565036097535,
"gpt-4o-mini-2024-07-18": 0.5696305884618997,
"llama-2-70b-chat": 0.5955935211645167,
"llama-3.1-70b-instruct": 0.5580710321113055,
"nemotron-4-340b-instruct": 0.5909476824028691,
"yi-large-preview": 0.5952869238841539
},
"gemini-advanced-0514": {
"athene-70b-0725": 0.519820681777397,
"bard-jan-24-gemini-pro": 0.49040074678712725,
"chatgpt-4o-latest": 0.4438920243191068,
"claude-1": 0.4925312817882607,
"claude-2.0": 0.525895343262826,
"claude-3-5-sonnet-20240620": 0.5122748179085177,
"claude-3-opus-20240229": 0.5181723093004097,
"gemini-1.5-pro-api-0409-preview": 0.3974564104422098,
"gemini-1.5-pro-exp-0801": 0.44219563224312397,
"gemini-advanced-0514": 0.5,
"gemini-pro-dev-api": 0.5219088424590324,
"glm-4-0520": 0.5319680879028288,
"gpt-3.5-turbo-0314": 0.4763667410118573,
"gpt-4o-2024-05-13": 0.5011750743841354,
"gpt-4o-2024-08-06": 0.5027912177061772,
"gpt-4o-mini-2024-07-18": 0.5120197361562043,
"llama-2-70b-chat": 0.5386432816976552,
"llama-3.1-70b-instruct": 0.5002702935966019,
"nemotron-4-340b-instruct": 0.5338552462294891,
"yi-large-preview": 0.5383269761251527
},
"gemini-pro-dev-api": {
"athene-70b-0725": 0.4979082058870976,
"bard-jan-24-gemini-pro": 0.4685183877340279,
"chatgpt-4o-latest": 0.4223649169173924,
"claude-1": 0.47064165506714317,
"claude-2.0": 0.5039955681382927,
"claude-3-5-sonnet-20240620": 0.4903556008902106,
"claude-3-opus-20240229": 0.4962575067822826,
"gemini-1.5-pro-api-0409-preview": 0.37665599217433055,
"gemini-1.5-pro-exp-0801": 0.4206885583210056,
"gemini-advanced-0514": 0.4780911575409677,
"gemini-pro-dev-api": 0.5,
"glm-4-0520": 0.5100875059468505,
"gpt-3.5-turbo-0314": 0.45455202627804153,
"gpt-4o-2024-05-13": 0.47926409658161856,
"gpt-4o-2024-08-06": 0.4808776977523424,
"gpt-4o-mini-2024-07-18": 0.49010046598318574,
"llama-2-70b-chat": 0.5167913032942285,
"llama-3.1-70b-instruct": 0.4783609385670775,
"nemotron-4-340b-instruct": 0.5119819532314825,
"yi-large-preview": 0.516473464718453
},
"glm-4-0520": {
"athene-70b-0725": 0.4878217278344273,
"bard-jan-24-gemini-pro": 0.4584836193730801,
"chatgpt-4o-latest": 0.41255135063749104,
"claude-1": 0.4606008217775131,
"claude-2.0": 0.49390707988312654,
"claude-3-5-sonnet-20240620": 0.48027577066139915,
"claude-3-opus-20240229": 0.48617208898399294,
"gemini-1.5-pro-api-0409-preview": 0.3672292773049691,
"gemini-1.5-pro-exp-0801": 0.4108862358958465,
"gemini-advanced-0514": 0.4680319120971712,
"gemini-pro-dev-api": 0.4899124940531495,
"glm-4-0520": 0.5,
"gpt-3.5-turbo-0314": 0.444566176363753,
"gpt-4o-2024-05-13": 0.4692023588423809,
"gpt-4o-2024-08-06": 0.47081271228911237,
"gpt-4o-mini-2024-07-18": 0.4800209406003484,
"llama-2-70b-chat": 0.5067083424471972,
"llama-3.1-70b-instruct": 0.4683011100825741,
"nemotron-4-340b-instruct": 0.5018953636400675,
"yi-large-preview": 0.5063902063717687
},
"gpt-3.5-turbo-0314": {
"athene-70b-0725": 0.5433726729881329,
"bard-jan-24-gemini-pro": 0.5140467524523086,
"chatgpt-4o-latest": 0.4673521170660229,
"claude-1": 0.5161759616621979,
"claude-2.0": 0.5494076540226519,
"claude-3-5-sonnet-20240620": 0.5358664583262915,
"claude-3-opus-20240229": 0.5417338743515567,
"gemini-1.5-pro-api-0409-preview": 0.4203172441711361,
"gemini-1.5-pro-exp-0801": 0.4656411399073841,
"gemini-advanced-0514": 0.5236332589881427,
"gemini-pro-dev-api": 0.5454479737219585,
"glm-4-0520": 0.5554338236362469,
"gpt-3.5-turbo-0314": 0.5,
"gpt-4o-2024-05-13": 0.5248055778856127,
"gpt-4o-2024-08-06": 0.5264175061108259,
"gpt-4o-mini-2024-07-18": 0.5356125299744643,
"llama-2-70b-chat": 0.5620498683755348,
"llama-3.1-70b-instruct": 0.5239029418245605,
"nemotron-4-340b-instruct": 0.5573051037168208,
"yi-large-preview": 0.5617365533684751
},
"gpt-4o-2024-05-13": {
"athene-70b-0725": 0.5186473446377271,
"bard-jan-24-gemini-pro": 0.48922615851167606,
"chatgpt-4o-latest": 0.4427320528773593,
"claude-1": 0.49135651083552223,
"claude-2.0": 0.5247232780927109,
"claude-3-5-sonnet-20240620": 0.511100383964325,
"claude-3-opus-20240229": 0.5169986868635432,
"gemini-1.5-pro-api-0409-preview": 0.3963313028546058,
"gemini-1.5-pro-exp-0801": 0.44103657808672025,
"gemini-advanced-0514": 0.4988249256158645,
"gemini-pro-dev-api": 0.5207359034183815,
"glm-4-0520": 0.5307976411576192,
"gpt-3.5-turbo-0314": 0.4751944221143874,
"gpt-4o-2024-05-13": 0.5,
"gpt-4o-2024-08-06": 0.501616164525399,
"gpt-4o-mini-2024-07-18": 0.5108452744903431,
"llama-2-70b-chat": 0.5374750140847713,
"llama-3.1-70b-instruct": 0.4990952180629769,
"nemotron-4-340b-instruct": 0.5326853730599583,
"yi-large-preview": 0.537158595794847
},
"gpt-4o-2024-08-06": {
"athene-70b-0725": 0.517033233446606,
"bard-jan-24-gemini-pro": 0.48761085688020456,
"chatgpt-4o-latest": 0.44113768022450583,
"claude-1": 0.4897409195588359,
"claude-2.0": 0.5231108073107922,
"claude-3-5-sonnet-20240620": 0.5094849000771304,
"claude-3-opus-20240229": 0.5153842129200518,
"gemini-1.5-pro-api-0409-preview": 0.39478565116378755,
"gemini-1.5-pro-exp-0801": 0.4394434963902465,
"gemini-advanced-0514": 0.4972087822938228,
"gemini-pro-dev-api": 0.5191223022476575,
"glm-4-0520": 0.5291872877108876,
"gpt-3.5-turbo-0314": 0.473582493889174,
"gpt-4o-2024-05-13": 0.4983838354746009,
"gpt-4o-2024-08-06": 0.5,
"gpt-4o-mini-2024-07-18": 0.5092297570723646,
"llama-2-70b-chat": 0.5358675389424615,
"llama-3.1-70b-instruct": 0.49747906828277433,
"nemotron-4-340b-instruct": 0.531075774838379,
"yi-large-preview": 0.5355509712390535
},
"gpt-4o-mini-2024-07-18": {
"athene-70b-0725": 0.507808386681544,
"bard-jan-24-gemini-pro": 0.47839098366657384,
"chatgpt-4o-latest": 0.43205557587413584,
"claude-1": 0.48051854119187704,
"claude-2.0": 0.5138929040607993,
"claude-3-5-sonnet-20240620": 0.5002552323803292,
"claude-3-opus-20240229": 0.5061579533816516,
"gemini-1.5-pro-api-0409-preview": 0.38599872196923046,
"gemini-1.5-pro-exp-0801": 0.4303694115381003,
"gemini-advanced-0514": 0.4879802638437957,
"gemini-pro-dev-api": 0.5098995340168142,
"glm-4-0520": 0.5199790593996516,
"gpt-3.5-turbo-0314": 0.4643874700255357,
"gpt-4o-2024-05-13": 0.4891547255096569,
"gpt-4o-2024-08-06": 0.4907702429276354,
"gpt-4o-mini-2024-07-18": 0.5,
"llama-2-70b-chat": 0.5266731022503263,
"llama-3.1-70b-instruct": 0.4882504047493449,
"nemotron-4-340b-instruct": 0.5218711102154154,
"yi-large-preview": 0.5263558063552674
},
"llama-2-70b-chat": {
"athene-70b-0725": 0.4811195552160165,
"bard-jan-24-gemini-pro": 0.4518289406610546,
"chatgpt-4o-latest": 0.40606343428200253,
"claude-1": 0.4539411733076995,
"claude-2.0": 0.4872008300180327,
"claude-3-5-sonnet-20240620": 0.4735814107159531,
"claude-3-opus-20240229": 0.4794713636760069,
"gemini-1.5-pro-api-0409-preview": 0.3610160908710415,
"gemini-1.5-pro-exp-0801": 0.40440647883548325,
"gemini-advanced-0514": 0.4613567183023448,
"gemini-pro-dev-api": 0.4832086967057715,
"glm-4-0520": 0.4932916575528028,
"gpt-3.5-turbo-0314": 0.43795013162446517,
"gpt-4o-2024-05-13": 0.4625249859152287,
"gpt-4o-2024-08-06": 0.46413246105753847,
"gpt-4o-mini-2024-07-18": 0.47332689774967374,
"llama-2-70b-chat": 0.5,
"llama-3.1-70b-instruct": 0.46162540860366846,
"nemotron-4-340b-instruct": 0.49518677639716263,
"yi-large-preview": 0.49968180936417794
},
"llama-3.1-70b-instruct": {
"athene-70b-0725": 0.5195508071470352,
"bard-jan-24-gemini-pro": 0.4901305556202252,
"chatgpt-4o-latest": 0.4436251505621573,
"claude-1": 0.4922610506835726,
"claude-2.0": 0.5256257671206118,
"claude-3-5-sonnet-20240620": 0.512004683628698,
"claude-3-opus-20240229": 0.5179023674394149,
"gemini-1.5-pro-api-0409-preview": 0.3971975143004503,
"gemini-1.5-pro-exp-0801": 0.4419289678886945,
"gemini-advanced-0514": 0.49972970640339814,
"gemini-pro-dev-api": 0.5216390614329225,
"glm-4-0520": 0.531698889917426,
"gpt-3.5-turbo-0314": 0.47609705817543935,
"gpt-4o-2024-05-13": 0.5009047819370231,
"gpt-4o-2024-08-06": 0.5025209317172257,
"gpt-4o-mini-2024-07-18": 0.5117495952506551,
"llama-2-70b-chat": 0.5383745913963316,
"llama-3.1-70b-instruct": 0.5,
"nemotron-4-340b-instruct": 0.5335861820021834,
"yi-large-preview": 0.5380582595922262
},
"nemotron-4-340b-instruct": {
"athene-70b-0725": 0.4859276634793583,
"bard-jan-24-gemini-pro": 0.45660191547771745,
"chatgpt-4o-latest": 0.4107151817570633,
"claude-1": 0.458717789260918,
"claude-2.0": 0.4920120852303797,
"claude-3-5-sonnet-20240620": 0.47838363949618123,
"claude-3-opus-20240229": 0.4842783735313118,
"gemini-1.5-pro-api-0409-preview": 0.3654693315901167,
"gemini-1.5-pro-exp-0801": 0.4090523175971309,
"gemini-advanced-0514": 0.4661447537705109,
"gemini-pro-dev-api": 0.4880180467685175,
"glm-4-0520": 0.49810463635993246,
"gpt-3.5-turbo-0314": 0.4426948962831791,
"gpt-4o-2024-05-13": 0.4673146269400416,
"gpt-4o-2024-08-06": 0.46892422516162097,
"gpt-4o-mini-2024-07-18": 0.47812888978458457,
"llama-2-70b-chat": 0.5048132236028374,
"llama-3.1-70b-instruct": 0.4664138179978165,
"nemotron-4-340b-instruct": 0.5,
"yi-large-preview": 0.5044950605041638
},
"yi-large-preview": {
"athene-70b-0725": 0.48143729978415806,
"bard-jan-24-gemini-pro": 0.4521441972422958,
"chatgpt-4o-latest": 0.4063704306655477,
"claude-1": 0.45425668238107064,
"claude-2.0": 0.48751881733159297,
"claude-3-5-sonnet-20240620": 0.4738987237070381,
"claude-3-opus-20240229": 0.47978902623790215,
"gemini-1.5-pro-api-0409-preview": 0.3613097481008948,
"gemini-1.5-pro-exp-0801": 0.40471307611584606,
"gemini-advanced-0514": 0.4616730238748474,
"gemini-pro-dev-api": 0.48352653528154693,
"glm-4-0520": 0.4936097936282313,
"gpt-3.5-turbo-0314": 0.4382634466315249,
"gpt-4o-2024-05-13": 0.462841404205153,
"gpt-4o-2024-08-06": 0.46444902876094657,
"gpt-4o-mini-2024-07-18": 0.4736441936447327,
"llama-2-70b-chat": 0.5003181906358221,
"llama-3.1-70b-instruct": 0.4619417404077737,
"nemotron-4-340b-instruct": 0.4955049394958363,
"yi-large-preview": 0.5
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

@@ -0,0 +1,66 @@
"""Fail-closed verifier for a saved Experiment 7-7 run."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
while chunk := handle.read(8 * 1024 * 1024):
digest.update(chunk)
return digest.hexdigest()
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("run_dir", type=Path)
parser.add_argument("--input", type=Path, help="Optionally re-hash the 2 GB Arena input")
args = parser.parse_args()
manifest_path = args.run_dir / "manifest.json"
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
failures: list[str] = []
if manifest.get("experiment") != "7-7":
failures.append("wrong experiment id")
if manifest.get("status") != "passed" or manifest.get("official_complete") is not True:
failures.append("run is not officially complete")
false_gates = sorted(name for name, passed in manifest.get("gates", {}).items() if passed is not True)
if false_gates:
failures.append(f"false gates: {false_gates}")
for name, expected in manifest.get("artifacts", {}).items():
path = args.run_dir / name
if not path.is_file():
failures.append(f"missing artifact: {name}")
continue
if path.stat().st_size != expected.get("bytes"):
failures.append(f"size mismatch: {name}")
if sha256_file(path) != expected.get("sha256"):
failures.append(f"sha256 mismatch: {name}")
project = Path(__file__).resolve().parents[1]
for name, expected_hash in manifest.get("sources", {}).items():
path = project / name
if not path.is_file() or sha256_file(path) != expected_hash:
failures.append(f"source mismatch: {name}")
if args.input:
expected = manifest["input"]
if args.input.stat().st_size != expected["bytes"]:
failures.append("input size mismatch")
if sha256_file(args.input) != expected["sha256"]:
failures.append("input sha256 mismatch")
result = {"valid": not failures, "failures": failures}
print(json.dumps(result, indent=2))
if failures:
raise SystemExit(1)
if __name__ == "__main__":
main()
+283
View File
@@ -0,0 +1,283 @@
"""
Visualization tools for Elo leaderboard analysis
"""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from typing import List, Tuple, Optional
import plotly.graph_objects as go
import plotly.express as px
def plot_leaderboard(leaderboard_data: list, top_n: int = 20, save_path: Optional[str] = None):
"""
Plot static leaderboard bar chart.
Args:
leaderboard_data: List of (model, rating, matches, wins) tuples
top_n: Number of top models to display
save_path: If provided, save figure to this path
"""
# Convert to DataFrame and get top N
df = pd.DataFrame(leaderboard_data, columns=['model', 'rating', 'matches', 'wins'])
df = df.head(top_n)
# Create figure
fig, ax = plt.subplots(figsize=(12, 8))
# Create horizontal bar chart
bars = ax.barh(range(len(df)), df['rating'], color=plt.cm.viridis(np.linspace(0, 1, len(df))))
# Customize
ax.set_yticks(range(len(df)))
ax.set_yticklabels(df['model'])
ax.set_xlabel('Elo Rating', fontsize=12)
ax.set_title(f'Model Leaderboard - Top {top_n} Models', fontsize=14, fontweight='bold')
ax.invert_yaxis() # Highest rating at top
# Add value labels on bars
for i, (rating, matches) in enumerate(zip(df['rating'], df['matches'])):
ax.text(rating + 10, i, f'{rating:.0f} ({matches} matches)',
va='center', fontsize=9)
ax.grid(axis='x', alpha=0.3)
plt.tight_layout()
if save_path:
plt.savefig(save_path, dpi=300, bbox_inches='tight')
print(f"Saved leaderboard to {save_path}")
plt.show()
def plot_win_rate_matrix(win_rate_df: pd.DataFrame,
top_n: int = 15,
save_path: Optional[str] = None):
"""
Plot heatmap of win rate matrix.
Args:
win_rate_df: DataFrame with win rates (rows beat columns)
top_n: Number of models to include
save_path: If provided, save figure to this path
"""
# Get top N models by average win rate
avg_win_rates = win_rate_df.mean(axis=1).sort_values(ascending=False)
top_models = avg_win_rates.head(top_n).index.tolist()
# Subset matrix
subset = win_rate_df.loc[top_models, top_models]
# Create figure
fig, ax = plt.subplots(figsize=(14, 12))
# Plot heatmap
sns.heatmap(subset, annot=True, fmt='.2f', cmap='RdYlGn', center=0.5,
vmin=0, vmax=1, square=True, linewidths=0.5,
cbar_kws={'label': 'Win Rate'}, ax=ax)
ax.set_title(f'Win Rate Matrix - Top {top_n} Models\n(Row vs Column)',
fontsize=14, fontweight='bold')
ax.set_xlabel('Opponent (Column)', fontsize=12)
ax.set_ylabel('Model (Row)', fontsize=12)
plt.tight_layout()
if save_path:
plt.savefig(save_path, dpi=300, bbox_inches='tight')
print(f"Saved win rate matrix to {save_path}")
plt.show()
def plot_rating_history(history_df: pd.DataFrame,
models: Optional[List[str]] = None,
top_n: int = 10,
save_path: Optional[str] = None):
"""
Plot rating evolution over time for selected models.
Args:
history_df: DataFrame with columns: date, model, rating
models: List of specific models to plot (if None, plot top N)
top_n: If models not specified, plot top N models by final rating
save_path: If provided, save figure to this path
"""
if models is None:
# Get top N models by final rating
final_date = history_df['date'].max()
final_ratings = history_df[history_df['date'] == final_date].nlargest(top_n, 'rating')
models = final_ratings['model'].tolist()
# Filter data
plot_data = history_df[history_df['model'].isin(models)].copy()
# Create figure
fig, ax = plt.subplots(figsize=(14, 8))
# Plot each model
for model in models:
model_data = plot_data[plot_data['model'] == model].sort_values('date')
ax.plot(model_data['date'], model_data['rating'], marker='o',
label=model, linewidth=2, markersize=4)
ax.set_xlabel('Date', fontsize=12)
ax.set_ylabel('Elo Rating', fontsize=12)
ax.set_title('Model Rating Evolution Over Time', fontsize=14, fontweight='bold')
ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left', fontsize=9)
ax.grid(alpha=0.3)
plt.xticks(rotation=45)
plt.tight_layout()
if save_path:
plt.savefig(save_path, dpi=300, bbox_inches='tight')
print(f"Saved rating history to {save_path}")
plt.show()
def plot_rating_distribution(leaderboard_data: list, save_path: Optional[str] = None):
"""
Plot distribution of ratings across all models.
Args:
leaderboard_data: List of (model, rating, matches, wins) tuples
save_path: If provided, save figure to this path
"""
df = pd.DataFrame(leaderboard_data, columns=['model', 'rating', 'matches', 'wins'])
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
# Histogram
ax1.hist(df['rating'], bins=30, color='steelblue', edgecolor='black', alpha=0.7)
ax1.axvline(df['rating'].mean(), color='red', linestyle='--',
linewidth=2, label=f'Mean: {df["rating"].mean():.1f}')
ax1.axvline(df['rating'].median(), color='green', linestyle='--',
linewidth=2, label=f'Median: {df["rating"].median():.1f}')
ax1.set_xlabel('Elo Rating', fontsize=12)
ax1.set_ylabel('Count', fontsize=12)
ax1.set_title('Rating Distribution', fontsize=13, fontweight='bold')
ax1.legend()
ax1.grid(alpha=0.3)
# Box plot
ax2.boxplot(df['rating'], vert=True)
ax2.set_ylabel('Elo Rating', fontsize=12)
ax2.set_title('Rating Box Plot', fontsize=13, fontweight='bold')
ax2.grid(alpha=0.3)
plt.tight_layout()
if save_path:
plt.savefig(save_path, dpi=300, bbox_inches='tight')
print(f"Saved rating distribution to {save_path}")
plt.show()
def create_interactive_leaderboard(history_df: pd.DataFrame, top_n: int = 15) -> go.Figure:
"""
Create interactive Plotly visualization of ranking evolution.
Args:
history_df: DataFrame with columns: date, model, rating, rank
top_n: Number of top models to include
Returns:
Plotly Figure object
"""
# Get top N models by final rating
final_date = history_df['date'].max()
final_ratings = history_df[history_df['date'] == final_date].nlargest(top_n, 'rating')
top_models = final_ratings['model'].tolist()
# Filter data
plot_data = history_df[history_df['model'].isin(top_models)].copy()
# Create figure
fig = go.Figure()
for model in top_models:
model_data = plot_data[plot_data['model'] == model].sort_values('date')
fig.add_trace(go.Scatter(
x=model_data['date'],
y=model_data['rating'],
mode='lines+markers',
name=model,
hovertemplate='<b>%{fullData.name}</b><br>' +
'Date: %{x}<br>' +
'Rating: %{y:.0f}<br>' +
'<extra></extra>'
))
fig.update_layout(
title='Interactive Model Rating Evolution',
xaxis_title='Date',
yaxis_title='Elo Rating',
hovermode='closest',
height=600,
legend=dict(
yanchor="top",
y=0.99,
xanchor="left",
x=0.01
)
)
return fig
def create_rank_evolution_chart(history_df: pd.DataFrame, top_n: int = 15) -> go.Figure:
"""
Create interactive rank evolution chart (lower rank number is better).
Args:
history_df: DataFrame with columns: date, model, rating, rank
top_n: Number of models to track
Returns:
Plotly Figure object
"""
# Get models that were ever in top N
models_in_top = history_df[history_df['rank'] <= top_n]['model'].unique()
# Filter data
plot_data = history_df[history_df['model'].isin(models_in_top)].copy()
# Create figure
fig = go.Figure()
for model in models_in_top:
model_data = plot_data[plot_data['model'] == model].sort_values('date')
fig.add_trace(go.Scatter(
x=model_data['date'],
y=model_data['rank'],
mode='lines+markers',
name=model,
hovertemplate='<b>%{fullData.name}</b><br>' +
'Date: %{x}<br>' +
'Rank: #%{y}<br>' +
'<extra></extra>'
))
fig.update_layout(
title=f'Model Rank Evolution (Top {top_n})',
xaxis_title='Date',
yaxis_title='Rank',
yaxis=dict(autorange='reversed'), # Lower rank at top
hovermode='closest',
height=600,
legend=dict(
yanchor="top",
y=0.99,
xanchor="right",
x=0.99
)
)
return fig