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.
@@ -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
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .ruby-version
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in glicko2.gemspec
4
+ gemspec
@@ -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.
@@ -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
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ task :default => :test
5
+
6
+ Rake::TestTask.new do |t|
7
+ t.libs << 'spec'
8
+ t.pattern = "spec/*_spec.rb"
9
+ end
@@ -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
@@ -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,3 @@
1
+ module Glicko2
2
+ VERSION = "0.1.3"
3
+ end
@@ -0,0 +1,5 @@
1
+ require 'minitest/benchmark'
2
+ require 'minitest/autorun'
3
+ require 'glicko2'
4
+
5
+ Rating = Struct.new(:rating, :rating_deviation, :volatility)
@@ -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
@@ -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
@@ -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
@@ -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