]> git.xonotic.org Git - xonotic/xonstat.git/commitdiff
Finish the glicko algorithm, add the typical example in main().
authorAnt Zucaro <azucaro@gmail.com>
Tue, 19 Dec 2017 16:48:27 +0000 (11:48 -0500)
committerAnt Zucaro <azucaro@gmail.com>
Tue, 19 Dec 2017 16:48:27 +0000 (11:48 -0500)
xonstat/glicko.py

index 1f1ca9fac853c5474e8f26229a302352b50c3d09..9f63675a80966c8f014da6a3a44a5f2d3b241bed 100644 (file)
@@ -1,8 +1,13 @@
 import logging
 import math
+import sys
 
 log = logging.getLogger(__name__)
 
+# DEBUG
+# log.addHandler(logging.StreamHandler())
+# log.setLevel(logging.DEBUG)
+
 # the default initial rating value
 MU = 1500
 
@@ -13,7 +18,8 @@ PHI = 350
 SIGMA = 0.06
 
 # the default system volatility constant
-TAU = 0.3
+#TAU = 0.3
+TAU = 0.5
 
 # the ratio to convert from/to glicko2
 GLICKO2_SCALE = 173.7178
@@ -42,15 +48,15 @@ class PlayerGlicko(object):
         )
 
 
-def g(phi):
+def calc_g(phi):
     return 1 / math.sqrt(1 + (3 * phi ** 2) / (math.pi ** 2))
 
 
-def e(mu, mu_j, phi_j):
-    return 1. / (1 + math.exp(-g(phi_j) * (mu - mu_j)))
+def calc_e(mu, mu_j, phi_j):
+    return 1. / (1 + math.exp(-calc_g(phi_j) * (mu - mu_j)))
 
 
-def v(gs, es):
+def calc_v(gs, es):
     """ Estimated variance of the team or player's ratings based only on game outcomes. """
     total = 0.0
     for i in range(len(gs)):
@@ -59,11 +65,114 @@ def v(gs, es):
     return 1. / total
 
 
-def delta(v, gs, es, results):
-    """ Compute the estimated improvement in rating by comparing the pre-period rating to the
-    performance rating based only on game outcomes. """
+def calc_delta(v, gs, es, results):
+    """
+    Compute the estimated improvement in rating by comparing the pre-period rating to the
+    performance rating based only on game outcomes.
+    """
     total = 0.0
     for i in range(len(gs)):
         total += gs[i] * (results[i] - es[i])
 
     return v * total
+
+
+def calc_sigma_bar(sigma, delta, phi, v, tau=TAU):
+    """ Compute the new volatility. """
+    epsilon = 0.000001
+    A = a = math.log(sigma**2)
+
+    # pre-compute some terms
+    delta_sq = delta ** 2
+    phi_sq = phi ** 2
+
+    def f(x):
+        e_up_x = math.e ** x
+        term_a = (e_up_x * (delta_sq - phi_sq - v - e_up_x)) / (2 * (phi_sq + v + e_up_x) ** 2)
+        term_b = (x - a) / tau ** 2
+        return term_a - term_b
+
+    if delta_sq > (phi_sq + v):
+        B = math.log(delta_sq - phi_sq - v)
+    else:
+        k = 1
+        while f(a - k * tau) < 0:
+            k += 1
+        B = a - k * tau
+
+    fa, fb = f(A), f(B)
+    while abs(B - A) > epsilon:
+        C = A + (A - B) * (fa / (fb - fa))
+        fc = f(C)
+
+        if fc * fb < 0:
+            A, fa = B, fb
+        else:
+            fa /= 2
+
+        B, fb = C, fc
+
+        log.debug("A={}, B={}, C={}, fA={}, fB={}, fC={}".format(A, B, C, fa, fb, fc))
+
+    return math.e ** (A / 2)
+
+
+def rate(player, opponents, results):
+    """
+    Calculate the ratings improvement for a given player, provided their opponents and
+    corresponding results versus them.
+    """
+    p_g2 = player.to_glicko2()
+
+    gs = []
+    es = []
+    for i in range(len(opponents)):
+        o_g2 = opponents[i].to_glicko2()
+        gs.append(calc_g(o_g2.phi))
+        es.append(calc_e(p_g2.mu, o_g2.mu, o_g2.phi))
+
+        # DEBUG
+        # log.debug("j={} muj={} phij={} g={} e={} s={}"
+                  # .format(i+1, o_g2.mu, o_g2.phi, gs[i], es[i], results[i]))
+
+    v = calc_v(gs, es)
+    delta = calc_delta(v, gs, es, results)
+    sigma_bar = calc_sigma_bar(p_g2.sigma, delta, p_g2.phi, v)
+
+    phi_tmp = math.sqrt(p_g2.phi ** 2 + sigma_bar ** 2)
+    phi_bar = 1/math.sqrt((1/phi_tmp**2) + (1/v))
+
+    sum_terms = 0.0
+    for i in range(len(opponents)):
+        sum_terms += gs[i] * (results[i] - es[i])
+
+    mu_bar = p_g2.mu + phi_bar**2 * sum_terms
+
+    new_rating = PlayerGlicko(mu_bar, phi_bar, sigma_bar).from_glicko2()
+
+    # DEBUG
+    # log.debug("v={}".format(v))
+    # log.debug("delta={}".format(delta))
+    # log.debug("sigma_temp={}".format(sigma_temp))
+    # log.debug("sigma_bar={}".format(sigma_bar))
+    # log.debug("phi_bar={}".format(phi_bar))
+    # log.debug("mu_bar={}".format(mu_bar))
+    # log.debug("new_rating: {} {} {}".format(new_rating.mu, new_rating.phi, new_rating.sigma))
+
+    return new_rating
+
+
+def main():
+    pA = PlayerGlicko(mu=1500, phi=200)
+    pB = PlayerGlicko(mu=1400, phi=30)
+    pC = PlayerGlicko(mu=1550, phi=100)
+    pD = PlayerGlicko(mu=1700, phi=300)
+
+    opponents = [pB, pC, pD]
+    results = [1, 0, 0]
+
+    rate(pA, opponents, results)
+
+
+if __name__ == "__main__":
+    sys.exit(main())