Advertisement
Guest User

glicko2.py

a guest
Feb 14th, 2016
68
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 17.75 KB | None | 0 0
  1. # -*- coding: utf-8 -*-
  2. """
  3. glicko2
  4. ~~~~~~~
  5. The Glicko2 rating system.
  6. :copyright: (c) 2012 by Heungsub Lee
  7. :license: BSD, see LICENSE for more details.
  8. =========================
  9. Applied to Art-Battles by Wolfram
  10. """
  11. import math
  12. import io
  13. import operator
  14. import datetime
  15.  
  16.  
  17. #: The actual score for win
  18. WIN = 1.
  19. #: The actual score for draw
  20. DRAW = 0.5
  21. #: The actual score for loss
  22. LOSS = 0.
  23.  
  24.  
  25. MU = 1500.0
  26. PHI = 350.0
  27. SIGMA = 0.06
  28. TAU = 1.0
  29. EPSILON = 0.000001
  30. #: A constant which is used to standardize the logistic function to
  31. #: `1/(1+exp(-x))` from `1/(1+10^(-r/400))`
  32. Q = math.log(10) / 400
  33. DAYS = [None, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
  34. HAT = [u"Participant", u"Rate", u"RD", u"AB", u"123ranks"]
  35.  
  36.  
  37. class Rating(object):
  38.  
  39. def __init__(self, mu=MU, phi=PHI, sigma=SIGMA):
  40. self.mu = mu
  41. self.phi = phi
  42. self.sigma = sigma
  43.  
  44. def __repr__(self):
  45. args = (self.mu, self.phi, self.sigma)
  46. return '(mu=%.3f, phi=%.3f, sigma=%.3f)' % args
  47.  
  48. def __lt__(self, other):
  49. return self.mu < other.mu
  50.  
  51.  
  52. class Glicko2(object):
  53.  
  54. def __init__(self, mu=MU, phi=PHI, sigma=SIGMA, tau=TAU, epsilon=EPSILON):
  55. self.mu = mu
  56. self.phi = phi
  57. self.sigma = sigma
  58. self.tau = tau
  59. self.epsilon = epsilon
  60.  
  61. def create_rating(self, mu=None, phi=None, sigma=None):
  62. if mu is None:
  63. mu = self.mu
  64. if phi is None:
  65. phi = self.phi
  66. if sigma is None:
  67. sigma = self.sigma
  68. return Rating(mu, phi, sigma)
  69.  
  70. def scale_down(self, rating, ratio=173.7178):
  71. mu = (rating.mu - self.mu) / ratio
  72. phi = rating.phi / ratio
  73. return self.create_rating(mu, phi, rating.sigma)
  74.  
  75. def scale_up(self, rating, ratio=173.7178):
  76. mu = rating.mu * ratio + self.mu
  77. phi = rating.phi * ratio
  78. return self.create_rating(mu, phi, rating.sigma)
  79.  
  80. def reduce_impact(self, rating):
  81. """The original form is `g(RD)`. This function reduces the impact of
  82. games as a function of an opponent's RD.
  83. """
  84. return 1 / math.sqrt(1 + (3 * rating.phi ** 2) / (math.pi ** 2))
  85.  
  86. def expect_score(self, rating, other_rating, impact):
  87. return 1. / (1 + math.exp(-impact * (rating.mu - other_rating.mu)))
  88.  
  89. def determine_sigma(self, rating, difference, variance):
  90. """Determines new sigma."""
  91. phi = rating.phi
  92. difference_squared = difference ** 2
  93. # 1. Let a = ln(s^2), and define f(x)
  94. alpha = math.log(rating.sigma ** 2)
  95.  
  96. def f(x):
  97. """This function is twice the conditional log-posterior density of
  98. phi, and is the optimality criterion.
  99. """
  100. tmp = phi ** 2 + variance + math.exp(x)
  101. a = math.exp(x) * (difference_squared - tmp) / (2 * tmp ** 2)
  102. b = (x - alpha) / (self.tau ** 2)
  103. return a - b
  104. # 2. Set the initial values of the iterative algorithm.
  105. a = alpha
  106. if difference_squared > phi ** 2 + variance:
  107. b = math.log(difference_squared - phi ** 2 - variance)
  108. else:
  109. k = 1
  110. while f(alpha - k * math.sqrt(self.tau ** 2)) < 0:
  111. k += 1
  112. b = alpha - k * math.sqrt(self.tau ** 2)
  113. # 3. Let fA = f(A) and f(B) = f(B)
  114. f_a, f_b = f(a), f(b)
  115. # 4. While |B-A| > e, carry out the following steps.
  116. # (a) Let C = A + (A - B)fA / (fB-fA), and let fC = f(C).
  117. # (b) If fCfB < 0, then set A <- B and fA <- fB; otherwise, just set
  118. # fA <- fA/2.
  119. # (c) Set B <- C and fB <- fC.
  120. # (d) Stop if |B-A| <= e. Repeat the above three steps otherwise.
  121. while abs(b - a) > self.epsilon:
  122. c = a + (a - b) * f_a / (f_b - f_a)
  123. f_c = f(c)
  124. if f_c * f_b < 0:
  125. a, f_a = b, f_b
  126. else:
  127. f_a /= 2
  128. b, f_b = c, f_c
  129. # 5. Once |B-A| <= e, set s' <- e^(A/2)
  130. return math.exp(1) ** (a / 2)
  131.  
  132. def rate(self, rating, series):
  133. # Step 2. For each player, convert the rating and RD's onto the
  134. # Glicko-2 scale.
  135. rating = self.scale_down(rating)
  136. # Step 3. Compute the quantity v. This is the estimated variance of the
  137. # team's/player's rating based only on game outcomes.
  138. # Step 4. Compute the quantity difference, the estimated improvement in
  139. # rating by comparing the pre-period rating to the performance
  140. # rating based only on game outcomes.
  141. d_square_inv = 0
  142. variance_inv = 0
  143. difference = 0
  144. for actual_score, other_rating in series:
  145. other_rating = self.scale_down(other_rating)
  146. impact = self.reduce_impact(other_rating)
  147. expected_score = self.expect_score(rating, other_rating, impact)
  148. variance_inv += impact ** 2 * expected_score * (1 - expected_score)
  149. difference += impact * (actual_score - expected_score)
  150. d_square_inv += (
  151. expected_score * (1 - expected_score) *
  152. (Q ** 2) * (impact ** 2))
  153. difference /= variance_inv
  154. variance = 1. / variance_inv
  155. denom = rating.phi ** -2 + d_square_inv
  156. phi = math.sqrt(1 / denom)
  157. # Step 5. Determine the new value, Sigma', of the sigma. This
  158. # computation requires iteration.
  159. sigma = self.determine_sigma(rating, difference, variance)
  160. # Step 6. Update the rating deviation to the new pre-rating period
  161. # value, Phi*.
  162. phi_star = math.sqrt(phi ** 2 + sigma ** 2)
  163. # Step 7. Update the rating and RD to the new values, Mu' and Phi'.
  164. phi = 1 / math.sqrt(1 / phi_star ** 2 + 1 / variance)
  165. mu = rating.mu + phi ** 2 * (difference / variance)
  166. # Step 8. Convert ratings and RD's back to original scale.
  167. return self.scale_up(self.create_rating(mu, phi, sigma))
  168.  
  169. def rate_1vs1(self, rating1, rating2, drawn=False):
  170. return (self.rate(rating1, [(DRAW if drawn else WIN, rating2)]),
  171. self.rate(rating2, [(DRAW if drawn else LOSS, rating1)]))
  172.  
  173. def quality_1vs1(self, rating1, rating2):
  174. expected_score1 = self.expect_score(rating1, rating2, self.reduce_impact(rating1))
  175. expected_score2 = self.expect_score(rating2, rating1, self.reduce_impact(rating2))
  176. expected_score = (expected_score1 + expected_score2) / 2
  177. return 2 * (0.5 - abs(0.5 - expected_score))
  178.  
  179.  
  180. class Round(object):
  181. def __init__(self, game):
  182. self.date = game[0][5:]
  183. # Game result represents as 2-dim list: sublists are ranks that contain
  184. # all names of participants, sharing this rank
  185. self.result = map(lambda k: k.split("="), game[1:])
  186.  
  187. def __repr__(self):
  188. c = type(self)
  189. args = (c.__module__, c.__name__, self.date, self.result)
  190. return '%s.%s(date=%s, result=%s)' % args
  191.  
  192.  
  193. class Combo(object):
  194. def __init__(self):
  195. self.content = []
  196.  
  197. def __repr__(self):
  198. return u", ".join(map(unicode, self.content))
  199.  
  200. def __lt__(self, other):
  201. return len(self.content) < len(other.content)
  202.  
  203. def __len__(self):
  204. return len(self.content)
  205.  
  206. def append(self, date):
  207. self.content.append(date)
  208.  
  209. def copy(self, other):
  210. self.clear()
  211. for i in other.content:
  212. self.append(i)
  213.  
  214. def clear(self):
  215. self.content = []
  216.  
  217. def reverse(self):
  218. return u", ".join(map(unicode, reversed(self.content)))
  219.  
  220. class Participant(object):
  221. def __init__(self, name):
  222. self.name = name
  223. self.date_of_last_game = None
  224. self.rating = Rating()
  225. self.games = 0
  226. self.ranks = [0, 0, 0]
  227. self.participation_combo = Combo()
  228. self.best_participation_combo = Combo()
  229. self.winning_combo = Combo()
  230. self.best_winning_combo = Combo()
  231.  
  232. def update_game(self, last_game_date, curr_game_date, rank):
  233. self.games += 1
  234. if last_game_date == self.date_of_last_game:
  235. self.participation_combo.append(curr_game_date)
  236. else:
  237. self.participation_combo.clear()
  238. self.participation_combo.append(curr_game_date)
  239. if rank == 0:
  240. self.winning_combo.append(curr_game_date)
  241. else:
  242. self.winning_combo.clear()
  243. self.update_best(self.best_winning_combo, self.winning_combo)
  244. self.update_best(self.best_participation_combo, self.participation_combo)
  245. if 0 <= rank <= 2:
  246. self.ranks[rank] += 1
  247. self.date_of_last_game = curr_game_date
  248.  
  249. def update_best(self, best, curr):
  250. if best < curr:
  251. best.copy(curr)
  252.  
  253.  
  254. def table_to_string(table, sorting_column=None, hat=HAT):
  255. if sorting_column is not None:
  256. table = sort_by_column(table, sorting_column)
  257. table.reverse()
  258. table = [hat] + table
  259. table = map(lambda l: map(lambda k: unicode(k), l), table)
  260. maxlen = [0 for _ in table[0]]
  261. for row in table:
  262. for i, j in enumerate(row):
  263. if len(j) > maxlen[i]:
  264. maxlen[i] = len(j)
  265. output_string = u""
  266. for row in table:
  267. for i, j in enumerate(row):
  268. output_string += j.ljust(maxlen[i]+1)
  269. output_string += u"\n"
  270. return output_string
  271.  
  272.  
  273. def print_table(filename, table, sorting_column=None, hat=HAT):
  274. output_file = io.open(filename, "w")
  275. output_file.write(table_to_string(table, sorting_column, hat))
  276. output_file.close()
  277.  
  278.  
  279. def sort_by_column(table, column):
  280. return sorted(table, key=operator.itemgetter(column))
  281.  
  282.  
  283. def date_difference(date1, date2):
  284. d1, m1, y1 = map(int, date1.split("."))
  285. d2, m2, y2 = map(int, date2.split("."))
  286. month_difference = 0
  287. if m1 > m2:
  288. m1, m2 = m2, m1
  289. inv = 1
  290. else:
  291. inv = -1
  292. for i in xrange(m1, m2):
  293. month_difference += DAYS[i]
  294. return (y1-y2)*365 + inv*month_difference + (d1-d2)
  295.  
  296.  
  297. def current_date():
  298. return unicode(datetime.datetime.now())[:10]
  299.  
  300.  
  301. def main(path="artbattle.txt", post_template="post_template.txt"):
  302. main_file = io.open(path)
  303. initial_lines = main_file.readlines()
  304. lines = map(lambda k: k.replace("\n", ""), initial_lines)
  305.  
  306. games = [] # List of games: any game is list of strings
  307. current_game = None
  308. for line in lines:
  309. if line[:5] == "game ":
  310. if current_game is not None:
  311. games.append(current_game)
  312. current_game = []
  313. current_game.append(line)
  314. games.append(current_game)
  315. games = map(Round, games) # Convert to Round class
  316.  
  317. player_dictionary = {} # Name: player
  318. art_battles_with_i_players = []
  319. glicko2 = Glicko2()
  320. change_rating_file = io.open("changes.txt", "w")
  321. log_file = io.open("log.txt", "w")
  322. ch_rate_table = []
  323. date_of_last_game = None
  324.  
  325. for game in games:
  326. # Adding new players
  327. names = [item for sublist in game.result for item in sublist] # Flatten the 2-dim list
  328. while len(art_battles_with_i_players) <= len(names):
  329. art_battles_with_i_players.append(Combo())
  330. art_battles_with_i_players[len(names)].append(game.date)
  331. for name in names:
  332. if name not in player_dictionary:
  333. player_dictionary[name] = Participant(name)
  334. # Getting list of ratings
  335. ratings = map(lambda k: player_dictionary[k].rating, names)
  336. # New ratings counting
  337. new_ratings = []
  338. rating_sum = 0
  339. dispersion_sum = 0
  340. volatility_sum = 0
  341. for rating in ratings:
  342. rating_sum += rating.mu
  343. dispersion_sum += rating.phi**2
  344. volatility_sum += rating.sigma
  345. opponent_number = 1.*(len(names)-1)
  346. log_dict = {}
  347. for i in xrange(len(ratings)):
  348. rating = ratings[i]
  349. alpha = 0
  350. defeated = 0.
  351. for block in game.result:
  352. beta = alpha + len(block)
  353. if i >= beta:
  354. defeated += LOSS * len(block)
  355. elif i < alpha:
  356. defeated += WIN * len(block)
  357. else:
  358. defeated += DRAW * (len(block)-1)
  359. alpha = beta
  360. mean_opp_rating = Rating((rating_sum-rating.mu)/opponent_number,
  361. math.sqrt((dispersion_sum-rating.phi**2)/opponent_number),
  362. (volatility_sum-rating.sigma)/opponent_number)
  363. new_ratings.append(glicko2.rate(rating, [[defeated/opponent_number, mean_opp_rating]]))
  364. log_dict[names[i]] = [defeated/opponent_number, mean_opp_rating]
  365. # Updating ratings
  366. changes = {} # Rating changes of participants of the current game
  367. for i in xrange(len(ratings)):
  368. ch = int(new_ratings[i].mu-player_dictionary[names[i]].rating.mu)
  369. changes[names[i]] = ("+" if ch > 0 else "") + str(ch) # Convert to string
  370. player_dictionary[names[i]].rating = new_ratings[i]
  371. # Updating game
  372. for rank, people in enumerate(game.result):
  373. for player in people:
  374. player_dictionary[player].update_game(date_of_last_game, game.date, rank)
  375. date_of_last_game = game.date
  376. # Rating changes output
  377. change_rating_file.write(unicode("\ngame " + game.date + "\n"))
  378. log_file.write(unicode("\ngame " + game.date + "\n"))
  379. ch_rate_table = []
  380. for name in names:
  381. ch_rate_table.append([name, int(player_dictionary[name].rating.mu), changes[name]])
  382. change_rating_file.write(table_to_string(ch_rate_table, hat=[u"Participant", u"Rate", u"Change"]))
  383. dynamic_rate_table = []
  384. for name in names:
  385. r = player_dictionary[name].rating
  386. dynamic_rate_table.append([name, r.mu, r.phi, r.sigma]+log_dict[name])
  387. log_file.write(table_to_string(dynamic_rate_table,
  388. hat=[u"Participant", u"Rate", u"RD", u"Volat", u"Result", u"MeanRate"]))
  389. change_rating_file.close()
  390. log_file.close()
  391. # Rating changes comment
  392. change_comment_file = io.open("comment.txt", "w")
  393. change_comment_file.write(u'<span class="spoiler"><span class="spoiler-title">'
  394. u'Рейтинги</span><span class="spoiler-body"><pre>')
  395. change_comment_file.write(table_to_string(ch_rate_table, hat=[u"Participant", u"Rate", u"Change"]))
  396. change_comment_file.write(u'</pre><span class="spoiler"><span class="spoiler-title">'
  397. u'Таблица</span><span class="spoiler-body"><pre>')
  398. output_table = []
  399. participation_combo_table = []
  400. winning_combo_table = []
  401. for key, value in player_dictionary.items():
  402. output_table.append([key, int(value.rating.mu), int(value.rating.phi), value.games, value.ranks])
  403. participation_combo_table.append([key, len(value.best_participation_combo), value.best_participation_combo])
  404. winning_combo_table.append([key, len(value.best_winning_combo), value.best_winning_combo])
  405. # Data debugger: print list of participants, sorted by alphabet for searching repetitions
  406. participant_file = io.open("artists.txt", "w")
  407. for name in sorted(player_dictionary.keys()):
  408. participant_file.write(unicode(name+"\n"))
  409. participant_file.close()
  410. # Print filtered rating table
  411. new_out_table = filter(lambda k: k[3] >= 5, output_table)
  412. print_table("confirmed_ratings.txt", new_out_table, 1)
  413. new_out_table = filter(lambda k: date_difference(date_of_last_game,
  414. player_dictionary[k[0]].date_of_last_game) < 200, output_table)
  415. print_table("filtered_ratings.txt", new_out_table, 1)
  416. print_table("combo_p.txt", participation_combo_table, 1, [u"Participant", u"Combo", u"Dates"])
  417. print_table("combo_w.txt", winning_combo_table, 1, [u"Participant", u"Combo", u"Dates"])
  418. change_comment_file.write(table_to_string(new_out_table, 1))
  419. change_comment_file.write(u'</pre></span></span></span></span>')
  420. change_comment_file.close()
  421. art_battles_with_i_players.reverse()
  422. abwipr_string = ""
  423. max_players = len(art_battles_with_i_players)-1
  424. for i, ab in enumerate(art_battles_with_i_players):
  425. if len(ab) > 0:
  426. abwipr_string += u"%i[%i]: %s\n" % ((max_players-i), len(ab), ab.reverse())
  427. try:
  428. post_file = io.open(post_template)
  429. post_string = u"".join(post_file.readlines())
  430. post_file.close()
  431. post_file = io.open("post.txt", "w")
  432. post_file.write(post_string % (current_date(), table_to_string(output_table, 1), "".join(initial_lines),
  433. len(player_dictionary.keys()), len(games),
  434. table_to_string(filter(lambda k: k[4] != [0, 0, 0], output_table), 4),
  435. table_to_string(filter(lambda k: k[3] >= 4, output_table), 3),
  436. table_to_string(filter(lambda k: k[1] >= 2, participation_combo_table), 1,
  437. [u"Participant", u"Combo", u"Dates"]),
  438. table_to_string(filter(lambda k: k[1] >= 2, winning_combo_table), 1,
  439. [u"Participant", u"Combo", u"Dates"]),
  440. abwipr_string[:-1]))
  441. post_file.close()
  442. except IOError:
  443. print 'Warning: file "%s" not found.' % post_template
  444.  
  445. if __name__ == '__main__':
  446. main()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement