compact-glicko2 0.1.3
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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +54 -0
- data/Rakefile +9 -0
- data/glicko2.gemspec +23 -0
- data/lib/glicko2.rb +34 -0
- data/lib/glicko2/normal_distribution.rb +64 -0
- data/lib/glicko2/player.rb +111 -0
- data/lib/glicko2/rating.rb +150 -0
- data/lib/glicko2/rating_period.rb +70 -0
- data/lib/glicko2/version.rb +3 -0
- data/spec/minitest_helper.rb +5 -0
- data/spec/normal_distribution_spec.rb +98 -0
- data/spec/player_spec.rb +76 -0
- data/spec/rating_period_spec.rb +45 -0
- data/spec/rating_spec.rb +53 -0
- data/spec/util_spec.rb +17 -0
- metadata +111 -0
    
        checksums.yaml
    ADDED
    
    | @@ -0,0 +1,7 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            SHA1:
         | 
| 3 | 
            +
              metadata.gz: 3839ca7a4bcb2fab2a76e939cd8134f9f7205d14
         | 
| 4 | 
            +
              data.tar.gz: 4ce1c41078fae71cce47432ecc9624b36acfeb33
         | 
| 5 | 
            +
            SHA512:
         | 
| 6 | 
            +
              metadata.gz: 4c5a4e20d4efabaf002dbac6f68e279ebcb7036b29dade5956907ba54bf04a1349d6468c1405919e5bc638250d7ccef65917c26d1221ecb96fa604f7778f730b
         | 
| 7 | 
            +
              data.tar.gz: d6fc172b730199abc8ca5613096d93e8553e3c0a85df85c9987fd1b412a661d29fd0f5aa281393f551236093aac2c52d1f92c318c66ed1525651164fbd449f7f
         | 
    
        data/.gitignore
    ADDED
    
    
    
        data/Gemfile
    ADDED
    
    
    
        data/LICENSE.txt
    ADDED
    
    | @@ -0,0 +1,22 @@ | |
| 1 | 
            +
            Copyright (c) 2012 James Fargher
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            MIT License
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Permission is hereby granted, free of charge, to any person obtaining
         | 
| 6 | 
            +
            a copy of this software and associated documentation files (the
         | 
| 7 | 
            +
            "Software"), to deal in the Software without restriction, including
         | 
| 8 | 
            +
            without limitation the rights to use, copy, modify, merge, publish,
         | 
| 9 | 
            +
            distribute, sublicense, and/or sell copies of the Software, and to
         | 
| 10 | 
            +
            permit persons to whom the Software is furnished to do so, subject to
         | 
| 11 | 
            +
            the following conditions:
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            The above copyright notice and this permission notice shall be
         | 
| 14 | 
            +
            included in all copies or substantial portions of the Software.
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
         | 
| 17 | 
            +
            EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
         | 
| 18 | 
            +
            MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
         | 
| 19 | 
            +
            NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
         | 
| 20 | 
            +
            LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
         | 
| 21 | 
            +
            OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
         | 
| 22 | 
            +
            WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
         | 
    
        data/README.md
    ADDED
    
    | @@ -0,0 +1,54 @@ | |
| 1 | 
            +
            # Glicko2
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            Implementation of Glicko2 ratings.
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Based on Mark Glickman's paper http://www.glicko.net/glicko/glicko2.pdf
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            ## Installation
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            Add this line to your application's Gemfile:
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                gem 'glicko2'
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            And then execute:
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                $ bundle
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            Or install it yourself as:
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                $ gem install glicko2
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            ## Usage
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            ```ruby
         | 
| 24 | 
            +
            require 'glicko2'
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            # Objects to store Glicko ratings
         | 
| 27 | 
            +
            Rating = Struct.new(:rating, :rating_deviation, :volatility)
         | 
| 28 | 
            +
            rating1 = Rating.new(1400, 30, 0.06)
         | 
| 29 | 
            +
            rating2 = Rating.new(1550, 100, 0.06)
         | 
| 30 | 
            +
             | 
| 31 | 
            +
            # Rating period with all participating ratings
         | 
| 32 | 
            +
            period = Glicko2::RatingPeriod.from_objs [rating1, rating2]
         | 
| 33 | 
            +
             | 
| 34 | 
            +
            # Register a game where rating1 wins against rating2
         | 
| 35 | 
            +
            period.game([rating1, rating2], [1,2])
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            # Generate the next rating period with updated players
         | 
| 38 | 
            +
            next_period = period.generate_next
         | 
| 39 | 
            +
             | 
| 40 | 
            +
            # Update all Glicko ratings
         | 
| 41 | 
            +
            next_period.players.each { |p| p.update_obj }
         | 
| 42 | 
            +
             | 
| 43 | 
            +
            # Output updated Glicko ratings
         | 
| 44 | 
            +
            puts rating1
         | 
| 45 | 
            +
            puts rating2
         | 
| 46 | 
            +
            ```
         | 
| 47 | 
            +
             | 
| 48 | 
            +
            ## Contributing
         | 
| 49 | 
            +
             | 
| 50 | 
            +
            1. Fork it
         | 
| 51 | 
            +
            2. Create your feature branch (`git checkout -b my-new-feature`)
         | 
| 52 | 
            +
            3. Commit your changes (`git commit -am 'Add some feature'`)
         | 
| 53 | 
            +
            4. Push to the branch (`git push origin my-new-feature`)
         | 
| 54 | 
            +
            5. Create new Pull Request
         | 
    
        data/Rakefile
    ADDED
    
    
    
        data/glicko2.gemspec
    ADDED
    
    | @@ -0,0 +1,23 @@ | |
| 1 | 
            +
            # coding: utf-8
         | 
| 2 | 
            +
            lib = File.expand_path('../lib', __FILE__)
         | 
| 3 | 
            +
            $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
         | 
| 4 | 
            +
            require 'glicko2/version'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            Gem::Specification.new do |gem|
         | 
| 7 | 
            +
              gem.name          = "compact-glicko2"
         | 
| 8 | 
            +
              gem.version       = Glicko2::VERSION
         | 
| 9 | 
            +
              gem.authors       = ["James Fargher", "Andrej Antas"]
         | 
| 10 | 
            +
              gem.email         = ["proglottis@gmail.com", "andrej@antas.cz"]
         | 
| 11 | 
            +
              gem.description   = %q{Implementation of Glicko2 ratings with compact rule}
         | 
| 12 | 
            +
              gem.summary       = %q{Implementation of Glicko2 ratings with compact rule}
         | 
| 13 | 
            +
              gem.homepage      = "https://github.com/redrick/glicko2"
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              gem.files         = `git ls-files`.split($/)
         | 
| 16 | 
            +
              gem.executables   = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
         | 
| 17 | 
            +
              gem.test_files    = gem.files.grep(%r{^(test|spec|features)/})
         | 
| 18 | 
            +
              gem.require_paths = ["lib"]
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              gem.add_development_dependency('bundler', '~> 1.3')
         | 
| 21 | 
            +
              gem.add_development_dependency('rake')
         | 
| 22 | 
            +
              gem.add_development_dependency('minitest', '~> 4.7.5')
         | 
| 23 | 
            +
            end
         | 
    
        data/lib/glicko2.rb
    ADDED
    
    | @@ -0,0 +1,34 @@ | |
| 1 | 
            +
            module Glicko2
         | 
| 2 | 
            +
              DEFAULT_VOLATILITY = 0.06
         | 
| 3 | 
            +
              DEFAULT_GLICKO_RATING = 1500.0
         | 
| 4 | 
            +
              DEFAULT_GLICKO_RATING_DEVIATION = 350.0
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              DEFAULT_CONFIG = {:volatility_change => 0.5}.freeze
         | 
| 7 | 
            +
             | 
| 8 | 
            +
              class DuplicatePlayerError < StandardError; end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              # Collection of helper methods
         | 
| 11 | 
            +
              class Util
         | 
| 12 | 
            +
                # Convert from a rank, where lower numbers win against higher numbers,
         | 
| 13 | 
            +
                # into Glicko scores where wins are `1`, draws are `0.5` and losses are `0`.
         | 
| 14 | 
            +
                #
         | 
| 15 | 
            +
                # @param [Integer] rank players rank
         | 
| 16 | 
            +
                # @param [Integer] other opponents rank
         | 
| 17 | 
            +
                # @return [Numeric] Glicko score
         | 
| 18 | 
            +
                def self.ranks_to_score(rank, other)
         | 
| 19 | 
            +
                  if rank < other
         | 
| 20 | 
            +
                    1.0
         | 
| 21 | 
            +
                  elsif rank == other
         | 
| 22 | 
            +
                    0.5
         | 
| 23 | 
            +
                  else
         | 
| 24 | 
            +
                    0.0
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
            end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            require "glicko2/version"
         | 
| 31 | 
            +
            require "glicko2/normal_distribution"
         | 
| 32 | 
            +
            require "glicko2/rating"
         | 
| 33 | 
            +
            require "glicko2/player"
         | 
| 34 | 
            +
            require "glicko2/rating_period"
         | 
| @@ -0,0 +1,64 @@ | |
| 1 | 
            +
            module Glicko2
         | 
| 2 | 
            +
              # Glicko ratings are represented with a rating and rating deviation. For this
         | 
| 3 | 
            +
              # gem it is assumed that ratings are normally distributed where rating and
         | 
| 4 | 
            +
              # rating deviation correspond to mean and standard deviation.
         | 
| 5 | 
            +
              class NormalDistribution
         | 
| 6 | 
            +
                include Comparable
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                attr_reader :mean, :standard_deviation
         | 
| 9 | 
            +
                alias_method :sd, :standard_deviation
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def initialize(mean, standard_deviation)
         | 
| 12 | 
            +
                  @mean = mean
         | 
| 13 | 
            +
                  @standard_deviation = standard_deviation
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                # Calculate the distribution variance
         | 
| 17 | 
            +
                #
         | 
| 18 | 
            +
                # @return [Numeric]
         | 
| 19 | 
            +
                def variance
         | 
| 20 | 
            +
                  standard_deviation ** 2.0
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                # Calculate the sum
         | 
| 24 | 
            +
                #
         | 
| 25 | 
            +
                # @param [NormalDistribution] other
         | 
| 26 | 
            +
                # @return [NormalDistribution]
         | 
| 27 | 
            +
                def +(other)
         | 
| 28 | 
            +
                  self.class.new(mean + other.mean, Math.sqrt(variance + other.variance))
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                # Calculate the difference
         | 
| 32 | 
            +
                #
         | 
| 33 | 
            +
                # @param [NormalDistribution] other
         | 
| 34 | 
            +
                # @return [NormalDistribution]
         | 
| 35 | 
            +
                def -(other)
         | 
| 36 | 
            +
                  self.class.new(mean - other.mean, Math.sqrt(variance + other.variance))
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                # Calculate the probability density at `x`
         | 
| 40 | 
            +
                #
         | 
| 41 | 
            +
                # @param [Numeric] x
         | 
| 42 | 
            +
                # @return [Numeric]
         | 
| 43 | 
            +
                def pdf(x)
         | 
| 44 | 
            +
                  1.0 / (sd * Math.sqrt(2.0 * Math::PI)) *
         | 
| 45 | 
            +
                    Math.exp(-(x - mean) ** 2.0 / 2.0 * variance)
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                # Calculate the cumulative distribution at `x`
         | 
| 49 | 
            +
                #
         | 
| 50 | 
            +
                # @param [Numeric] x
         | 
| 51 | 
            +
                # @return [Numeric]
         | 
| 52 | 
            +
                def cdf(x)
         | 
| 53 | 
            +
                  0.5 * (1.0 + Math.erf((x - mean) / (sd * Math.sqrt(2.0))))
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                def <=>(other)
         | 
| 57 | 
            +
                  mean <=> other.mean
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                def to_s
         | 
| 61 | 
            +
                  "#<NormalDistribution mean=#{mean}, sd=#{sd}>"
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
              end
         | 
| 64 | 
            +
            end
         | 
| @@ -0,0 +1,111 @@ | |
| 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 | 
            +
                attr_reader :rating, :obj
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                # Create a {Player} from a seed object, converting from Glicko
         | 
| 29 | 
            +
                # ratings to Glicko2.
         | 
| 30 | 
            +
                #
         | 
| 31 | 
            +
                # @param [#rating,#rating_deviation,#volatility] obj seed values object
         | 
| 32 | 
            +
                # @return [Player] constructed instance.
         | 
| 33 | 
            +
                def self.from_obj(obj, config=nil)
         | 
| 34 | 
            +
                  rating = Rating.from_glicko_rating(obj.rating, obj.rating_deviation,
         | 
| 35 | 
            +
                                                     obj.volatility, config)
         | 
| 36 | 
            +
                  new(rating, obj)
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                # @param [Numeric] mean player mean
         | 
| 40 | 
            +
                # @param [Numeric] sd player standard deviation
         | 
| 41 | 
            +
                # @param [Numeric] volatility player volatility
         | 
| 42 | 
            +
                # @param [#rating,#rating_deviation,#volatility] obj seed values object
         | 
| 43 | 
            +
                def initialize(rating, obj=nil)
         | 
| 44 | 
            +
                  @rating = rating
         | 
| 45 | 
            +
                  @obj = obj
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                # Create new {Player} with updated values.
         | 
| 49 | 
            +
                #
         | 
| 50 | 
            +
                # This method will not modify any objects that are passed into it.
         | 
| 51 | 
            +
                #
         | 
| 52 | 
            +
                # @param [Array<Player>] others list of opponent players
         | 
| 53 | 
            +
                # @param [Array<Numeric>] scores list of correlating scores (`0` for a loss,
         | 
| 54 | 
            +
                #   `0.5` for a draw and `1` for a win).
         | 
| 55 | 
            +
                # @return [Player]
         | 
| 56 | 
            +
                def generate_next(others, scores)
         | 
| 57 | 
            +
                  if others.compact.length < 1
         | 
| 58 | 
            +
                    generate_next_without_games
         | 
| 59 | 
            +
                  else
         | 
| 60 | 
            +
                    others = others.compact.map{ |other| other.rating }
         | 
| 61 | 
            +
                    generate_next_with_games(others, scores)
         | 
| 62 | 
            +
                  end
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                # Update seed object with this player's values
         | 
| 66 | 
            +
                def update_obj
         | 
| 67 | 
            +
                  glicko_rating = rating.to_glicko_rating
         | 
| 68 | 
            +
                  @obj.rating = glicko_rating.mean
         | 
| 69 | 
            +
                  @obj.rating_deviation = glicko_rating.standard_deviation
         | 
| 70 | 
            +
                  @obj.volatility = volatility
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                def mean
         | 
| 74 | 
            +
                  rating.mean
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                def standard_deviation
         | 
| 78 | 
            +
                  rating.standard_deviation
         | 
| 79 | 
            +
                end
         | 
| 80 | 
            +
                alias_method :sd, :standard_deviation
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                def volatility
         | 
| 83 | 
            +
                  rating.volatility
         | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                def to_s
         | 
| 87 | 
            +
                  "#<Player rating=#{rating}, obj=#{obj}>"
         | 
| 88 | 
            +
                end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                private
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                def generate_next_without_games
         | 
| 93 | 
            +
                  next_rating = Rating.new(mean, rating.standard_deviation_pre,
         | 
| 94 | 
            +
                                           volatility, rating.config)
         | 
| 95 | 
            +
                  self.class.new(next_rating, obj)
         | 
| 96 | 
            +
                end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                def generate_next_with_games(others, scores)
         | 
| 99 | 
            +
                  rating = generate_next_rating(others, scores)
         | 
| 100 | 
            +
                  self.class.new(rating, obj)
         | 
| 101 | 
            +
                end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                def generate_next_rating(others, scores)
         | 
| 104 | 
            +
                  _v = rating.estimated_variance(others)
         | 
| 105 | 
            +
                  _volatility = rating.next_volatility(others, scores, _v)
         | 
| 106 | 
            +
                  _sd = rating.next_standard_deviation(_v)
         | 
| 107 | 
            +
                  _mean = rating.next_mean(others, scores, _sd)
         | 
| 108 | 
            +
                  Rating.new(_mean, _sd, _volatility, rating.config)
         | 
| 109 | 
            +
                end
         | 
| 110 | 
            +
              end
         | 
| 111 | 
            +
            end
         | 
| @@ -0,0 +1,150 @@ | |
| 1 | 
            +
            module Glicko2
         | 
| 2 | 
            +
              class Rating < NormalDistribution
         | 
| 3 | 
            +
                GLICKO_GRADIENT = 173.7178
         | 
| 4 | 
            +
                GLICKO_INTERCEPT = DEFAULT_GLICKO_RATING
         | 
| 5 | 
            +
                MIN_SD = DEFAULT_GLICKO_RATING_DEVIATION / GLICKO_GRADIENT
         | 
| 6 | 
            +
                TOLERANCE = 0.0000001
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                attr_reader :volatility, :config
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                def initialize(mean, sd, volatility=nil, config=nil)
         | 
| 11 | 
            +
                  super(mean, sd)
         | 
| 12 | 
            +
                  @volatility = volatility || DEFAULT_VOLATILITY
         | 
| 13 | 
            +
                  @config = config || DEFAULT_CONFIG
         | 
| 14 | 
            +
                  @e = {}
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def self.from_glicko_rating(r, rd, volatility=nil, config=nil)
         | 
| 18 | 
            +
                  new((r - GLICKO_INTERCEPT) / GLICKO_GRADIENT, rd / GLICKO_GRADIENT,
         | 
| 19 | 
            +
                      volatility, config)
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                def to_glicko_rating
         | 
| 23 | 
            +
                  NormalDistribution.new(GLICKO_GRADIENT * mean + GLICKO_INTERCEPT,
         | 
| 24 | 
            +
                                         GLICKO_GRADIENT * sd)
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                # Calculate `g(phi)` as defined in the Glicko2 paper
         | 
| 28 | 
            +
                #
         | 
| 29 | 
            +
                # @return [Numeric]
         | 
| 30 | 
            +
                def gravity
         | 
| 31 | 
            +
                  @gravity ||= 1 / Math.sqrt(1 + 3 * variance / Math::PI ** 2)
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                # Calculate `E(mu, mu_j, phi_j)` as defined in the Glicko2 paper
         | 
| 35 | 
            +
                #
         | 
| 36 | 
            +
                # @param [Player] other the `j` player
         | 
| 37 | 
            +
                # @return [Numeric]
         | 
| 38 | 
            +
                def expected_fractional_score(other)
         | 
| 39 | 
            +
                  @e[other] ||= 1 / (1 + Math.exp(-other.gravity * (mean - other.mean)))
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                # Calculate the estimated variance of the team's/player's rating based only
         | 
| 43 | 
            +
                # on the game outcomes.
         | 
| 44 | 
            +
                #
         | 
| 45 | 
            +
                # @param [Array<Player>] others other participating players.
         | 
| 46 | 
            +
                # @return [Numeric]
         | 
| 47 | 
            +
                def estimated_variance(others)
         | 
| 48 | 
            +
                  return 0.0 if others.length < 1
         | 
| 49 | 
            +
                  others.reduce(0) do |v, other|
         | 
| 50 | 
            +
                    e_other = expected_fractional_score(other)
         | 
| 51 | 
            +
                    v + other.gravity ** 2 * e_other * (1 - e_other)
         | 
| 52 | 
            +
                  end ** -1
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                # Calculate the estimated improvement in rating by comparing the
         | 
| 56 | 
            +
                # pre-period rating to the performance rating based only on game outcomes.
         | 
| 57 | 
            +
                #
         | 
| 58 | 
            +
                # @param [Array<Player>] others list of opponent players
         | 
| 59 | 
            +
                # @param [Array<Numeric>] scores list of correlating scores (`0` for a loss,
         | 
| 60 | 
            +
                #   `0.5` for a draw and `1` for a win).
         | 
| 61 | 
            +
                # @return [Numeric]
         | 
| 62 | 
            +
                def delta(others, scores)
         | 
| 63 | 
            +
                  others.zip(scores).reduce(0) do |d, (other, score)|
         | 
| 64 | 
            +
                    d + other.gravity * (score - expected_fractional_score(other))
         | 
| 65 | 
            +
                  end * estimated_variance(others)
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                # Calculate `f(x)` as defined in the Glicko2 paper
         | 
| 69 | 
            +
                #
         | 
| 70 | 
            +
                # @param [Numeric] x
         | 
| 71 | 
            +
                # @param [Numeric] d the result of calculating {#delta}
         | 
| 72 | 
            +
                # @param [Numeric] v the result of calculating {#estimated_variance}
         | 
| 73 | 
            +
                # @return [Numeric]
         | 
| 74 | 
            +
                def f(x, d, v)
         | 
| 75 | 
            +
                  f_part1(x, d, v) - f_part2(x)
         | 
| 76 | 
            +
                end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                # Calculate the pre-game standard deviation
         | 
| 79 | 
            +
                #
         | 
| 80 | 
            +
                # This slightly increases the standard deviation in case the player has
         | 
| 81 | 
            +
                # been stagnant for a rating period.
         | 
| 82 | 
            +
                def standard_deviation_pre
         | 
| 83 | 
            +
                  [Math.sqrt(variance + volatility ** 2), MIN_SD].min
         | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                # Calculate the new value of the volatility
         | 
| 87 | 
            +
                #
         | 
| 88 | 
            +
                # @param [Numeric] d the result of calculating {#delta}
         | 
| 89 | 
            +
                # @param [Numeric] v the result of calculating {#estimated_variance}
         | 
| 90 | 
            +
                # @return [Numeric]
         | 
| 91 | 
            +
                def next_volatility(others, scores, v)
         | 
| 92 | 
            +
                  d, a, b = next_volatility_setup(others, scores, v)
         | 
| 93 | 
            +
                  fa = f(a, d, v)
         | 
| 94 | 
            +
                  fb = f(b, d, v)
         | 
| 95 | 
            +
                  while (b - a).abs > TOLERANCE
         | 
| 96 | 
            +
                    c = a + (a - b) * fa / (fb - fa)
         | 
| 97 | 
            +
                    fc = f(c, d, v)
         | 
| 98 | 
            +
                    if fc * fb < 0
         | 
| 99 | 
            +
                      a = b
         | 
| 100 | 
            +
                      fa = fb
         | 
| 101 | 
            +
                    else
         | 
| 102 | 
            +
                      fa /= 2.0
         | 
| 103 | 
            +
                    end
         | 
| 104 | 
            +
                    b = c
         | 
| 105 | 
            +
                    fb = fc
         | 
| 106 | 
            +
                  end
         | 
| 107 | 
            +
                  Math.exp(a / 2.0)
         | 
| 108 | 
            +
                end
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                def next_standard_deviation(v)
         | 
| 111 | 
            +
                  1 / Math.sqrt(1 / standard_deviation_pre ** 2 + 1 / v)
         | 
| 112 | 
            +
                end
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                def next_mean(others, scores, next_sd)
         | 
| 115 | 
            +
                  others.zip(scores).reduce(0) { |x, (other, score)|
         | 
| 116 | 
            +
                    x + other.gravity * (score - expected_fractional_score(other))
         | 
| 117 | 
            +
                  } * next_sd ** 2.0 + mean
         | 
| 118 | 
            +
                end
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                def to_s
         | 
| 121 | 
            +
                  "#<Rating mean=#{mean}, sd=#{sd}, volatility=#{volatility}>"
         | 
| 122 | 
            +
                end
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                private
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                def f_part1(x, d, v)
         | 
| 127 | 
            +
                  exp_x = Math.exp(x)
         | 
| 128 | 
            +
                  sd_sq = variance
         | 
| 129 | 
            +
                  (exp_x * (d ** 2 - sd_sq - v - exp_x)) / (2 * (sd_sq + v + exp_x) ** 2)
         | 
| 130 | 
            +
                end
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                def f_part2(x)
         | 
| 133 | 
            +
                  (x - Math::log(volatility ** 2)) / config[:volatility_change] ** 2
         | 
