Last active
April 22, 2019 14:17
-
-
Save cheran-senthil/7c1c63eb8caef9dcfd5815361de41b26 to your computer and use it in GitHub Desktop.
Codeforces Rating Calculator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import time | |
from collections import Counter | |
import requests | |
def timer(func): | |
def wrapper(*args, **kwargs): | |
beg = time.time() | |
res = func(*args, **kwargs) | |
print("Elapsed:", time.time() - beg) | |
return res | |
return wrapper | |
@timer | |
def predict(rows, prev_ratings): | |
calc = CodeforcesRatingCalculator(rows, prev_ratings) | |
return calc.calculate_rating_changes() | |
ROUND533DIV2 = 1151 | |
GLOBALROUND2 = 1119 | |
EDUROUND62 = 1140 | |
contest = EDUROUND62 | |
url = f"https://codeforces.com/api/contest.standings?contestId={contest}" | |
resp = requests.get(url) | |
data = resp.json() | |
url2 = f"https://codeforces.com/api/contest.ratingChanges?contestId={contest}" | |
resp2 = requests.get(url2) | |
data2 = resp2.json() | |
resp.status_code, resp2.status_code | |
rows = data["result"]["rows"] | |
rows = [ | |
(row["party"]["members"][0]["handle"], row["rank"], row["points"], row["penalty"]) | |
for row in rows | |
] | |
prev_ratings, new_ratings = {}, {} | |
changes = data2["result"] | |
rated_for = set() | |
for change in changes: | |
rated_for.add(change["handle"]) | |
prev_ratings[change["handle"]] = change["oldRating"] | |
new_ratings[change["handle"]] = change["newRating"] | |
rows = [row for row in rows if row[0] in rated_for] | |
prev_ratings[rows[0][0]], new_ratings[rows[0][0]] | |
predicted = predict(rows, prev_ratings) | |
difs = Counter() | |
difdif = [] | |
for handle, _, _, _ in rows: | |
old = prev_ratings[handle] | |
change = predicted[handle] | |
new = new_ratings[handle] | |
dif = new - (old + change) | |
if abs(dif) > 2: | |
print(handle, f"truedelta={new - old}", f"predict={change}", f"dif={dif}") | |
difs[dif] += 1 | |
if round(dif): | |
difdif.append(round(dif)) | |
print(len(predicted), sorted(difs.items())) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Adapted from Codeforces code to recalculate ratings | |
by Mike Mirzayanov (mirzayanovmr@gmail.com) at https://codeforces.com/contest/1/submission/13861109 | |
Updated to use the current rating formula. | |
""" | |
from dataclasses import dataclass | |
import numpy as np | |
from numpy.fft import fft, ifft | |
def intdiv(x, y): | |
return -(-x // y) if x < 0 else x // y | |
@dataclass | |
class Contestant: | |
party: str | |
points: float | |
penalty: int | |
rating: int | |
need_rating: int = 0 | |
delta: int = 0 | |
rank: float = 0.0 | |
seed: float = 0.0 | |
class CodeforcesRatingCalculator: | |
def __init__(self, standing_rows, prev_ratings): | |
"""Calculate Codeforces rating changes and seeds given contest and user information.""" | |
self.contestants = [ | |
Contestant(handle, points, penalty, prev_ratings[handle]) | |
for handle, _, points, penalty in standing_rows | |
] | |
self._precalc_seed() | |
self._reassign_ranks() | |
self._process() | |
self._update_delta() | |
def calculate_rating_changes(self): | |
"""Return a mapping between contestants and their corresponding delta.""" | |
return {contestant.party: contestant.delta for contestant in self.contestants} | |
def get_seed(self, rating, me=None): | |
"""Get seed given a rating and user.""" | |
seed = self.seed[rating] | |
if me: | |
seed -= self.elo_win_prob[rating - me.rating] | |
return seed | |
def _precalc_seed(self): | |
MAX = 6144 | |
# Precompute the ELO win probability for all possible rating differences. | |
self.elo_win_prob = np.roll(1 / (1 + pow(10, np.arange(-MAX, MAX) / 400)), -MAX) | |
# Compute the rating histogram. | |
count = np.zeros(2 * MAX) | |
for a in self.contestants: | |
count[a.rating] += 1 | |
# Precompute the seed for all possible ratings using FFT. | |
self.seed = 1 + ifft(fft(count) * fft(self.elo_win_prob)).real | |
def _reassign_ranks(self): | |
"""Find the rank of each contestant.""" | |
contestants = self.contestants | |
contestants.sort(key=lambda o: (-o.points, o.penalty)) | |
points = penalty = rank = None | |
for i in reversed(range(len(contestants))): | |
if contestants[i].points != points or contestants[i].penalty != penalty: | |
rank = i + 1 | |
points = contestants[i].points | |
penalty = contestants[i].penalty | |
contestants[i].rank = rank | |
def _process(self): | |
"""Process and assign approximate delta for each contestant.""" | |
for a in self.contestants: | |
a.seed = self.get_seed(a.rating, a) | |
mid_rank = (a.rank * a.seed) ** 0.5 | |
a.need_rating = self._rank_to_rating(mid_rank, a) | |
a.delta = intdiv(a.need_rating - a.rating, 2) | |
def _rank_to_rating(self, rank, me): | |
"""Binary Search to find the performance rating for a given rank.""" | |
left, right = 1, 8000 | |
while right - left > 1: | |
mid = (left + right) // 2 | |
if self.get_seed(mid, me) < rank: | |
right = mid | |
else: | |
left = mid | |
return left | |
def _update_delta(self): | |
"""Update the delta of each contestant.""" | |
contestants = self.contestants | |
n = len(contestants) | |
contestants.sort(key=lambda o: -o.rating) | |
correction = intdiv(-sum(c.delta for c in contestants), n) - 1 | |
for contestant in contestants: | |
contestant.delta += correction | |
zero_sum_count = min(4 * round(n ** 0.5), n) | |
delta_sum = -sum(contestants[i].delta for i in range(zero_sum_count)) | |
correction = min(0, max(-10, intdiv(delta_sum, zero_sum_count))) | |
for contestant in contestants: | |
contestant.delta += correction |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
orz