Source code for rstt.ranking.inferer.glicko2

from .glicko import Glicko
from ..rating import GlickoRating, Glicko2Rating

import math


[docs] class Glicko2(Glicko): def __init__(self, mu: float = 1500, tau: float = 0.3, epsilon: float = 0.000000005, *args, **kwargs): """Glicko2 Inference Implement the `glicko-2 <https://www.glicko.net/glicko/glicko2.pdf>`_ rating system as descried by Prof. Mark E. Glickman. Parameters ---------- mu : float, optional The default mu of the rating, used to scaled down the GlickoRating, by default 1500.0 tau: float, optional Glicko2 Inference parameter. Tau constrains the change in volatility over time. Reasonable choices are between 0.3 and 1.2, by default 0.3 epsilon: float, optional Glicko2 Inference parameter. Convergence tolerance of the Illinois algorithm used in step 5 of rating update, by default 0.000000005 .. note:: Glicko2 is implemented using Glicko Inference with q := 1.0 """ super().__init__(q=1, *args, **kwargs) # --- system variable --- # self.tau = tau # reasonable choices are between 0.3 and 1.2 # --- scaling constant --- # self.scaling = 173.718 self.base_mu = mu # algorithm constant self.epsilon = epsilon ''' NOTE on epsilon for _step5 The value does not match 'The value e = 0.000001 is a sufficiently small choice' Does not reproduce the iteration of the calculation example e = 0.000000005 does produce very similar numerical values, and number of steps! Value of A,B fA, fB in step5 iteration epsilon = 0.00000001 -5.626821433520073 -6.126821433520073 -0.0005355033552526004 1.999675064776017 -5.626821433520073 -5.626955295265377 -0.0002677516776263002 1.5228332788134666e-08 epsilong = 0.000000001 -5.626821433520073 -6.126821433520073 -0.0005355033552526004 1.999675064776017 -5.626821433520073 -5.626955295265377 -0.0002677516776263002 1.5228332788134666e-08 -5.626955295265377 -5.626955287652446 1.5228332788134666e-08 -1.5227465554983055e-08 -5.626955295265377 -5.626955291458803 7.614166394067333e-09 -0.0005354469812191957 self.epsilon = 0.000000005 -5.626821433520073 -6.126821433520073 -0.0005355033552526004 1.999675064776017 -5.626821433520073 -5.626955295265377 -0.0002677516776263002 1.5228332788134666e-08 -5.626955295265377 -5.626955287652446 1.5228332788134666e-08 -1.5227465554983055e-08 . ''' ''' NOTE: - Glicko2 behaves like Glicko with a q parameters set to 1. This can be seen when comparing g(phi) of glicko2 with g(RD) in glicko This can be seen when comparing v() of glicko2 with d2() of glicko - step4, the DELTA function is similar to glicko r' value (Glcko.newRating) But I could not figure a clean mathematical relationship. Depending on its nature, it could justify a refactoring of Glicko FIXME: - Glicko2.expectedScore(mu, mu_j, phi_j) '==' Glicko.expectedScore(r, rj, RDj) with base e and lc constant 1. since the base is not a parameter, we implement an E() method This can be improved ''' # --- equations --- # # v(): -> glicko.d2 with q = 1 # g(phi) -> glicko.G with q = 1
[docs] def expectedScore(self, rating1: GlickoRating, rating2: GlickoRating, *args, **kwargs): numerator = 1 denominator = 1 + math.exp(-self.G(rating2.sigma) * (rating1.mu-rating2.mu) ) return numerator / denominator
[docs] def f(self, x: float, phi2: float, delta2: float, v: float, a: float, tau: float): numerator = math.exp(x) * (delta2 - phi2 - v - math.exp(x)) denominator = 2 * (phi2 + v + math.exp(x))**2 return (numerator / denominator) - ((x - a) / tau**2)
# --- algorithm --- # def _step1(self, tau: float): """Ranking Initialisation Step 1. description of the algorithm describes operation on the ranking status. Not the rating computation process as per RSTT workflow (i.e Inference.rate). A system constant is either to be tuned by the ranking.handler, or better, in the ranking.forward (a) rating initialisation is not a rstt.inference setting, but a ranking.datamodel one (b) most recent rating, i.e ranking.datamodel.get(player), againt not inference related Thus _step1 is implemented as a tau set method. NOTE: - From reading, "system constant" I assume tau should be evaluated on the entire rating period, not for each player - unclear if it is to be computed every rating period, or tune it once. """ self.tau = tau def _step2(self, prior: Glicko2Rating) -> Glicko2Rating: return Glicko2Rating(mu=(prior.mu-self.base_mu)/self.scaling, sigma=prior.sigma/self.scaling, volatility=prior.volatility) def _step3(self, rating1: GlickoRating, games: list[tuple[GlickoRating, float]]): return self.d2(rating1, games) def _step4(self, rating1: GlickoRating, games: list[tuple[GlickoRating, float]], v: float) -> float: DELTA = sum([self.G(rating2.sigma) * (sj-self.expectedScore(rating1, rating2)) for rating2, sj in games]) return v * DELTA def _step5(self, phi2: float, vol2: float, v: float, delta2: float): """determine new volatility value, based on Illinois algorithm TODO: - check procedure's comments """ # 1) set A A = math.log(vol2) # 2) initial values if delta2 > phi2 + v: B = math.log(delta2 - phi2 - v) else: k = 1 while self.f(A-k*self.tau, phi2, delta2, v, A, self.tau) < 0: k = k + 1 B = A - k*self.tau if k == 5: break B = A - k*self.tau # 3) fA = self.f(A, phi2, delta2, v, A, self.tau) fB = self.f(B, phi2, delta2, v, A, self.tau) # 4) while abs(B-A) > self.epsilon: # (a) C = A+(A-B)*fA/(fB-fA) fC = self.f(C, phi2, delta2, v, A, self.tau) # (b) if fC*fB <= 0: A = B fA = fB else: fA = fA/2 # (c) B = C fB = fC # (d) is the while condition # 5) post volatility return math.exp(A/2) def _step6(self, phi: float, volatility: float): return math.sqrt(phi**2 + volatility**2) def _step7(self, rating: GlickoRating, phi_star: float, v: float, games: list[tuple[GlickoRating, float]]): phi_prime = 1 / math.sqrt(1/phi_star**2 + 1/v) mu_prime = rating.mu + phi_prime**2 * sum([self.G(rj.sigma)*(sj - self.expectedScore(rating, rj)) for rj, sj in games]) return mu_prime, phi_prime def _step8(self, mu_prime: float, phi_prime: float): mu = self.scaling * mu_prime + self.base_mu sigma = self.scaling * phi_prime return mu, sigma # --- Inference --- #
[docs] def rate(self, rating: Glicko2Rating, ratings_opponents: list[Glicko2Rating], scores: list[float]): # !!! self._step1(...) needs to be performed in the forward method ''' NOTE: scaling up and down rating - within the rate method increase number of operation. - within the forward method makes it harder to read and maintain. ''' # step2 - scaling down r = self._step2(rating) rs = [self._step2(opp) for opp in ratings_opponents] # NOTE: unecessary formating -> change method signature games = [(rating, score) for rating, score in zip(rs, scores)] # perform steps v = self._step3(r, games) delta = self._step4(r, games, v) post_volatility = self._step5(r.sigma**2, r.volatility**2, v, delta**2) phi_star = self._step6(phi=r.sigma, volatility=post_volatility) mu_prime, phi_prime = self._step7(r, phi_star, v, games) post_mu, post_rd = self._step8(mu_prime, phi_prime) return Glicko2Rating(post_mu, post_rd, post_volatility)
''' TODO: - User & Dev DocString - Add author comments and Note - Check default values - What about predictions ? - Edit docs/*.rst files to include new features - add Glicko2 Ranking - CleanUp Code - CleanUp Test - merge glikco 1&2 test variables ? '''