| 134 | 
            +
                end
         | 
| 135 | 
            +
             | 
| 136 | 
            +
                def next_volatility_setup(others, scores, v)
         | 
| 137 | 
            +
                  d = delta(others, scores)
         | 
| 138 | 
            +
                  a = Math::log(volatility ** 2)
         | 
| 139 | 
            +
                  if d > variance + v
         | 
| 140 | 
            +
                    b = Math.log(d - variance - v)
         | 
| 141 | 
            +
                  else
         | 
| 142 | 
            +
                    k = 1
         | 
| 143 | 
            +
                    k += 1 while f(a - k * config[:volatility_change], d, v) < 0
         | 
| 144 | 
            +
                    b = a - k * config[:volatility_change]
         | 
| 145 | 
            +
                  end
         | 
| 146 | 
            +
                  [d, a, b]
         | 
| 147 | 
            +
                end
         | 
| 148 | 
            +
             | 
| 149 | 
            +
              end
         | 
| 150 | 
            +
            end
         | 
| @@ -0,0 +1,70 @@ | |
| 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({}) do |memo, player|
         | 
| 14 | 
            +
                    raise DuplicatePlayerError if memo[player.obj] != nil
         | 
| 15 | 
            +
                    memo[player.obj] = player
         | 
| 16 | 
            +
                    memo
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                # Create rating period from list of seed objects
         | 
| 21 | 
            +
                #
         | 
| 22 | 
            +
                # @param [Array<#rating,#rating_deviation,#volatility>] objs seed value objects
         | 
| 23 | 
            +
                # @return [RatingPeriod]
         | 
| 24 | 
            +
                def self.from_objs(objs, config=DEFAULT_CONFIG)
         | 
| 25 | 
            +
                  new(objs.map { |obj| Player.from_obj(obj, config) })
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                # Register a game with this rating period
         | 
| 29 | 
            +
                #
         | 
| 30 | 
            +
                # @param [Array<#rating,#rating_deviation,#volatility>] game_seeds ratings participating in a game
         | 
| 31 | 
            +
                # @param [Array<Integer>] ranks corresponding ranks
         | 
| 32 | 
            +
                def game(game_seeds, ranks)
         | 
| 33 | 
            +
                  game_seeds.zip(ranks).each do |seed, rank|
         | 
| 34 | 
            +
                    game_seeds.zip(ranks).each do |other, other_rank|
         | 
| 35 | 
            +
                      next if seed == other
         | 
| 36 | 
            +
                      @games[player(seed)] << [player(other),
         | 
| 37 | 
            +
                                             Util.ranks_to_score(rank, other_rank)]
         | 
| 38 | 
            +
                    end
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                # Generate a new {RatingPeriod} with a new list of updated {Player}
         | 
| 43 | 
            +
                #
         | 
| 44 | 
            +
                # @return [RatingPeriod]
         | 
| 45 | 
            +
                def generate_next
         | 
| 46 | 
            +
                  p = []
         | 
| 47 | 
            +
                  @players.each do |player|
         | 
| 48 | 
            +
                    games = @games[player]
         | 
| 49 | 
            +
                    if games.compact.length > 0
         | 
| 50 | 
            +
                      p << player.generate_next(*games.transpose)
         | 
| 51 | 
            +
                    else
         | 
| 52 | 
            +
                      p << player.generate_next([], [])
         | 
| 53 | 
            +
                    end
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
                  self.class.new(p)
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                # Fetch the player associated with a seed object
         | 
| 59 | 
            +
                #
         | 
| 60 | 
            +
                # @param [#rating,#rating_deviation,#volatility] obj seed object
         | 
| 61 | 
            +
                # @return [Player]
         | 
| 62 | 
            +
                def player(obj)
         | 
| 63 | 
            +
                  @cache[obj]
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                def to_s
         | 
| 67 | 
            +
                  "#<RatingPeriod players=#{@players}"
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
              end
         | 
| 70 | 
            +
            end
         | 
| @@ -0,0 +1,98 @@ | |
| 1 | 
            +
            require 'minitest_helper'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            describe Glicko2::NormalDistribution do
         | 
| 4 | 
            +
              describe "#variance" do
         | 
| 5 | 
            +
                it "must return the square of the standard deviation" do
         | 
| 6 | 
            +
                  Glicko2::NormalDistribution.new(1.0, 1.0).variance.must_equal 1.0 ** 2.0
         | 
| 7 | 
            +
                  Glicko2::NormalDistribution.new(1.0, 2.0).variance.must_equal 2.0 ** 2.0
         | 
| 8 | 
            +
                  Glicko2::NormalDistribution.new(1.0, 10.0).variance.must_equal 10.0 ** 2.0
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
              end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              describe "#+" do
         | 
| 13 | 
            +
                let(:dist1) { Glicko2::NormalDistribution.new(10.0, 0.5) }
         | 
| 14 | 
            +
                let(:dist2) { Glicko2::NormalDistribution.new(5.0, 1.0) }
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                it "must sum the means" do
         | 
| 17 | 
            +
                  (dist1 + dist2).mean.must_equal 15.0
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                it "must sqrt the sum of the variances" do
         | 
| 21 | 
            +
                  (dist1 + dist2).sd.must_equal Math.sqrt(0.5 ** 2.0 + 1.0 ** 2.0)
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
              describe "#-" do
         | 
| 26 | 
            +
                let(:dist1) { Glicko2::NormalDistribution.new(10.0, 0.5) }
         | 
| 27 | 
            +
                let(:dist2) { Glicko2::NormalDistribution.new(5.0, 1.0) }
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                it "must subtract the means" do
         | 
| 30 | 
            +
                  (dist1 - dist2).mean.must_equal 5.0
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                it "must sqrt the sum of the variances" do
         | 
| 34 | 
            +
                  (dist1 - dist2).sd.must_equal Math.sqrt(0.5 ** 2.0 + 1.0 ** 2.0)
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
              end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
              describe "#pdf" do
         | 
| 39 | 
            +
                describe "standard normal" do
         | 
| 40 | 
            +
                  let(:dist) { Glicko2::NormalDistribution.new(0.0, 1.0) }
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  it "must calculate PDF at x" do
         | 
