Commit e8ca6ba1 authored by uudlo's avatar uudlo
Browse files

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 ...@@ -7,6 +7,9 @@ from collections import Counter
from itertools import groupby from itertools import groupby
import math import math
from numpy import power from numpy import power
from datetime import timedelta
from .glicko2 import Glicko2
from .cache import get_cached, invalidate_caches from .cache import get_cached, invalidate_caches
from sys import modules from sys import modules
...@@ -68,59 +71,76 @@ def elo_history(): ...@@ -68,59 +71,76 @@ def elo_history():
# hidden method with computating # hidden method with computating
def _elo_history(): def _elo_history():
elo_ratings = {} glicko2 = Glicko2(800, 100, 0.1, 1.2)
start = 800
k = 16
players = Player.objects.all() players = Player.objects.all()
ratings = _glicko2(players=players, periods=_get_rating_periods(), glicko2=glicko2)
ratings_return = {}
for player in players: for player in players:
elo_ratings[player] = [start] ratings_return[player] = [rating.rating for rating in ratings[player]]
return ratings_return
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)
# 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( def _glicko2(players, periods, glicko2):
elo_ratings[game.player0][-1] + delta_allein ratings = {}
)
elo_ratings[game.player1].append( for player in players:
elo_ratings[game.player1][-1] + delta_team ratings[player] = [glicko2.create_rating()]
)
elo_ratings[game.player2].append( participated_game = {}
elo_ratings[game.player2][-1] + delta_team 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 # value conversion
BASE_TO_TYPE = { 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