glicko2 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +4 -8
- data/lib/glicko2.rb +31 -133
- data/lib/glicko2/player.rb +189 -0
- data/lib/glicko2/rating_period.rb +66 -0
- data/lib/glicko2/version.rb +1 -1
- data/spec/minitest_helper.rb +1 -0
- data/spec/player_spec.rb +4 -0
- data/spec/rating_period_spec.rb +19 -30
- data/spec/util_spec.rb +17 -0
- metadata +7 -2
    
        data/README.md
    CHANGED
    
    | @@ -28,15 +28,11 @@ Rating = Struct.new(:rating, :rating_deviation, :volatility) | |
| 28 28 | 
             
            rating1 = Rating.new(1400, 30, 0.06)
         | 
| 29 29 | 
             
            rating2 = Rating.new(1550, 100, 0.06)
         | 
| 30 30 |  | 
| 31 | 
            -
            #  | 
| 32 | 
            -
             | 
| 33 | 
            -
            player2 = Glicko2::Player.from_obj(rating2)
         | 
| 31 | 
            +
            # Rating period with all participating ratings
         | 
| 32 | 
            +
            period = Glicko2::RatingPeriod.from_objs [rating1, rating2]
         | 
| 34 33 |  | 
| 35 | 
            -
            #  | 
| 36 | 
            -
            period | 
| 37 | 
            -
             | 
| 38 | 
            -
            # Register a game in this rating period
         | 
| 39 | 
            -
            period.game([player1, player2], [1,2])
         | 
| 34 | 
            +
            # Register a game where rating1 wins against rating2
         | 
| 35 | 
            +
            period.game([rating1, rating2], [1,2])
         | 
| 40 36 |  | 
| 41 37 | 
             
            # Generate the next rating period with updated players
         | 
| 42 38 | 
             
            next_period = period.generate_next
         | 
    
        data/lib/glicko2.rb
    CHANGED
    
    | @@ -1,119 +1,43 @@ | |
| 1 1 | 
             
            require "glicko2/version"
         | 
| 2 | 
            +
            require "glicko2/player"
         | 
| 3 | 
            +
            require "glicko2/rating_period"
         | 
| 2 4 |  | 
| 3 5 | 
             
            module Glicko2
         | 
| 4 | 
            -
              TOLERANCE = 0.0000001
         | 
| 5 6 | 
             
              DEFAULT_VOLATILITY = 0.06
         | 
| 6 7 | 
             
              DEFAULT_GLICKO_RATING = 1500.0
         | 
| 7 8 | 
             
              DEFAULT_GLICKO_RATING_DEVIATION = 350.0
         | 
| 8 9 |  | 
| 9 | 
            -
              GLICKO_GRADIENT = 173.7178
         | 
| 10 | 
            -
              GLICKO_INTERCEPT = DEFAULT_GLICKO_RATING
         | 
| 11 | 
            -
             | 
| 12 10 | 
             
              VOLATILITY_CHANGE = 0.5
         | 
| 13 11 |  | 
| 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 | 
            -
                end
         | 
| 44 | 
            -
             | 
| 45 | 
            -
                def delta(others, scores)
         | 
| 46 | 
            -
                  others.zip(scores).reduce(0) do |d, (other, score)|
         | 
| 47 | 
            -
                    d + other.g * (score - e(other))
         | 
| 48 | 
            -
                  end * variance(others)
         | 
| 49 | 
            -
                end
         | 
| 50 | 
            -
             | 
| 51 | 
            -
                def f_part1(x, others, scores)
         | 
| 52 | 
            -
                  sd_sq = sd ** 2
         | 
| 53 | 
            -
                  v = variance(others)
         | 
| 54 | 
            -
                  _x = Math.exp(x)
         | 
| 55 | 
            -
                  (_x * (delta(others, scores) ** 2 - sd_sq - v - _x)) / (2 * (sd_sq + v + _x) ** 2)
         | 
| 56 | 
            -
                end
         | 
| 57 | 
            -
             
         | 
| 58 | 
            -
                def f_part2(x)
         | 
| 59 | 
            -
                  (x - Math::log(volatility ** 2)) / VOLATILITY_CHANGE ** 2
         | 
| 60 | 
            -
                end
         | 
| 61 | 
            -
             
         | 
| 62 | 
            -
                def f(x, others, scores)
         | 
| 63 | 
            -
                  f_part1(x, others, scores) - f_part2(x)
         | 
| 64 | 
            -
                end
         | 
| 65 | 
            -
             
         | 