| 43 | 
            +
                    dist.pdf(-5.0).must_be_close_to 0.00000149, 0.00000001
         | 
| 44 | 
            +
                    dist.pdf(-2.5).must_be_close_to 0.01752830, 0.00000001
         | 
| 45 | 
            +
                    dist.pdf(-1.0).must_be_close_to 0.24197072, 0.00000001
         | 
| 46 | 
            +
                    dist.pdf(0.0).must_be_close_to 0.39894228, 0.00000001
         | 
| 47 | 
            +
                    dist.pdf(1.0).must_be_close_to 0.24197072, 0.00000001
         | 
| 48 | 
            +
                    dist.pdf(2.5).must_be_close_to 0.01752830, 0.00000001
         | 
| 49 | 
            +
                    dist.pdf(5.0).must_be_close_to 0.00000149, 0.00000001
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
              end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
              describe "#cdf" do
         | 
| 55 | 
            +
                describe "standard normal" do
         | 
| 56 | 
            +
                  let(:dist) { Glicko2::NormalDistribution.new(0.0, 1.0) }
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  it "must calculate CDF at x" do
         | 
| 59 | 
            +
                    dist.cdf(-5.0).must_be_close_to 0.00000029, 0.00000001
         | 
| 60 | 
            +
                    dist.cdf(-2.5).must_be_close_to 0.00620967, 0.00000001
         | 
| 61 | 
            +
                    dist.cdf(-1.0).must_be_close_to 0.15865525, 0.00000001
         | 
| 62 | 
            +
                    dist.cdf(0.0).must_be_close_to 0.50000000, 0.00000001
         | 
| 63 | 
            +
                    dist.cdf(1.0).must_be_close_to 0.84134475, 0.00000001
         | 
| 64 | 
            +
                    dist.cdf(2.5).must_be_close_to 0.99379033, 0.00000001
         | 
| 65 | 
            +
                    dist.cdf(5.0).must_be_close_to 0.99999971, 0.00000001
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
              end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
              describe "#<=>" do
         | 
| 71 | 
            +
                let(:dist1) { Glicko2::NormalDistribution.new(10.0, 0.5) }
         | 
| 72 | 
            +
                let(:dist2) { Glicko2::NormalDistribution.new(5.0, 1.0) }
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                it "must compare the same mean" do
         | 
| 75 | 
            +
                  (dist1 <=> dist1).must_equal 0
         | 
| 76 | 
            +
                  (dist2 <=> dist2).must_equal 0
         | 
| 77 | 
            +
                end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                it "must compare against smaller mean" do
         | 
| 80 | 
            +
                  (dist1 <=> dist2).must_equal 1
         | 
| 81 | 
            +
                end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                it "must compare against larger mean" do
         | 
| 84 | 
            +
                  (dist2 <=> dist1).must_equal -1
         | 
| 85 | 
            +
                end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                describe "Comparable" do
         | 
| 88 | 
            +
                  it "must compare" do
         | 
| 89 | 
            +
                    (dist1 == dist1).must_equal true
         | 
| 90 | 
            +
                    (dist2 == dist2).must_equal true
         | 
| 91 | 
            +
                    (dist1 > dist2).must_equal true
         | 
| 92 | 
            +
                    (dist1 < dist2).must_equal false
         | 
| 93 | 
            +
                    (dist2 > dist1).must_equal false
         | 
| 94 | 
            +
                    (dist2 < dist1).must_equal true
         | 
| 95 | 
            +
                  end
         | 
| 96 | 
            +
                end
         | 
| 97 | 
            +
              end
         | 
| 98 | 
            +
            end
         | 
    
        data/spec/player_spec.rb
    ADDED
    
    | @@ -0,0 +1,76 @@ | |
| 1 | 
            +
            require 'minitest_helper'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            describe Glicko2::Player do
         | 
| 4 | 
            +
              before do
         | 
| 5 | 
            +
                @player = Glicko2::Player.from_obj(Rating.new(1500, 200, 0.06))
         | 
| 6 | 
            +
                @player1 = Glicko2::Player.from_obj(Rating.new(1400, 30, 0.06))
         | 
| 7 | 
            +
                @player2 = Glicko2::Player.from_obj(Rating.new(1550, 100, 0.06))
         | 
| 8 | 
            +
                @player3 = Glicko2::Player.from_obj(Rating.new(1700, 300, 0.06))
         | 
| 9 | 
            +
                @others = [@player1, @player2, @player3]
         | 
| 10 | 
            +
                @scores = [1, 0, 0]
         | 
| 11 | 
            +
              end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
              describe ".from_obj" do
         | 
| 14 | 
            +
                it "must create player from an object as example" do
         | 
| 15 | 
            +
                  @player.mean.must_be_close_to 0, 0.0001
         | 
| 16 | 
            +
                  @player.sd.must_be_close_to 1.1513, 0.0001
         | 
| 17 | 
            +
                  @player.volatility.must_equal 0.06
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                it "must create player from an object as example 1" do
         | 
| 21 | 
            +
                  @player1.mean.must_be_close_to -0.5756, 0.0001
         | 
| 22 | 
            +
                  @player1.sd.must_be_close_to 0.1727, 0.0001
         | 
| 23 | 
            +
                  @player1.volatility.must_equal 0.06
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                it "must create player from an object as example 2" do
         | 
| 27 | 
            +
                  @player2.mean.must_be_close_to 0.2878, 0.0001
         | 
| 28 | 
            +
                  @player2.sd.must_be_close_to 0.5756, 0.0001
         | 
| 29 | 
            +
                  @player2.volatility.must_equal 0.06
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                it "must create player from an object as example 3" do
         | 
| 33 | 
            +
                  @player3.mean.must_be_close_to 1.1513, 0.0001
         | 
| 34 | 
            +
                  @player3.sd.must_be_close_to 1.7269, 0.0001
         | 
| 35 | 
            +
                  @player3.volatility.must_equal 0.06
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
              end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
              describe "#generate_next" do
         | 
| 40 | 
            +
                it "must be close to example" do
         | 
| 41 | 
            +
                  p = @player.generate_next(@others, @scores)
         | 
