forked from sublee/glicko2
-
Notifications
You must be signed in to change notification settings - Fork 0
/
glicko2.py
173 lines (149 loc) · 6.26 KB
/
glicko2.py
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
# -*- 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'
#: The actual score for win
WIN = 1.
#: The actual score for draw
DRAW = 0.5
#: The actual score for loss
LOSS = 0.
MU = 1500
PHI = 350
SIGMA = 0.06
TAU = 1.0
EPSILON = 0.000001
#: A constant which is used to standardize the logistic function to
#: `1/(1+exp(-x))` from `1/(1+10^(-r/400))`
Q = math.log(10) / 400
class Rating(object):
def __init__(self, mu=MU, phi=PHI, sigma=SIGMA):
self.mu = mu
self.phi = phi
self.sigma = sigma
def __repr__(self):
c = type(self)
args = (c.__module__, c.__name__, self.mu, self.phi, self.sigma)
return '%s.%s(mu=%.3f, phi=%.3f, sigma=%.3f)' % args
class Glicko2(object):
def __init__(self, mu=MU, phi=PHI, sigma=SIGMA, tau=TAU, epsilon=EPSILON):
self.mu = mu
self.phi = phi
self.sigma = sigma
self.tau = tau
self.epsilon = epsilon
def create_rating(self, mu=None, phi=None, sigma=None):
if mu is None:
mu = self.mu
if phi is None:
phi = self.phi
if sigma is None:
sigma = self.sigma
return Rating(mu, phi, sigma)
def scale_down(self, rating, ratio=173.7178):
mu = (rating.mu - self.mu) / ratio
phi = rating.phi / ratio
return self.create_rating(mu, phi, rating.sigma)
def scale_up(self, rating, ratio=173.7178):
mu = rating.mu * ratio + self.mu
phi = rating.phi * ratio
return self.create_rating(mu, phi, rating.sigma)
def reduce_impact(self, rating):
"""The original form is `g(RD)`. This function reduces the impact of
games as a function of an opponent's RD.
"""
return 1 / math.sqrt(1 + (3 * rating.phi ** 2) / (math.pi ** 2))
def expect_score(self, rating, other_rating, impact):
return 1. / (1 + math.exp(-impact * (rating.mu - other_rating.mu)))
def determine_sigma(self, rating, difference, variance):
"""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):
"""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 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 math.exp(1) ** (a / 2)
def rate(self, rating, series):
# Step 2. For each player, convert the rating and RD's 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.
d_square_inv = 0
variance_inv = 0
difference = 0
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)
d_square_inv += (
expected_score * (1 - expected_score) *
(Q ** 2) * (impact ** 2))
difference /= variance_inv
variance = 1. / variance_inv
denom = rating.phi ** -2 + d_square_inv
mu = rating.mu + Q / denom * (difference / variance_inv)
phi = math.sqrt(1 / denom)
# 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(phi ** 2 + sigma ** 2)
# Step 7. Update the rating and RD to the new values, Mu' and Phi'.
phi = 1 / math.sqrt(1 / phi_star ** 2 + 1 / variance)
mu = rating.mu + phi ** 2 * (difference / variance)
# Step 8. Convert ratings and RD's back to original scale.
return self.scale_up(self.create_rating(mu, phi, sigma))
def rate_1vs1(self, rating1, rating2, drawn=False):
return (self.rate(rating1, [(DRAW if drawn else WIN, rating2)]),
self.rate(rating2, [(DRAW if drawn else LOSS, rating1)]))
def quality_1vs1(self, rating1, rating2):
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))