| 66 | 
            -
                def generate_next(others, scores)
         | 
| 67 | 
            -
                  if others.length < 1
         | 
| 68 | 
            -
                    sd_pre = Math.sqrt(sd ** 2 + volatility ** 2)
         | 
| 69 | 
            -
                    return self.class.new(mean, sd_pre, volatility, obj) if others.length < 1
         | 
| 70 | 
            -
                  end
         | 
| 71 | 
            -
                  _v = variance(others)
         | 
| 72 | 
            -
                  a = Math::log(volatility ** 2)
         | 
| 73 | 
            -
                  if delta(others, scores) > sd ** 2 + _v
         | 
| 74 | 
            -
                    b = Math.log(_delta - sd ** 2 - _v)
         | 
| 75 | 
            -
                  else
         | 
| 76 | 
            -
                    k = 1
         | 
| 77 | 
            -
                    k += 1 while f(a - k * VOLATILITY_CHANGE, others, scores) < 0
         | 
| 78 | 
            -
                    b = a - k * VOLATILITY_CHANGE
         | 
| 79 | 
            -
                  end
         | 
| 80 | 
            -
                  fa = f(a, others, scores)
         | 
| 81 | 
            -
                  fb = f(b, others, scores)
         | 
| 82 | 
            -
                  while (b - a).abs > TOLERANCE
         | 
| 83 | 
            -
                    c = a + (a - b) * fa / (fb - fa)
         | 
| 84 | 
            -
                    fc = f(c, others, scores)
         | 
| 85 | 
            -
                    if fc * fb < 0
         | 
| 86 | 
            -
                      a = b
         | 
| 87 | 
            -
                      fa = fb
         | 
| 88 | 
            -
                    else
         | 
| 89 | 
            -
                      fa /= 2.0
         | 
| 90 | 
            -
                    end
         | 
| 91 | 
            -
                    b = c
         | 
| 92 | 
            -
                    fb = fc
         | 
| 93 | 
            -
                  end
         | 
| 94 | 
            -
                  volatility1 = Math.exp(a / 2.0)
         | 
| 95 | 
            -
                  sd_pre = Math.sqrt(sd ** 2 + volatility1 ** 2)
         | 
| 96 | 
            -
                  sd1 = 1 / Math.sqrt(1 / sd_pre ** 2 + 1 / _v)
         | 
| 97 | 
            -
                  mean1 = mean + sd1 ** 2 * others.zip(scores).reduce(0) {|x, (other, score)| x + other.g * (score - e(other)) }
         | 
| 98 | 
            -
                  self.class.new(mean1, sd1, volatility1, obj)
         | 
| 99 | 
            -
                end
         | 
| 100 | 
            -
             | 
| 101 | 
            -
                def update_obj
         | 
| 102 | 
            -
                  @obj.rating = GLICKO_GRADIENT * mean + GLICKO_INTERCEPT
         | 
| 103 | 
            -
                  @obj.rating_deviation = GLICKO_GRADIENT * sd
         | 
| 104 | 
            -
                  @obj.volatility = volatility
         | 
| 105 | 
            -
                end
         | 
| 106 | 
            -
             | 
| 107 | 
            -
                def to_s
         | 
| 108 | 
            -
                  "#<Player mean=#{mean}, sd=#{sd}, volatility=#{volatility}, obj=#{obj}>"
         | 
| 109 | 
            -
                end
         | 
| 110 | 
            -
              end
         | 
| 111 | 
            -
             | 
| 112 | 
            -
              class RatingPeriod
         | 
| 113 | 
            -
                def initialize(players)
         | 
| 114 | 
            -
                  @players = players.reduce({}) { |memo, player| memo[player] = []; memo }
         | 
| 115 | 
            -
                end
         | 
| 116 | 
            -
             | 
| 12 | 
            +
              # Collection of helper methods
         | 
| 13 | 
            +
              class Util
         | 
| 14 | 
            +
                GLICKO_GRADIENT = 173.7178
         | 
| 15 | 
            +
                GLICKO_INTERCEPT = DEFAULT_GLICKO_RATING
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                # Convert from the original Glicko scale to Glicko2 scale
         | 
| 18 | 
            +
                #
         | 
| 19 | 
            +
                # @param [Numeric] r Glicko rating
         | 
| 20 | 
            +
                # @param [Numeric] rd Glicko rating deviation
         | 
| 21 | 
            +
                # @return [Array<Numeric>]
         | 
| 22 | 
            +
                def self.to_glicko2(r, rd)
         | 