| 42 | 
            +
                  p.mean.must_be_close_to -0.2069, 0.0001
         | 
| 43 | 
            +
                  p.sd.must_be_close_to 0.8722, 0.0001
         | 
| 44 | 
            +
                  p.volatility.must_be_close_to 0.05999, 0.00001
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                it "must allow players that did not play and games" do
         | 
| 48 | 
            +
                  p = @player.generate_next([], [])
         | 
| 49 | 
            +
                  p.mean.must_equal @player.mean
         | 
| 50 | 
            +
                  p.volatility.must_equal @player.volatility
         | 
| 51 | 
            +
                  p.sd.must_be_close_to Math.sqrt(@player.sd ** 2 + @player.volatility ** 2)
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                it "must not decay rating deviation above default" do
         | 
| 55 | 
            +
                  @player = Glicko2::Player.from_obj(Rating.new(1500, Glicko2::DEFAULT_GLICKO_RATING_DEVIATION, 0.06))
         | 
| 56 | 
            +
                  p = @player.generate_next([], [])
         | 
| 57 | 
            +
                  p.update_obj
         | 
| 58 | 
            +
                  p.obj.rating_deviation.must_equal Glicko2::DEFAULT_GLICKO_RATING_DEVIATION
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                bench_performance_linear "default" do |n|
         | 
| 62 | 
            +
                  @player.generate_next(@others * n, @scores * n)
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
              end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
              describe "#update_obj" do
         | 
| 67 | 
            +
                it "must update object to be close to example" do
         | 
| 68 | 
            +
                  p = @player.generate_next(@others, @scores)
         | 
| 69 | 
            +
                  p.update_obj
         | 
| 70 | 
            +
                  obj = p.obj
         | 
| 71 | 
            +
                  obj.rating.must_be_close_to 1464.06, 0.01
         | 
| 72 | 
            +
                  obj.rating_deviation.must_be_close_to 151.52, 0.01
         | 
| 73 | 
            +
                  obj.volatility.must_be_close_to 0.05999, 0.00001
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
              end
         | 
| 76 | 
            +
            end
         | 
| @@ -0,0 +1,45 @@ | |
| 1 | 
            +
            require 'minitest_helper'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            describe Glicko2::RatingPeriod do
         | 
| 4 | 
            +
              before do
         | 
| 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 | 
            +
                @players = [@player, @player1, @player2, @player3]
         | 
| 10 | 
            +
                @period = Glicko2::RatingPeriod.from_objs(@players)
         | 
| 11 | 
            +
              end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
              describe "#initialize" do
         | 
| 14 | 
            +
                it "must raise if two players are identical" do
         | 
| 15 | 
            +
                  proc {
         | 
| 16 | 
            +
                    Glicko2::RatingPeriod.from_objs([@player, @player])
         | 
| 17 | 
            +
                  }.must_raise Glicko2::DuplicatePlayerError
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
              end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
              describe "#generate_next" do
         | 
| 22 | 
            +
                it "must be close to example" do
         | 
| 23 | 
            +
                  @period.game([@player, @player1], [1, 2])
         | 
| 24 | 
            +
                  @period.game([@player, @player2], [2, 1])
         | 
| 25 | 
            +
                  @period.game([@player, @player3], [2, 1])
         | 
| 26 | 
            +
                  @period.generate_next.players.each { |p| p.update_obj }
         | 
| 27 | 
            +
                  obj = @player
         | 
| 28 | 
            +
                  obj.rating.must_be_close_to 1464.06, 0.01
         | 
| 29 | 
            +
                  obj.rating_deviation.must_be_close_to 151.52, 0.01
         | 
| 30 | 
            +
                  obj.volatility.must_be_close_to 0.05999, 0.00001
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                it "must process non-competing players" do
         | 
| 34 | 
            +
                  @period.game([@player, @player1], [1, 2])
         | 
| 35 | 
            +
                  @period.generate_next
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                bench_performance_linear "default" do |n|
         | 
| 39 | 
            +
                  n.times do
         | 
| 40 | 
            +
                    @period.game(@players.sample(2), [1, 2])
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
                  @period.generate_next
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
              end
         | 
| 45 | 
            +
            end
         | 
    
        data/spec/rating_spec.rb
    ADDED
    
    | @@ -0,0 +1,53 @@ | |
| 1 | 
            +
            require 'minitest_helper'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            describe Glicko2::Rating do
         | 
| 4 | 
            +
              before do
         | 
| 5 | 
            +
                @rating = Glicko2::Rating.from_glicko_rating(1500, 200)
         | 
| 6 | 
            +
                @rating1 = Glicko2::Rating.from_glicko_rating(1400, 30)
         | 
| 7 | 
            +
                @rating2 = Glicko2::Rating.from_glicko_rating(1550, 100)
         | 
| 8 | 
            +
                @rating3 = Glicko2::Rating.from_glicko_rating(1700, 300)
         | 
| 9 | 
            +
                @others = [@rating1, @rating2, @rating3]
         | 
| 10 | 
            +
                @scores = [1, 0, 0]
         | 
| 11 | 
            +
              end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
              describe "#gravity" do
         | 
| 14 | 
            +
                it "must be close to example 1" do
         | 
| 15 | 
            +
                  @rating1.gravity.must_be_close_to 0.9955, 0.0001
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                it "must be close to example 2" do
         | 
| 19 | 
            +
                  @rating2.gravity.must_be_close_to 0.9531, 0.0001
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                it "must be close to example 3" do
         | 
| 23 | 
            +
                  @rating3.gravity.must_be_close_to 0.7242, 0.0001
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
              describe "#expected_fractional_score" do
         | 
| 28 | 
            +
                it "must be close to example 1" do
         | 
| 29 | 
            +
                  @rating.expected_fractional_score(@rating1).must_be_close_to 0.639
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                it "must be close to example 2" do
         | 
| 33 | 
            +
                  @rating.expected_fractional_score(@rating2).must_be_close_to 0.432
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                it "must be close to example 3" do
         | 
| 37 | 
            +
                  @rating.expected_fractional_score(@rating3).must_be_close_to 0.303
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
              end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
              describe "#estimated_variance" do
         | 
| 42 | 
            +
                it "must be close to example" do
         | 
| 43 | 
            +
                  @rating.estimated_variance(@others).must_be_close_to 1.7785
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
              end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
              describe "#delta" do
         | 
| 48 | 
            +
                it "must be close to example" do
         | 
| 49 | 
            +
                  @rating.delta(@others, @scores).must_be_close_to -0.4834
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
              end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
            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
    ADDED
    
    | @@ -0,0 +1,111 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: compact-glicko2
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: 0.1.3
         | 
| 5 | 
            +
            platform: ruby
         | 
| 6 | 
            +
            authors:
         | 
| 7 | 
            +
            - James Fargher
         | 
| 8 | 
            +
            - Andrej Antas
         | 
| 9 | 
            +
            autorequire: 
         | 
| 10 | 
            +
            bindir: bin
         | 
| 11 | 
            +
            cert_chain: []
         | 
| 12 | 
            +
            date: 2015-08-20 00:00:00.000000000 Z
         | 
| 13 | 
            +
            dependencies:
         | 
| 14 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 15 | 
            +
              name: bundler
         | 
| 16 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 17 | 
            +
                requirements:
         | 
| 18 | 
            +
                - - "~>"
         | 
| 19 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 20 | 
            +
                    version: '1.3'
         | 
| 21 | 
            +
              type: :development
         | 
| 22 | 
            +
              prerelease: false
         | 
| 23 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 24 | 
            +
                requirements:
         | 
| 25 | 
            +
                - - "~>"
         | 
| 26 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 27 | 
            +
                    version: '1.3'
         | 
| 28 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 29 | 
            +
              name: rake
         | 
| 30 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 31 | 
            +
                requirements:
         | 
| 32 | 
            +
                - - ">="
         | 
| 33 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 34 | 
            +
                    version: '0'
         | 
| 35 | 
            +
              type: :development
         | 
| 36 | 
            +
              prerelease: false
         | 
| 37 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 38 | 
            +
                requirements:
         | 
| 39 | 
            +
                - - ">="
         | 
| 40 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 41 | 
            +
                    version: '0'
         | 
| 42 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 43 | 
            +
              name: minitest
         | 
| 44 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 45 | 
            +
                requirements:
         | 
| 46 | 
            +
                - - "~>"
         | 
| 47 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 48 | 
            +
                    version: 4.7.5
         | 
| 49 | 
            +
              type: :development
         | 
| 50 | 
            +
              prerelease: false
         | 
| 51 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 52 | 
            +
                requirements:
         | 
| 53 | 
            +
                - - "~>"
         | 
| 54 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 55 | 
            +
                    version: 4.7.5
         | 
| 56 | 
            +
            description: Implementation of Glicko2 ratings with compact rule
         | 
| 57 | 
            +
            email:
         | 
| 58 | 
            +
            - proglottis@gmail.com
         | 
| 59 | 
            +
            - andrej@antas.cz
         | 
| 60 | 
            +
            executables: []
         | 
| 61 | 
            +
            extensions: []
         | 
| 62 | 
            +
            extra_rdoc_files: []
         | 
| 63 | 
            +
            files:
         | 
| 64 | 
            +
            - ".gitignore"
         | 
| 65 | 
            +
            - Gemfile
         | 
| 66 | 
            +
            - LICENSE.txt
         | 
| 67 | 
            +
            - README.md
         | 
| 68 | 
            +
            - Rakefile
         | 
| 69 | 
            +
            - glicko2.gemspec
         | 
| 70 | 
            +
            - lib/glicko2.rb
         | 
| 71 | 
            +
            - lib/glicko2/normal_distribution.rb
         | 
| 72 | 
            +
            - lib/glicko2/player.rb
         | 
| 73 | 
            +
            - lib/glicko2/rating.rb
         | 
| 74 | 
            +
            - lib/glicko2/rating_period.rb
         | 
| 75 | 
            +
            - lib/glicko2/version.rb
         | 
| 76 | 
            +
            - spec/minitest_helper.rb
         | 
| 77 | 
            +
            - spec/normal_distribution_spec.rb
         | 
| 78 | 
            +
            - spec/player_spec.rb
         | 
| 79 | 
            +
            - spec/rating_period_spec.rb
         | 
| 80 | 
            +
            - spec/rating_spec.rb
         | 
| 81 | 
            +
            - spec/util_spec.rb
         | 
| 82 | 
            +
            homepage: https://github.com/redrick/glicko2
         | 
| 83 | 
            +
            licenses: []
         | 
| 84 | 
            +
            metadata: {}
         | 
| 85 | 
            +
            post_install_message: 
         | 
| 86 | 
            +
            rdoc_options: []
         | 
| 87 | 
            +
            require_paths:
         | 
| 88 | 
            +
            - lib
         | 
| 89 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 90 | 
            +
              requirements:
         | 
| 91 | 
            +
              - - ">="
         | 
| 92 | 
            +
                - !ruby/object:Gem::Version
         | 
| 93 | 
            +
                  version: '0'
         | 
| 94 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 95 | 
            +
              requirements:
         | 
| 96 | 
            +
              - - ">="
         | 
| 97 | 
            +
                - !ruby/object:Gem::Version
         | 
| 98 | 
            +
                  version: '0'
         | 
| 99 | 
            +
            requirements: []
         | 
| 100 | 
            +
            rubyforge_project: 
         | 
| 101 | 
            +
            rubygems_version: 2.2.2
         | 
| 102 | 
            +
            signing_key: 
         | 
| 103 | 
            +
            specification_version: 4
         | 
| 104 | 
            +
            summary: Implementation of Glicko2 ratings with compact rule
         | 
| 105 | 
            +
            test_files:
         | 
| 106 | 
            +
            - spec/minitest_helper.rb
         | 
| 107 | 
            +
            - spec/normal_distribution_spec.rb
         | 
| 108 | 
            +
            - spec/player_spec.rb
         | 
| 109 | 
            +
            - spec/rating_period_spec.rb
         | 
| 110 | 
            +
            - spec/rating_spec.rb
         | 
| 111 | 
            +
            - spec/util_spec.rb
         |