66
77import argparse
88import json
9+ from collections import Counter
910from pathlib import Path
10- from typing import Any
1111
1212import pandas as pd
1313
14+ from codeclash .analysis .llm_as_judge .categorize_actions import _all_categories as ACTION_CATEGORIES
1415from codeclash .analysis .llm_as_judge .utils import Instance
1516from codeclash .utils .log import get_logger
1617
1718logger = get_logger ("AggregateResults" , emoji = "📊" )
1819
20+ # Version constants for specific data_ids
21+ BIG_QUESTIONS_VERSION = 7
22+ ACTION_CATEGORIES_VERSION = 3
23+
24+
25+ class ResultsAggregator :
26+ """Class for aggregating LLM-as-judge evaluation results."""
27+
28+ def __init__ (self ):
29+ self .big_questions_data_id = f"big_questions_v{ BIG_QUESTIONS_VERSION } "
30+ self .action_categories_data_id = f"action_categories_v{ ACTION_CATEGORIES_VERSION } "
31+
32+ def aggregate_results_to_dataframe (self , input_dir : Path ) -> pd .DataFrame :
33+ """Aggregate all llm_as_judge.json results from the input directory into a DataFrame.
34+
35+ Returns:
36+ DataFrame with flattened structure containing all evaluation data merged by instance_id
37+ """
38+ # Dictionary to collect all data by instance_id
39+ instance_data_dict = {}
40+ llm_judge_files = list (input_dir .rglob ("llm_as_judge.json" ))
41+
42+ logger .info (f"Found { len (llm_judge_files )} llm_as_judge.json files" )
43+
44+ for file_path in llm_judge_files :
45+ logger .debug (f"Processing { file_path } " )
46+
47+ try :
48+ content = file_path .read_text ().strip ()
49+ if not content :
50+ logger .warning (f"Skipping empty file: { file_path } " )
51+ continue
52+
53+ file_data = json .loads (content )
54+
55+ # Process each data_id and instance
56+ for data_id , instances in file_data .items ():
57+ # Only process allowed data_ids
58+ if data_id not in [self .action_categories_data_id , self .big_questions_data_id ]:
59+ continue
60+
61+ for instance_id , instance_data in instances .items ():
62+ # Initialize instance data if not seen before
63+ if instance_id not in instance_data_dict :
64+ instance_data_dict [instance_id ] = self ._initialize_instance_row (instance_data )
65+
66+ # Add data specific to this data_id
67+ self ._add_data_id_results (instance_data_dict [instance_id ], data_id , instance_data )
68+
69+ except json .JSONDecodeError as e :
70+ logger .error (f"Failed to parse JSON in { file_path } : { e } " )
71+ except Exception as e :
72+ logger .error (f"Error processing { file_path } : { e } " , exc_info = True )
73+
74+ # Convert to list of rows for DataFrame
75+ rows = list (instance_data_dict .values ())
76+ df = pd .DataFrame (rows )
77+
78+ # Count instances for each data_id source
79+ action_categories_count = 0
80+ big_questions_count = 0
81+
82+ for row in rows :
83+ data_ids = row .get ("data_ids" , [])
84+ if self .action_categories_data_id in data_ids :
85+ action_categories_count += 1
86+ if self .big_questions_data_id in data_ids :
87+ big_questions_count += 1
88+
89+ logger .info (f"Created DataFrame with { len (df )} rows and { len (df .columns )} columns" )
90+ logger .info (f"Instances with { self .action_categories_data_id } : { action_categories_count } " )
91+ logger .info (f"Instances with { self .big_questions_data_id } : { big_questions_count } " )
92+
93+ return df
94+
95+ def _initialize_instance_row (self , instance_data : dict ) -> dict :
96+ """Initialize a row with basic instance metadata and NaN values for all data types."""
97+ instance = Instance .model_validate (instance_data ["instance" ])
98+ model_name , opponent_model_name = instance .get_lm_name_self_opponent ()
99+ current_round_win_rate , next_round_win_rate = instance .get_current_next_round_win_rate ()
100+
101+ # Create base row with instance metadata
102+ row = {
103+ "instance_id" : instance .instance_id ,
104+ "tournament_name" : instance .tournament_name ,
105+ "player_name" : instance .player_name ,
106+ "round_number" : instance .round_number ,
107+ "model_name" : model_name ,
108+ "opponent_model_name" : opponent_model_name ,
109+ "current_round_win_rate" : current_round_win_rate ,
110+ "next_round_win_rate" : next_round_win_rate ,
111+ }
112+
113+ # Initialize all action category counts to NaN (will be overwritten if action categories data exists)
114+ for category in ACTION_CATEGORIES :
115+ row [f"category_count_{ category } " ] = pd .NA
116+
117+ return row
118+
119+ def _add_data_id_results (self , row : dict , data_id : str , instance_data : dict ) -> None :
120+ """Add results from a specific data_id to the row."""
121+ # Add the data_id information
122+ if "data_ids" not in row :
123+ row ["data_ids" ] = []
124+ row ["data_ids" ].append (data_id )
125+
126+ # Delegate to specific handlers based on data_id type
127+ if data_id == self .action_categories_data_id :
128+ self ._add_action_categories_results (row , instance_data )
129+ elif data_id == self .big_questions_data_id :
130+ self ._add_big_questions_results (row , instance_data )
131+ # Ignore other data_ids that are not handled
132+
133+ def _add_action_categories_results (self , row : dict , instance_data : dict ) -> None :
134+ """Add action categories results to the row."""
135+ if "result" in instance_data :
136+ result_data = instance_data ["result" ]
137+
138+ # Handle action category counts specially
139+ categories = result_data .get ("categories" , [])
140+ self ._add_action_category_counts (row , categories )
141+
142+ # Add all other result data without prefix
143+ for key , value in result_data .items ():
144+ row [key ] = value
145+
146+ def _add_big_questions_results (self , row : dict , instance_data : dict ) -> None :
147+ """Add big questions results to the row."""
148+ if "result" in instance_data :
149+ result_data = instance_data ["result" ]
150+
151+ # Add all result data without prefix
152+ for key , value in result_data .items ():
153+ row [key ] = value
154+
155+ def _add_action_category_counts (self , row : dict , categories : list ) -> None :
156+ """Add category counts to the row for action categories data."""
157+ # Initialize all category counts to 0 (this ensures missing categories are 0, not NaN)
158+ for category in ACTION_CATEGORIES :
159+ row [f"c_{ category } " ] = 0
160+
161+ # Count occurrences of each category
162+ if categories :
163+ category_counts = Counter (cat .get ("category" ) for cat in categories if cat .get ("category" ))
164+ for category , count in category_counts .items ():
165+ if category in ACTION_CATEGORIES :
166+ row [f"c_{ category } " ] = count
167+
19168
20169def aggregate_results_to_dataframe (input_dir : Path ) -> pd .DataFrame :
21- """Aggregate all llm_as_judge.json results from the input directory into a DataFrame.
22-
23- Returns:
24- DataFrame with flattened structure containing all evaluation data
25- """
26- rows = []
27- llm_judge_files = list (input_dir .rglob ("llm_as_judge.json" ))
28-
29- logger .info (f"Found { len (llm_judge_files )} llm_as_judge.json files" )
30-
31- for file_path in llm_judge_files :
32- logger .debug (f"Processing { file_path } " )
33-
34- try :
35- content = file_path .read_text ().strip ()
36- if not content :
37- logger .warning (f"Skipping empty file: { file_path } " )
38- continue
39-
40- file_data = json .loads (content )
41-
42- # Process each data_id and instance
43- for data_id , instances in file_data .items ():
44- for instance_id , instance_data in instances .items ():
45- # Extract instance metadata
46- instance = Instance .model_validate (instance_data ["instance" ])
47- model_name , opponent_model_name = instance .get_lm_name_self_opponent ()
48- current_round_win_rate , next_round_win_rate = instance .get_current_next_round_win_rate ()
49-
50- # Create a flat row with all information
51- row = {
52- "data_id" : data_id ,
53- "instance_id" : instance_id ,
54- "tournament_name" : instance .tournament_name ,
55- "player_name" : instance .player_name ,
56- "round_number" : instance .round_number ,
57- "model_name" : model_name ,
58- "opponent_model_name" : opponent_model_name ,
59- "current_round_win_rate" : current_round_win_rate ,
60- "next_round_win_rate" : next_round_win_rate ,
61- }
62-
63- # Add all evaluation results
64- if "result" in instance_data :
65- result_data = instance_data ["result" ]
66- for key , value in result_data .items ():
67- row [key ] = value
68-
69- rows .append (row )
70-
71- except json .JSONDecodeError as e :
72- logger .error (f"Failed to parse JSON in { file_path } : { e } " )
73- except Exception as e :
74- logger .error (f"Error processing { file_path } : { e } " , exc_info = True )
75-
76- df = pd .DataFrame (rows )
77- logger .info (f"Created DataFrame with { len (df )} rows and { len (df .columns )} columns" )
78-
79- return df
170+ """Convenience function for backward compatibility."""
171+ aggregator = ResultsAggregator ()
172+ return aggregator .aggregate_results_to_dataframe (input_dir )
80173
81174
82175def main () -> None :
83176 parser = argparse .ArgumentParser (description = "Aggregate LLM-as-judge evaluation results to Parquet" )
84177 parser .add_argument ("input_dir" , type = Path , help = "Path to the input directory containing tournament results" )
85- parser .add_argument ("-o" , "--output-file" , type = Path ,
86- help = "Path to the output Parquet file" , default = "aggregated_results.parquet" )
178+ parser .add_argument (
179+ "-o" , "--output-file" , type = Path , help = "Path to the output Parquet file" , default = "aggregated_results.parquet"
180+ )
87181 args = parser .parse_args ()
88-
182+
89183 if not args .input_dir .exists ():
90184 logger .error (f"Input directory does not exist: { args .input_dir } " )
91185 return
92-
186+
93187 logger .info (f"Aggregating results from { args .input_dir } " )
94- df = aggregate_results_to_dataframe (args .input_dir )
95-
188+ aggregator = ResultsAggregator ()
189+ df = aggregator .aggregate_results_to_dataframe (args .input_dir )
190+
96191 df .to_parquet (args .output_file , compression = "snappy" , index = False )
97192 logger .info (f"Wrote aggregated results to { args .output_file } " )
98193
99194
100195if __name__ == "__main__" :
101- main ()
196+ main ()
0 commit comments