| 23 | 
            +
                  [(r - GLICKO_INTERCEPT) / GLICKO_GRADIENT, rd / GLICKO_GRADIENT]
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                # Convert from the Glicko2 scale to the original Glicko scale
         | 
| 27 | 
            +
                #
         | 
| 28 | 
            +
                # @param [Numeric] m Glicko2 mean
         | 
| 29 | 
            +
                # @param [Numeric] sd Glicko2 standard deviation
         | 
| 30 | 
            +
                # @return [Array<Numeric>]
         | 
| 31 | 
            +
                def self.to_glicko(m, sd)
         | 
| 32 | 
            +
                  [GLICKO_GRADIENT * m + GLICKO_INTERCEPT, GLICKO_GRADIENT * sd]
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                # Convert from a rank, where lower numbers win against higher numbers,
         | 
| 36 | 
            +
                # into Glicko scores where wins are `1`, draws are `0.5` and losses are `0`.
         | 
| 37 | 
            +
                #
         | 
| 38 | 
            +
                # @param [Integer] rank players rank
         | 
| 39 | 
            +
                # @param [Integer] other opponents rank
         | 
| 40 | 
            +
                # @return [Numeric] Glicko score
         | 
| 117 41 | 
             
                def self.ranks_to_score(rank, other)
         | 
| 118 42 | 
             
                  if rank < other
         | 
| 119 43 | 
             
                    1.0
         | 
| @@ -123,31 +47,5 @@ module Glicko2 | |
| 123 47 | 
             
                    0.0
         | 
| 124 48 | 
             
                  end
         | 
| 125 49 | 
             
                end
         | 
| 126 | 
            -
             | 
| 127 | 
            -
                def game(game_players, ranks)
         | 
| 128 | 
            -
                  game_players.zip(ranks).each do |player, rank|
         | 
| 129 | 
            -
                    game_players.zip(ranks).each do |other, other_rank|
         | 
| 130 | 
            -
                      next if player == other
         | 
| 131 | 
            -
                      @players[player] << [other, self.class.ranks_to_score(rank, other_rank)]
         | 
| 132 | 
            -
                    end
         | 
| 133 | 
            -
                  end
         | 
| 134 | 
            -
                end
         | 
| 135 | 
            -
             | 
| 136 | 
            -
                def generate_next
         | 
| 137 | 
            -
                  p = []
         | 
| 138 | 
            -
                  @players.each do |player, games|
         | 
| 139 | 
            -
                    p << player.generate_next(*games.transpose)
         | 
| 140 | 
            -
                  end
         | 
| 141 | 
            -
                  self.class.new(p)
         | 
| 142 | 
            -
                end
         | 
| 143 | 
            -
             | 
| 144 | 
            -
                def players
         | 
| 145 | 
            -
                  @players.keys
         | 
| 146 | 
            -
                end
         | 
| 147 | 
            -
             | 
| 148 | 
            -
                def to_s
         | 
| 149 | 
            -
                  "#<RatingPeriod players=#{@players.keys}"
         | 
| 150 | 
            -
                end
         | 
| 151 50 | 
             
              end
         | 
| 152 | 
            -
             | 
| 153 51 | 
             
            end
         | 
| @@ -0,0 +1,189 @@ | |
| 1 | 
            +
            module Glicko2
         | 
| 2 | 
            +
              # Calculates a new Glicko2 ranking based on a seed object and game outcomes.
         | 
| 3 | 
            +
              #
         | 
| 4 | 
            +
              # The example from the Glicko2 paper, where a player wins against the first
         | 
| 5 | 
            +
              # opponent, but then looses against the next two:
         | 
| 6 | 
            +
              #
         | 
| 7 | 
            +
              #   Rating = Struct.new(:rating, :rating_deviation, :volatility)
         | 
| 8 | 
            +
              #
         | 
| 9 | 
            +
              #   player_seed = Rating.new(1500, 200, 0.06)
         | 
| 10 | 
            +
              #   opponent1_seed = Rating.new(1400, 30, 0.06)
         | 
| 11 | 
            +
              #   opponent2_seed = Rating.new(1550, 100, 0.06)
         | 
| 12 | 
            +
              #   opponent3_seed = Rating.new(1700, 300, 0.06)
         | 
| 13 | 
            +
              #
         | 
| 14 | 
            +
              #   player = Glicko2::Player.from_obj(player_seed)
         | 
| 15 | 
            +
              #   opponent1 = Glicko2::Player.from_obj(opponent1_seed)
         | 
| 16 | 
            +
              #   opponent2 = Glicko2::Player.from_obj(opponent2_seed)
         | 
