Skip to content

Commit 6bb2068

Browse files
committed
Enh(viz): Updates to styling of round score/win rate dists
1 parent 588486a commit 6bb2068

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__(
@@ -1334,6 +1343,123 @@ def write_latex_table(results: dict[str, dict], output_dir: Path) -> None:
13341343
logger.info(f"Saved LaTeX table: {output_file}")
13351344

13361345

1346+
def write_bootstrap_metrics_table(bootstrap_results: dict[str, dict], output_dir: Path, game: str = "ALL") -> None:
1347+
"""Write LaTeX table comparing bootstrap metrics between methods.
1348+
1349+
Args:
1350+
bootstrap_results: Dictionary mapping bootstrap type to metrics dict
1351+
output_dir: Directory to save the LaTeX file
1352+
game: Game name for the table caption
1353+
"""
1354+
output_dir.mkdir(parents=True, exist_ok=True)
1355+
output_file = output_dir / "bootstrap_metrics.tex"
1356+
1357+
lines = []
1358+
lines.append(r"\begin{tabular}{lcc}")
1359+
lines.append(r"\toprule")
1360+
lines.append(r"Metric & Nonparametric & Parametric \\")
1361+
lines.append(r"\midrule")
1362+
1363+
if "nonparametric" in bootstrap_results and "parametric" in bootstrap_results:
1364+
nonparam = bootstrap_results["nonparametric"]
1365+
param = bootstrap_results["parametric"]
1366+
1367+
metrics = [
1368+
("Kendall's $\\tau$", "kendall_tau"),
1369+
("Spearman's $\\rho$", "spearman_rho"),
1370+
("Footrule (normalized)", "footrule"),
1371+
("Top-1 consistency", "top1_consistency"),
1372+
("Pairwise order agreement", "pairwise_agreement"),
1373+
]
1374+
1375+
for display_name, key in metrics:
1376+
nonparam_val = nonparam.get(key, float("nan"))
1377+
param_val = param.get(key, float("nan"))
1378+
lines.append(rf"{display_name} & {nonparam_val:.3f} & {param_val:.3f} \\")
1379+
1380+
lines.append(r"\bottomrule")
1381+
lines.append(r"\end{tabular}")
1382+
1383+
output_file.write_text("\n".join(lines) + "\n")
1384+
logger.info(f"Saved bootstrap metrics table: {output_file}")
1385+
1386+
1387+
def write_latex_table_plain(results: dict[str, dict], output_dir: Path) -> None:
1388+
"""Write LaTeX table with plain ELO scores and uncertainties (no bar charts).
1389+
1390+
Args:
1391+
results: Dictionary mapping game name to fit results
1392+
output_dir: Directory to save the LaTeX file
1393+
"""
1394+
output_dir.mkdir(parents=True, exist_ok=True)
1395+
output_file = output_dir / "elo_table_plain.tex"
1396+
1397+
single_arena_games = ["BattleSnake", "CoreWar", "Halite", "HuskyBench", "RoboCode", "RobotRumble"]
1398+
games_in_table = [g for g in single_arena_games if g in results]
1399+
1400+
if "ALL" not in results:
1401+
logger.warning("No 'ALL' game found in results, skipping LaTeX table generation")
1402+
return
1403+
1404+
all_result = results["ALL"]
1405+
all_players = all_result["players"]
1406+
all_strengths = all_result["strengths"]
1407+
all_elos = {p: BradleyTerryFitter.bt_to_elo(s) for p, s in zip(all_players, all_strengths)}
1408+
sorted_players = sorted(all_elos.items(), key=lambda x: x[1], reverse=True)
1409+
1410+
has_uncertainties = "elo_std" in all_result
1411+
1412+
lines = []
1413+
lines.append(r"\begin{table}[t]")
1414+
lines.append(r"\centering")
1415+
lines.append(r"\caption{ELO ratings" + (" with uncertainties" if has_uncertainties else "") + r"}")
1416+
lines.append(r"\label{tab:elo_ratings}")
1417+
lines.append(r"\begin{tabular}{l" + "r" * len(games_in_table) + "r}")
1418+
lines.append(r"\toprule")
1419+
1420+
display_names = [g.replace("HuskyBench", "Poker") for g in games_in_table]
1421+
header_parts = ["Model"] + display_names + ["All"]
1422+
lines.append(" & ".join(header_parts) + r" \\")
1423+
lines.append(r"\midrule")
1424+
1425+
for player, all_elo in sorted_players:
1426+
display_name = MODEL_TO_DISPLAY_NAME.get(player, player)
1427+
row_parts = [display_name.replace("_", r"\_")]
1428+
1429+
for game_name in games_in_table:
1430+
if game_name in results:
1431+
game_result = results[game_name]
1432+
if player in game_result["players"]:
1433+
idx = game_result["players"].index(player)
1434+
strength = game_result["strengths"][idx]
1435+
elo = BradleyTerryFitter.bt_to_elo(strength)
1436+
if has_uncertainties and "elo_std" in game_result:
1437+
sigma = game_result["elo_std"][idx]
1438+
row_parts.append(rf"${int(elo)} \pm {int(sigma)}$")
1439+
else:
1440+
row_parts.append(str(int(elo)))
1441+
else:
1442+
row_parts.append("--")
1443+
else:
1444+
row_parts.append("--")
1445+
1446+
if has_uncertainties:
1447+
all_idx = all_result["players"].index(player)
1448+
all_sigma = all_result["elo_std"][all_idx]
1449+
row_parts.append(rf"$\mathbf{{{int(all_elo)} \pm {int(all_sigma)}}}$")
1450+
else:
1451+
row_parts.append(rf"$\mathbf{{{int(all_elo)}}}$")
1452+
1453+
lines.append(" & ".join(row_parts) + r" \\")
1454+
1455+
lines.append(r"\bottomrule")
1456+
lines.append(r"\end{tabular}")
1457+
lines.append(r"\end{table}")
1458+
1459+
output_file.write_text("\n".join(lines) + "\n")
1460+
logger.info(f"Saved plain LaTeX table: {output_file}")
1461+
1462+
13371463
if __name__ == "__main__":
13381464
parser = argparse.ArgumentParser(description="Build win matrix and fit Bradley-Terry model")
13391465
parser.add_argument("-d", "--log_dir", type=Path, default=LOCAL_LOG_DIR)
@@ -1406,10 +1532,12 @@ def write_latex_table(results: dict[str, dict], output_dir: Path) -> None:
14061532
plotter.create_validation_plots(args.output_dir, regularization=args.regularization)
14071533
plotter.create_elo_plots(args.output_dir)
14081534
write_latex_table(results, args.output_dir)
1535+
write_latex_table_plain(results, args.output_dir)
14091536

14101537
if uncertainties_supported:
1538+
bootstrap_results = {}
14111539
for bootstrap_type in ["nonparametric", "parametric"]:
1412-
BootStrapRankStability(
1540+
bootstrap_results[bootstrap_type] = BootStrapRankStability(
14131541
builder,
14141542
n_bootstrap=1000,
14151543
game="ALL",
@@ -1418,6 +1546,7 @@ def write_latex_table(results: dict[str, dict], output_dir: Path) -> None:
14181546
bootstrap_type=bootstrap_type,
14191547
output_dir=args.output_dir,
14201548
).run()
1549+
write_bootstrap_metrics_table(bootstrap_results, args.output_dir, game="ALL")
14211550

14221551
logger.info("Running EloVsMaxRounds analysis")
14231552
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)