Skip to content

Commit 991aaa0

Browse files
committed
update elo to include include-round-0 flag for ladder construction
1 parent 7bd24cb commit 991aaa0

2 files changed

Lines changed: 52 additions & 24 deletions

File tree

  • .claude/skills/create-arena-ladder
  • codeclash/analysis/metrics

.claude/skills/create-arena-ladder/SKILL.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,14 @@ Local validation is necessary but NOT sufficient — a local shim skips Docker,
104104
`GITHUB_TOKEN=$(gh auth token) uv run codeclash ladder make configs/ablations/ladder/make_<a>.yaml --workers N`
105105
→ logs land under `logs/ladder/<A>/`. Fast arenas run on a laptop; big ones (e.g. SCML's
106106
~1275 pairs) want an AWS box under `tmux`/`nohup`.
107-
4. Rank: `python -m codeclash.analysis.metrics.elo -d logs/ladder/<A> --output-dir assets/<a>_elo`
107+
4. Rank: `uv run python -m codeclash.analysis.metrics.elo -d logs/ladder/<A> --include-round-0 --output-dir assets/<a>_elo`
108108
→ prints the Bradley-Terry/Elo order (weakest → strongest); that ordering IS the ladder.
109-
Tip: run a cheap low-`sims` pilot first and eyeball that baselines sit near the bottom.
109+
- **`--include-round-0` is REQUIRED for ladder construction.** `ladder make` uses
110+
`tournament.rounds: 0`, so round 0 IS the match; without the flag every tournament is
111+
dropped (round 0 is normally the excluded identical-codebases baseline) and the fit
112+
crashes on an empty matrix. Do NOT pass it for normal multi-round PvP/climbing Elo.
113+
- Use `uv run python`, not bare `python` — the analysis deps (matplotlib) live in the uv venv.
114+
- Tip: run a cheap low-`sims` pilot first and eyeball that baselines sit near the bottom.
110115

111116
## Phase 5 — Assemble the ranked configs
112117

codeclash/analysis/metrics/elo.py

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def __init__(
4040
score_type: SCORING_TYPES = "per_round_tertiary",
4141
max_round: int = 15,
4242
only_specific_round: bool = False,
43+
include_round_0: bool = False,
4344
):
4445
"""This class builds a win matrix from a log directory, it doesn't fit anything yet.
4546
It also adds a "ALL" game to the win matrix, which is the sum of all games.
@@ -62,6 +63,9 @@ def __init__(
6263
6364
The `max_round` parameter controls the maximum number of rounds to include in the score calculation (default: 15).
6465
The `only_specific_round` parameter controls whether to only include the specific round (True) or all rounds up to max_round (False).
66+
The `include_round_0` parameter controls whether round 0 is counted. In normal PvP/climbing
67+
tournaments round 0 is the identical-codebases baseline and is excluded. For ladder
68+
construction (`ladder make`, `tournament.rounds: 0`) round 0 IS the match, so set this True.
6569
"""
6670
self.win_matrix: dict[str, dict[tuple[str, str], list[float]]] = defaultdict(
6771
lambda: defaultdict(lambda: [0.0, 0.0])
@@ -71,6 +75,7 @@ def __init__(
7175
self.score_type = score_type
7276
self.max_round = max_round
7377
self.only_specific_round = only_specific_round
78+
self.include_round_0 = include_round_0
7479
self._samples: dict[str, dict[tuple[str, str], list[tuple[float, float]]]] = defaultdict(
7580
lambda: defaultdict(list)
7681
)
@@ -154,13 +159,19 @@ def _process_tournament(self, metadata_path: Path) -> None:
154159
return
155160

156161
player_names = [p["name"] for p in players]
157-
models = [p["config"]["model"]["model_name"].strip("@") for p in players]
162+
models = []
163+
for p in players:
164+
try:
165+
models.append(p["config"]["model"]["model_name"].strip("@"))
166+
except KeyError:
167+
# Ladder bots have no model config; identify by branch (flatten "/" to keep years distinct).
168+
models.append(p["name"].removeprefix("human/").replace("/", "__"))
158169

159170
# Aggregate scores for each round
160171
p1_round_scores = []
161172
p2_round_scores = []
162173
for idx, stats in metadata["round_stats"].items():
163-
if idx == "0":
174+
if idx == "0" and not self.include_round_0:
164175
continue
165176

166177
round_num = int(idx)
@@ -1547,6 +1558,12 @@ def write_latex_table_plain(results: dict[str, dict], output_dir: Path) -> None:
15471558
parser = argparse.ArgumentParser(description="Build win matrix and fit Bradley-Terry model")
15481559
parser.add_argument("-d", "--log_dir", type=Path, default=LOCAL_LOG_DIR)
15491560
parser.add_argument("--print-matrix", action="store_true", help="Print win matrix")
1561+
parser.add_argument(
1562+
"--include-round-0",
1563+
action="store_true",
1564+
help="Count round 0 (normally the excluded identical-codebases baseline). REQUIRED for "
1565+
"ladder construction (`ladder make` uses tournament.rounds: 0, so round 0 IS the match).",
1566+
)
15501567
parser.add_argument(
15511568
"-s",
15521569
"--score-type",
@@ -1578,7 +1595,9 @@ def write_latex_table_plain(results: dict[str, dict], output_dir: Path) -> None:
15781595
args = parser.parse_args()
15791596

15801597
builder = ScoreMatrixBuilder(
1581-
all_games_normalization_scheme=args.all_normalization_scheme, score_type=args.score_type
1598+
all_games_normalization_scheme=args.all_normalization_scheme,
1599+
score_type=args.score_type,
1600+
include_round_0=args.include_round_0,
15821601
)
15831602
builder.build(args.log_dir)
15841603

@@ -1632,22 +1651,26 @@ def write_latex_table_plain(results: dict[str, dict], output_dir: Path) -> None:
16321651
).run()
16331652
write_bootstrap_metrics_table(bootstrap_results, args.output_dir, game="ALL")
16341653

1635-
logger.info("Running EloVsMaxRounds analysis")
1636-
EloVsMaxRounds(
1637-
log_dir=args.log_dir,
1638-
max_rounds=15,
1639-
all_games_normalization_scheme=args.all_normalization_scheme,
1640-
score_type=args.score_type,
1641-
regularization=args.regularization,
1642-
output_dir=args.output_dir,
1643-
).run()
1644-
1645-
logger.info("Running EloOnlyAtRound analysis")
1646-
EloOnlyAtRound(
1647-
log_dir=args.log_dir,
1648-
max_rounds=15,
1649-
all_games_normalization_scheme=args.all_normalization_scheme,
1650-
score_type=args.score_type,
1651-
regularization=args.regularization,
1652-
output_dir=args.output_dir,
1653-
).run()
1654+
# Max-round analyses are multi-round-only; skip them for single-round ladder round-robins.
1655+
if not args.include_round_0:
1656+
logger.info("Running EloVsMaxRounds analysis")
1657+
EloVsMaxRounds(
1658+
log_dir=args.log_dir,
1659+
max_rounds=15,
1660+
all_games_normalization_scheme=args.all_normalization_scheme,
1661+
score_type=args.score_type,
1662+
regularization=args.regularization,
1663+
output_dir=args.output_dir,
1664+
).run()
1665+
1666+
logger.info("Running EloOnlyAtRound analysis")
1667+
EloOnlyAtRound(
1668+
log_dir=args.log_dir,
1669+
max_rounds=15,
1670+
all_games_normalization_scheme=args.all_normalization_scheme,
1671+
score_type=args.score_type,
1672+
regularization=args.regularization,
1673+
output_dir=args.output_dir,
1674+
).run()
1675+
else:
1676+
logger.info("Skipping EloVsMaxRounds / EloOnlyAtRound (ladder mode: single round-0 round-robin)")

0 commit comments

Comments
 (0)