| 17 | 
            +
              #   opponent3 = Glicko2::Player.from_obj(opponent3_seed)
         | 
| 18 | 
            +
              #
         | 
| 19 | 
            +
              #   new_player = player.generate_next([opponent1, opponent2, opponent3],
         | 
| 20 | 
            +
              #                                     [1, 0, 0])
         | 
| 21 | 
            +
              #   new_player.update_obj
         | 
| 22 | 
            +
              #
         | 
| 23 | 
            +
              #   puts player_seed
         | 
| 24 | 
            +
              #
         | 
| 25 | 
            +
              class Player
         | 
| 26 | 
            +
                TOLERANCE = 0.0000001
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                attr_reader :mean, :sd, :volatility, :obj
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                # Create a {Player} from a seed object, converting from Glicko
         | 
| 31 | 
            +
                # ratings to Glicko2.
         | 
| 32 | 
            +
                #
         | 
| 33 | 
            +
                # @param [#rating,#rating_deviation,#volatility] obj seed values object
         | 
| 34 | 
            +
                # @return [Player] constructed instance.
         | 
| 35 | 
            +
                def self.from_obj(obj)
         | 
| 36 | 
            +
                  mean, sd = Util.to_glicko2(obj.rating, obj.rating_deviation)
         | 
| 37 | 
            +
                  new(mean, sd, obj.volatility, obj)
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                # @param [Numeric] mean player mean
         | 
| 41 | 
            +
                # @param [Numeric] sd player standard deviation
         | 
| 42 | 
            +
                # @param [Numeric] volatility player volatility
         | 
| 43 | 
            +
                # @param [#rating,#rating_deviation,#volatility] obj seed values object
         | 
| 44 | 
            +
                def initialize(mean, sd, volatility, obj=nil)
         | 
| 45 | 
            +
                  @mean = mean
         | 
| 46 | 
            +
                  @sd = sd
         | 
| 47 | 
            +
                  @volatility = volatility
         | 
| 48 | 
            +
                  @obj = obj
         | 
| 49 | 
            +
                  @e = {}
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                # Calculate `g(phi)` as defined in the Glicko2 paper
         | 
| 53 | 
            +
                #
         | 
| 54 | 
            +
                # @return [Numeric]
         | 
| 55 | 
            +
                def g
         | 
| 56 | 
            +
                  @g ||= 1 / Math.sqrt(1 + 3 * sd ** 2 / Math::PI ** 2)
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                # Calculate `E(mu, mu_j, phi_j)` as defined in the Glicko2 paper
         | 
| 60 | 
            +
                #
         | 
| 61 | 
            +
                # @param [Player] other the `j` player
         | 
| 62 | 
            +
                # @return [Numeric]
         | 
| 63 | 
            +
                def e(other)
         | 
| 64 | 
            +
                  @e[other] ||= 1 / (1 + Math.exp(-other.g * (mean - other.mean)))
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                # Calculate the estimated variance of the team's/player's rating based only
         | 
| 68 | 
            +
                # on the game outcomes.
         | 
| 69 | 
            +
                #
         | 
| 70 | 
            +
                # @param [Array<Player>] others other participating players.
         | 
| 71 | 
            +
                # @return [Numeric]
         | 
| 72 | 
            +
                def variance(others)
         | 
| 73 | 
            +
                  return 0.0 if others.length < 1
         | 
| 74 | 
            +
                  others.reduce(0) do |v, other|
         | 
| 75 | 
            +
                    e_other = e(other)
         | 
| 76 | 
            +
                    v + other.g ** 2 * e_other * (1 - e_other)
         | 
| 77 | 
            +
                  end ** -1
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                # Calculate the estimated improvement in rating by comparing the
         | 
| 81 | 
            +
                # pre-period rating to the performance rating based only on game outcomes.
         | 
| 82 | 
            +
                #
         | 
| 83 | 
            +
                # @param [Array<Player>] others list of opponent players
         | 
| 84 | 
            +
                # @param [Array<Numeric>] scores list of correlating scores (`0` for a loss,
         | 
| 85 | 
            +
                #   `0.5` for a draw and `1` for a win).
         | 
| 86 | 
            +
                # @return [Numeric]
         | 
| 87 | 
            +
                def delta(others, scores)
         | 
| 88 | 
            +
                  others.zip(scores).reduce(0) do |d, (other, score)|
         | 
| 89 | 
            +
                    d + other.g * (score - e(other))
         | 
| 90 | 
            +
                  end * variance(others)
         | 
