55
66import matplotlib .pyplot as plt
77import numpy as np
8+ from matplotlib .ticker import AutoMinorLocator
89from 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
1112from codeclash .constants import LOCAL_LOG_DIR
1213from codeclash .games import BattleCodeGame , DummyGame
1314from 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} \n Median: { median_score :.3f} \n N: { 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} \n Median: { median_score :.3f} \n N: { 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:
224227if __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