diff --git a/rb/glicko2.rb b/rb/glicko2.rb index 80bcf21..149bfcb 100755 --- a/rb/glicko2.rb +++ b/rb/glicko2.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +#!/usr/bin/env ruby -w # # @@ -38,179 +38,181 @@ # 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 ) + # util func + def Glicko2.g(deviation) + 1.0 / (Math.sqrt(1.0 + 3.0 * deviation ** 2.0 / (Math::PI ** 2.0))) + end - # 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.E(rating, rating_opponent, deviation_opponent) + 1.0 / (1.0 + Math.exp(-Glicko2.g(deviation_opponent)*(rating - rating_opponent))); + end - # wipe our result lists - clear_results + # Update rating based on current results list, and clear results. + def update + # util func - # copy new values - @g2deviation = new_deviation - @g2volatility = new_volatility - @g2rating = new_rating - end + # bail if no opponents set + if @results.empty? + @g2deviation = Math.sqrt(@g2deviation**2.0 + @g2volatility**2.0) + return + 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 old mode 100644 new mode 100755 index e104ca6..fd55d46 --- 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