| 91 | 
            +
                end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                # Calculate `f(x)` as defined in the Glicko2 paper
         | 
| 94 | 
            +
                #
         | 
| 95 | 
            +
                # @param [Numeric] x
         | 
| 96 | 
            +
                # @param [Numeric] d the result of calculating {#delta}
         | 
| 97 | 
            +
                # @param [Numeric] v the result of calculating {#variance}
         | 
| 98 | 
            +
                # @return [Numeric]
         | 
| 99 | 
            +
                def f(x, d, v)
         | 
| 100 | 
            +
                  f_part1(x, d, v) - f_part2(x)
         | 
| 101 | 
            +
                end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                # Calculate the new value of the volatility
         | 
| 104 | 
            +
                #
         | 
| 105 | 
            +
                # @param [Numeric] d the result of calculating {#delta}
         | 
| 106 | 
            +
                # @param [Numeric] v the result of calculating {#variance}
         | 
| 107 | 
            +
                # @return [Numeric]
         | 
| 108 | 
            +
                def volatility1(d, v)
         | 
| 109 | 
            +
                  a = Math::log(volatility ** 2)
         | 
| 110 | 
            +
                  if d > sd ** 2 + v
         | 
| 111 | 
            +
                    b = Math.log(d - sd ** 2 - v)
         | 
| 112 | 
            +
                  else
         | 
| 113 | 
            +
                    k = 1
         | 
| 114 | 
            +
                    k += 1 while f(a - k * VOLATILITY_CHANGE, d, v) < 0
         | 
| 115 | 
            +
                    b = a - k * VOLATILITY_CHANGE
         | 
| 116 | 
            +
                  end
         | 
| 117 | 
            +
                  fa = f(a, d, v)
         | 
| 118 | 
            +
                  fb = f(b, d, v)
         | 
| 119 | 
            +
                  while (b - a).abs > TOLERANCE
         | 
| 120 | 
            +
                    c = a + (a - b) * fa / (fb - fa)
         | 
| 121 | 
            +
                    fc = f(c, d, v)
         | 
| 122 | 
            +
                    if fc * fb < 0
         | 
| 123 | 
            +
                      a = b
         | 
| 124 | 
            +
                      fa = fb
         | 
| 125 | 
            +
                    else
         | 
| 126 | 
            +
                      fa /= 2.0
         | 
| 127 | 
            +
                    end
         | 
| 128 | 
            +
                    b = c
         | 
| 129 | 
            +
                    fb = fc
         | 
| 130 | 
            +
                  end
         | 
| 131 | 
            +
                  Math.exp(a / 2.0)
         | 
| 132 | 
            +
                end
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                # Create new {Player} with updated values.
         | 
| 135 | 
            +
                #
         | 
| 136 | 
            +
                # This method will not modify any objects that are passed into it.
         | 
| 137 | 
            +
                #
         | 
| 138 | 
            +
                # @param [Array<Player>] others list of opponent players
         | 
| 139 | 
            +
                # @param [Array<Numeric>] scores list of correlating scores (`0` for a loss,
         | 
| 140 | 
            +
                #   `0.5` for a draw and `1` for a win).
         | 
| 141 | 
            +
                # @return [Player]
         | 
| 142 | 
            +
                def generate_next(others, scores)
         | 
| 143 | 
            +
                  if others.length < 1
         | 
| 144 | 
            +
                    generate_next_without_games
         | 
| 145 | 
            +
                  else
         | 
| 146 | 
            +
                    generate_next_with_games(others, scores)
         | 
| 147 | 
            +
                  end
         | 
| 148 | 
            +
                end
         | 
| 149 | 
            +
             | 
| 150 | 
            +
                # Update seed object with this player's values
         | 
| 151 | 
            +
                def update_obj
         | 
| 152 | 
            +
                  @obj.rating, @obj.rating_deviation = Util.to_glicko(mean, sd)
         | 
| 153 | 
            +
                  @obj.volatility = volatility
         | 
| 154 | 
            +
                end
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                def to_s
         | 
| 157 | 
            +
                  "#<Player mean=#{mean}, sd=#{sd}, volatility=#{volatility}, obj=#{obj}>"
         | 
| 158 | 
            +
                end
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                private
         | 
| 161 | 
            +
             | 
| 162 | 
            +
                def generate_next_without_games
         | 
| 163 | 
            +
                  sd_pre = Math.sqrt(sd ** 2 + volatility ** 2)
         | 
| 164 | 
            +
                  self.class.new(mean, sd_pre, volatility, obj)
         | 
| 165 | 
            +
                end
         | 
| 166 | 
            +
             | 
| 167 | 
            +
                def generate_next_with_games(others, scores)
         | 
| 168 | 
            +
                  _v = variance(others)
         | 
| 169 | 
            +
                  _d = delta(others, scores)
         | 
| 170 | 
            +
                  _volatility = volatility1(_d, _v)
         | 
| 171 | 
            +
                  sd_pre = Math.sqrt(sd ** 2 + _volatility ** 2)
         | 
| 172 | 
            +
                  _sd = 1 / Math.sqrt(1 / sd_pre ** 2 + 1 / _v)
         | 
| 173 | 
            +
                  _mean = mean + _sd ** 2 * others.zip(scores).reduce(0) {
         | 
| 174 | 
            +
                    |x, (other, score)| x + other.g * (score - e(other))
         | 
| 175 | 
            +
                  }
         | 
| 176 | 
            +
                  self.class.new(_mean, _sd, _volatility, obj)
         | 
| 177 | 
            +
                end
         | 
| 178 | 
            +
             | 
| 179 | 
            +
                def f_part1(x, d, v)
         | 
| 180 | 
            +
                  exp_x = Math.exp(x)
         | 
| 181 | 
            +
                  sd_sq = sd ** 2
         | 
| 182 | 
            +
                  (exp_x * (d ** 2 - sd_sq - v - exp_x)) / (2 * (sd_sq + v + exp_x) ** 2)
         | 
| 183 | 
            +
                end
         | 
| 184 | 
            +
             | 
| 185 | 
            +
                def f_part2(x)
         | 
| 186 | 
            +
                  (x - Math::log(volatility ** 2)) / VOLATILITY_CHANGE ** 2
         | 
| 187 | 
            +
                end
         | 
| 188 | 
            +
              end
         | 
| 189 | 
            +
            end
         | 
| @@ -0,0 +1,66 @@ | |
| 1 | 
            +
            module Glicko2
         | 
| 2 | 
            +
              # Glicko ratings are calculated in bulk at the end of arbitrary, but fixed
         | 
| 3 | 
            +
              # length, periods named rating periods. Where a period is fixed to be long
         | 
| 4 | 
            +
              # enough that the average number of games that each player has played in is
         | 
| 5 | 
            +
              # about 5 to 10 games. It could be weekly, monthly or more as required.
         | 
| 6 | 
            +
              class RatingPeriod
         | 
| 7 | 
            +
                attr_reader :players
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                # @param [Array<Player>] players
         | 
| 10 | 
            +
                def initialize(players)
         | 
| 11 | 
            +
                  @players = players
         | 
| 12 | 
            +
                  @games = Hash.new { |h, k| h[k] = [] }
         | 
| 13 | 
            +
                  @cache = players.reduce({}) { |memo, player| memo[player.obj] = player; memo }
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                # Create rating period from list of seed objects
         | 
| 17 | 
            +
                #
         | 
| 18 | 
            +
                # @param [Array<#rating,#rating_deviation,#volatility>] objs seed value objects
         | 
| 19 | 
            +
                # @return [RatingPeriod]
         | 
| 20 | 
            +
                def self.from_objs(objs)
         | 
| 21 | 
            +
                  new(objs.map { |obj| Player.from_obj(obj) })
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                # Register a game with this rating period
         | 
| 25 | 
            +
                #
         | 
| 26 | 
            +
                # @param [Array<#rating,#rating_deviation,#volatility>] game_seeds ratings participating in a game
         | 
| 27 | 
            +
                # @param [Array<Integer>] ranks corresponding ranks
         | 
| 28 | 
            +
                def game(game_seeds, ranks)
         | 
| 29 | 
            +
                  game_seeds.zip(ranks).each do |seed, rank|
         | 
| 30 | 
            +
                    game_seeds.zip(ranks).each do |other, other_rank|
         | 
| 31 | 
            +
                      next if seed == other
         | 
| 32 | 
            +
                      @games[player(seed)] << [player(other),
         | 
| 33 | 
            +
                                             Util.ranks_to_score(rank, other_rank)]
         | 
| 34 | 
            +
                    end
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                # Generate a new {RatingPeriod} with a new list of updated {Player}
         | 
| 39 | 
            +
                #
         | 
| 40 | 
            +
                # @return [RatingPeriod]
         | 
| 41 | 
            +
                def generate_next
         | 
| 42 | 
            +
                  p = []
         | 
| 43 | 
            +
                  @players.each do |player|
         | 
| 44 | 
            +
                    games = @games[player]
         | 
| 45 | 
            +
                    if games.length > 0
         | 
| 46 | 
            +
                      p << player.generate_next(*games.transpose)
         | 
| 47 | 
            +
                    else
         | 
| 48 | 
            +
                      p << player.generate_next([], [])
         | 
| 49 | 
            +
                    end
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
                  self.class.new(p)
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                # Fetch the player associated with a seed object
         | 
| 55 | 
            +
                #
         | 
| 56 | 
            +
                # @param [#rating,#rating_deviation,#volatility] obj seed object
         | 
| 57 | 
            +
                # @return [Player]
         | 
| 58 | 
            +
                def player(obj)
         | 
| 59 | 
            +
                  @cache[obj]
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                def to_s
         | 
| 63 | 
            +
                  "#<RatingPeriod players=#{@players}"
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
              end
         | 
| 66 | 
            +
            end
         | 
    
        data/lib/glicko2/version.rb
    CHANGED
    
    
    
        data/spec/minitest_helper.rb
    CHANGED
    
    
    
        data/spec/player_spec.rb
    CHANGED
    
    | @@ -92,6 +92,10 @@ describe Glicko2::Player do | |
| 92 92 | 
             
                  p.volatility.must_equal @player.volatility
         | 
| 93 93 | 
             
                  p.sd.must_be_close_to Math.sqrt(@player.sd ** 2 + @player.volatility ** 2)
         | 
| 94 94 | 
             
                end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                bench_performance_linear "default" do |n|
         | 
| 97 | 
            +
                  @player.generate_next(@others * n, @scores * n)
         | 
| 98 | 
            +
                end
         | 
| 95 99 | 
             
              end
         | 
| 96 100 |  | 
| 97 101 | 
             
              describe "#update_obj" do
         | 
    
        data/spec/rating_period_spec.rb
    CHANGED
    
    | @@ -2,47 +2,36 @@ require 'minitest_helper' | |
| 2 2 |  | 
| 3 3 | 
             
            describe Glicko2::RatingPeriod do
         | 
| 4 4 | 
             
              before do
         | 
| 5 | 
            -
                @player =  | 
| 6 | 
            -
                @player1 =  | 
| 7 | 
            -
                @player2 =  | 
| 8 | 
            -
                @player3 =  | 
| 5 | 
            +
                @player = Rating.new(1500, 200, 0.06)
         | 
| 6 | 
            +
                @player1 = Rating.new(1400, 30, 0.06)
         | 
| 7 | 
            +
                @player2 = Rating.new(1550, 100, 0.06)
         | 
| 8 | 
            +
                @player3 = Rating.new(1700, 300, 0.06)
         | 
| 9 9 | 
             
                @players = [@player, @player1, @player2, @player3]
         | 
| 10 | 
            -
                @period = Glicko2::RatingPeriod. | 
| 10 | 
            +
                @period = Glicko2::RatingPeriod.from_objs(@players)
         | 
| 11 11 | 
             
              end
         | 
| 12 12 |  | 
| 13 | 
            -
              describe " | 
| 14 | 
            -
                it "must assign players" do
         | 
| 15 | 
            -
                  @period.players.must_include @player
         | 
| 16 | 
            -
                  @period.players.must_include @player1
         | 
| 17 | 
            -
                  @period.players.must_include @player2
         | 
| 18 | 
            -
                  @period.players.must_include @player3
         | 
| 19 | 
            -
                end
         | 
| 20 | 
            -
              end
         | 
| 21 | 
            -
             | 
| 22 | 
            -
              describe ".ranks_to_score" do
         | 
| 23 | 
            -
                it "must return 1.0 when rank is less" do
         | 
| 24 | 
            -
                  Glicko2::RatingPeriod.ranks_to_score(1, 2).must_equal 1.0
         | 
| 25 | 
            -
                end
         | 
| 26 | 
            -
             | 
| 27 | 
            -
                it "must return 0.5 when rank is equal" do
         | 
| 28 | 
            -
                  Glicko2::RatingPeriod.ranks_to_score(1, 1).must_equal 0.5
         | 
| 29 | 
            -
                end
         | 
| 30 | 
            -
             | 
| 31 | 
            -
                it "must return 0.0 when rank is more" do
         | 
| 32 | 
            -
                  Glicko2::RatingPeriod.ranks_to_score(2, 1).must_equal 0.0
         | 
| 33 | 
            -
                end
         | 
| 34 | 
            -
              end
         | 
| 35 | 
            -
             | 
| 36 | 
            -
              describe "complete rating period" do
         | 
| 13 | 
            +
              describe "#generate_next" do
         | 
| 37 14 | 
             
                it "must be close to example" do
         | 
| 38 15 | 
             
                  @period.game([@player, @player1], [1, 2])
         | 
| 39 16 | 
             
                  @period.game([@player, @player2], [2, 1])
         | 
| 40 17 | 
             
                  @period.game([@player, @player3], [2, 1])
         | 
| 41 18 | 
             
                  @period.generate_next.players.each { |p| p.update_obj }
         | 
| 42 | 
            -
                  obj = @player | 
| 19 | 
            +
                  obj = @player
         | 
| 43 20 | 
             
                  obj.rating.must_be_close_to 1464.06, 0.01
         | 
| 44 21 | 
             
                  obj.rating_deviation.must_be_close_to 151.52, 0.01
         | 
| 45 22 | 
             
                  obj.volatility.must_be_close_to 0.05999, 0.00001
         | 
| 46 23 | 
             
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                it "must process non-competing players" do
         | 
| 26 | 
            +
                  @period.game([@player, @player1], [1, 2])
         | 
| 27 | 
            +
                  @period.generate_next
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                bench_performance_linear "default" do |n|
         | 
| 31 | 
            +
                  n.times do
         | 
| 32 | 
            +
                    @period.game(@players.sample(2), [1, 2])
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
                  @period.generate_next
         | 
| 35 | 
            +
                end
         | 
| 47 36 | 
             
              end
         | 
| 48 37 | 
             
            end
         | 
    
        data/spec/util_spec.rb
    ADDED
    
    | @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            require 'minitest_helper'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            describe Glicko2::Util do
         | 
| 4 | 
            +
              describe ".ranks_to_score" do
         | 
| 5 | 
            +
                it "must return 1.0 when rank is less" do
         | 
| 6 | 
            +
                  Glicko2::Util.ranks_to_score(1, 2).must_equal 1.0
         | 
| 7 | 
            +
                end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                it "must return 0.5 when rank is equal" do
         | 
| 10 | 
            +
                  Glicko2::Util.ranks_to_score(1, 1).must_equal 0.5
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                it "must return 0.0 when rank is more" do
         | 
| 14 | 
            +
                  Glicko2::Util.ranks_to_score(2, 1).must_equal 0.0
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: glicko2
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.0 | 
| 4 | 
            +
              version: 0.1.0
         | 
| 5 5 | 
             
              prerelease: 
         | 
| 6 6 | 
             
            platform: ruby
         | 
| 7 7 | 
             
            authors:
         | 
| @@ -9,7 +9,7 @@ authors: | |
| 9 9 | 
             
            autorequire: 
         | 
| 10 10 | 
             
            bindir: bin
         | 
| 11 11 | 
             
            cert_chain: []
         | 
| 12 | 
            -
            date: 2012-12- | 
| 12 | 
            +
            date: 2012-12-26 00:00:00.000000000 Z
         | 
| 13 13 | 
             
            dependencies:
         | 
| 14 14 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 15 15 | 
             
              name: minitest
         | 
| @@ -41,10 +41,13 @@ files: | |
| 41 41 | 
             
            - Rakefile
         | 
| 42 42 | 
             
            - glicko2.gemspec
         | 
| 43 43 | 
             
            - lib/glicko2.rb
         | 
| 44 | 
            +
            - lib/glicko2/player.rb
         | 
| 45 | 
            +
            - lib/glicko2/rating_period.rb
         | 
| 44 46 | 
             
            - lib/glicko2/version.rb
         | 
| 45 47 | 
             
            - spec/minitest_helper.rb
         | 
| 46 48 | 
             
            - spec/player_spec.rb
         | 
| 47 49 | 
             
            - spec/rating_period_spec.rb
         | 
| 50 | 
            +
            - spec/util_spec.rb
         | 
| 48 51 | 
             
            homepage: https://github.com/proglottis/glicko2
         | 
| 49 52 | 
             
            licenses: []
         | 
| 50 53 | 
             
            post_install_message: 
         | 
| @@ -73,3 +76,5 @@ test_files: | |
| 73 76 | 
             
            - spec/minitest_helper.rb
         | 
| 74 77 | 
             
            - spec/player_spec.rb
         | 
| 75 78 | 
             
            - spec/rating_period_spec.rb
         | 
| 79 | 
            +
            - spec/util_spec.rb
         | 
| 80 | 
            +
            has_rdoc: 
         |