Commit e8ca6ba1 authored by uudlo's avatar uudlo

Implement the glicko2 rating system

The implementation is based on the description from http://www.glicko.net/glicko/glicko2.pdf.
parent 93a7780c
from math import sqrt, pi, exp, log
class Rating:
def __init__(self, outer):
self.outer = outer
self.rating = outer.start_rating
self.rd = outer.start_rd
self.volatility = outer.start_volatility
def __str__(self):
return "rating: {}\nrd: {}\nvolatility: {}".format(self.rating, self.rd, self.volatility)
class ScaledRating:
def __init__(self, rating):
self.ratio = 173.7178
self.rating = rating
self.outer = rating.outer
self.mu = (rating.rating - rating.outer.start_rating) / self.ratio
self.phi = rating.rd / self.ratio
self.sigma = rating.volatility
def unscale(self):
r = Rating(self.outer)
r.rating = self.ratio * self.mu + self.outer.start_rating
r.rd = self.ratio * self.phi
r.volatility = self.sigma
return r
def enshure_rd(self):
max_phi = self.outer.start_rd / self.ratio
if self.phi > max_phi:
self.phi = max_phi
class Glicko2:
def create_rating(self):
return Rating(self)
def __init__(self, start_rating, start_rd, start_volatility, tau):
self.start_rating = start_rating
self.start_rd = start_rd
self.start_volatility = start_volatility
self.tau = tau
@staticmethod
def g(phi):
return 1/(sqrt(1+(3*(phi**2))/pi**2))
@staticmethod
def E(mu, mu_j, phi_j):
return 1/(1+exp(-Glicko2.g(phi_j)*(mu-mu_j)))
def update_rating(self, rating, opponents, results):
# step 2
s_rating = ScaledRating(rating)
s_opponents = [ScaledRating(r) for r in opponents]
# check rd
# rd is not allowed to get higher than the start_rd
for r in [s_rating] + s_opponents:
r.enshure_rd()
# step 6
if not opponents:
s_rating.phi = sqrt(s_rating.phi ** 2 + s_rating.sigma ** 2)
s_rating.enshure_rd()
return s_rating.unscale()
# step 3
v = 0
for r in s_opponents:
v += (Glicko2.g(r.phi) ** 2) * Glicko2.E(s_rating.mu, r.mu, r.phi) * (1 - Glicko2.E(s_rating.mu, r.mu, r.phi))
v = v ** (-1)
# step 4
delta = 0
for (r, s) in zip(s_opponents, results):
delta += Glicko2.g(r.phi) * (s - Glicko2.E(s_rating.mu, r.mu, r.phi))
delta *= v
# step 5
a = log(s_rating.sigma**2)
def f(x):
return (exp(x) * (delta ** 2 - s_rating.phi ** 2 - v - exp(x))) / (2 * (s_rating.phi ** 2 + v + exp(x))) - (x - a) / (self.tau**2)
epsilon = 0.000001
A = a
B = 0.0
if delta ** 2 > (s_rating.phi ** 2 + v):
B = log(delta ** 2 - s_rating.phi ** 2 - v)
else:
k = 1
B = a - k * self.tau
while f(B) < 0:
k += 1
B = a - k * self.tau
f_A = f(A)
f_B = f(B)
while abs(B - A) > epsilon:
C = A + (((A - B) * f_A) / (f_B - f_A))
f_C = f(C)
if f_C * f_B < 0:
A = B
f_A = f_B
else:
f_A = f_A / 2
B = C
f_B = f_C
sigma_ = exp(A / 2)
# step 6
phi_s = sqrt(s_rating.phi ** 2 + sigma_ ** 2)
# step 7
phi_ = 1 / sqrt((1 / (phi_s ** 2)) + (1 / v))
mu_ = 0
for (r, s) in zip(s_opponents, results):
mu_ += Glicko2.g(r.phi)*(s - Glicko2.E(s_rating.mu, r.mu, r.phi))
mu_ *= phi_**2
mu_ += s_rating.mu
s_rating.sigma = sigma_
s_rating.phi = phi_
s_rating.mu = mu_
# step 8
s_rating.enshure_rd()
return s_rating.unscale()
......@@ -7,6 +7,9 @@ from collections import Counter
from itertools import groupby
import math
from numpy import power
from datetime import timedelta
from .glicko2 import Glicko2
from .cache import get_cached, invalidate_caches
from sys import modules
......@@ -68,59 +71,76 @@ def elo_history():
# hidden method with computating
def _elo_history():
elo_ratings = {}
start = 800
k = 16
glicko2 = Glicko2(800, 100, 0.1, 1.2)
players = Player.objects.all()
ratings = _glicko2(players=players, periods=_get_rating_periods(), glicko2=glicko2)
ratings_return = {}
for player in players:
elo_ratings[player] = [start]
for game in Game.objects.all().order_by('date'):
# the ratings from the other players stay the same
for player in (
players
.exclude(name=game.player0.name)
.exclude(name=game.player1.name)
.exclude(name=game.player2.name)
):
elo_ratings[player].append(elo_ratings[player][-1])
# ratings
r_allein = elo_ratings[game.player0][-1]
r_team = (elo_ratings[game.player1][-1] + elo_ratings[game.player2][-1]) / 2
# normalized ratings
R_allein = power(10, r_allein / 400)
R_team = power(10, r_team / 400)
# expected outcome
E_allein = R_allein / (R_allein + R_team)
E_team = R_team / (R_allein + R_team)
# calculate the resulting rating changes from the game
if game.won:
r_allein_new = r_allein + k * (1 - E_allein)
r_team_new = r_team + k * (0 - E_team)
else:
r_allein_new = r_allein + k * (0 - E_allein)
r_team_new = r_team + k * (1 - E_team)
ratings_return[player] = [rating.rating for rating in ratings[player]]
return ratings_return
# deltas to the old rating
delta_allein = r_allein_new - r_allein
delta_team = (r_team_new - r_team) / 2
elo_ratings[game.player0].append(
elo_ratings[game.player0][-1] + delta_allein
)
elo_ratings[game.player1].append(
elo_ratings[game.player1][-1] + delta_team
)
elo_ratings[game.player2].append(
elo_ratings[game.player2][-1] + delta_team
)
def _glicko2(players, periods, glicko2):
ratings = {}
for player in players:
ratings[player] = [glicko2.create_rating()]
participated_game = {}
for player in players:
participated_game[player] = False
for period in periods:
updates_opponents = {}
updates_results = {}
for player in players:
updates_opponents[player] = []
updates_results[player] = []
for game in period:
rating0 = ratings[game.player0][-1]
rating1 = ratings[game.player1][-1]
rating2 = ratings[game.player2][-1]
updates_opponents[game.player0].extend([rating1, rating2])
updates_results[game.player0].extend([game.won, game.won])
updates_opponents[game.player1].extend([rating0])
updates_results[game.player1].extend([1-game.won])
updates_opponents[game.player2].extend([rating0])
updates_results[game.player2].extend([1-game.won])
participated_game[game.player0] = True
participated_game[game.player1] = True
participated_game[game.player2] = True
for player in players:
if participated_game[player]:
ratings[player].append(glicko2.update_rating(ratings[player][-1], updates_opponents[player], updates_results[player]))
else:
ratings[player].append(ratings[player][-1])
return ratings
def _get_rating_periods():
games_sorted = Game.objects.all().order_by('date')
periods = [[]]
if not games_sorted:
return periods
last_date = games_sorted[0].date
for game in games_sorted:
monday1 = (last_date - timedelta(days=last_date.weekday()))
monday2 = (game.date - timedelta(days=game.date.weekday()))
weeks_between = (monday2 - monday1).days / 7
while weeks_between > 0:
weeks_between -= 1
periods.append([])
periods[-1].append(game)
last_date = game.date
return periods
return elo_ratings
# value conversion
BASE_TO_TYPE = {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment