Skip to content

Commit 70346c2

Browse files
committed
Merge branch 'main' of github.com:emagedoc/CodeClash
2 parents 88237f3 + 6bb2068 commit 70346c2

3 files changed

Lines changed: 249 additions & 143 deletions

File tree

codeclash/analysis/metrics/elo2.py

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -871,7 +871,7 @@ def _create_elo_violin_plot(
871871
self._save_plot(output_dir, f"{self.game}_elo_violin_{self.bootstrap_type}")
872872
plt.close()
873873

874-
def run(self) -> None:
874+
def run(self) -> dict:
875875
game = self.game
876876
assert game in self.builder.win_matrix, f"Game '{game}' not found in win matrix"
877877

@@ -988,6 +988,15 @@ def run(self) -> None:
988988
self._create_rank_matrix_plot(players, rank_samples, bootstrap_dir)
989989
self._create_elo_violin_plot(players, elo_samples, baseline_elos, bootstrap_dir)
990990

991+
return {
992+
"kendall_tau": mean_tau,
993+
"spearman_rho": mean_rho,
994+
"footrule": mean_foot,
995+
"top1_consistency": top1_consistency,
996+
"pairwise_agreement": pairwise_agreement,
997+
"topk_overlap": {k: float(np.mean(topk_overlap[k])) for k in topks},
998+
}
999+
9911000

9921001
class EloVsMaxRounds:
9931002
def __init__(
@@ -1375,6 +1384,123 @@ def write_website_results(results: dict[str, dict], output_dir: Path) -> None:
13751384
logger.info(f"Saved leaderboard JSON: {output_file}")
13761385

13771386

1387+
def write_bootstrap_metrics_table(bootstrap_results: dict[str, dict], output_dir: Path, game: str = "ALL") -> None:
1388+
"""Write LaTeX table comparing bootstrap metrics between methods.
1389+
1390+
Args:
1391+
bootstrap_results: Dictionary mapping bootstrap type to metrics dict
1392+
output_dir: Directory to save the LaTeX file
1393+
game: Game name for the table caption
1394+
"""
1395+
output_dir.mkdir(parents=True, exist_ok=True)
1396+
output_file = output_dir / "bootstrap_metrics.tex"
1397+
1398+
lines = []
1399+
lines.append(r"\begin{tabular}{lcc}")
1400+
lines.append(r"\toprule")
1401+
lines.append(r"Metric & Nonparametric & Parametric \\")
1402+
lines.append(r"\midrule")
1403+
1404+
if "nonparametric" in bootstrap_results and "parametric" in bootstrap_results:
1405+
nonparam = bootstrap_results["nonparametric"]
1406+
param = bootstrap_results["parametric"]
1407+
1408+
metrics = [
1409+
("Kendall's $\\tau$", "kendall_tau"),
1410+
("Spearman's $\\rho$", "spearman_rho"),
1411+
("Footrule (normalized)", "footrule"),
1412+
("Top-1 consistency", "top1_consistency"),
1413+
("Pairwise order agreement", "pairwise_agreement"),
1414+
]
1415+
1416+
for display_name, key in metrics:
1417+
nonparam_val = nonparam.get(key, float("nan"))
1418+
param_val = param.get(key, float("nan"))
1419+
lines.append(rf"{display_name} & {nonparam_val:.3f} & {param_val:.3f} \\")
1420+
1421+
lines.append(r"\bottomrule")
1422+
lines.append(r"\end{tabular}")
1423+
1424+
output_file.write_text("\n".join(lines) + "\n")
1425+
logger.info(f"Saved bootstrap metrics table: {output_file}")
1426+
1427+
1428+
def write_latex_table_plain(results: dict[str, dict], output_dir: Path) -> None:
1429+
"""Write LaTeX table with plain ELO scores and uncertainties (no bar charts).
1430+
1431+
Args:
1432+
results: Dictionary mapping game name to fit results
1433+
output_dir: Directory to save the LaTeX file
1434+
"""
1435+
output_dir.mkdir(parents=True, exist_ok=True)
1436+
output_file = output_dir / "elo_table_plain.tex"
1437+
1438+
single_arena_games = ["BattleSnake", "CoreWar", "Halite", "HuskyBench", "RoboCode", "RobotRumble"]
1439+
games_in_table = [g for g in single_arena_games if g in results]
1440+
1441+
if "ALL" not in results:
1442+
logger.warning("No 'ALL' game found in results, skipping LaTeX table generation")
1443+
return
1444+
1445+
all_result = results["ALL"]
1446+
all_players = all_result["players"]
1447+
all_strengths = all_result["strengths"]
1448+
all_elos = {p: BradleyTerryFitter.bt_to_elo(s) for p, s in zip(all_players, all_strengths)}
1449+
sorted_players = sorted(all_elos.items(), key=lambda x: x[1], reverse=True)
1450+
1451+
has_uncertainties = "elo_std" in all_result
1452+
1453+
lines = []
1454+
lines.append(r"\begin{table}[t]")
1455+
lines.append(r"\centering")
1456+
lines.append(r"\caption{ELO ratings" + (" with uncertainties" if has_uncertainties else "") + r"}")
1457+
lines.append(r"\label{tab:elo_ratings}")
1458+
lines.append(r"\begin{tabular}{l" + "r" * len(games_in_table) + "r}")
1459+
lines.append(r"\toprule")
1460+
1461+
display_names = [g.replace("HuskyBench", "Poker") for g in games_in_table]
1462+
header_parts = ["Model"] + display_names + ["All"]
1463+
lines.append(" & ".join(header_parts) + r" \\")
1464+
lines.append(r"\midrule")
1465+
1466+
for player, all_elo in sorted_players:
1467+
display_name = MODEL_TO_DISPLAY_NAME.get(player, player)
1468+
row_parts = [display_name.replace("_", r"\_")]
1469+
1470+
for game_name in games_in_table:
1471+
if game_name in results:
1472+
game_result = results[game_name]
1473+
if player in game_result["players"]:
1474+
idx = game_result["players"].index(player)
1475+
strength = game_result["strengths"][idx]
1476+
elo = BradleyTerryFitter.bt_to_elo(strength)
1477+
if has_uncertainties and "elo_std" in game_result:
1478+
sigma = game_result["elo_std"][idx]
1479+
row_parts.append(rf"${int(elo)} \pm {int(sigma)}$")
1480+
else:
1481+
row_parts.append(str(int(elo)))
1482+
else:
1483+
row_parts.append("--")
1484+
else:
1485+
row_parts.append("--")
1486+
1487+
if has_uncertainties:
1488+
all_idx = all_result["players"].index(player)
1489+
all_sigma = all_result["elo_std"][all_idx]
1490+
row_parts.append(rf"$\mathbf{{{int(all_elo)} \pm {int(all_sigma)}}}$")
1491+
else:
1492+
row_parts.append(rf"$\mathbf{{{int(all_elo)}}}$")
1493+
1494+
lines.append(" & ".join(row_parts) + r" \\")
1495+
1496+
lines.append(r"\bottomrule")
1497+
lines.append(r"\end{tabular}")
1498+
lines.append(r"\end{table}")
1499+
1500+
output_file.write_text("\n".join(lines) + "\n")
1501+
logger.info(f"Saved plain LaTeX table: {output_file}")
1502+
1503+
13781504
if __name__ == "__main__":
13791505
parser = argparse.ArgumentParser(description="Build win matrix and fit Bradley-Terry model")
13801506
parser.add_argument("-d", "--log_dir", type=Path, default=LOCAL_LOG_DIR)
@@ -1448,10 +1574,12 @@ def write_website_results(results: dict[str, dict], output_dir: Path) -> None:
14481574
plotter.create_elo_plots(args.output_dir)
14491575
write_latex_table(results, args.output_dir)
14501576
write_website_results(results, args.output_dir)
1577+
write_latex_table_plain(results, args.output_dir)
14511578

14521579
if uncertainties_supported:
1580+
bootstrap_results = {}
14531581
for bootstrap_type in ["nonparametric", "parametric"]:
1454-
BootStrapRankStability(
1582+
bootstrap_results[bootstrap_type] = BootStrapRankStability(
14551583
builder,
14561584
n_bootstrap=1000,
14571585
game="ALL",
@@ -1460,6 +1588,7 @@ def write_website_results(results: dict[str, dict], output_dir: Path) -> None:
14601588
bootstrap_type=bootstrap_type,
14611589
output_dir=args.output_dir,
14621590
).run()
1591+
write_bootstrap_metrics_table(bootstrap_results, args.output_dir, game="ALL")
14631592

14641593
logger.info("Running EloVsMaxRounds analysis")
14651594
EloVsMaxRounds(

codeclash/analysis/viz/round_score_distribution.py

Lines changed: 58 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55

66
import matplotlib.pyplot as plt
77
import numpy as np
8+
from matplotlib.ticker import AutoMinorLocator
89
from tqdm import tqdm
910

10-
from codeclash.analysis.viz.utils import MODEL_TO_DISPLAY_NAME
11+
from codeclash.analysis.viz.utils import ASSETS_DIR, FONT_BOLD, MODEL_TO_DISPLAY_NAME
1112
from codeclash.constants import LOCAL_LOG_DIR
1213
from codeclash.games import BattleCodeGame, DummyGame
1314
from codeclash.utils.log import get_logger
@@ -88,79 +89,79 @@ def plot_stratified(
8889
data_by_category: dict[str, list[float]], output_path: Path, *, title: str, by_model: bool = False
8990
) -> None:
9091
"""Plot normalized scores stratified by category (game or model)."""
91-
all_scores = [s for scores in data_by_category.values() for s in scores]
92-
9392
# Determine category order
9493
if by_model:
95-
category_names = sorted(data_by_category.keys(), key=lambda m: MODEL_TO_DISPLAY_NAME.get(m, m))
94+
# Sort by full model name (including prefix), then strip prefix for display
95+
category_names = sorted(data_by_category.keys())
9696
else:
9797
category_names = sorted(data_by_category.keys())
9898

99-
# Create subplots: 1 for all + 1 per category
100-
n_plots = 1 + len(category_names)
101-
n_cols = 2
99+
# Create subplots: 3 columns, no "All" plot
100+
n_plots = len(category_names)
101+
n_cols = 3
102102
n_rows = (n_plots + n_cols - 1) // n_cols
103103

104-
fig, axes = plt.subplots(n_rows, n_cols, figsize=(14, 5 * n_rows))
104+
# Use 4x4 for all plots
105+
fig, axes = plt.subplots(n_rows, n_cols, figsize=(4 * n_cols, 4 * n_rows))
105106
axes = axes.flatten() if n_plots > 1 else [axes]
106107

107108
bins = np.linspace(0, 1, 51)
108109

109-
# Plot all combined
110-
ax = axes[0]
111-
ax.hist(all_scores, bins=bins, edgecolor="black", alpha=0.7)
112-
ax.set_xlabel("Normalized Score", fontsize=10, fontweight="bold")
113-
ax.set_ylabel("Frequency", fontsize=10, fontweight="bold")
114-
ax.set_title("All Models" if by_model else "All Games", fontsize=12, fontweight="bold")
115-
ax.set_xlim(0, 1)
116-
ax.grid(True, alpha=0.3, axis="y")
117-
118-
mean_score = np.mean(all_scores)
119-
median_score = np.median(all_scores)
120-
stats_text = f"Mean: {mean_score:.3f}\nMedian: {median_score:.3f}\nN: {len(all_scores)}"
121-
ax.text(
122-
0.98,
123-
0.98,
124-
stats_text,
125-
transform=ax.transAxes,
126-
verticalalignment="top",
127-
horizontalalignment="right",
128-
fontsize=9,
129-
bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
130-
)
131-
132110
# Plot per category
133-
for idx, category_name in enumerate(category_names, start=1):
111+
for idx, category_name in enumerate(category_names):
134112
ax = axes[idx]
135113
scores = data_by_category[category_name]
136114

137-
ax.hist(scores, bins=bins, edgecolor="black", alpha=0.7)
138-
ax.set_xlabel("Normalized Score", fontsize=10, fontweight="bold")
139-
ax.set_ylabel("Frequency", fontsize=10, fontweight="bold")
140-
display_name = MODEL_TO_DISPLAY_NAME.get(category_name, category_name) if by_model else category_name
141-
ax.set_title(display_name, fontsize=12, fontweight="bold")
115+
ax.hist(scores, bins=bins, edgecolor="black", alpha=0.7, density=True)
116+
ax.set_xlabel("Normalized Score", fontproperties=FONT_BOLD, fontsize=12)
117+
ax.set_ylabel("Density", fontproperties=FONT_BOLD, fontsize=12)
118+
119+
# Set tick labels to also use bold font
120+
for label in ax.get_xticklabels() + ax.get_yticklabels():
121+
label.set_fontproperties(FONT_BOLD)
122+
123+
if by_model:
124+
display_name = MODEL_TO_DISPLAY_NAME.get(category_name, category_name)
125+
else:
126+
display_name = category_name.replace("Halite", "Poker")
127+
ax.set_title(display_name, fontproperties=FONT_BOLD, fontsize=14)
142128
ax.set_xlim(0, 1)
143-
ax.grid(True, alpha=0.3, axis="y")
144-
145-
mean_score = np.mean(scores)
146-
median_score = np.median(scores)
147-
stats_text = f"Mean: {mean_score:.3f}\nMedian: {median_score:.3f}\nN: {len(scores)}"
148-
ax.text(
149-
0.98,
150-
0.98,
151-
stats_text,
152-
transform=ax.transAxes,
153-
verticalalignment="top",
154-
horizontalalignment="right",
155-
fontsize=9,
156-
bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
157-
)
129+
130+
# Add minor ticks on both axes
131+
ax.xaxis.set_minor_locator(AutoMinorLocator())
132+
ax.yaxis.set_minor_locator(AutoMinorLocator())
133+
134+
# Show ticks on all sides
135+
ax.tick_params(top=True, right=True, which="both")
136+
137+
# Add gridlines for both x and y
138+
ax.grid(True, alpha=0.3, axis="both", which="major")
139+
140+
# Add dashed black line at 50%
141+
ax.axvline(0.5, color="black", linestyle="--", linewidth=1.5, alpha=0.7)
142+
143+
# Add stats text only for by_model plots
144+
if by_model:
145+
mean_score = np.mean(scores)
146+
median_score = np.median(scores)
147+
stats_text = f"Mean: {mean_score:.3f} Median: {median_score:.3f}"
148+
ax.text(
149+
0.5,
150+
0.93,
151+
stats_text,
152+
transform=ax.transAxes,
153+
verticalalignment="top",
154+
horizontalalignment="center",
155+
fontproperties=FONT_BOLD,
156+
fontsize=10,
157+
bbox=dict(boxstyle="square", facecolor="white", edgecolor="black", linewidth=1),
158+
)
158159

159160
# Hide unused subplots
160161
for idx in range(n_plots, len(axes)):
161162
axes[idx].set_visible(False)
162163

163-
plt.suptitle(title, fontsize=14, fontweight="bold")
164+
plt.suptitle(title, fontproperties=FONT_BOLD, fontsize=16)
164165
plt.tight_layout(rect=[0, 0, 1, 0.97])
165166

166167
plt.savefig(output_path, dpi=300, bbox_inches="tight")
@@ -169,7 +170,7 @@ def plot_stratified(
169170
plt.close()
170171

171172

172-
def main(log_dir: Path, output_by_game: Path, output_by_model: Path) -> None:
173+
def main(log_dir: Path) -> None:
173174
"""Calculate normalized scores and plot histograms stratified by game and by model."""
174175
logger.info(f"Processing tournaments from {log_dir}")
175176

@@ -205,6 +206,7 @@ def main(log_dir: Path, output_by_game: Path, output_by_model: Path) -> None:
205206
)
206207

207208
# Plot by game
209+
output_by_game = ASSETS_DIR / "round_score_distribution_by_game.pdf"
208210
plot_stratified(
209211
scores_by_game,
210212
output_by_game,
@@ -213,6 +215,7 @@ def main(log_dir: Path, output_by_game: Path, output_by_model: Path) -> None:
213215
)
214216

215217
# Plot by model
218+
output_by_model = ASSETS_DIR / "round_score_distribution_by_model.pdf"
216219
plot_stratified(
217220
scores_by_model,
218221
output_by_model,
@@ -224,18 +227,6 @@ def main(log_dir: Path, output_by_game: Path, output_by_model: Path) -> None:
224227
if __name__ == "__main__":
225228
parser = argparse.ArgumentParser(description="Plot distribution of normalized player scores (valid rounds only)")
226229
parser.add_argument("-d", "--log_dir", type=Path, default=LOCAL_LOG_DIR, help="Path to log directory")
227-
parser.add_argument(
228-
"--output_by_game",
229-
type=Path,
230-
default=Path("assets/round_score_distribution_by_game.pdf"),
231-
help="Output path for by-game plot",
232-
)
233-
parser.add_argument(
234-
"--output_by_model",
235-
type=Path,
236-
default=Path("assets/round_score_distribution_by_model.pdf"),
237-
help="Output path for by-model plot",
238-
)
239230
args = parser.parse_args()
240231

241-
main(args.log_dir, args.output_by_game, args.output_by_model)
232+
main(args.log_dir)

0 commit comments

Comments
 (0)