Skip to content

Instantly share code, notes, and snippets.

@cheran-senthil
Last active April 22, 2019 14:17
Show Gist options
  • Save cheran-senthil/7c1c63eb8caef9dcfd5815361de41b26 to your computer and use it in GitHub Desktop.
Save cheran-senthil/7c1c63eb8caef9dcfd5815361de41b26 to your computer and use it in GitHub Desktop.
Codeforces Rating Calculator
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()))
"""
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
@aryamanarora
Copy link

orz

@aryamanarora
Copy link

OOP in python is geniosity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment