From 723952b178d65500f996b897798a83f274f850bc Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Tue, 26 Apr 2011 18:03:21 -0700 Subject: [PATCH 1/6] formatting updates --- cpp/glicko2.cpp | 42 +++---- cpp/glicko2.h | 296 +++++++++++++++++++++---------------------- py/glicko2.py | 12 +- rb/glicko2.rb | 304 ++++++++++++++++++++++----------------------- rb/test_glicko2.rb | 46 +++---- 5 files changed, 350 insertions(+), 350 deletions(-) diff --git a/cpp/glicko2.cpp b/cpp/glicko2.cpp index 5f1e431..9be4739 100644 --- a/cpp/glicko2.cpp +++ b/cpp/glicko2.cpp @@ -33,33 +33,33 @@ class Glicko2_impl { -public: + public: - // constructors - Glicko2_impl(); - Glicko2_impl(const Glicko2_impl& rhs); + // constructors + Glicko2_impl(); + Glicko2_impl(const Glicko2_impl& rhs); - // copy assignment - Glicko2_impl& operator=(const Glicko2_impl& rhs); + // copy assignment + Glicko2_impl& operator=(const Glicko2_impl& rhs); - // destructor - virtual ~Glicko2_impl(); + // destructor + virtual ~Glicko2_impl(); - // system constants - static const double dvolatility; + // system constants + static const double dvolatility; - // rating data - double rating; - double deviation; - double volatility; + // rating data + double rating; + double deviation; + double volatility; - // result data (copy of each opponent and results, 0.0, 0.5, or 1.0) - std::vector opponents; - std::vector results; + // result data (copy of each opponent and results, 0.0, 0.5, or 1.0) + std::vector opponents; + std::vector results; - // utility functions - double g(const double& deviation); - double E(const double& rating, const double& rating_opponent, const double& deviation_opponent); + // utility functions + double g(const double& deviation); + double E(const double& rating, const double& rating_opponent, const double& deviation_opponent); }; @@ -116,7 +116,7 @@ Glicko2_impl::~Glicko2_impl() double Glicko2_impl::g(const double& deviation) { - #define PI_SQUARED (9.86960440108935861883) +#define PI_SQUARED (9.86960440108935861883) return 1.0 / (sqrt(1.0 + 3.0 * deviation * deviation / PI_SQUARED)); } diff --git a/cpp/glicko2.h b/cpp/glicko2.h index 889fc77..4aeca8c 100644 --- a/cpp/glicko2.h +++ b/cpp/glicko2.h @@ -47,195 +47,195 @@ class Glicko2_impl; */ class Glicko2 { -public: + public: - /** - * Enumeration of possible results. - */ - enum RESULT - { - /** - * A win result, internally 1.0. - */ - WIN, - /** - * A loss result, internally 0.0. + * Enumeration of possible results. */ - LOSS, + enum RESULT + { + /** + * A win result, internally 1.0. + */ + WIN, + + /** + * A loss result, internally 0.0. + */ + LOSS, + + /** + * A draw result, internally 0.5. + */ + DRAW + }; + + /** - * A draw result, internally 0.5. + * Default constructor. Initializes to a rating of 1500, a rating deviation + * of 350, and a volatility of 0.06. */ - DRAW - }; + Glicko2(); + + /** + * Copy constructor. + * + * @param rhs Object to copy. + */ + Glicko2(const Glicko2& rhs); + + /** + * Constructor with rating, rating deviation, and volatility specified. + * + * @param rating Initial rating. + * @param deviation Initial rating deviation. + * @param volatility Initial volatility. + */ + Glicko2(double rating, double deviation, double volatility); - /** - * Default constructor. Initializes to a rating of 1500, a rating deviation - * of 350, and a volatility of 0.06. - */ - Glicko2(); - - /** - * Copy constructor. - * - * @param rhs Object to copy. - */ - Glicko2(const Glicko2& rhs); - - /** - * Constructor with rating, rating deviation, and volatility specified. - * - * @param rating Initial rating. - * @param deviation Initial rating deviation. - * @param volatility Initial volatility. - */ - Glicko2(double rating, double deviation, double volatility); + /** + * Copy assignment operator. + * + * @param rhs Object to copy. + * + * @return Reference to self after copy assignment. + */ + Glicko2& operator=(const Glicko2& rhs); - /** - * Copy assignment operator. - * - * @param rhs Object to copy. - * - * @return Reference to self after copy assignment. - */ - Glicko2& operator=(const Glicko2& rhs); + /** + * Destructor. + */ + virtual ~Glicko2(); - /** - * Destructor. - */ - virtual ~Glicko2(); + /** + * Less than comparison operator. + * + * @param rhs Object to compare against. + * + * @return true if this object's rating is less than rhs's rating; false otherwise. + */ + bool operator< (const Glicko2& rhs); - /** - * Less than comparison operator. - * - * @param rhs Object to compare against. - * - * @return true if this object's rating is less than rhs's rating; false otherwise. - */ - bool operator< (const Glicko2& rhs); + /** + * Get the current rating. This rating is a Glicko rating, not a Glicko2 rating. + * For details, please see http://www.glicko.com/ + * + * @return + */ + double GetRating() const; + + /** + * 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 + */ + double GetDeviation() const; + + /** + * Get the current rating volatility. + * + * @return + */ + double GetVolatility() const; - /** - * Get the current rating. This rating is a Glicko rating, not a Glicko2 rating. - * For details, please see http://www.glicko.com/ - * - * @return - */ - double GetRating() const; + /** + * Set Glicko rating. Internally this is converted to a Glicko2 rating. + * For details, please see http://www.glicko.com/ + * + * @param rating Rating. + */ + void SetRating(double rating); - /** - * 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 - */ - double GetDeviation() const; + /** + * Set Glicko rating deviation. Internally this is converted to a Glicko-2 RD. + * For details, please see http://www.glicko.com/ + * + * @param deviation Rating deviation. + */ + void SetDeviation(double deviation); - /** - * Get the current rating volatility. - * - * @return - */ - double GetVolatility() const; + /** + * Set rating volatility. + * + * @param volatility Rating volatility. + */ + void SetVolatility(double volatility); - /** - * Set Glicko rating. Internally this is converted to a Glicko2 rating. - * For details, please see http://www.glicko.com/ - * - * @param rating Rating. - */ - void SetRating(double rating); - - /** - * Set Glicko rating deviation. Internally this is converted to a Glicko-2 RD. - * For details, please see http://www.glicko.com/ - * - * @param deviation Rating deviation. - */ - void SetDeviation(double deviation); - - /** - * Set rating volatility. - * - * @param volatility Rating volatility. - */ - void SetVolatility(double volatility); + /** + * Clear all results previously added via AddResult(), AddWin(), AddLoss(), + * and/or AddDraw(). This method is called automatically whenever Update() + * is called. + */ + void ClearResults(); - /** - * Clear all results previously added via AddResult(), AddWin(), AddLoss(), - * and/or AddDraw(). This method is called automatically whenever Update() - * is called. - */ - void ClearResults(); + /** + * Add a result to this rating. Note that no calculation is performed until + * Update() is called. + * + * @param opponent Other player in contest. + * @param result WIN, LOSS, or DRAW; from the point of view of this player. + */ + void AddResult(const Glicko2& opponent, RESULT result); - /** - * Add a result to this rating. Note that no calculation is performed until - * Update() is called. - * - * @param opponent Other player in contest. - * @param result WIN, LOSS, or DRAW; from the point of view of this player. - */ - void AddResult(const Glicko2& opponent, RESULT result); + /** + * Add a win result to this rating. Note that no calculation is performed until + * Update() is called. + * + * @param opponent Other (losing) player in contest. + */ + void AddWin(const Glicko2& opponent); + + /** + * Add a loss result to this rating. Note that no calculation is performed until + * Update() is called. + * + * @param opponent Other (winning) player in contest. + */ + void AddLoss(const Glicko2& opponent); + + /** + * Add a draw result to this rating. Note that no calculation is performed until + * Update() is called. + * + * @param opponent Other (drawing) player in contest. + */ + void AddDraw(const Glicko2& opponent); - /** - * Add a win result to this rating. Note that no calculation is performed until - * Update() is called. - * - * @param opponent Other (losing) player in contest. - */ - void AddWin(const Glicko2& opponent); - - /** - * Add a loss result to this rating. Note that no calculation is performed until - * Update() is called. - * - * @param opponent Other (winning) player in contest. - */ - void AddLoss(const Glicko2& opponent); - - /** - * Add a draw result to this rating. Note that no calculation is performed until - * Update() is called. - * - * @param opponent Other (drawing) player in contest. - */ - void AddDraw(const Glicko2& opponent); + /** + * Update rating based on current results list, and clear results. + */ + void Update(); - /** - * Update rating based on current results list, and clear results. - */ - void Update(); + private: - - -private: - - /** - * Private Implementation. - */ - Glicko2_impl* pimpl; + /** + * Private Implementation. + */ + Glicko2_impl* pimpl; }; diff --git a/py/glicko2.py b/py/glicko2.py index b47061d..06f3af7 100644 --- a/py/glicko2.py +++ b/py/glicko2.py @@ -30,14 +30,14 @@ 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/ """ @@ -229,12 +229,12 @@ if __name__ == "__main__": 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() diff --git a/rb/glicko2.rb b/rb/glicko2.rb index 80bcf21..009ba86 100755 --- a/rb/glicko2.rb +++ b/rb/glicko2.rb @@ -38,179 +38,179 @@ # class Glicko2 - protected + protected - # Glicko2 reprensentation (Glicko2 uses different scales from Glicko) - attr_accessor :g2rating, :g2deviation, :g2volatility - - # a nested Class to hold a single result - Result = Struct.new(:opponent, :result) + # Glicko2 reprensentation (Glicko2 uses different scales from Glicko) + attr_accessor :g2rating, :g2deviation, :g2volatility - public + # a nested Class to hold a single result + Result = Struct.new(:opponent, :result) - WIN = 1.0 - LOSS = 0.0 - DRAW = 0.5 - - # 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. - def initialize(rating = 1500.0, deviation = 350.0, volatility = 0.06) - @dvolatility = 0.3 - self.rating = rating - self.deviation = deviation - self.volatility = volatility - clear_results - end + public - include Comparable + WIN = 1.0 + LOSS = 0.0 + DRAW = 0.5 - # Comparison operator - def <=>(other) - self.rating <=> other.rating - end + # 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. + def initialize(rating = 1500.0, deviation = 350.0, volatility = 0.06) + @dvolatility = 0.3 + self.rating = rating + self.deviation = deviation + self.volatility = volatility + clear_results + end - # Get the current rating. This rating is a Glicko rating, not a Glicko2 rating. - # For details, please see http://www.glicko.com/ - def rating - (@g2rating * 173.7178) + 1500.0 - end + include Comparable - # Set Glicko rating. Internally this is converted to a Glicko2 rating. - # For details, please see http://www.glicko.com/ - def rating=(r) - @g2rating = (r - 1500.0) / 173.7178 - end + # Comparison operator + def <=>(other) + self.rating <=> other.rating + end - # Get the current rating deviation. This is a Glicko rating deviation, not a - # Glicko-2 RD. For details, please see http://www.glicko.com/ - def deviation - @g2deviation * 173.7178 - end + # Get the current rating. This rating is a Glicko rating, not a Glicko2 rating. + # For details, please see http://www.glicko.com/ + def rating + (@g2rating * 173.7178) + 1500.0 + end - # Set Glicko rating deviation. Internally this is converted to a Glicko-2 RD. - # For details, please see http://www.glicko.com/ - def deviation=(d) - @g2deviation = d / 173.7178 - end + # Set Glicko rating. Internally this is converted to a Glicko2 rating. + # For details, please see http://www.glicko.com/ + def rating=(r) + @g2rating = (r - 1500.0) / 173.7178 + end - # Get the current rating volatility. - def volatility - @g2volatility - end + # Get the current rating deviation. This is a Glicko rating deviation, not a + # Glicko-2 RD. For details, please see http://www.glicko.com/ + def deviation + @g2deviation * 173.7178 + end - # Set rating volatility. - def volatility=(v) - @g2volatility = v - end + # Set Glicko rating deviation. Internally this is converted to a Glicko-2 RD. + # For details, please see http://www.glicko.com/ + def deviation=(d) + @g2deviation = d / 173.7178 + end - # 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. - def clear_results - @opponents = [] - @results = [] - end + # Get the current rating volatility. + def volatility + @g2volatility + end - # Add a result to this rating. Note that no calculation is performed until - # update() is called. - def add_result(opponent,result) - @results.push( Result.new(opponent.clone, result) ) - end + # Set rating volatility. + def volatility=(v) + @g2volatility = v + end - # Add a win result to this rating. Note that no calculation is performed until - # update() is called. - def add_win(opponent) - add_result(opponent,Glicko2::WIN) - end + # 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. + def clear_results + @opponents = [] + @results = [] + end - # Add a loss result to this rating. Note that no calculation is performed until - # update() is called. - def add_loss(opponent) - add_result(opponent,Glicko2::LOSS) - end + # Add a result to this rating. Note that no calculation is performed until + # update() is called. + def add_result(opponent,result) + @results.push( Result.new(opponent.clone, result) ) + end - # Add a draw result to this rating. Note that no calculation is performed until - # update() is called. - def add_draw(opponent) - add_result(opponent,Glicko2::DRAW) - end - - # Update rating based on current results list, and clear results. - def update - - # util func - def Glicko2.g(deviation) - 1.0 / (Math.sqrt(1.0 + 3.0 * deviation ** 2.0 / (Math::PI ** 2.0))) - end - - # util func - def Glicko2.E(rating, rating_opponent, deviation_opponent) - 1.0 / (1.0 + Math.exp(-Glicko2.g(deviation_opponent)*(rating - rating_opponent))); - end - - # bail if no opponents set - if @results.empty? - return nil - end + # Add a win result to this rating. Note that no calculation is performed until + # update() is called. + def add_win(opponent) + add_result(opponent,Glicko2::WIN) + end - # compute variance + # Add a loss result to this rating. Note that no calculation is performed until + # update() is called. + def add_loss(opponent) + add_result(opponent,Glicko2::LOSS) + end - #variance = 0.0 - #@results.inject(0.0) do |variance,r| - # g_i = Glicko2.g(r.opponent.g2deviation) - # e_i = Glicko2.E(@g2rating,r.opponent.g2rating,r.opponent.g2deviation) - # g_i ** 2.0 * e_i * (1.0 - e_i) - #end + # Add a draw result to this rating. Note that no calculation is performed until + # update() is called. + def add_draw(opponent) + add_result(opponent,Glicko2::DRAW) + end - variance = 0.0 - @results.each do |r| - g_i = Glicko2.g(r.opponent.g2deviation) - e_i = Glicko2.E(@g2rating,r.opponent.g2rating,r.opponent.g2deviation) - variance += g_i ** 2.0 * e_i * (1.0 - e_i) - end - variance = 1.0 / variance - - # compute delta - delta = 0.0 - @results.each do |r| - delta += Glicko2.g(r.opponent.g2deviation) * (r.result - Glicko2.E(@g2rating, r.opponent.g2rating, r.opponent.g2deviation)) - end - delta *= variance - - # determine new volatility - new_volatility = 0.0 - a = Math.log( @g2volatility**2.0 ) - x = 0.0 - x_new = a - while ( (x - x_new).abs > 0.0000001 ) - x = x_new - d = @g2deviation**2.0 + variance + Math.exp(x) - h1 = -(x - a)/(@dvolatility**2.0) - 0.5*Math.exp(x)/d + 0.5*Math.exp(x)*(delta/d)*(delta/d) - h2 = -1.0/(@dvolatility**2.0) - 0.5*Math.exp(x)*(@g2deviation**2.0+variance)/(d**2.0) + 0.5*(delta**2.0)*Math.exp(x)*((@g2deviation**2.0) + variance - Math.exp(x))/(d**3.0) - x_new = x - h1/h2 - end - new_volatility = Math.exp(x_new / 2.0) - - # update the rating deviation to the new pre-rating period value - pre_deviation = Math.sqrt( @g2deviation**2.0 + new_volatility**2.0 ) + # Update rating based on current results list, and clear results. + def update - # update the rating and deviation - new_deviation = 1.0 / (Math.sqrt( 1.0/(pre_deviation**2.0) + 1.0 / variance)) - new_rating = 0.0 - @results.each do |r| - new_rating += Glicko2.g(r.opponent.g2deviation) * (r.result - Glicko2.E(@g2rating, r.opponent.g2rating, r.opponent.g2deviation)) - end - new_rating = new_rating * new_deviation**2.0 - new_rating += @g2rating + # util func + def Glicko2.g(deviation) + 1.0 / (Math.sqrt(1.0 + 3.0 * deviation ** 2.0 / (Math::PI ** 2.0))) + end - # wipe our result lists - clear_results + # util func + def Glicko2.E(rating, rating_opponent, deviation_opponent) + 1.0 / (1.0 + Math.exp(-Glicko2.g(deviation_opponent)*(rating - rating_opponent))); + end - # copy new values - @g2deviation = new_deviation - @g2volatility = new_volatility - @g2rating = new_rating - end + # bail if no opponents set + if @results.empty? + return nil + end + + # compute variance + + #variance = 0.0 + #@results.inject(0.0) do |variance,r| + # g_i = Glicko2.g(r.opponent.g2deviation) + # e_i = Glicko2.E(@g2rating,r.opponent.g2rating,r.opponent.g2deviation) + # g_i ** 2.0 * e_i * (1.0 - e_i) + #end + + variance = 0.0 + @results.each do |r| + g_i = Glicko2.g(r.opponent.g2deviation) + e_i = Glicko2.E(@g2rating,r.opponent.g2rating,r.opponent.g2deviation) + variance += g_i ** 2.0 * e_i * (1.0 - e_i) + end + variance = 1.0 / variance + + # compute delta + delta = 0.0 + @results.each do |r| + delta += Glicko2.g(r.opponent.g2deviation) * (r.result - Glicko2.E(@g2rating, r.opponent.g2rating, r.opponent.g2deviation)) + end + delta *= variance + + # determine new volatility + new_volatility = 0.0 + a = Math.log( @g2volatility**2.0 ) + x = 0.0 + x_new = a + while ( (x - x_new).abs > 0.0000001 ) + x = x_new + d = @g2deviation**2.0 + variance + Math.exp(x) + h1 = -(x - a)/(@dvolatility**2.0) - 0.5*Math.exp(x)/d + 0.5*Math.exp(x)*(delta/d)*(delta/d) + h2 = -1.0/(@dvolatility**2.0) - 0.5*Math.exp(x)*(@g2deviation**2.0+variance)/(d**2.0) + 0.5*(delta**2.0)*Math.exp(x)*((@g2deviation**2.0) + variance - Math.exp(x))/(d**3.0) + x_new = x - h1/h2 + end + new_volatility = Math.exp(x_new / 2.0) + + # update the rating deviation to the new pre-rating period value + pre_deviation = Math.sqrt( @g2deviation**2.0 + new_volatility**2.0 ) + + # update the rating and deviation + new_deviation = 1.0 / (Math.sqrt( 1.0/(pre_deviation**2.0) + 1.0 / variance)) + new_rating = 0.0 + @results.each do |r| + new_rating += Glicko2.g(r.opponent.g2deviation) * (r.result - Glicko2.E(@g2rating, r.opponent.g2rating, r.opponent.g2deviation)) + end + new_rating = new_rating * new_deviation**2.0 + new_rating += @g2rating + + # wipe our result lists + clear_results + + # copy new values + @g2deviation = new_deviation + @g2volatility = new_volatility + @g2rating = new_rating + end end diff --git a/rb/test_glicko2.rb b/rb/test_glicko2.rb index e104ca6..a2da9ca 100644 --- a/rb/test_glicko2.rb +++ b/rb/test_glicko2.rb @@ -5,30 +5,30 @@ require 'test/unit' class TestGlicko2 < Test::Unit::TestCase - def test_simple - a = Glicko2.new(1500.0, 200.0, 0.06) - b = Glicko2.new(1400.0, 30.0, 0.06) - c = Glicko2.new(1550.0, 100.0, 0.06) - d = Glicko2.new(1700.0, 300.0, 0.06) - - a.add_win(b) - a.add_loss(c) - a.add_result(d,0.0) - - assert_equal(1500.0, a.rating) - assert_equal(200.0, a.deviation) - - a.update() - - assert_in_delta(1464.05,a.rating,0.01) - assert_in_delta(151.516,a.deviation,0.01) + def test_simple + a = Glicko2.new(1500.0, 200.0, 0.06) + b = Glicko2.new(1400.0, 30.0, 0.06) + c = Glicko2.new(1550.0, 100.0, 0.06) + d = Glicko2.new(1700.0, 300.0, 0.06) - a.update() - - assert_in_delta(1464.05,a.rating,0.01) - assert_in_delta(151.516,a.deviation,0.01) + a.add_win(b) + a.add_loss(c) + a.add_result(d,0.0) + + assert_equal(1500.0, a.rating) + assert_equal(200.0, a.deviation) + + a.update() + + assert_in_delta(1464.05,a.rating,0.01) + assert_in_delta(151.516,a.deviation,0.01) + + a.update() + + assert_in_delta(1464.05,a.rating,0.01) + assert_in_delta(151.516,a.deviation,0.01) + + end - end - end From b24081414756ff4f1c61b3883249cfa6eff1da58 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Mon, 19 Dec 2011 15:10:38 -0800 Subject: [PATCH 2/6] update() now handled case correctly when a player hasn't participated in a session * when a player has no results, deviation is slightly increased properly now --- rb/glicko2.rb | 25 +++++++++++++------------ rb/test_glicko2.rb | 6 +++--- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/rb/glicko2.rb b/rb/glicko2.rb index 009ba86..3d5c0ab 100755 --- a/rb/glicko2.rb +++ b/rb/glicko2.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +#!/usr/bin/env ruby -w # # @@ -136,22 +136,23 @@ class Glicko2 add_result(opponent,Glicko2::DRAW) end + # util func + def Glicko2.g(deviation) + 1.0 / (Math.sqrt(1.0 + 3.0 * deviation ** 2.0 / (Math::PI ** 2.0))) + end + + # util func + def Glicko2.E(rating, rating_opponent, deviation_opponent) + 1.0 / (1.0 + Math.exp(-Glicko2.g(deviation_opponent)*(rating - rating_opponent))); + end + # Update rating based on current results list, and clear results. def update - # util func - def Glicko2.g(deviation) - 1.0 / (Math.sqrt(1.0 + 3.0 * deviation ** 2.0 / (Math::PI ** 2.0))) - end - - # util func - def Glicko2.E(rating, rating_opponent, deviation_opponent) - 1.0 / (1.0 + Math.exp(-Glicko2.g(deviation_opponent)*(rating - rating_opponent))); - end - # bail if no opponents set if @results.empty? - return nil + @g2deviation = Math.sqrt(@g2deviation**2.0 + @g2volatility**2.0) + return end # compute variance diff --git a/rb/test_glicko2.rb b/rb/test_glicko2.rb index a2da9ca..ccf6d1a 100644 --- a/rb/test_glicko2.rb +++ b/rb/test_glicko2.rb @@ -1,6 +1,6 @@ -#!/usr/bin/env ruby +#!/usr/bin/env ruby -w -require 'glicko2' +require_relative 'glicko2' require 'test/unit' class TestGlicko2 < Test::Unit::TestCase @@ -26,7 +26,7 @@ class TestGlicko2 < Test::Unit::TestCase a.update() assert_in_delta(1464.05,a.rating,0.01) - assert_in_delta(151.516,a.deviation,0.01) + assert_in_delta(151.875,a.deviation,0.01) end From 59537258f7e8be039be15621beb8110bd0c18482 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Mon, 19 Dec 2011 15:23:11 -0800 Subject: [PATCH 3/6] refactoring (some formatting, mostly ruby'ization) --- rb/glicko2.rb | 41 +++++++++++++---------------------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/rb/glicko2.rb b/rb/glicko2.rb index 3d5c0ab..ad17826 100755 --- a/rb/glicko2.rb +++ b/rb/glicko2.rb @@ -108,8 +108,8 @@ class Glicko2 # and/or add_draw(). This method is called automatically whenever update() # is called. def clear_results - @opponents = [] - @results = [] + @opponents = Array.new + @results = Array.new end # Add a result to this rating. Note that no calculation is performed until @@ -156,35 +156,23 @@ class Glicko2 end # compute variance - - #variance = 0.0 - #@results.inject(0.0) do |variance,r| - # g_i = Glicko2.g(r.opponent.g2deviation) - # e_i = Glicko2.E(@g2rating,r.opponent.g2rating,r.opponent.g2deviation) - # g_i ** 2.0 * e_i * (1.0 - e_i) - #end - - variance = 0.0 - @results.each do |r| + variance = 1.0 / @results.inject(0.0) do |sum,r| g_i = Glicko2.g(r.opponent.g2deviation) - e_i = Glicko2.E(@g2rating,r.opponent.g2rating,r.opponent.g2deviation) - variance += g_i ** 2.0 * e_i * (1.0 - e_i) + e_i = Glicko2.E(@g2rating, r.opponent.g2rating, r.opponent.g2deviation) + sum + g_i**2.0 * e_i * (1.0 - e_i) end - variance = 1.0 / variance # compute delta - delta = 0.0 - @results.each do |r| - delta += Glicko2.g(r.opponent.g2deviation) * (r.result - Glicko2.E(@g2rating, r.opponent.g2rating, r.opponent.g2deviation)) + delta = variance * @results.inject(0.0) do |sum,r| + sum + Glicko2.g(r.opponent.g2deviation) * (r.result - Glicko2.E(@g2rating, r.opponent.g2rating, r.opponent.g2deviation)) end - delta *= variance # determine new volatility new_volatility = 0.0 - a = Math.log( @g2volatility**2.0 ) + a = Math.log(@g2volatility**2.0) x = 0.0 x_new = a - while ( (x - x_new).abs > 0.0000001 ) + while ((x - x_new).abs > 0.0000001) x = x_new d = @g2deviation**2.0 + variance + Math.exp(x) h1 = -(x - a)/(@dvolatility**2.0) - 0.5*Math.exp(x)/d + 0.5*Math.exp(x)*(delta/d)*(delta/d) @@ -194,16 +182,13 @@ class Glicko2 new_volatility = Math.exp(x_new / 2.0) # update the rating deviation to the new pre-rating period value - pre_deviation = Math.sqrt( @g2deviation**2.0 + new_volatility**2.0 ) + pre_deviation = Math.sqrt(@g2deviation**2.0 + new_volatility**2.0) # update the rating and deviation - new_deviation = 1.0 / (Math.sqrt( 1.0/(pre_deviation**2.0) + 1.0 / variance)) - new_rating = 0.0 - @results.each do |r| - new_rating += Glicko2.g(r.opponent.g2deviation) * (r.result - Glicko2.E(@g2rating, r.opponent.g2rating, r.opponent.g2deviation)) + new_deviation = 1.0 / (Math.sqrt(1.0/(pre_deviation**2.0) + 1.0 / variance)) + new_rating = @g2rating + new_deviation**2.0 * @results.inject(0.0) do |sum,r| + sum + Glicko2.g(r.opponent.g2deviation) * (r.result - Glicko2.E(@g2rating, r.opponent.g2rating, r.opponent.g2deviation)) end - new_rating = new_rating * new_deviation**2.0 - new_rating += @g2rating # wipe our result lists clear_results From fbce23608e175599fb69ac7b4f8b39828dbeae5e Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Mon, 19 Dec 2011 17:13:45 -0800 Subject: [PATCH 4/6] add p_win method and misc cleanup and refactoring * change g2rating,etc. to attr_reader (instead of attr_accessor) * move @dvolatility into DVOL constant * add p_win * make comparison operator use p_win(self,other) * use << when adding to results array instead of .push() * improved comments; mostly from Glicko2 document * minor variable renaming to improve readability * clear results array after ratings are finally updated (no functional change, just cleaner) --- rb/glicko2.rb | 78 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 50 insertions(+), 28 deletions(-) diff --git a/rb/glicko2.rb b/rb/glicko2.rb index ad17826..b6f1bc3 100755 --- a/rb/glicko2.rb +++ b/rb/glicko2.rb @@ -41,7 +41,7 @@ class Glicko2 protected # Glicko2 reprensentation (Glicko2 uses different scales from Glicko) - attr_accessor :g2rating, :g2deviation, :g2volatility + attr_reader :g2rating, :g2deviation, :g2volatility # a nested Class to hold a single result Result = Struct.new(:opponent, :result) @@ -52,11 +52,13 @@ class Glicko2 LOSS = 0.0 DRAW = 0.5 + # system constant, determines delta volatility over time; should be [0.3,1.2] + DVOL = 0.3 + # 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. def initialize(rating = 1500.0, deviation = 350.0, volatility = 0.06) - @dvolatility = 0.3 self.rating = rating self.deviation = deviation self.volatility = volatility @@ -67,7 +69,12 @@ class Glicko2 # Comparison operator def <=>(other) - self.rating <=> other.rating + p_win(other) <=> 0.5 + end + + # get the probability of beating opponent + def p_win(opponent) + 1.0 / (1.0 + 10.0**(((opponent.rating - self.rating) / (400.0 * Math.sqrt(1.0 + 0.0000100723986 * (self.deviation**2.0 + opponent.deviation**2.0)))))) end # Get the current rating. This rating is a Glicko rating, not a Glicko2 rating. @@ -115,7 +122,7 @@ class Glicko2 # Add a result to this rating. Note that no calculation is performed until # update() is called. def add_result(opponent,result) - @results.push( Result.new(opponent.clone, result) ) + @results << Result.new(opponent.clone, result) end # Add a win result to this rating. Note that no calculation is performed until @@ -136,67 +143,82 @@ class Glicko2 add_result(opponent,Glicko2::DRAW) end - # util func + # util func 'g' def Glicko2.g(deviation) 1.0 / (Math.sqrt(1.0 + 3.0 * deviation ** 2.0 / (Math::PI ** 2.0))) end - # util func + # util func 'E' def Glicko2.E(rating, rating_opponent, deviation_opponent) 1.0 / (1.0 + Math.exp(-Glicko2.g(deviation_opponent)*(rating - rating_opponent))); end # Update rating based on current results list, and clear results. def update - - # bail if no opponents set + # Note that if a player does not compete during the rating period, then + # only Step 6 applies. if @results.empty? + # In this case, the player's rating and volatility parameters remain the + # same, but the RD increases according to: @g2deviation = Math.sqrt(@g2deviation**2.0 + @g2volatility**2.0) return end - # compute variance + # Step 1. Determine a rating and RD for each player at the onset of the + # rating period. The system constant which constrains the change in + # volatility over time, needs to be set prior to application of the system. + # Reasonable choices are between 0.3 and 1.2, though the system should be + # tested to decide which value results in greatest predictive accuracy + # ... (ratings already stored in instance) + + # Step 2. For each player, convert the ratings and RD's onto the Glicko-2 + # scale. + # ... (ratings already stored in G2 format) + + # Step 3. Compute the quantity v. This is the estimated variance of the + # team's/player's rating based only on game outcomes. variance = 1.0 / @results.inject(0.0) do |sum,r| g_i = Glicko2.g(r.opponent.g2deviation) e_i = Glicko2.E(@g2rating, r.opponent.g2rating, r.opponent.g2deviation) sum + g_i**2.0 * e_i * (1.0 - e_i) end - # compute delta + # Step 4. Compute the quantity delta, the estimated improvement in rating + # by comparing the pre-period rating to the performance rating based only + # on game outcomes. delta = variance * @results.inject(0.0) do |sum,r| sum + Glicko2.g(r.opponent.g2deviation) * (r.result - Glicko2.E(@g2rating, r.opponent.g2rating, r.opponent.g2deviation)) end - # determine new volatility - new_volatility = 0.0 - a = Math.log(@g2volatility**2.0) - x = 0.0 - x_new = a - while ((x - x_new).abs > 0.0000001) - x = x_new - d = @g2deviation**2.0 + variance + Math.exp(x) - h1 = -(x - a)/(@dvolatility**2.0) - 0.5*Math.exp(x)/d + 0.5*Math.exp(x)*(delta/d)*(delta/d) - h2 = -1.0/(@dvolatility**2.0) - 0.5*Math.exp(x)*(@g2deviation**2.0+variance)/(d**2.0) + 0.5*(delta**2.0)*Math.exp(x)*((@g2deviation**2.0) + variance - Math.exp(x))/(d**3.0) - x_new = x - h1/h2 + # Step 5. Determine the new value of the volatility. + a = Math.log(@g2volatility**2.0) + x0, x = 0.0, a + while ((x0 - x).abs > 0.0000001) + x0 = x + d = @g2deviation**2.0 + variance + Math.exp(x0) + h1 = -(x0 - a)/(DVOL**2.0) - 0.5*Math.exp(x0)/d + 0.5*Math.exp(x0)*(delta/d)*(delta/d) + h2 = -1.0/(DVOL**2.0) - 0.5*Math.exp(x0)*(@g2deviation**2.0+variance)/(d**2.0) + 0.5*(delta**2.0)*Math.exp(x0)*((@g2deviation**2.0) + variance - Math.exp(x0))/(d**3.0) + x = x0 - h1/h2 end - new_volatility = Math.exp(x_new / 2.0) + new_volatility = Math.exp(x / 2.0) - # update the rating deviation to the new pre-rating period value + # Step 6. Update the rating deviation to the new pre-rating period value. pre_deviation = Math.sqrt(@g2deviation**2.0 + new_volatility**2.0) - # update the rating and deviation + # Step 7. Update the rating and RD to the new values. new_deviation = 1.0 / (Math.sqrt(1.0/(pre_deviation**2.0) + 1.0 / variance)) new_rating = @g2rating + new_deviation**2.0 * @results.inject(0.0) do |sum,r| sum + Glicko2.g(r.opponent.g2deviation) * (r.result - Glicko2.E(@g2rating, r.opponent.g2rating, r.opponent.g2deviation)) end - # wipe our result lists - clear_results - - # copy new values + # Step 8. Convert ratings and RD's back to original scale. @g2deviation = new_deviation @g2volatility = new_volatility @g2rating = new_rating + + # wipe our result lists + clear_results end + end From 26d92ea617d521f0598713dbcab89786fe6eb723 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Mon, 19 Dec 2011 17:31:58 -0800 Subject: [PATCH 5/6] misc. cleanup * minor variable rename for clarity (x => x1) * use temps in long calculation (rating update) for clarity --- rb/glicko2.rb | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/rb/glicko2.rb b/rb/glicko2.rb index b6f1bc3..3163491 100755 --- a/rb/glicko2.rb +++ b/rb/glicko2.rb @@ -191,16 +191,16 @@ class Glicko2 end # Step 5. Determine the new value of the volatility. - a = Math.log(@g2volatility**2.0) - x0, x = 0.0, a - while ((x0 - x).abs > 0.0000001) - x0 = x + a = Math.log(@g2volatility**2.0) + x0, x1 = 0.0, a + while ((x0 - x1).abs > 0.0000001) + x0 = x1 d = @g2deviation**2.0 + variance + Math.exp(x0) h1 = -(x0 - a)/(DVOL**2.0) - 0.5*Math.exp(x0)/d + 0.5*Math.exp(x0)*(delta/d)*(delta/d) h2 = -1.0/(DVOL**2.0) - 0.5*Math.exp(x0)*(@g2deviation**2.0+variance)/(d**2.0) + 0.5*(delta**2.0)*Math.exp(x0)*((@g2deviation**2.0) + variance - Math.exp(x0))/(d**3.0) - x = x0 - h1/h2 + x1 = x0 - h1/h2 end - new_volatility = Math.exp(x / 2.0) + new_volatility = Math.exp(x1 / 2.0) # Step 6. Update the rating deviation to the new pre-rating period value. pre_deviation = Math.sqrt(@g2deviation**2.0 + new_volatility**2.0) @@ -208,7 +208,9 @@ class Glicko2 # Step 7. Update the rating and RD to the new values. new_deviation = 1.0 / (Math.sqrt(1.0/(pre_deviation**2.0) + 1.0 / variance)) new_rating = @g2rating + new_deviation**2.0 * @results.inject(0.0) do |sum,r| - sum + Glicko2.g(r.opponent.g2deviation) * (r.result - Glicko2.E(@g2rating, r.opponent.g2rating, r.opponent.g2deviation)) + g_i = Glicko2.g(r.opponent.g2deviation) + e_i = Glicko2.E(@g2rating, r.opponent.g2rating, r.opponent.g2deviation) + sum + g_i * (r.result - e_i) end # Step 8. Convert ratings and RD's back to original scale. From 6d99d0088519d255bb35ef2715cad27d6b3b28be Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Mon, 19 Dec 2011 17:39:28 -0800 Subject: [PATCH 6/6] cleanup * pull more stuff into temps to clarify long equations --- rb/glicko2.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rb/glicko2.rb b/rb/glicko2.rb index 3163491..880b9c2 100755 --- a/rb/glicko2.rb +++ b/rb/glicko2.rb @@ -183,11 +183,13 @@ class Glicko2 sum + g_i**2.0 * e_i * (1.0 - e_i) end - # Step 4. Compute the quantity delta, the estimated improvement in rating + # Step 4. Compute the quantity 'delta', the estimated improvement in rating # by comparing the pre-period rating to the performance rating based only # on game outcomes. delta = variance * @results.inject(0.0) do |sum,r| - sum + Glicko2.g(r.opponent.g2deviation) * (r.result - Glicko2.E(@g2rating, r.opponent.g2rating, r.opponent.g2deviation)) + g_i = Glicko2.g(r.opponent.g2deviation) + e_i = Glicko2.E(@g2rating, r.opponent.g2rating, r.opponent.g2deviation) + sum + g_i * (r.result - e_i) end # Step 5. Determine the new value of the volatility.