Source code for rstt.ranking.inferer.elo

import rstt.utils.functions as uf

from typeguard import typechecked


[docs] class Elo: def __init__(self, k: float = 20.0, lc: float = 400.0, base: float = 10.0): """Eo Inferer Simple implementation based on `wikipedia <https://en.wikipedia.org/wiki/Elo_rating_system#Theory>`_ Parameters ---------- k : float, optional The K-factor, by default 20.0 lc : float, optional The constant dividing the ratings difference in the expected score formula, by default 400.0. """ self.base = base self.lc = lc self.K = k # QUEST: should the base implementation support distribution function as parameters
[docs] @typechecked def rate(self, rating_groups: list[list[float]], scores: list[float], *args, **kwars) -> list[list[float]]: """Rate method for elo Parameters ---------- rating_groups : List[List[float]] Elo ratings formated by teams, for example [[elo_player1], [elo_player2]]. scores : List[float] corresponding scores of the ratings, for example [[1.0],[0.0]] assuming player1 won the duel. Returns ------- List[List[float]] updated ratings in the formats [[new_elo1][new_elo2]] """ ''' !!! NOBUG: Take great care when reading the code, both calls are valid: - updating ratings based on one game score: elo.rate(rating_groups=[[1500],[1600]], scores=[0.0, 1.0]) -> output two ratings - updating one player rating based on one game score: elo.rate(rating_groups=[[1500], [1600]], scores=[0.0]) -> output one rating This can be confusing as hell, however the requierements are: - support 1 versus 1 games update game by game - support 1 versus 1 games update in on computation - match input/output sysntax and type of others rating systems. ''' # Deal with bad function calls if len(rating_groups) != 2: msg = f"Expect two ratings groups, got {len(rating_groups)}" raise ValueError(msg) if len(rating_groups[0]) != 1: msg = f"Expect only one rating in the first ratings group, got {len(rating_groups[0])}" raise ValueError(msg) if len(rating_groups[1]) == 1 and len(scores) not in [1, 2]: msg = f"For 1-versus-1 update, Elo Expect \'scores\' of len 2, received {len(scores)}" raise ValueError(msg) if len(rating_groups[1]) != 1 and len(rating_groups[1]) != len(scores): msg = f"Incompatible args call, 2nd ratings group must be of length equal to the scores, received {len(rating_groups[1])} and {len(scores)}" raise ValueError(msg) if len(rating_groups[0]) == len(rating_groups[1]) == 1 and len(scores) == 2: # one 1-versus-1 case [[r1], [r2]] = rating_groups [s1, s2] = scores new_rating1 = self.post_rating( prior_rating=r1, ratings_opponents=[r2], scores=[s1]) new_rating2 = self.post_rating( prior_rating=r2, ratings_opponents=[r1], scores=[s2]) return [[new_rating1], [new_rating2]] else: # many 1-versus-1 case [[r1], rs] = rating_groups return [[self.post_rating(prior_rating=r1, ratings_opponents=rs, scores=scores)]]
[docs] @typechecked def expectedScore(self, rating1: float, rating2: float) -> float: """Compute the expected score Parameters ---------- rating1 : float a rating rating2 : float another rating Returns ------- float expected result of the player with rating1 against the player with rating2 """ return uf.logistic_elo(base=self.base, diff=rating1-rating2, constant=self.lc)
[docs] def post_rating(self, prior_rating: float, ratings_opponents: list[float], scores: list[float]): """post_rating Update the rating of a player given a list of opponent's ratings and corresponding scores against. Parameters ---------- prior_rating : float a rating to update ratings_opponents : list[float] opponent's ratings scores : list[float] scores associated to the prior_rating Returns ------- float post rating """ return prior_rating + self.K * (sum(scores) - sum([self.expectedScore(prior_rating, rating2) for rating2 in ratings_opponents]))