]> git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/glicko.py
Add a KReduction class to scale points.
[xonotic/xonstat.git] / xonstat / glicko.py
1 import logging
2 import math
3 import sys
4
5 from xonstat.models import PlayerGlicko
6
7 log = logging.getLogger(__name__)
8
9 # DEBUG
10 # log.addHandler(logging.StreamHandler())
11 # log.setLevel(logging.DEBUG)
12
13 # the default system volatility constant
14 TAU = 0.3
15
16
17 def calc_g(phi):
18     return 1 / math.sqrt(1 + (3 * phi ** 2) / (math.pi ** 2))
19
20
21 def calc_e(mu, mu_j, phi_j):
22     return 1. / (1 + math.exp(-calc_g(phi_j) * (mu - mu_j)))
23
24
25 def calc_v(gs, es):
26     """ Estimated variance of the team or player's ratings based only on game outcomes. """
27     total = 0.0
28     for i in range(len(gs)):
29         total += (gs[i] ** 2) * es[i] * (1-es[i])
30
31     return 1. / total
32
33
34 def calc_delta(v, gs, es, results):
35     """
36     Compute the estimated improvement in rating by comparing the pre-period rating to the
37     performance rating based only on game outcomes.
38     """
39     total = 0.0
40     for i in range(len(gs)):
41         total += gs[i] * (results[i] - es[i])
42
43     return v * total
44
45
46 def calc_sigma_bar(sigma, delta, phi, v, tau=TAU):
47     """ Compute the new volatility. """
48     epsilon = 0.000001
49     A = a = math.log(sigma**2)
50
51     # pre-compute some terms
52     delta_sq = delta ** 2
53     phi_sq = phi ** 2
54
55     def f(x):
56         e_up_x = math.e ** x
57         term_a = (e_up_x * (delta_sq - phi_sq - v - e_up_x)) / (2 * (phi_sq + v + e_up_x) ** 2)
58         term_b = (x - a) / tau ** 2
59         return term_a - term_b
60
61     if delta_sq > (phi_sq + v):
62         B = math.log(delta_sq - phi_sq - v)
63     else:
64         k = 1
65         while f(a - k * tau) < 0:
66             k += 1
67         B = a - k * tau
68
69     fa, fb = f(A), f(B)
70     while abs(B - A) > epsilon:
71         C = A + (A - B) * (fa / (fb - fa))
72         fc = f(C)
73
74         if fc * fb < 0:
75             A, fa = B, fb
76         else:
77             fa /= 2
78
79         B, fb = C, fc
80
81         # DEBUG
82         # log.debug("A={}, B={}, C={}, fA={}, fB={}, fC={}".format(A, B, C, fa, fb, fc))
83
84     return math.e ** (A / 2)
85
86
87 def rate(player, opponents, results):
88     """
89     Calculate the ratings improvement for a given player, provided their opponents and
90     corresponding results versus them.
91     """
92     p_g2 = player.to_glicko2()
93
94     gs = []
95     es = []
96     for i in range(len(opponents)):
97         o_g2 = opponents[i].to_glicko2()
98         gs.append(calc_g(o_g2.phi))
99         es.append(calc_e(p_g2.mu, o_g2.mu, o_g2.phi))
100
101         # DEBUG
102         # log.debug("j={} muj={} phij={} g={} e={} s={}"
103                   # .format(i+1, o_g2.mu, o_g2.phi, gs[i], es[i], results[i]))
104
105     v = calc_v(gs, es)
106     delta = calc_delta(v, gs, es, results)
107     sigma_bar = calc_sigma_bar(p_g2.sigma, delta, p_g2.phi, v)
108
109     phi_tmp = math.sqrt(p_g2.phi ** 2 + sigma_bar ** 2)
110     phi_bar = 1/math.sqrt((1/phi_tmp**2) + (1/v))
111
112     sum_terms = 0.0
113     for i in range(len(opponents)):
114         sum_terms += gs[i] * (results[i] - es[i])
115
116     mu_bar = p_g2.mu + phi_bar**2 * sum_terms
117
118     new_rating = PlayerGlicko(player.player_id, player.game_type_cd, player.category, mu_bar,
119                               phi_bar, sigma_bar).from_glicko2()
120
121     # DEBUG
122     # log.debug("v={}".format(v))
123     # log.debug("delta={}".format(delta))
124     # log.debug("sigma_temp={}".format(sigma_temp))
125     # log.debug("sigma_bar={}".format(sigma_bar))
126     # log.debug("phi_bar={}".format(phi_bar))
127     # log.debug("mu_bar={}".format(mu_bar))
128     # log.debug("new_rating: {} {} {}".format(new_rating.mu, new_rating.phi, new_rating.sigma))
129
130     return new_rating
131
132
133 class KReduction:
134     """
135     Scale the points gained or lost for players based on time played in the given game.
136     """
137     def __init__(self, full_time=600, min_time=120, min_ratio=0.5):
138         # full time is the time played to count the player in a game
139         self.full_time = full_time
140
141         # min time is the time played to count the player at all in a game
142         self.min_time = min_time
143
144         # min_ratio is the ratio of the game's time to be played to be counted fully (provided
145         # they went past `full_time` and `min_time` above.
146         self.min_ratio = min_ratio
147
148     def eval(self, my_time, match_time):
149         # kick out players who didn't play enough of the match
150         if my_time < self.min_time:
151             return 0.0
152
153         if my_time < self.min_ratio * match_time:
154             return 0.0
155
156         # scale based on time played versus what is defined as `full_time`
157         if my_time < self.full_time:
158             k = my_time / float(self.full_time)
159         else:
160             k = 1.0
161
162         return k
163
164
165 # Parameters for reduction of points
166 KREDUCTION = KReduction()
167
168
169 def main():
170     # the example in the actual Glicko2 paper, for verification purposes
171     pA = PlayerGlicko(1, "duel", mu=1500, phi=200)
172     pB = PlayerGlicko(2, "duel", mu=1400, phi=30)
173     pC = PlayerGlicko(3, "duel", mu=1550, phi=100)
174     pD = PlayerGlicko(4, "duel", mu=1700, phi=300)
175
176     opponents = [pB, pC, pD]
177     results = [1, 0, 0]
178
179     rate(pA, opponents, results)
180
181
182 if __name__ == "__main__":
183      sys.exit(main())