158 lines
4.9 KiB
CoffeeScript
158 lines
4.9 KiB
CoffeeScript
|
# Formula from Glickman's paper:
|
||
|
# http://glicko.net/glicko/glicko2.pdf
|
||
|
|
||
|
config =
|
||
|
TAU: 0.5
|
||
|
DEFAULT_RATING: 1500
|
||
|
DEFAULT_DEVIATION: 350
|
||
|
DEFAULT_VOLATILITY: 0.06
|
||
|
CONVERGENCE_TOLERANCE: 0.000001
|
||
|
|
||
|
GLICKO_CONSTANT = 173.1718
|
||
|
PI_SQUARED = Math.PI * Math.PI
|
||
|
|
||
|
ratingFromGlicko = (rating) ->
|
||
|
(rating - 1500) / GLICKO_CONSTANT
|
||
|
|
||
|
deviationFromGlicko = (deviation) ->
|
||
|
deviation / GLICKO_CONSTANT
|
||
|
|
||
|
g = (deviation) ->
|
||
|
1 / Math.sqrt(1 + 3 * deviation * deviation / PI_SQUARED)
|
||
|
|
||
|
E = (mu, opponentMu, opponentPhi) ->
|
||
|
1 / (1 + Math.exp(-g(opponentPhi) * (mu - opponentMu)))
|
||
|
|
||
|
_extractPlayer = (player) ->
|
||
|
{rating, deviation, volatility} = player
|
||
|
|
||
|
createPlayer = ->
|
||
|
rating: config.DEFAULT_RATING
|
||
|
deviation: config.DEFAULT_DEVIATION
|
||
|
volatility: config.DEFAULT_VOLATILITY
|
||
|
|
||
|
calculate = (player, matches, options = {}) ->
|
||
|
# Step 1: Initialize tau. Players with no rating are assumed to have been
|
||
|
# handled already using the createPlayer function.
|
||
|
tau = options.systemConstant || config.TAU
|
||
|
extractPlayer = (options.extractPlayer || _extractPlayer)
|
||
|
glickoPlayer = extractPlayer(player)
|
||
|
playerRating = ratingFromGlicko(glickoPlayer.rating)
|
||
|
playerDeviation = deviationFromGlicko(glickoPlayer.deviation)
|
||
|
playerVolatility = glickoPlayer.volatility
|
||
|
|
||
|
# Step 2: Convert from Glicko to Glicko2
|
||
|
gPlayers = []
|
||
|
ePlayers = []
|
||
|
scores = []
|
||
|
for match in matches
|
||
|
{opponent, score} = match
|
||
|
glickoOpponent = extractPlayer(opponent)
|
||
|
opponentRating = ratingFromGlicko(opponent.rating)
|
||
|
opponentDeviation = deviationFromGlicko(opponent.deviation)
|
||
|
gPlayers.push g(opponentDeviation)
|
||
|
ePlayers.push E(playerRating, opponentRating, opponentDeviation)
|
||
|
scores.push score
|
||
|
|
||
|
# Step 3: Compute estimated variance (v)
|
||
|
estimatedVariance = calculateEstimatedVariance(gPlayers, ePlayers)
|
||
|
|
||
|
# Step 4: Compute estimated improvement in rating (delta)
|
||
|
improvementSum = calculateImprovementSum(gPlayers, ePlayers, scores)
|
||
|
|
||
|
# Step 5: Determine new volatility (delta prime)
|
||
|
newVolatility = calculateNewVolatility(improvementSum, playerDeviation,
|
||
|
estimatedVariance, playerVolatility, tau)
|
||
|
|
||
|
# Step 6: Update rating deviation to new pre-rating period value
|
||
|
periodValue = playerDeviation * playerDeviation + newVolatility * newVolatility
|
||
|
|
||
|
# Step 7: Determine new rating and rating deviation
|
||
|
newDeviation = 1 / Math.sqrt(1 / periodValue + 1 / estimatedVariance)
|
||
|
newRating = playerRating + newDeviation * newDeviation * improvementSum
|
||
|
|
||
|
# Step 8: Convert back to Glicko scale
|
||
|
rating = GLICKO_CONSTANT * newRating + config.DEFAULT_RATING
|
||
|
deviation = GLICKO_CONSTANT * newDeviation
|
||
|
volatility = newVolatility
|
||
|
|
||
|
# Return results
|
||
|
return {rating, deviation, volatility}
|
||
|
|
||
|
calculateImprovementSum = (gPlayers, ePlayers, scores) ->
|
||
|
improvementSum = 0.0
|
||
|
for gPlayer, i in gPlayers
|
||
|
improvementSum += gPlayer * (scores[i] - ePlayers[i])
|
||
|
improvementSum
|
||
|
|
||
|
calculateEstimatedVariance = (gPlayers, ePlayers) ->
|
||
|
estimatedVariance = 0.0
|
||
|
for gPlayer, i in gPlayers
|
||
|
gSquared = gPlayer * gPlayer
|
||
|
ePlayer = ePlayers[i]
|
||
|
estimatedVariance += gSquared * ePlayer * (1.0 - ePlayer)
|
||
|
estimatedVariance = 1.0 / estimatedVariance
|
||
|
estimatedVariance
|
||
|
|
||
|
calculateNewVolatility = (improvementSum, deviation, variance, volatility, tau) ->
|
||
|
# Step 5.1
|
||
|
A = a = Math.log(volatility * volatility)
|
||
|
improvement = variance * improvementSum
|
||
|
deviationSquared = deviation * deviation
|
||
|
varianceSquared = variance * variance
|
||
|
improvementSquared = improvement * improvement
|
||
|
tauSquared = tau * tau
|
||
|
convergenceTolerance = config.CONVERGENCE_TOLERANCE
|
||
|
fIter = (x) -> f(deviationSquared, improvementSquared, tauSquared, variance, a, x)
|
||
|
|
||
|
# Step 5.2
|
||
|
if improvementSquared > varianceSquared + variance
|
||
|
B = Math.log(improvementSquared - varianceSquared - variance)
|
||
|
else
|
||
|
absTau = Math.abs(tau)
|
||
|
k = 1
|
||
|
k += 1 while fIter(a - k * absTau) < 0
|
||
|
B = a - k * absTau
|
||
|
|
||
|
# Step 5.3
|
||
|
fA = fIter(A)
|
||
|
fB = fIter(B)
|
||
|
while Math.abs(B - A) > convergenceTolerance
|
||
|
# Step 5.4a
|
||
|
C = A + (A - B) * fA / (fB - fA)
|
||
|
fC = fIter(C)
|
||
|
# Step 5.4b
|
||
|
if fC * fB < 0 then [A, fA] = [B, fB] else fA /= 2
|
||
|
# Step 5.4c
|
||
|
[B, fB] = [C, fC]
|
||
|
|
||
|
# Step 5.5
|
||
|
return Math.exp(A / 2)
|
||
|
|
||
|
f = (deviationSquared, improvementSquared, tauSquared, variance, a, x) ->
|
||
|
expX = Math.exp(x)
|
||
|
denominator = deviationSquared + variance + expX
|
||
|
((expX * (improvementSquared - denominator) /
|
||
|
(2 * denominator * denominator)) - ((x - a) / tauSquared))
|
||
|
|
||
|
getRatingEstimate = (rating, deviation) ->
|
||
|
return 0 if deviation > 100
|
||
|
rds = deviation * deviation
|
||
|
sqr = sqrt(15.905694331435 * (rds + 221781.21786254))
|
||
|
inner = (1500.0 - rating) * Math.PI / sqr
|
||
|
return Math.floor(10000.0 / (1.0 + Math.pow(10.0, inner)) + 0.5) / 100.0
|
||
|
|
||
|
module.exports = {
|
||
|
calculate
|
||
|
calculateImprovementSum
|
||
|
calculateNewVolatility
|
||
|
calculateEstimatedVariance
|
||
|
createPlayer
|
||
|
ratingFromGlicko
|
||
|
deviationFromGlicko
|
||
|
getRatingEstimate
|
||
|
config
|
||
|
g
|
||
|
E
|
||
|
}
|