From a8d34856b259b9be4e471b3c1b698674e37395d0 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Tue, 30 Jul 2013 01:42:45 -0700 Subject: [PATCH] first version in go --- go/glicko2.go | 213 +++++++++++++++++++++++++++++++++++++++++++++ go/glicko2_test.go | 47 ++++++++++ 2 files changed, 260 insertions(+) create mode 100644 go/glicko2.go create mode 100644 go/glicko2_test.go diff --git a/go/glicko2.go b/go/glicko2.go new file mode 100644 index 0000000..9285079 --- /dev/null +++ b/go/glicko2.go @@ -0,0 +1,213 @@ +// Glicko-2 Rating calculator package. +// +// This package 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/ +// +package glicko2 + +import ( + "fmt" + "math" +) + +const dvolatility float64 = 0.3 + +// Glicko2 state reprensentation (Glicko2 uses different scales from Glicko) +type Glicko2 struct { + rating float64 + deviation float64 + volatility float64 + wins [](*Glicko2) + losses [](*Glicko2) + draws [](*Glicko2) +} + +// Initializer with rating, rating deviation, and volatility specified. +func NewGlicko2(rating float64, deviation float64, volatility float64) *Glicko2 { + return newGlicko2(rating, deviation, volatility) +} + +// Initializer with default rating, rating deviation, and volatility. +func NewDefaultGlicko2() *Glicko2 { + return newGlicko2(1500.0, 350.0, 0.06) +} + +// helper for initialization +func newGlicko2(rating float64, deviation float64, volatility float64) *Glicko2 { + g := new(Glicko2) + g.SetRating(rating) + g.SetDeviation(deviation) + g.SetVolatility(volatility) + return g +} + +// Get the current rating. This rating is a Glicko rating, not a Glicko2 rating. +// For details, please see http://www.glicko.com/ +func (g *Glicko2) Rating() float64 { + return ((g.rating * 173.7178) + 1500.0) +} + +// Set Glicko rating. Internally this is converted to a Glicko2 rating. +// For details, please see http://www.glicko.com/ +func (g *Glicko2) SetRating(rating float64) { + g.rating = (rating - 1500.0) / 173.7178 +} + +// Get the current rating deviation. This is a Glicko rating deviation, not a +// Glicko-2 RD. For details, please see http://www.glicko.com/ +func (g *Glicko2) Deviation() float64 { + return (g.deviation * 173.7178) +} + +// Set Glicko rating deviation. Internally this is converted to a Glicko-2 RD. +// For details, please see http://www.glicko.com/ +func (g *Glicko2) SetDeviation(deviation float64) { + g.deviation = deviation / 173.7178 +} + +// Get the current rating volatility. +func (g *Glicko2) Volatility() float64 { + return g.volatility +} + +// Set rating volatility. +func (g *Glicko2) SetVolatility(volatility float64) { + g.volatility = volatility +} + +// helper to make a copy of a Glicko2 struct +func (g *Glicko2) duplicate() *Glicko2 { + c := *g + return &c +} + +// Clear all results previously added via add_result(), add_win(), add_loss(), +// and/or add_draw(). This method is called automatically whenever update() +// is called. +func (g *Glicko2) ClearResults() { + g.wins = nil + g.losses = nil + g.draws = nil +} + +// Add a win result to this rating. Note that no calculation is performed until +// Update() is called. +func (g *Glicko2) AddWin(loser *Glicko2) { + g.wins = append(g.wins, loser.duplicate()) +} + +// Add a loss result to this rating. Note that no calculation is performed until +// Update() is called. +func (g *Glicko2) AddLoss(winner *Glicko2) { + g.losses = append(g.losses, winner.duplicate()) +} + +// Add a draw result to this rating. Note that no calculation is performed until +// Update() is called. +func (g *Glicko2) AddDraw(drawer *Glicko2) { + g.draws = append(g.draws, drawer.duplicate()) +} + +// Print a Glicko2 struct +func (g *Glicko2) Print() { + fmt.Printf(" rating: %v\n", g.Rating()) + fmt.Printf(" deviation: %v\n", g.Deviation()) + fmt.Printf("volatility: %v\n", g.Volatility()) + for _, win := range g.wins { + fmt.Printf(" *beat\n") + fmt.Printf(" rating: %v\n", win.Rating()) + fmt.Printf(" deviation: %v\n", win.Deviation()) + fmt.Printf(" volatility: %v\n", win.Volatility()) + } +} + +// util func +func calcG(deviation float64) float64 { + return 1.0 / (math.Sqrt(1.0 + 3.0*(deviation*deviation)/(math.Pi*math.Pi))) +} + +// util func +func calcE(rating float64, rating_opponent float64, deviation_opponent float64) float64 { + return 1.0 / (1.0 + math.Exp(-calcG(deviation_opponent)*(rating-rating_opponent))) +} + +// Update rating based on current results list, and clear results. +func (g *Glicko2) Update() { + + // merge wins, losses, draws slices for convenience + results := append(append(g.wins, g.losses...), g.draws...) + + // bail if no opponents set + if len(results) == 0 { + g.deviation = math.Sqrt((g.deviation * g.deviation) + (g.volatility * g.volatility)) + return + } + + // compute variance + variance := 0.0 + for _, r := range results { + g_i := calcG(r.deviation) + e_i := calcE(g.rating, r.rating, r.deviation) + variance += (g_i * g_i) * e_i * (1.0 - e_i) + } + variance = 1.0 / variance + + // compute delta + delta := 0.0 + for _, r := range g.wins { + delta += calcG(r.deviation) * (1.0 - calcE(g.rating, r.rating, r.deviation)) + } + for _, r := range g.losses { + delta += calcG(r.deviation) * (0.0 - calcE(g.rating, r.rating, r.deviation)) + } + for _, r := range g.draws { + delta += calcG(r.deviation) * (0.5 - calcE(g.rating, r.rating, r.deviation)) + } + delta *= variance + + // determine new volatility + new_volatility := 0.0 + a := math.Log((g.volatility * g.volatility)) + x := 0.0 + x_new := a + for math.Abs(x-x_new) > 0.0000001 { + x = x_new + d := (g.deviation * g.deviation) + variance + math.Exp(x) + h1 := -(x-a)/(dvolatility*dvolatility) - 0.5*math.Exp(x)/d + 0.5*math.Exp(x)*(delta/d)*(delta/d) + h2 := -1.0/(dvolatility*dvolatility) - 0.5*math.Exp(x)*((g.deviation*g.deviation)+variance)/(d*d) + 0.5*(delta*delta)*math.Exp(x)*((g.deviation*g.deviation)+variance-math.Exp(x))/(d*d*d) + 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((g.deviation * g.deviation) + (new_volatility * new_volatility)) + + // update the rating and deviation + new_deviation := 1.0 / (math.Sqrt(1.0/(pre_deviation * pre_deviation) + 1.0/variance)) + new_rating := 0.0 + for _, r := range g.wins { + new_rating += calcG(r.deviation) * (1.0 - calcE(g.rating, r.rating, r.deviation)) + } + for _, r := range g.losses { + new_rating += calcG(r.deviation) * (0.0 - calcE(g.rating, r.rating, r.deviation)) + } + for _, r := range g.draws { + new_rating += calcG(r.deviation) * (0.5 - calcE(g.rating, r.rating, r.deviation)) + } + new_rating = new_rating * (new_deviation * new_deviation) + new_rating += g.rating + + // wipe our result lists + g.ClearResults() + + // copy new values + g.deviation = new_deviation + g.volatility = new_volatility + g.rating = new_rating +} diff --git a/go/glicko2_test.go b/go/glicko2_test.go new file mode 100644 index 0000000..807fa1a --- /dev/null +++ b/go/glicko2_test.go @@ -0,0 +1,47 @@ +package glicko2 + +import ( + "testing" + "math" +) + +func inRange(want float64, have float64, delta float64) bool { + return math.Abs(want-have) <= delta +} + +func TestGlicko2(t *testing.T) { + a := NewGlicko2(1500.0, 200.0, 0.06) + b := NewGlicko2(1400.0, 30.0, 0.06) + c := NewGlicko2(1550.0, 100.0, 0.06) + d := NewGlicko2(1700.0, 300.0, 0.06) + + a.AddWin(b) + a.AddLoss(c) + a.AddLoss(d) + + if a.Rating() != 1500.0 { + t.Error() + } + if a.Deviation() != 200.0 { + t.Error() + } + + a.Update() + + if !inRange(a.Rating(),1464.05,0.01) { + t.Error() + } + if !inRange(a.Deviation(),151.516,0.01) { + t.Error() + } + + a.Update() + + if !inRange(a.Rating(),1464.05,0.01) { + t.Error() + } + if !inRange(a.Deviation(),151.875,0.01) { + t.Error() + } + +}