Skip to content

Commit 3bdbf47

Browse files
committed
Merge branch 'main' of github.com:emagedoc/CodeClash
2 parents e7ab204 + 60cf4de commit 3bdbf47

6 files changed

Lines changed: 888 additions & 68 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*.ipynb
55
*.traj.json
66
logs/**
7+
*.pdf
78

89
# Kilian's specific ignores
910
batch_submit.sh

codeclash/analysis/llm_as_judge/aggregate_results.py

Lines changed: 162 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -6,96 +6,191 @@
66

77
import argparse
88
import json
9+
from collections import Counter
910
from pathlib import Path
10-
from typing import Any
1111

1212
import pandas as pd
1313

14+
from codeclash.analysis.llm_as_judge.categorize_actions import _all_categories as ACTION_CATEGORIES
1415
from codeclash.analysis.llm_as_judge.utils import Instance
1516
from codeclash.utils.log import get_logger
1617

1718
logger = 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

20169
def 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

82175
def 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

100195
if __name__ == "__main__":
101-
main()
196+
main()

0 commit comments

Comments
 (0)