5 from xonstat.models import PlayerGlicko, Game, PlayerGameStat
7 log = logging.getLogger(__name__)
10 # log.addHandler(logging.StreamHandler())
11 # log.setLevel(logging.DEBUG)
13 # the default system volatility constant
18 return 1 / math.sqrt(1 + (3 * phi ** 2) / (math.pi ** 2))
21 def calc_e(mu, mu_j, phi_j):
22 return 1. / (1 + math.exp(-calc_g(phi_j) * (mu - mu_j)))
26 """ Estimated variance of the team or player's ratings based only on game outcomes. """
28 for i in range(len(gs)):
29 total += (gs[i] ** 2) * es[i] * (1-es[i])
34 def calc_delta(v, gs, es, results):
36 Compute the estimated improvement in rating by comparing the pre-period rating to the
37 performance rating based only on game outcomes.
40 for i in range(len(gs)):
41 total += gs[i] * (results[i] - es[i])
46 def calc_sigma_bar(sigma, delta, phi, v, tau=TAU):
47 """ Compute the new volatility. """
49 A = a = math.log(sigma**2)
51 # pre-compute some terms
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
61 if delta_sq > (phi_sq + v):
62 B = math.log(delta_sq - phi_sq - v)
65 while f(a - k * tau) < 0:
70 while abs(B - A) > epsilon:
71 C = A + (A - B) * (fa / (fb - fa))
82 # log.debug("A={}, B={}, C={}, fA={}, fB={}, fC={}".format(A, B, C, fa, fb, fc))
84 return math.e ** (A / 2)
87 def rate(player, opponents, results):
89 Calculate the ratings improvement for a given player, provided their opponents and
90 corresponding results versus them.
92 p_g2 = player.to_glicko2()
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))
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]))
106 delta = calc_delta(v, gs, es, results)
107 sigma_bar = calc_sigma_bar(p_g2.sigma, delta, p_g2.phi, v)
109 phi_tmp = math.sqrt(p_g2.phi ** 2 + sigma_bar ** 2)
110 phi_bar = 1/math.sqrt((1/phi_tmp**2) + (1/v))
113 for i in range(len(opponents)):
114 sum_terms += gs[i] * (results[i] - es[i])
116 mu_bar = p_g2.mu + phi_bar**2 * sum_terms
118 new_rating = PlayerGlicko(player.player_id, player.game_type_cd, player.category, mu_bar,
119 phi_bar, sigma_bar).from_glicko2()
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))
135 Scale the points gained or lost for players based on time played in the given game.
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
141 # min time is the time played to count the player at all in a game
142 self.min_time = min_time
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
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:
153 if my_time < self.min_ratio * match_time:
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)
165 # Parameters for reduction of points
166 KREDUCTION = KReduction()
169 class GlickoWIP(object):
170 """ A work-in-progress Glicko value. """
171 def __init__(self, pg):
173 Initialize a GlickoWIP instance.
174 :param pg: the player's PlayerGlicko record.
176 # the player's current (or base) PlayerGlicko record
179 # the list of k factors for each game in the ranking period
182 # the list of opponents (PlayerGlicko or PlayerGlickoBase) in the ranking period
185 # the list of results for those games in the ranking period
189 class GlickoProcessor(object):
191 Processes the given list games using the Glicko2 algorithm.
193 def __init__(self, session):
195 Create a GlickoProcessor instance.
197 :param session: the SQLAlchemy session to use for fetching/saving records.
198 :param game_ids: the list of game_ids that need to be processed.
200 self.session = session
203 def scorefactor(self, si, sj, game_type_cd):
205 Calculate the real scorefactor of the game. This is how players
206 actually performed, which is compared to their expected performance.
208 :param si: the score per second of player I
209 :param sj: the score per second of player J
210 :param game_type_cd: the game type of the game in question
213 scorefactor_real = si / float(si + sj)
215 # duels are done traditionally - a win nets
216 # full points, not the score factor
217 if game_type_cd == 'duel':
219 if scorefactor_real > 0.5:
220 scorefactor_real = 1.0
222 elif scorefactor_real < 0.5:
223 scorefactor_real = 0.0
224 # nothing to do here for draws
226 return scorefactor_real
228 def pingfactor(self, pi, pj):
230 Calculate the ping differences between the two players, but only if both have them.
232 :param pi: the latency of player I
233 :param pj: the latency of player J
236 if pi is None or pj is None or pi < 0 or pj < 0:
241 return float(pi)/(pi+pj)
243 def load(self, game_id):
245 Load all of the needed information from the database.
248 game = self.session.query(Game).filter(Game.game_id==game_id).one()
250 log.error("Game ID {} not found.".format(game_id))
254 pgstats_raw = self.session.query(PlayerGameStat)\
255 .filter(PlayerGameStat.game_id==game_id)\
256 .filter(PlayerGameStat.player_id > 2)\
259 # ensure warmup isn't included in the pgstat records
260 for pgstat in pgstats_raw:
261 if pgstat.alivetime > game.duration:
262 pgstat.alivetime = game.duration
264 log.error("Error fetching player_game_stat records for game {}".format(self.game_id))
269 Calculate the Glicko2 ratings, deviations, and volatility updates for the records loaded.
274 def save(self, session):
276 Put all changed PlayerElo and PlayerGameStat instances into the
277 session to be updated or inserted upon commit.
283 # the example in the actual Glicko2 paper, for verification purposes
284 pA = PlayerGlicko(1, "duel", mu=1500, phi=200)
285 pB = PlayerGlicko(2, "duel", mu=1400, phi=30)
286 pC = PlayerGlicko(3, "duel", mu=1550, phi=100)
287 pD = PlayerGlicko(4, "duel", mu=1700, phi=300)
289 opponents = [pB, pC, pD]
292 rate(pA, opponents, results)
295 if __name__ == "__main__":