├── .gitignore ├── LICENSE ├── glicko2.py ├── glicko2tests.py ├── setup.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2016, Heungsub Lee 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /glicko2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | glicko2 4 | ~~~~~~~ 5 | 6 | The Glicko2 rating system. 7 | 8 | :copyright: (c) 2012 by Heungsub Lee 9 | :license: BSD, see LICENSE for more details. 10 | """ 11 | import math 12 | 13 | __version__ = '0.0.dev' 14 | 15 | #: The actual score for win 16 | WIN = 1. 17 | #: The actual score for draw 18 | DRAW = 0.5 19 | #: The actual score for loss 20 | LOSS = 0. 21 | 22 | 23 | MU = 1500 24 | PHI = 350 25 | SIGMA = 0.06 26 | TAU = 1.0 27 | EPSILON = 0.000001 28 | 29 | 30 | class Rating(object): 31 | def __init__(self, mu=MU, phi=PHI, sigma=SIGMA): 32 | self.mu = mu 33 | self.phi = phi 34 | self.sigma = sigma 35 | 36 | def __repr__(self): 37 | c = type(self) 38 | args = (c.__module__, c.__name__, self.mu, self.phi, self.sigma) 39 | return '%s.%s(mu=%.3f, phi=%.3f, sigma=%.3f)' % args 40 | 41 | 42 | class Glicko2(object): 43 | def __init__(self, mu=MU, phi=PHI, sigma=SIGMA, tau=TAU, epsilon=EPSILON): 44 | self.mu = mu 45 | self.phi = phi 46 | self.sigma = sigma 47 | self.tau = tau 48 | self.epsilon = epsilon 49 | 50 | def create_rating(self, mu=None, phi=None, sigma=None): 51 | if mu is None: 52 | mu = self.mu 53 | if phi is None: 54 | phi = self.phi 55 | if sigma is None: 56 | sigma = self.sigma 57 | return Rating(mu, phi, sigma) 58 | 59 | def scale_down(self, rating, ratio=173.7178): 60 | mu = (rating.mu - self.mu) / ratio 61 | phi = rating.phi / ratio 62 | return self.create_rating(mu, phi, rating.sigma) 63 | 64 | def scale_up(self, rating, ratio=173.7178): 65 | mu = rating.mu * ratio + self.mu 66 | phi = rating.phi * ratio 67 | return self.create_rating(mu, phi, rating.sigma) 68 | 69 | def reduce_impact(self, rating): 70 | """The original form is `g(RD)`. This function reduces the impact of 71 | games as a function of an opponent's RD. 72 | """ 73 | return 1. / math.sqrt(1 + (3 * rating.phi ** 2) / (math.pi ** 2)) 74 | 75 | def expect_score(self, rating, other_rating, impact): 76 | return 1. / (1 + math.exp(-impact * (rating.mu - other_rating.mu))) 77 | 78 | def determine_sigma(self, rating, difference, variance): 79 | """Determines new sigma.""" 80 | phi = rating.phi 81 | difference_squared = difference ** 2 82 | # 1. Let a = ln(s^2), and define f(x) 83 | alpha = math.log(rating.sigma ** 2) 84 | 85 | def f(x): 86 | """This function is twice the conditional log-posterior density of 87 | phi, and is the optimality criterion. 88 | """ 89 | tmp = phi ** 2 + variance + math.exp(x) 90 | a = math.exp(x) * (difference_squared - tmp) / (2 * tmp ** 2) 91 | b = (x - alpha) / (self.tau ** 2) 92 | return a - b 93 | 94 | # 2. Set the initial values of the iterative algorithm. 95 | a = alpha 96 | if difference_squared > phi ** 2 + variance: 97 | b = math.log(difference_squared - phi ** 2 - variance) 98 | else: 99 | k = 1 100 | while f(alpha - k * math.sqrt(self.tau ** 2)) < 0: 101 | k += 1 102 | b = alpha - k * math.sqrt(self.tau ** 2) 103 | # 3. Let fA = f(A) and f(B) = f(B) 104 | f_a, f_b = f(a), f(b) 105 | # 4. While |B-A| > e, carry out the following steps. 106 | # (a) Let C = A + (A - B)fA / (fB-fA), and let fC = f(C). 107 | # (b) If fCfB < 0, then set A <- B and fA <- fB; otherwise, just set 108 | # fA <- fA/2. 109 | # (c) Set B <- C and fB <- fC. 110 | # (d) Stop if |B-A| <= e. Repeat the above three steps otherwise. 111 | while abs(b - a) > self.epsilon: 112 | c = a + (a - b) * f_a / (f_b - f_a) 113 | f_c = f(c) 114 | if f_c * f_b < 0: 115 | a, f_a = b, f_b 116 | else: 117 | f_a /= 2 118 | b, f_b = c, f_c 119 | # 5. Once |B-A| <= e, set s' <- e^(A/2) 120 | return math.exp(1) ** (a / 2) 121 | 122 | def rate(self, rating, series): 123 | # Step 2. For each player, convert the rating and RD's onto the 124 | # Glicko-2 scale. 125 | rating = self.scale_down(rating) 126 | # Step 3. Compute the quantity v. This is the estimated variance of the 127 | # team's/player's rating based only on game outcomes. 128 | # Step 4. Compute the quantity difference, the estimated improvement in 129 | # rating by comparing the pre-period rating to the performance 130 | # rating based only on game outcomes. 131 | variance_inv = 0 132 | difference = 0 133 | if not series: 134 | # If the team didn't play in the series, do only Step 6 135 | phi_star = math.sqrt(rating.phi ** 2 + rating.sigma ** 2) 136 | return self.scale_up(self.create_rating(rating.mu, phi_star, rating.sigma)) 137 | for actual_score, other_rating in series: 138 | other_rating = self.scale_down(other_rating) 139 | impact = self.reduce_impact(other_rating) 140 | expected_score = self.expect_score(rating, other_rating, impact) 141 | variance_inv += impact ** 2 * expected_score * (1 - expected_score) 142 | difference += impact * (actual_score - expected_score) 143 | difference /= variance_inv 144 | variance = 1. / variance_inv 145 | # Step 5. Determine the new value, Sigma', ot the sigma. This 146 | # computation requires iteration. 147 | sigma = self.determine_sigma(rating, difference, variance) 148 | # Step 6. Update the rating deviation to the new pre-rating period 149 | # value, Phi*. 150 | phi_star = math.sqrt(rating.phi ** 2 + sigma ** 2) 151 | # Step 7. Update the rating and RD to the new values, Mu' and Phi'. 152 | phi = 1. / math.sqrt(1 / phi_star ** 2 + 1 / variance) 153 | mu = rating.mu + phi ** 2 * (difference / variance) 154 | # Step 8. Convert ratings and RD's back to original scale. 155 | return self.scale_up(self.create_rating(mu, phi, sigma)) 156 | 157 | def rate_1vs1(self, rating1, rating2, drawn=False): 158 | return (self.rate(rating1, [(DRAW if drawn else WIN, rating2)]), 159 | self.rate(rating2, [(DRAW if drawn else LOSS, rating1)])) 160 | 161 | def quality_1vs1(self, rating1, rating2): 162 | expected_score1 = self.expect_score(rating1, rating2, self.reduce_impact(rating1)) 163 | expected_score2 = self.expect_score(rating2, rating1, self.reduce_impact(rating2)) 164 | expected_score = (expected_score1 + expected_score2) / 2 165 | return 2 * (0.5 - abs(0.5 - expected_score)) 166 | -------------------------------------------------------------------------------- /glicko2tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from glicko2 import Glicko2, WIN, DRAW, LOSS 3 | 4 | 5 | class almost(object): 6 | 7 | def __init__(self, val, precision=3): 8 | self.val = val 9 | self.precision = precision 10 | 11 | def almost_equals(self, val1, val2): 12 | if round(val1, self.precision) == round(val2, self.precision): 13 | return True 14 | fmt = '%.{0}f'.format(self.precision) 15 | mantissa = lambda f: int((fmt % f).replace('.', '')) 16 | return abs(mantissa(val1) - mantissa(val2)) <= 1 17 | 18 | def __eq__(self, other): 19 | try: 20 | if not self.almost_equals(self.val.volatility, other.volatility): 21 | return False 22 | except AttributeError: 23 | pass 24 | return (self.almost_equals(self.val.mu, other.mu) and 25 | self.almost_equals(self.val.sigma, other.sigma)) 26 | 27 | def __repr__(self): 28 | return repr(self.val) 29 | 30 | 31 | def test_glickman_example(): 32 | env = Glicko2(tau=0.5) 33 | r1 = env.create_rating(1500, 200, 0.06) 34 | r2 = env.create_rating(1400, 30) 35 | r3 = env.create_rating(1550, 100) 36 | r4 = env.create_rating(1700, 300) 37 | rated = env.rate(r1, [(WIN, r2), (LOSS, r3), (LOSS, r4)]) 38 | # env.create_rating2(1464.06, 151.52, 0.05999) 39 | assert almost(rated) == env.create_rating(1464.051, 151.515, 0.05999) 40 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import with_statement 3 | 4 | import re 5 | 6 | from setuptools import setup 7 | from setuptools.command.test import test 8 | 9 | 10 | # detect the current version 11 | with open('glicko2.py') as f: 12 | version = re.search(r'__version__\s*=\s*\'(.+?)\'', f.read()).group(1) 13 | assert version 14 | 15 | 16 | # use pytest instead 17 | def run_tests(self): 18 | test_file = re.sub(r'\.pyc$', '.py', __import__(self.test_suite).__file__) 19 | raise SystemExit(__import__('pytest').main([test_file])) 20 | test.run_tests = run_tests 21 | 22 | 23 | setup( 24 | name='glicko2', 25 | version=version, 26 | license='BSD', 27 | author='Heungsub Lee', 28 | author_email='s'r'u'r'b'r'@'r's'r'u'r'b'r'l'r'.'r'e'r'e', 29 | url='http://github.com/sublee/glicko2', 30 | description='The Glicko-2 rating system', 31 | long_description=__doc__, 32 | platforms='any', 33 | py_modules=['glicko2'], 34 | classifiers=['Development Status :: 1 - Planning', 35 | 'Intended Audience :: Developers', 36 | 'Intended Audience :: Science/Research', 37 | 'License :: OSI Approved :: BSD License', 38 | 'Operating System :: OS Independent', 39 | 'Programming Language :: Python', 40 | 'Programming Language :: Python :: 2', 41 | 'Programming Language :: Python :: 2.5', 42 | 'Programming Language :: Python :: 2.6', 43 | 'Programming Language :: Python :: 2.7', 44 | 'Programming Language :: Python :: 3', 45 | 'Programming Language :: Python :: 3.1', 46 | 'Programming Language :: Python :: 3.2', 47 | 'Programming Language :: Python :: 3.3', 48 | 'Programming Language :: Python :: Implementation :: CPython', 49 | 'Programming Language :: Python :: Implementation :: Jython', 50 | 'Programming Language :: Python :: Implementation :: PyPy', 51 | 'Topic :: Games/Entertainment'], 52 | install_requires=['distribute'], 53 | test_suite='glicko2tests', 54 | tests_require=['pytest'], 55 | ) 56 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py25, py26, py27, py31, py32, py33, pypy, jython 3 | 4 | [testenv] 5 | commands = {envpython} setup.py test 6 | --------------------------------------------------------------------------------