-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathgoglicko.go
217 lines (192 loc) · 5.33 KB
/
goglicko.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
// Implementation of the Glicko 2 Rating system, for rating players. Glicko is
// an improvoment on ELO, but is much more computationally intensive.
//
// For more information, see:
//
// http://www.glicko.net/glicko/glicko2.pdf
//
// http://en.wikipedia.org/wiki/Glicko_rating_system
//
// The calculation process is broken into 8 steps.
//
// Step 1:
// Determine initial values.
//
// Step 2:
// Convert to Glicko2 Scale from the Glicko1 scale.
//
// Step 3:
// Compute (v), the estimated variance based only on game outcomes.
//
// Step 4:
// Compute the quantity Delta, the estimated improvement.
//
// Step 5:
// Determine the new value, sigma', of the volatility, in an iterative process.
//
// Step 6:
// Update the rating deviation to the new pre-rating period value, φ_z
//
// Step 7:
// Update the rating and RD to the new values, μ′ and φ′:
//
// Step 8:
// Convert back to the Glicko1 scale.
package goglicko
import (
"fmt"
"math"
)
// Overrideable Defaults
var (
// Constrains the volatility. Typically set between 0.3 and 1.2. Often
// refered to as the 'system' constant.
DefaultTau = 0.3
DefaultRat = 1500.0 // Default starting rating
DefaultDev = 350.0 // Default starting deviation
DefaultVol = 0.06 // Default starting volatility
)
// Miscellaneous Mathematical constants.
const (
piSq = math.Pi * math.Pi // π^2
// Constant transformation value, to transform between Glicko 2 and Glicko 1
glicko2Scale = 173.7178
)
// Used to indicate who won/lost/tied the game.
type Result float64
const (
Win Result = 1
Loss Result = 0
Draw Result = 0.5
)
////////////////////////////
// Sundry of Helper Funcs //
////////////////////////////
// Ensure that two floats are equal, given some epsilon.
func floatsMostlyEqual(v1, v2, epsilon float64) bool {
return math.Abs(v1-v2) < epsilon
}
// Square function for convenience
func sq(x float64) float64 {
return x * x
}
// The E function. Written as E(μ,μ_j,φ_j).
// For readability, instead of greek we use the variables
// r: rating of player
// ri: rating of opponent
// devi: deviation of opponent
func ee(r, ri, devi float64) float64 {
return 1.0 / (1 + math.Exp(-gee(devi)*(r-ri)))
}
// The g function. Written as g(φ).
// For readability, instead of greek we use the variables
// dev: The deviation of a player's rating
func gee(dev float64) float64 {
return 1 / math.Sqrt(1+3*dev*dev/piSq)
}
// Estimate the variance of the team/player's rating based only on game
// outcomes. Note, it must be true that len(ees) == len(gees).
func estVariance(gees, ees []float64) float64 {
out := 0.0
for i := range gees {
out += sq(gees[i]) * ees[i] * (1 - ees[i])
}
return 1.0 / out
}
// Estimate the improvement in rating by comparing the pre-period rating to the
// performance rating, based only on game outcomes.
//
// Note: This function is like the 'delta' in the algorithm, but here we don't
// multiply by the estimated variance.
func estImprovePartial(gees, ees []float64, r []Result) float64 {
out := 0.0
for i := range gees {
out += gees[i] * (float64(r[i]) - ees[i])
}
return out
}
// Calculate the new volatility for a Player.
func newVolatility(estVar, estImp float64, p *Rating) float64 {
epsilon := 0.000001
a := math.Log(sq(p.Volatility))
deltaSq := sq(estImp)
phiSq := sq(p.Deviation)
tauSq := sq(DefaultTau)
maxIter := 100
f := func(x float64) float64 {
eX := math.Exp(x)
return eX*(deltaSq-phiSq-estVar-eX)/
(2*sq(phiSq+estVar+eX)) - (x-a)/tauSq
}
A := a
B := 0.0
if deltaSq > (phiSq + estVar) {
B = math.Log(deltaSq - phiSq - estVar)
} else {
val := -1.0
k := 1
for ; val < 0; k++ {
val = f(a - float64(k)*DefaultTau)
}
B = a - float64(k)*DefaultTau
}
// Now: A < ln(sigma'^2) < B
fA := f(A)
fB := f(B)
fC := 0.0
iter := 0
for math.Abs(B-A) > epsilon && iter < maxIter {
C := A + (A-B)*fA/(fB-fA)
fC = f(C)
if fC*fB < 0 {
A = B
fA = fB
} else {
fA = fA / 2
}
B = C
fB = fC
iter++
}
if iter == maxIter-1 {
fmt.Errorf("Fall through! Too many iterations")
}
newVol := math.Exp(A / 2)
return newVol
}
// Calculate the new Deviation. This is just the L2-norm of the deviation and
// the volatility.
func newDeviation(dev, newVol, estVar float64) float64 {
phip := math.Sqrt(dev*dev + newVol*newVol)
return 1.0 / math.Sqrt(1.0/(phip*phip)+1.0/(estVar))
}
// Calculate the new Rating.
func newRatingVal(oldRating, newDev, estImpPart float64) float64 {
return oldRating + newDev*newDev*estImpPart
}
func CalculateRating(player *Rating, opponents []*Rating, res []Result) (*Rating, error) {
if len(opponents) != len(res) {
return nil, fmt.Errorf("Number of opponents must == number of results. %v != %v",
len(opponents), len(res))
}
p2 := player.ToGlicko2()
gees := make([]float64, len(opponents))
ees := make([]float64, len(opponents))
for i := range opponents {
o := opponents[i].ToGlicko2()
gees[i] = gee(o.Deviation)
ees[i] = ee(p2.Rating, o.Rating, o.Deviation)
}
estVar := estVariance(gees, ees)
estImpPart := estImprovePartial(gees, ees, res)
estImp := estVar * estImpPart
newVol := newVolatility(estVar, estImp, p2)
newDev := newDeviation(p2.Deviation, newVol, estVar)
newRating := newRatingVal(p2.Rating, newDev, estImpPart)
rt := NewRating(newRating, newDev, newVol).FromGlicko2()
// Upper bound by the Default Deviation.
if rt.Deviation > DefaultDev {
rt.Deviation = DefaultDev
}
return rt, nil
}