├── .hgignore ├── glicko2.py └── glicko2_tests.py /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | 3 | # ignore all temporary files 4 | ~* 5 | 6 | # ignore all compiled python files 7 | *.pyc -------------------------------------------------------------------------------- /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 | 89 | def _newVol(self, rating_list, RD_list, outcome_list, v): 90 | """ Calculating the new volatility as per the Glicko2 system. 91 | 92 | _newVol(list, list, list) -> float 93 | 94 | """ 95 | i = 0 96 | delta = self._delta(rating_list, RD_list, outcome_list, v) 97 | a = math.log(math.pow(self.vol, 2)) 98 | tau = self._tau 99 | x0 = a 100 | x1 = 0 101 | 102 | while x0 != x1: 103 | # New iteration, so x(i) becomes x(i-1) 104 | x0 = x1 105 | d = math.pow(self.__rating, 2) + v + math.exp(x0) 106 | h1 = -(x0 - a) / math.pow(tau, 2) - 0.5 * math.exp(x0) \ 107 | / d + 0.5 * math.exp(x0) * math.pow(delta / d, 2) 108 | h2 = -1 / math.pow(tau, 2) - 0.5 * math.exp(x0) * \ 109 | (math.pow(self.__rating, 2) + v) \ 110 | / math.pow(d, 2) + 0.5 * math.pow(delta, 2) * math.exp(x0) \ 111 | * (math.pow(self.__rating, 2) + v - math.exp(x0)) / math.pow(d, 3) 112 | x1 = x0 - (h1 / h2) 113 | 114 | return math.exp(x1 / 2) 115 | 116 | def _delta(self, rating_list, RD_list, outcome_list, v): 117 | """ The delta function of the Glicko2 system. 118 | 119 | _delta(list, list, list) -> float 120 | 121 | """ 122 | tempSum = 0 123 | for i in range(len(rating_list)): 124 | tempSum += self._g(RD_list[i]) * (outcome_list[i] - self._E(rating_list[i], RD_list[i])) 125 | return v * tempSum 126 | 127 | def _v(self, rating_list, RD_list): 128 | """ The v function of the Glicko2 system. 129 | 130 | _v(list[int], list[int]) -> float 131 | 132 | """ 133 | tempSum = 0 134 | for i in range(len(rating_list)): 135 | tempE = self._E(rating_list[i], RD_list[i]) 136 | tempSum += math.pow(self._g(RD_list[i]), 2) * tempE * (1 - tempE) 137 | return 1 / tempSum 138 | 139 | def _E(self, p2rating, p2RD): 140 | """ The Glicko E function. 141 | 142 | _E(int) -> float 143 | 144 | """ 145 | return 1 / (1 + math.exp(-1 * self._g(p2RD) * \ 146 | (self.__rating - p2rating))) 147 | 148 | def _g(self, RD): 149 | """ The Glicko2 g(RD) function. 150 | 151 | _g() -> float 152 | 153 | """ 154 | return 1 / math.sqrt(1 + 3 * math.pow(RD, 2) / math.pow(math.pi, 2)) 155 | 156 | def did_not_compete(self): 157 | """ Applies Step 6 of the algorithm. Use this for 158 | players who did not compete in the rating period. 159 | 160 | did_not_compete() -> None 161 | 162 | """ 163 | self._preRatingRD() 164 | -------------------------------------------------------------------------------- /glicko2_tests.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 glicko2 27 | import timeit 28 | 29 | def exampleCase(): 30 | # Create a player called Ryan 31 | Ryan = glicko2.Player() 32 | # Following the example at: 33 | # http://math.bu.edu/people/mg/glicko/glicko2.doc/example.html 34 | # Pretend Ryan (of rating 1500 and rating deviation 350) 35 | # plays players of ratings 1400, 1550 and 1700 36 | # and rating deviations 30, 100 and 300 respectively 37 | # with outcomes 1, 0 and 0. 38 | #sprint "Old Rating: " + str(Ryan.rating) 39 | print("Old Rating Deviation: " + str(Ryan.rd)) 40 | print("Old Volatility: " + str(Ryan.vol)) 41 | Ryan.update_player([x for x in [1400, 1550, 1700]], 42 | [x for x in [30, 100, 300]], [1, 0, 0]) 43 | print("New Rating: " + str(Ryan.rating)) 44 | print("New Rating Deviation: " + str(Ryan.rd)) 45 | print("New Volatility: " + str(Ryan.vol)) 46 | 47 | def timingExample(runs = 10000): 48 | print("\nThe time taken to perform " + str(runs)) 49 | print("separate calculations (in seconds) was:") 50 | timeTaken = timeit.Timer("Ryan = glicko2.Player(); \ 51 | Ryan.update_player([x \ 52 | for x in [1400, 1550, 1700]], \ 53 | [x for x in [30, 100, 300]], [1, 0, 0])", \ 54 | "import glicko2").repeat(1, 10000) 55 | print(round(timeTaken[0], 4)) 56 | 57 | if __name__ == "__main__": 58 | exampleCase() 59 | timingExample() 60 | --------------------------------------------------------------------------------