|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Plot the cumulative total number of created files vs round.""" |
| 3 | + |
| 4 | +import argparse |
| 5 | +import json |
| 6 | + |
| 7 | +import matplotlib.pyplot as plt |
| 8 | +import pandas as pd |
| 9 | +from matplotlib.font_manager import FontProperties |
| 10 | + |
| 11 | +from codeclash.analysis.viz.scatter_codebase_organization import ( |
| 12 | + ASSETS_SUBFOLDER, |
| 13 | + DATA_CACHE, |
| 14 | +) |
| 15 | +from codeclash.analysis.viz.utils import FONT_BOLD, MODEL_TO_COLOR, MODEL_TO_DISPLAY_NAME |
| 16 | + |
| 17 | + |
| 18 | +def calculate_file_counts_by_extension_at_round(data: list, target_round: int = 15) -> pd.DataFrame: |
| 19 | + """Calculate total file counts by extension at a specific round.""" |
| 20 | + results = [] |
| 21 | + |
| 22 | + for entry in data: |
| 23 | + player = entry["player"] |
| 24 | + tournament = entry["tournament"] |
| 25 | + file_history = entry["file_history"] |
| 26 | + |
| 27 | + # Count files by extension that were created at or before target_round |
| 28 | + extension_counts = {} |
| 29 | + for filename, history in file_history.items(): |
| 30 | + # Find creation round |
| 31 | + creation_round = None |
| 32 | + for round_num, op, _, _ in history: |
| 33 | + if op == "created": |
| 34 | + creation_round = round_num |
| 35 | + break |
| 36 | + |
| 37 | + if creation_round is not None and creation_round <= target_round: |
| 38 | + # Extract extension |
| 39 | + if "." in filename: |
| 40 | + ext = filename.rsplit(".", 1)[1] |
| 41 | + else: |
| 42 | + ext = "no_extension" |
| 43 | + |
| 44 | + extension_counts[ext] = extension_counts.get(ext, 0) + 1 |
| 45 | + |
| 46 | + # Store result |
| 47 | + result_row = { |
| 48 | + "player": player, |
| 49 | + "tournament": tournament, |
| 50 | + } |
| 51 | + result_row.update(extension_counts) |
| 52 | + results.append(result_row) |
| 53 | + |
| 54 | + df = pd.DataFrame(results).fillna(0) |
| 55 | + |
| 56 | + # Aggregate by player (mean across tournaments) |
| 57 | + extension_cols = [col for col in df.columns if col not in ["player", "tournament"]] |
| 58 | + |
| 59 | + agg_dict = {col: "mean" for col in extension_cols} |
| 60 | + player_agg = df.groupby("player").agg(agg_dict).reset_index() |
| 61 | + |
| 62 | + # Rename columns to count_<ext> |
| 63 | + rename_dict = {col: f"count_{col}" for col in extension_cols} |
| 64 | + |
| 65 | + return player_agg.rename(columns=rename_dict) |
| 66 | + |
| 67 | + |
| 68 | +def calculate_cumulative_created_files_per_round(data: list, max_round: int = 15) -> pd.DataFrame: |
| 69 | + """Calculate cumulative total number of created files per player per round.""" |
| 70 | + results = [] |
| 71 | + |
| 72 | + for entry in data: |
| 73 | + player = entry["player"] |
| 74 | + tournament = entry["tournament"] |
| 75 | + file_history = entry["file_history"] |
| 76 | + |
| 77 | + # Get all creation rounds |
| 78 | + creation_rounds = [] |
| 79 | + for _, history in file_history.items(): |
| 80 | + for round_num, op, _, _ in history: |
| 81 | + if op == "created": |
| 82 | + creation_rounds.append(round_num) |
| 83 | + |
| 84 | + # Calculate cumulative counts per round (up to max_round) |
| 85 | + for round_num in range(max_round + 1): |
| 86 | + cumulative_count = sum(1 for r in creation_rounds if r <= round_num) |
| 87 | + results.append( |
| 88 | + {"player": player, "tournament": tournament, "round": round_num, "total_files": cumulative_count} |
| 89 | + ) |
| 90 | + |
| 91 | + return pd.DataFrame(results) |
| 92 | + |
| 93 | + |
| 94 | +def filter_outlier_tournaments_by_total_files_99p(created_files_df: pd.DataFrame) -> pd.DataFrame: |
| 95 | + """Filter out player-tournament trajectories above the player's 99th percentile. |
| 96 | +
|
| 97 | + Uses the maximum cumulative total per player-tournament to compute the cutoff, |
| 98 | + mirroring the 99% filtering logic from the bar chart. |
| 99 | + """ |
| 100 | + max_totals = ( |
| 101 | + created_files_df.groupby(["player", "tournament"], as_index=False)["total_files"] |
| 102 | + .max() |
| 103 | + .rename(columns={"total_files": "max_total_files"}) |
| 104 | + ) |
| 105 | + q99_per_player = max_totals.groupby("player")["max_total_files"].transform(lambda s: s.quantile(0.99)) |
| 106 | + valid_pairs = max_totals[max_totals["max_total_files"] <= q99_per_player][["player", "tournament"]] |
| 107 | + return created_files_df.merge(valid_pairs, on=["player", "tournament"], how="inner") |
| 108 | + |
| 109 | + |
| 110 | +def plot_total_created_files_over_rounds(created_files_df: pd.DataFrame): |
| 111 | + """Line plot showing cumulative total created files over rounds per model. |
| 112 | +
|
| 113 | + - X: Round number |
| 114 | + - Y: Total number of files created (cumulative, mean across tournaments) |
| 115 | + - One line per model, colored consistently |
| 116 | + """ |
| 117 | + # Aggregate by player and round (mean across all tournaments) |
| 118 | + print("------") |
| 119 | + print(created_files_df.groupby(["player"]).agg({"total_files": "max"}).reset_index()) |
| 120 | + print("------") |
| 121 | + agg_created = ( |
| 122 | + created_files_df |
| 123 | + # .query("total_files < 10_000") # remove outliers |
| 124 | + .groupby(["player", "round"]) |
| 125 | + .agg({"total_files": "mean"}) |
| 126 | + .reset_index() |
| 127 | + ) |
| 128 | + |
| 129 | + # Figure styling to match other viz |
| 130 | + plt.figure(figsize=(6, 6)) |
| 131 | + |
| 132 | + # Plot one line per model with consistent color & legend label |
| 133 | + seen_labels = set() |
| 134 | + for player in sorted(agg_created["player"].unique()): |
| 135 | + player_data = agg_created[agg_created["player"] == player] |
| 136 | + color = MODEL_TO_COLOR.get(player, "#333333") |
| 137 | + label = MODEL_TO_DISPLAY_NAME.get(player, player) |
| 138 | + |
| 139 | + # Avoid duplicate legend entries |
| 140 | + plot_label = label if label not in seen_labels else None |
| 141 | + if plot_label: |
| 142 | + seen_labels.add(label) |
| 143 | + |
| 144 | + plt.plot( |
| 145 | + player_data["round"], |
| 146 | + player_data["total_files"], |
| 147 | + color=color, |
| 148 | + linewidth=2.5, |
| 149 | + label=plot_label, |
| 150 | + alpha=0.9, |
| 151 | + ) |
| 152 | + |
| 153 | + # Axes labels |
| 154 | + plt.xlabel("Round", fontproperties=FONT_BOLD, fontsize=18) |
| 155 | + plt.ylabel("Total created files", fontproperties=FONT_BOLD, fontsize=18) |
| 156 | + plt.xticks(fontproperties=FONT_BOLD, fontsize=14) |
| 157 | + plt.yticks(fontproperties=FONT_BOLD, fontsize=14) |
| 158 | + |
| 159 | + # Grid & legend |
| 160 | + plt.grid(True, alpha=0.3) |
| 161 | + legend_font = FontProperties(fname=FONT_BOLD.get_file(), size=14) |
| 162 | + plt.legend(loc="best", prop=legend_font) |
| 163 | + |
| 164 | + plt.tight_layout() |
| 165 | + OUTPUT_FILE = ASSETS_SUBFOLDER / "line_chart_total_created_files_vs_round.pdf" |
| 166 | + plt.savefig(OUTPUT_FILE, bbox_inches="tight") |
| 167 | + print(f"Saved total created files plot to {OUTPUT_FILE}") |
| 168 | + |
| 169 | + |
| 170 | +def main(): |
| 171 | + data = [] |
| 172 | + with open(DATA_CACHE) as f: |
| 173 | + print("Loading data from", DATA_CACHE) |
| 174 | + data = [json.loads(line) for line in f] |
| 175 | + print(f"Found {len(data)} player-tournament entries in cache.") |
| 176 | + |
| 177 | + print("\n=== Calculating File Counts by Extension at Round 15 ===") |
| 178 | + extension_counts_df = calculate_file_counts_by_extension_at_round(data, target_round=15) |
| 179 | + print(extension_counts_df) |
| 180 | + output_csv = ASSETS_SUBFOLDER / "file_counts_by_extension_round15.csv" |
| 181 | + extension_counts_df.to_csv(output_csv, index=False) |
| 182 | + print(f"Saved file counts by extension to {output_csv}") |
| 183 | + |
| 184 | + print("\n=== Calculating Cumulative Created Files Per Round ===") |
| 185 | + created_files_df = calculate_cumulative_created_files_per_round(data) |
| 186 | + print(created_files_df.head(20)) |
| 187 | + # Apply 99% per-player filtering (mirrors bar chart logic) |
| 188 | + created_files_df = filter_outlier_tournaments_by_total_files_99p(created_files_df) |
| 189 | + |
| 190 | + print("\n=== Plotting Total Created Files Over Rounds ===") |
| 191 | + plot_total_created_files_over_rounds(created_files_df) |
| 192 | + |
| 193 | + |
| 194 | +if __name__ == "__main__": |
| 195 | + parser = argparse.ArgumentParser(description="Plot cumulative total created files vs round") |
| 196 | + args = parser.parse_args() |
| 197 | + main() |
0 commit comments