forked from sublee/glicko2
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathglicko2.py
More file actions
210 lines (180 loc) · 7.39 KB
/
glicko2.py
File metadata and controls
210 lines (180 loc) · 7.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
glicko2
~~~~~~~
The Glicko2 rating system.
:copyright: (c) 2012 by Heungsub Lee
:license: BSD, see LICENSE for more details.
"""
import math
__version__ = "0.0.dev"
from typing import List, Tuple
# pylint: disable=invalid-name,missing-function-docstring,missing-class-docstring
# pylint: disable=too-many-arguments,too-few-public-methods,consider-using-f-string
#: The actual score for win
WIN = 1.0
#: The actual score for draw
DRAW = 0.5
#: The actual score for loss
LOSS = 0.0
MU = 1500.0
PHI = 350.0
SIGMA = 0.06
TAU = 1.0
EPSILON = 0.000001
class Rating:
def __init__(self, mu: float = MU, phi: float = PHI, sigma: float = SIGMA) -> None:
self.mu = mu
self.phi = phi
self.sigma = sigma
def __repr__(self) -> str:
c = type(self)
args = (c.__module__, c.__name__, self.mu, self.phi, self.sigma)
return "%s.%s(mu=%.3f, phi=%.3f, sigma=%.3f)" % args
def __lt__(self, other) -> bool: # type: ignore
return bool(self.mu < other.mu)
def __le__(self, other) -> bool: # type: ignore
return bool(self.mu <= other.mu)
def __gt__(self, other) -> bool: # type: ignore
return bool(self.mu > other.mu)
def __ge__(self, other) -> bool: # type: ignore
return bool(self.mu >= other.mu)
def __eq__(self, other) -> bool: # type: ignore
return bool(self.mu == other.mu)
def __ne__(self, other) -> bool: # type: ignore
return bool(self.mu != other.mu)
class Glicko2:
def __init__(
self,
mu: float = MU,
phi: float = PHI,
sigma: float = SIGMA,
tau: float = TAU,
epsilon: float = EPSILON,
) -> None:
self.mu = mu
self.phi = phi
self.sigma = sigma
self.tau = tau
self.epsilon = epsilon
def create_rating(
self, mu: float = -1.0, phi: float = -1.0, sigma: float = -1.0
) -> Rating:
if mu == -1:
mu = self.mu
if phi == -1:
phi = self.phi
if sigma == -1.0:
sigma = self.sigma
return Rating(mu, phi, sigma)
def scale_down(self, rating: Rating, ratio: float = 173.7178) -> Rating:
mu = (rating.mu - self.mu) / ratio
phi = rating.phi / ratio
return self.create_rating(mu, phi, rating.sigma)
def scale_up(self, rating: Rating, ratio: float = 173.7178) -> Rating:
mu = rating.mu * ratio + self.mu
phi = rating.phi * ratio
return self.create_rating(mu, phi, rating.sigma)
@staticmethod
def reduce_impact(rating: Rating) -> float:
"""The original form is `g(RD)`. This function reduces the impact of
games as a function of an opponent's RD.
"""
return 1.0 / math.sqrt(1 + (3 * rating.phi**2) / (math.pi**2))
@staticmethod
def expect_score(rating: Rating, other_rating: Rating, impact: float) -> float:
return 1.0 / (1 + math.exp(-impact * (rating.mu - other_rating.mu)))
def determine_sigma(
self, rating: Rating, difference: float, variance: float
) -> float:
"""Determines new sigma."""
phi = rating.phi
difference_squared = difference**2
# 1. Let a = ln(s^2), and define f(x)
alpha = math.log(rating.sigma**2)
def f(x: float) -> float:
"""This function is twice the conditional log-posterior density of
phi, and is the optimality criterion.
"""
tmp = phi**2 + variance + math.exp(x)
_a = math.exp(x) * (difference_squared - tmp) / (2 * tmp**2)
_b = (x - alpha) / (self.tau**2)
return float(_a - _b)
# 2. Set the initial values of the iterative algorithm.
a = alpha
if difference_squared > phi**2 + variance:
b = math.log(difference_squared - phi**2 - variance)
else:
k = 1
while f(alpha - k * math.sqrt(self.tau**2)) < 0:
k += 1
b = alpha - k * math.sqrt(self.tau**2)
# 3. Let fA = f(A) and f(B) = f(B)
f_a, f_b = f(a), f(b)
# 4. While |B-A| > e, carry out the following steps.
# (a) Let C = A + (A - B)fA / (fB-fA), and let fC = f(C).
# (b) If fCfB < 0, then set A <- B and fA <- fB; otherwise, just set
# fA <- fA/2.
# (c) Set B <- C and fB <- fC.
# (d) Stop if |B-A| <= e. Repeat the above three steps otherwise.
while abs(b - a) > self.epsilon:
c = a + (a - b) * f_a / (f_b - f_a)
f_c = f(c)
if f_c * f_b < 0:
a, f_a = b, f_b
else:
f_a /= 2
b, f_b = c, f_c
# 5. Once |B-A| <= e, set s' <- e^(A/2)
return float(math.exp(1) ** (a / 2))
def rate(self, rating: Rating, series: List[tuple]) -> Rating:
# Step 2. For each player, convert the rating and RDs onto the
# Glicko-2 scale.
rating = self.scale_down(rating)
# Step 3. Compute the quantity v. This is the estimated variance of the
# team's/player's rating based only on game outcomes.
# Step 4. Compute the quantity difference, the estimated improvement in
# rating by comparing the pre-period rating to the performance
# rating based only on game outcomes.
variance_inv = 0.0
difference = 0.0
if not series:
# If the team didn't play in the series, do only Step 6
phi_star = math.sqrt(rating.phi**2 + rating.sigma**2)
return self.scale_up(self.create_rating(rating.mu, phi_star, rating.sigma))
for actual_score, other_rating in series:
other_rating = self.scale_down(other_rating)
impact = self.reduce_impact(other_rating)
expected_score = self.expect_score(rating, other_rating, impact)
variance_inv += impact**2 * expected_score * (1 - expected_score)
difference += impact * (actual_score - expected_score)
difference /= variance_inv
variance = 1.0 / variance_inv
# Step 5. Determine the new value, Sigma(*), ot the sigma. This
# computation requires iteration.
sigma = self.determine_sigma(rating, difference, variance)
# Step 6. Update the rating deviation to the new pre-rating period
# value, Phi*.
phi_star = math.sqrt(rating.phi**2 + sigma**2)
# Step 7. Update the rating and RD to the new values, Mu' and Phi'.
phi = 1.0 / math.sqrt(1 / phi_star**2 + 1 / variance)
mu = rating.mu + phi**2 * (difference / variance)
# Step 8. Convert ratings and RDs back to original scale.
return self.scale_up(self.create_rating(mu, phi, sigma))
def rate_1vs1(
self, rating1: Rating, rating2: Rating, drawn: bool = False
) -> Tuple[Rating, Rating]:
return (
self.rate(rating1, [(DRAW if drawn else WIN, rating2)]),
self.rate(rating2, [(DRAW if drawn else LOSS, rating1)]),
)
def quality_1vs1(self, rating1: Rating, rating2: Rating) -> float:
expected_score1 = self.expect_score(
rating1, rating2, self.reduce_impact(rating1)
)
expected_score2 = self.expect_score(
rating2, rating1, self.reduce_impact(rating2)
)
expected_score = (expected_score1 + expected_score2) / 2
return 2 * (0.5 - abs(0.5 - expected_score))