├── .github └── workflows │ └── test.yml ├── .gitignore ├── .hgignore ├── LICENSE ├── README.md ├── glicko2 ├── __init__.py └── glicko2.py ├── setup.py └── tests ├── __init__.py └── tests.py /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: ["3.7", "3.8", "3.9", "3.10"] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | # You can test your matrix by printing the current Python version 20 | - name: Display Python version 21 | run: python -m unittest discover 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | 3 | # ignore all temporary files 4 | ~* 5 | 6 | # ignore all compiled python files 7 | *.pyc -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Based on lmeyerov's update to Ryan Kirkman's pyglicko2 2 | https://code.google.com/r/lmeyerov-glicko2update/ 3 | https://code.google.com/p/pyglicko2/ 4 | 5 | Copyright (c) 2009 Ryan Kirkman 6 | 7 | Permission is hereby granted, free of charge, to any person 8 | obtaining a copy of this software and associated documentation 9 | files (the "Software"), to deal in the Software without 10 | restriction, including without limitation the rights to use, 11 | copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the 13 | Software is furnished to do so, subject to the following 14 | conditions: 15 | 16 | The above copyright notice and this permission notice shall be 17 | included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | OTHER DEALINGS IN THE SOFTWARE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | glicko2 2 | ======= 3 | 4 | This is my fork of lmeyerov's update to Ryan Kirkman's pyglicko2 5 | [![Build Status](https://travis-ci.org/deepy/glicko2.png?branch=master)](https://travis-ci.org/deepy/glicko2) 6 | -------------------------------------------------------------------------------- /glicko2/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from glicko2.glicko2 import * -------------------------------------------------------------------------------- /glicko2/glicko2.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2009 Ryan Kirkman 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without 7 | restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | OTHER DEALINGS IN THE SOFTWARE. 24 | """ 25 | 26 | import math 27 | 28 | class Player: 29 | # Class attribute 30 | # The system constant, which constrains 31 | # the change in volatility over time. 32 | _tau = 0.5 33 | 34 | def getRating(self): 35 | return (self.__rating * 173.7178) + 1500 36 | 37 | def setRating(self, rating): 38 | self.__rating = (rating - 1500) / 173.7178 39 | 40 | rating = property(getRating, setRating) 41 | 42 | def getRd(self): 43 | return self.__rd * 173.7178 44 | 45 | def setRd(self, rd): 46 | self.__rd = rd / 173.7178 47 | 48 | rd = property(getRd, setRd) 49 | 50 | def __init__(self, rating = 1500, rd = 350, vol = 0.06): 51 | # For testing purposes, preload the values 52 | # assigned to an unrated player. 53 | self.setRating(rating) 54 | self.setRd(rd) 55 | self.vol = vol 56 | 57 | def _preRatingRD(self): 58 | """ Calculates and updates the player's rating deviation for the 59 | beginning of a rating period. 60 | 61 | preRatingRD() -> None 62 | 63 | """ 64 | self.__rd = math.sqrt(math.pow(self.__rd, 2) + math.pow(self.vol, 2)) 65 | 66 | def update_player(self, rating_list, RD_list, outcome_list): 67 | """ Calculates the new rating and rating deviation of the player. 68 | 69 | update_player(list[int], list[int], list[bool]) -> None 70 | 71 | """ 72 | # Convert the rating and rating deviation values for internal use. 73 | rating_list = [(x - 1500) / 173.7178 for x in rating_list] 74 | RD_list = [x / 173.7178 for x in RD_list] 75 | 76 | v = self._v(rating_list, RD_list) 77 | self.vol = self._newVol(rating_list, RD_list, outcome_list, v) 78 | self._preRatingRD() 79 | 80 | self.__rd = 1 / math.sqrt((1 / math.pow(self.__rd, 2)) + (1 / v)) 81 | 82 | tempSum = 0 83 | for i in range(len(rating_list)): 84 | tempSum += self._g(RD_list[i]) * \ 85 | (outcome_list[i] - self._E(rating_list[i], RD_list[i])) 86 | self.__rating += math.pow(self.__rd, 2) * tempSum 87 | 88 | #step 5 89 | def _newVol(self, rating_list, RD_list, outcome_list, v): 90 | """ Calculating the new volatility as per the Glicko2 system. 91 | 92 | Updated for Feb 22, 2012 revision. -Leo 93 | 94 | _newVol(list, list, list, float) -> float 95 | 96 | """ 97 | #step 1 98 | a = math.log(self.vol**2) 99 | eps = 0.000001 100 | A = a 101 | 102 | #step 2 103 | B = None 104 | delta = self._delta(rating_list, RD_list, outcome_list, v) 105 | tau = self._tau 106 | if (delta ** 2) > ((self.__rd**2) + v): 107 | B = math.log(delta**2 - self.__rd**2 - v) 108 | else: 109 | k = 1 110 | while self._f(a - k * math.sqrt(tau**2), delta, v, a) < 0: 111 | k = k + 1 112 | B = a - k * math.sqrt(tau **2) 113 | 114 | #step 3 115 | fA = self._f(A, delta, v, a) 116 | fB = self._f(B, delta, v, a) 117 | 118 | #step 4 119 | while math.fabs(B - A) > eps: 120 | #a 121 | C = A + ((A - B) * fA)/(fB - fA) 122 | fC = self._f(C, delta, v, a) 123 | #b 124 | if fC * fB <= 0: 125 | A = B 126 | fA = fB 127 | else: 128 | fA = fA/2.0 129 | #c 130 | B = C 131 | fB = fC 132 | 133 | #step 5 134 | return math.exp(A / 2) 135 | 136 | def _f(self, x, delta, v, a): 137 | ex = math.exp(x) 138 | num1 = ex * (delta**2 - self.__rating**2 - v - ex) 139 | denom1 = 2 * ((self.__rating**2 + v + ex)**2) 140 | return (num1 / denom1) - ((x - a) / (self._tau**2)) 141 | 142 | def _delta(self, rating_list, RD_list, outcome_list, v): 143 | """ The delta function of the Glicko2 system. 144 | 145 | _delta(list, list, list) -> float 146 | 147 | """ 148 | tempSum = 0 149 | for i in range(len(rating_list)): 150 | tempSum += self._g(RD_list[i]) * (outcome_list[i] - self._E(rating_list[i], RD_list[i])) 151 | return v * tempSum 152 | 153 | def _v(self, rating_list, RD_list): 154 | """ The v function of the Glicko2 system. 155 | 156 | _v(list[int], list[int]) -> float 157 | 158 | """ 159 | tempSum = 0 160 | for i in range(len(rating_list)): 161 | tempE = self._E(rating_list[i], RD_list[i]) 162 | tempSum += math.pow(self._g(RD_list[i]), 2) * tempE * (1 - tempE) 163 | return 1 / tempSum 164 | 165 | def _E(self, p2rating, p2RD): 166 | """ The Glicko E function. 167 | 168 | _E(int) -> float 169 | 170 | """ 171 | return 1 / (1 + math.exp(-1 * self._g(p2RD) * \ 172 | (self.__rating - p2rating))) 173 | 174 | def _g(self, RD): 175 | """ The Glicko2 g(RD) function. 176 | 177 | _g() -> float 178 | 179 | """ 180 | return 1 / math.sqrt(1 + 3 * math.pow(RD, 2) / math.pow(math.pi, 2)) 181 | 182 | def did_not_compete(self): 183 | """ Applies Step 6 of the algorithm. Use this for 184 | players who did not compete in the rating period. 185 | 186 | did_not_compete() -> None 187 | 188 | """ 189 | self._preRatingRD() 190 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="glicko2", 8 | version="2.1.0", 9 | author="Alexander Nordlund", 10 | author_email="deep.alexander@gmail.com", 11 | description="Python implementation of glicko2", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/deepy/glicko2", 15 | packages=setuptools.find_packages(), 16 | classifiers=[ 17 | "Programming Language :: Python :: 3", 18 | "Development Status :: 6 - Mature", 19 | "License :: OSI Approved :: MIT License", 20 | "Operating System :: OS Independent", 21 | "Intended Audience :: Developers", 22 | "Intended Audience :: Science/Research", 23 | "Topic :: Scientific/Engineering :: Mathematics", 24 | ], 25 | python_requires='>=3.7', 26 | ) 27 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deepy/glicko2/7317af0aa8012c499d2cc3eb19bd9bc8375287ab/tests/__init__.py -------------------------------------------------------------------------------- /tests/tests.py: -------------------------------------------------------------------------------- 1 | import glicko2 2 | import unittest 3 | 4 | class testCases(unittest.TestCase): 5 | 6 | def setUp(self): 7 | # Feb222012 example. 8 | self.P1 = glicko2.Player() 9 | self.P1.setRd(200) 10 | self.P1.update_player([1400, 1550, 1700], [30, 100, 300], [1, 0, 0]) 11 | # Original Ryan example. 12 | self.Ryan = glicko2.Player() 13 | self.Ryan.update_player([1400, 1550, 1700], 14 | [30, 100, 300], [1, 0, 0]) 15 | 16 | 17 | def test_rating(self): 18 | self.assertEqual(round(self.P1.rating, 2), 1464.05) 19 | 20 | def test_ratingDeviation(self): 21 | self.assertEqual(round(self.P1.rd, 2), 151.52) 22 | 23 | def test_volatility(self): 24 | self.assertEqual(round(self.P1.vol, 5), 0.05999) 25 | 26 | def test_ryan_rating(self): 27 | self.assertEqual(round(self.Ryan.rating, 2), 1441.53) 28 | 29 | def test_ryan_ratingDeviant(self): 30 | self.assertEqual(round(self.Ryan.rd, 2), 193.23) 31 | 32 | def test_ryan_volatility(self): 33 | self.assertEqual(round(self.Ryan.vol, 5), 0.05999) 34 | 35 | if __name__ == "__main__": 36 | unittest.main() 37 | --------------------------------------------------------------------------------