241 lines
7.8 KiB
Python
241 lines
7.8 KiB
Python
#
|
|
#
|
|
# Copyright (c) 2004 Stephen Waits
|
|
#
|
|
# This software is provided 'as-is', without any express or implied warranty. In
|
|
# no event will the authors be held liable for any damages arising from the use
|
|
# of this software.
|
|
#
|
|
# Permission is granted to anyone to use this software for any purpose,
|
|
# including commercial applications, and to alter it and redistribute it freely,
|
|
# subject to the following restrictions:
|
|
#
|
|
# 1. The origin of this software must not be misrepresented; you must not claim
|
|
# that you wrote the original software. If you use this software in a
|
|
# product, an acknowledgment in the product documentation would be
|
|
# appreciated but is not required.
|
|
#
|
|
# 2. Altered source versions must be plainly marked as such, and must not be
|
|
# misrepresented as being the original software.
|
|
#
|
|
# 3. This notice may not be removed or altered from any source distribution.
|
|
#
|
|
#
|
|
|
|
|
|
import math
|
|
import copy
|
|
|
|
|
|
class Glicko2:
|
|
"""
|
|
Glicko-2 Rating calculator class.
|
|
|
|
This class implements the Glicko-2 rating system algorithm written by
|
|
Professor Mark E. Glickman. All rating inputs and outputs are Glicko ratings,
|
|
but internally everything is converted to and considered Glicko-2 ratings.
|
|
|
|
Glicko-2 is an improvement on the original Glicko, which was, in turn, an
|
|
improvement on the ELO system.
|
|
|
|
The Glicko-2 system is specified on http://www.glicko.com/
|
|
"""
|
|
|
|
WIN = 1.0
|
|
LOSS = 0.0
|
|
DRAW = 0.5
|
|
|
|
dvolatility = 0.3
|
|
|
|
def __init__ (self, rating = 1500.0, deviation = 350.0, volatility = 0.06):
|
|
"""
|
|
Constructor with rating, rating deviation, and volatility optionally
|
|
specified. If nothing specified, nitializes to a rating of 1500, a
|
|
rating deviation of 350, and a volatility of 0.06.
|
|
"""
|
|
|
|
# creat state vars
|
|
self.__rating = 0.0
|
|
self.__deviation = 0.0
|
|
self.__volatility = 0.0
|
|
|
|
# create empty result lists
|
|
self.__opponents = []
|
|
self.__results = []
|
|
|
|
# initialize state
|
|
self.SetRating(rating)
|
|
self.SetDeviation(deviation)
|
|
self.SetVolatility(volatility)
|
|
|
|
def __cmp__(self, other):
|
|
"""Comparison operator"""
|
|
return cmp( self.__rating, other.__rating )
|
|
|
|
|
|
def GetRating (self):
|
|
"""
|
|
Get the current rating. This rating is a Glicko rating, not a Glicko2 rating.
|
|
For details, please see http://www.glicko.com/
|
|
"""
|
|
return (self.__rating * 173.7178) + 1500.0
|
|
|
|
def GetDeviation (self):
|
|
"""
|
|
Get the current rating deviation. This is a Glicko rating deviation, not a
|
|
Glicko-2 RD. For details, please see http://www.glicko.com/
|
|
"""
|
|
return self.__deviation * 173.7178
|
|
|
|
def GetVolatility (self):
|
|
"""
|
|
Get the current rating volatility.
|
|
"""
|
|
return self.__volatility
|
|
|
|
def SetRating (self, rating):
|
|
"""
|
|
Set Glicko rating. Internally this is converted to a Glicko2 rating.
|
|
For details, please see http://www.glicko.com/
|
|
"""
|
|
self.__rating = (rating - 1500.0) / 173.7178
|
|
|
|
def SetDeviation(self, deviation):
|
|
"""
|
|
Set Glicko rating deviation. Internally this is converted to a Glicko-2 RD.
|
|
For details, please see http://www.glicko.com/
|
|
"""
|
|
self.__deviation = deviation / 173.7178
|
|
|
|
def SetVolatility(self, volatility):
|
|
"""
|
|
Set rating volatility.
|
|
"""
|
|
self.__volatility = volatility
|
|
|
|
def ClearResults (self):
|
|
"""
|
|
Clear all results previously added via AddResult(), AddWin(), AddLoss(),
|
|
and/or AddDraw(). This method is called automatically whenever Update()
|
|
is called.
|
|
"""
|
|
self.__opponents = []
|
|
self.__results = []
|
|
|
|
def AddResult (self, opponent, result):
|
|
"""
|
|
Add a result to this rating. Note that no calculation is performed until
|
|
Update() is called.
|
|
"""
|
|
self.__opponents.append(copy.copy(opponent))
|
|
self.__results.append(copy.copy(result)) # copy probably not needed
|
|
|
|
def AddWin (self, opponent):
|
|
"""
|
|
Add a win result to this rating. Note that no calculation is performed until
|
|
Update() is called.
|
|
"""
|
|
self.AddResult(opponent, self.WIN)
|
|
|
|
def AddLoss (self, opponent):
|
|
"""
|
|
Add a loss result to this rating. Note that no calculation is performed until
|
|
Update() is called.
|
|
"""
|
|
self.AddResult(opponent, self.LOSS)
|
|
|
|
def AddDraw (self, opponent):
|
|
"""
|
|
Add a draw result to this rating. Note that no calculation is performed until
|
|
Update() is called.
|
|
"""
|
|
self.AddResult(opponent, self.DRAW)
|
|
|
|
def Update (self):
|
|
"""
|
|
Update rating based on current results list, and clear results.
|
|
"""
|
|
|
|
# util func
|
|
def g (deviation):
|
|
"""
|
|
Internal utility function.
|
|
"""
|
|
return 1.0 / (math.sqrt(1.0 + 3.0 * deviation * deviation / (math.pi * math.pi)));
|
|
|
|
# util func
|
|
def E (rating, rating_opponent, deviation_opponent):
|
|
"""
|
|
Internal utility function.
|
|
"""
|
|
return 1.0 / (1.0 + math.exp(-g(deviation_opponent)*(rating - rating_opponent)));
|
|
|
|
# bail if no opponents set
|
|
if len(self.__opponents) == 0:
|
|
return
|
|
|
|
# compute variance
|
|
variance = 0.0
|
|
for opp in self.__opponents:
|
|
g_i = g(opp.__deviation)
|
|
E_i = E(self.__rating,opp.__rating,opp.__deviation)
|
|
variance += g_i * g_i * E_i * (1.0 - E_i)
|
|
variance = 1.0 / variance
|
|
|
|
# compute delta
|
|
delta = 0.0
|
|
for i in range(len(self.__opponents)):
|
|
delta += g(self.__opponents[i].__deviation) * (self.__results[i] - E(self.__rating, self.__opponents[i].__rating, self.__opponents[i].__deviation))
|
|
delta *= variance
|
|
|
|
# determine new volatility
|
|
new_volatility = 0.0
|
|
a = math.log( math.pow(self.__volatility,2.0) )
|
|
x = 0.0
|
|
x_new = a
|
|
while ( abs(x - x_new) > 0.0000001 ):
|
|
x = x_new;
|
|
d = math.pow(self.__deviation,2.0) + variance + math.exp(x)
|
|
h1 = -(x - a)/(math.pow(self.dvolatility,2.0)) - 0.5*math.exp(x)/d + 0.5*math.exp(x)*(delta/d)*(delta/d)
|
|
h2 = -1.0/(math.pow(self.dvolatility,2.0)) - 0.5*math.exp(x)*(math.pow(self.__deviation,2.0)+variance)/(pow(d,2.0)) + 0.5*(math.pow(delta,2.0))*math.exp(x)*((math.pow(self.__deviation,2.0)) + variance - math.exp(x))/(pow(d,3.0))
|
|
x_new = x - h1/h2
|
|
new_volatility = math.exp(x_new / 2.0)
|
|
|
|
# update the rating deviation to the new pre-rating period value
|
|
pre_deviation = math.sqrt( math.pow(self.__deviation,2.0) + math.pow(new_volatility,2.0) )
|
|
|
|
# update the rating and deviation
|
|
new_deviation = 1.0 / (math.sqrt( 1.0/(pow(pre_deviation,2.0)) + 1.0 / variance))
|
|
new_rating = 0.0
|
|
for i in range(len(self.__opponents)):
|
|
new_rating += g(self.__opponents[i].__deviation) * (self.__results[i] - E(self.__rating, self.__opponents[i].__rating, self.__opponents[i].__deviation))
|
|
new_rating = new_rating * pow(new_deviation,2.0)
|
|
new_rating += self.__rating
|
|
|
|
# copy new values
|
|
self.__rating = new_rating
|
|
self.__deviation = new_deviation
|
|
self.__volatility = new_volatility
|
|
|
|
# wipe our result lists
|
|
self.ClearResults()
|
|
|
|
|
|
|
|
# test code
|
|
if __name__ == "__main__":
|
|
|
|
A = Glicko2(1500.0, 200.0, 0.06)
|
|
B = Glicko2(1400.0, 30.0, 0.06)
|
|
C = Glicko2(1550.0, 100.0, 0.06)
|
|
D = Glicko2(1700.0, 300.0, 0.06)
|
|
|
|
A.AddWin(B)
|
|
A.AddLoss(C)
|
|
A.AddResult(D,Glicko2.LOSS)
|
|
|
|
A.Update();
|
|
|
|
print "rating",A.GetRating(), "deviation",A.GetDeviation()
|
|
|