@@ -56,6 +56,40 @@ def calculate_round_weight_exponential(round_num, total_rounds, alpha=2.0):
5656 return raw_weight * norm_factor
5757
5858
59+ def update_profiles (prof_and_score , round_weight , k_factor ):
60+ """Update ELO profiles for two players based on their scores and round weight
61+
62+ Args:
63+ prof_and_score: List of tuples [(ModelEloProfile, score), ...] for two players
64+ round_weight: Weight for the current round (affects K-factor)
65+ k_factor: Base K-factor for ELO calculation
66+ """
67+ p1_prof , p1_raw_score = prof_and_score [0 ]
68+ p2_prof , p2_raw_score = prof_and_score [1 ]
69+
70+ # Normalize scores so they sum to 1.0 (required for proper ELO)
71+ total_score = p1_raw_score + p2_raw_score
72+ if total_score > 0 :
73+ p1_score = p1_raw_score / total_score
74+ p2_score = p2_raw_score / total_score
75+ else :
76+ # If both players scored 0, treat as a tie
77+ p1_score = p2_score = 0.5
78+
79+ expected_p1 = expected_score (p1_prof .rating , p2_prof .rating )
80+
81+ # Apply round weighting to K-factor
82+ weighted_k_factor = k_factor * round_weight
83+ rating_change = weighted_k_factor * (p1_score - expected_p1 )
84+
85+ expected_p2 = expected_score (p2_prof .rating , p1_prof .rating )
86+ check = weighted_k_factor * (p2_score - expected_p2 )
87+ assert abs (check + rating_change ) < 1e-6 , "Weighted ELO rating changes do not sum to zero!"
88+
89+ p1_prof .rating += rating_change
90+ p2_prof .rating -= rating_change # Zero-sum property
91+
92+
5993def main (log_dir : Path , k_factor : int , starting_elo : int , weighting_function : str , alpha : float ):
6094 print (f"Calculating weighted ELO ratings from logs in { log_dir } ..." )
6195 print (f"Using K_FACTOR={ k_factor } , STARTING_ELO={ starting_elo } " )
@@ -84,66 +118,46 @@ def main(log_dir: Path, k_factor: int, starting_elo: int, weighting_function: st
84118 # Determine total rounds for weighting calculation
85119 total_rounds = len ([k for k in metadata ["round_stats" ].keys () if k != "0" ])
86120
87- if len (p2m ) = = 2 :
121+ if len (p2m ) ! = 2 :
88122 # Only process if there are exactly 2 players
89- for idx , stats in metadata ["round_stats" ].items ():
90- if idx == "0" :
91- # Skip initial round
92- continue
93-
94- # Calculate round weight
95- current_round = int (idx )
96- if weighting_function == "linear" :
97- round_weight = calculate_round_weight_linear (current_round , total_rounds )
98- elif weighting_function == "exponential" :
99- round_weight = calculate_round_weight_exponential (current_round , total_rounds , alpha )
100- else : # none
101- round_weight = 1.0
102-
103- prof_and_score = []
104- valid_submits = sum (
105- [x ["valid_submit" ] for x in stats ["player_stats" ].values () if x .get ("valid_submit" ) is not None ]
106- )
107-
108- for k , v in stats ["player_stats" ].items ():
109- if k != RESULT_TIE :
110- if v ["score" ] is None :
111- # Not sure why this happens, but just skip it
112- continue
113- s = v ["score" ] * 1.0 / sims
114- if valid_submits == 1 and v ["valid_submit" ]:
115- # FOR BACKWARDS COMPATIBILITY: If only one player submitted, give them full point
116- s = 1.0
117- prof = player_profiles [f"{ arena } .{ p2m [k ]} " ]
118- prof .rounds_played += 1
119- prof_and_score .append ((prof , s ))
120-
121- # Update ELO ratings - should only happen once per match
122- if len (prof_and_score ) == 2 :
123- p1_prof , p1_raw_score = prof_and_score [0 ]
124- p2_prof , p2_raw_score = prof_and_score [1 ]
125-
126- # Normalize scores so they sum to 1.0 (required for proper ELO)
127- total_score = p1_raw_score + p2_raw_score
128- if total_score > 0 :
129- p1_score = p1_raw_score / total_score
130- p2_score = p2_raw_score / total_score
131- else :
132- # If both players scored 0, treat as a tie
133- p1_score = p2_score = 0.5
134-
135- expected_p1 = expected_score (p1_prof .rating , p2_prof .rating )
136-
137- # Apply round weighting to K-factor
138- weighted_k_factor = k_factor * round_weight
139- rating_change = weighted_k_factor * (p1_score - expected_p1 )
140-
141- expected_p2 = expected_score (p2_prof .rating , p1_prof .rating )
142- check = weighted_k_factor * (p2_score - expected_p2 )
143- assert abs (check + rating_change ) < 1e-6 , "Weighted ELO rating changes do not sum to zero!"
144-
145- p1_prof .rating += rating_change
146- p2_prof .rating -= rating_change # Zero-sum property
123+ continue
124+
125+ for idx , stats in metadata ["round_stats" ].items ():
126+ if idx == "0" :
127+ # Skip initial round
128+ continue
129+
130+ # Calculate round weight
131+ current_round = int (idx )
132+ if weighting_function == "linear" :
133+ round_weight = calculate_round_weight_linear (current_round , total_rounds )
134+ elif weighting_function == "exponential" :
135+ round_weight = calculate_round_weight_exponential (current_round , total_rounds , alpha )
136+ else : # none
137+ round_weight = 1.0
138+
139+ prof_and_score = []
140+ valid_submits = sum (
141+ [x ["valid_submit" ] for x in stats ["player_stats" ].values () if x .get ("valid_submit" ) is not None ]
142+ )
143+
144+ for k , v in stats ["player_stats" ].items ():
145+ if k != RESULT_TIE :
146+ if v ["score" ] is None :
147+ # Not sure why this happens, but just skip it
148+ continue
149+ s = v ["score" ] * 1.0 / sims
150+ if valid_submits == 1 and v ["valid_submit" ]:
151+ # FOR BACKWARDS COMPATIBILITY: If only one player submitted, give them full point
152+ s = 1.0
153+ prof = player_profiles [f"{ arena } .{ p2m [k ]} " ]
154+ prof .rounds_played += 1
155+ prof_and_score .append ((prof , s ))
156+
157+ # Update ELO ratings - should only happen once per match
158+ if len (prof_and_score ) != 2 :
159+ continue
160+ update_profiles (prof_and_score , round_weight , k_factor )
147161
148162 print ("=" * 50 )
149163 print ("Player ELO profiles:" )
@@ -162,7 +176,7 @@ def main(log_dir: Path, k_factor: int, starting_elo: int, weighting_function: st
162176 total_games [mid ] = total_games .get (mid , 0 ) + profile .rounds_played
163177
164178 print ("\n Weighted average ELO per player (across all games):" )
165- calc_avg_elo = lambda total_elo , games : total_elo / games if games > 0 else 0.0
179+ calc_avg_elo = lambda total_elo , games : total_elo / games
166180 lines = [
167181 f" - { pid } : Weighted Avg ELO { calc_avg_elo (weighted_elo [pid ], total_games [pid ]):.1f} (Games: { total_games [pid ]} )"
168182 for pid in weighted_elo
0 commit comments