6 from xonstat.models import *
9 log = logging.getLogger(__name__)
13 def __init__(self, global_K = 15, initial = 100, floor = 100, logdistancefactor = math.log(10)/float(400), maxlogdistance = math.log(10)):
14 self.global_K = global_K
15 self.initial = initial
17 self.logdistancefactor = logdistancefactor
18 self.maxlogdistance = maxlogdistance
22 def __init__(self, fulltime, mintime, minratio, games_min, games_max, games_factor):
23 self.fulltime = fulltime
24 self.mintime = mintime
25 self.minratio = minratio
26 self.games_min = games_min
27 self.games_max = games_max
28 self.games_factor = games_factor
30 def eval(self, mygames, mytime, matchtime):
31 if mytime < self.mintime:
33 if mytime < self.minratio * matchtime:
35 if mytime < self.fulltime:
36 k = mytime / float(self.fulltime)
39 if mygames >= self.games_max:
40 k *= self.games_factor
41 elif mygames > self.games_min:
42 k *= 1.0 - (1.0 - self.games_factor) * (mygames - self.games_min) / float(self.games_max - self.games_min)
47 """EloWIP is a work-in-progress Elo value. It contains all of the
48 attributes necessary to calculate Elo deltas for a given game."""
49 def __init__(self, player_id, pgstat=None):
50 # player_id this belongs to
51 self.player_id = player_id
53 # score per second in the game
54 self.score_per_second = 0.0
56 # seconds alive during a given game
62 # current player_game_stat record
65 # Elo algorithm K-factor
68 # Elo points accumulator, which is not adjusted by the K-factor
71 # elo points delta accumulator for the game, which IS adjusted
75 def should_save(self):
76 """Determines if the elo and pgstat attributes of this instance should
77 be persisted to the database"""
81 return "<EloWIP(player_id={}, score_per_second={}, alivetime={}, \
82 elo={}, pgstat={}, k={}, adjustment={}, elo_delta={})>".\
83 format(self.player_id, self.score_per_second, self.alivetime, \
84 self.elo, self.pgstat, self.k, self.adjustment, self.elo_delta)
88 """EloProcessor is a container for holding all of the intermediary AND
89 final values used to calculate Elo deltas for all players in a given
91 def __init__(self, session, game, pgstats):
93 # game which we are processing
96 # work-in-progress values, indexed by player
99 # used to determine if a pgstat record is elo-eligible
100 def elo_eligible(pgs):
101 return pgs.player_id > 2 and pgs.alivetime > timedelta(seconds=0)
103 # only process elos for elo-eligible players
104 for pgstat in filter(elo_eligible, pgstats):
105 self.wip[pgstat.player_id] = EloWIP(pgstat.player_id, pgstat)
107 # determine duration from the maximum alivetime
108 # of the players if the game doesn't have one
110 if game.duration is not None:
111 self.duration = game.duration.seconds
113 self.duration = max(i.alivetime.seconds for i in elostats)
115 # Calculate the score_per_second and alivetime values for each player.
116 # Warmups may mess up the player alivetime values, so this is a
117 # failsafe to put the alivetime ceiling to be the game's duration.
118 for e in self.wip.values():
119 if e.pgstat.alivetime.seconds > self.duration:
120 e.score_per_second = e.pgstat.score/float(self.duration)
121 e.alivetime = self.duration
123 e.score_per_second = e.pgstat.score/float(e.pgstat.alivetime.seconds)
124 e.alivetime = e.pgstat.alivetime.seconds
126 # Fetch current Elo values for all players. For players that don't yet
127 # have an Elo record, we'll give them a default one.
128 for e in session.query(PlayerElo).\
129 filter(PlayerElo.player_id.in_(self.wip.keys())).\
130 filter(PlayerElo.game_type_cd==game.game_type_cd).all():
131 self.wip[e.player_id].elo = e
133 for pid in self.wip.keys():
134 if self.wip[pid].elo is None:
135 self.wip[pid].elo = PlayerElo(pid, game.game_type_cd, ELOPARMS.initial)
137 # determine k reduction
138 self.wip[pid].k = KREDUCTION.eval(self.wip[pid].elo.games,
139 self.wip[pid].alivetime, self.duration)
141 # we don't process the players who have a zero K factor
142 self.wip = { e.player_id:e for e in self.wip.values() if e.k > 0.0}
144 # now actually process elos
148 # for w in self.wip.values():
149 # log.debug(w.player_id)
152 def scorefactor(self, si, sj):
153 """Calculate the real scorefactor of the game. This is how players
154 actually performed, which is compared to their expected performance as
155 predicted by their Elo values."""
156 scorefactor_real = si / float(si + sj)
158 # duels are done traditionally - a win nets
159 # full points, not the score factor
160 if self.game.game_type_cd == 'duel':
162 if scorefactor_real > 0.5:
163 scorefactor_real = 1.0
165 elif scorefactor_real < 0.5:
166 scorefactor_real = 0.0
167 # nothing to do here for draws
169 return scorefactor_real
172 """Perform the core Elo calculation, storing the values in the "wip"
173 dict for passing upstream."""
174 if len(self.wip.keys()) < 2:
179 pids = self.wip.keys()
180 for i in xrange(0, len(pids)):
181 ei = self.wip[pids[i]].elo
182 for j in xrange(i+1, len(pids)):
183 ej = self.wip[pids[j]].elo
184 si = self.wip[pids[i]].score_per_second
185 sj = self.wip[pids[j]].score_per_second
192 si, sj = 1, 1 # a draw
195 scorefactor_real = self.scorefactor(si, sj)
197 # expected score factor by elo
198 elodiff = min(ep.maxlogdistance, max(-ep.maxlogdistance,
199 (float(ei.elo) - float(ej.elo)) * ep.logdistancefactor))
200 scorefactor_elo = 1 / (1 + math.exp(-elodiff))
202 # initial adjustment values, which we may modify with additional rules
203 adjustmenti = scorefactor_real - scorefactor_elo
204 adjustmentj = scorefactor_elo - scorefactor_real
207 # log.debug("(New) Player i: {0}".format(ei.player_id))
208 # log.debug("(New) Player i's K: {0}".format(self.wip[pids[i]].k))
209 # log.debug("(New) Player j: {0}".format(ej.player_id))
210 # log.debug("(New) Player j's K: {0}".format(self.wip[pids[j]].k))
211 # log.debug("(New) Scorefactor real: {0}".format(scorefactor_real))
212 # log.debug("(New) Scorefactor elo: {0}".format(scorefactor_elo))
213 # log.debug("(New) adjustment i: {0}".format(adjustmenti))
214 # log.debug("(New) adjustment j: {0}".format(adjustmentj))
216 if scorefactor_elo > 0.5:
217 # player i is expected to win
218 if scorefactor_real > 0.5:
219 # he DID win, so he should never lose points.
220 adjustmenti = max(0, adjustmenti)
222 # he lost, but let's make it continuous (making him lose less points in the result)
223 adjustmenti = (2 * scorefactor_real - 1) * scorefactor_elo
225 # player j is expected to win
226 if scorefactor_real > 0.5:
227 # he lost, but let's make it continuous (making him lose less points in the result)
228 adjustmentj = (1 - 2 * scorefactor_real) * (1 - scorefactor_elo)
230 # he DID win, so he should never lose points.
231 adjustmentj = max(0, adjustmentj)
233 self.wip[pids[i]].adjustment += adjustmenti
234 self.wip[pids[j]].adjustment += adjustmentj
238 old_elo = float(w.elo.elo)
239 new_elo = max(float(w.elo.elo) + w.adjustment * w.k * ep.global_K / float(len(pids) - 1), ep.floor)
240 w.elo_delta = new_elo - old_elo
244 w.elo.update_dt = datetime.datetime.utcnow()
246 # log.debug("Setting Player {0}'s Elo delta to {1}. Elo is now {2}\
247 # (was {3}).".format(pid, w.elo_delta, new_elo, old_elo))
249 def save(self, session):
250 """Put all changed PlayerElo and PlayerGameStat instances into the
251 session to be updated or inserted upon commit."""
252 # first, save all of the player_elo values
253 for w in self.wip.values():
257 w.pgstat.elo_delta = w.elo_delta
258 session.add(w.pgstat)
260 log.debug("Unable to save Elo delta value for player_id {0}".format(w.player_id))
263 # parameters for K reduction
264 # this may be touched even if the DB already exists
265 KREDUCTION = KReduction(600, 120, 0.5, 0, 32, 0.2)
267 # parameters for chess elo
268 # only global_K may be touched even if the DB already exists
269 # we start at K=200, and fall to K=40 over the first 20 games
270 ELOPARMS = EloParms(global_K = 200)