glicko2 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3264135c077789c8ce7d6f49a216c3ac50dafeb4
4
- data.tar.gz: c621d811f861685e08a85c60d5252e4868231842
3
+ metadata.gz: 5b475b65a53c6af5c404f010b22a616f3f645617
4
+ data.tar.gz: d837d51c7cbc959b810140ea1139d4c654109a22
5
5
  SHA512:
6
- metadata.gz: 8d34d1bab836561f0fefb5f58807387af31d7d21adfd31344b547b8d804f75ba16c8ffd5a45d6982a620aca49c9e8374fb5ed93744b9050dea158536c4be0b61
7
- data.tar.gz: 27fbeaf711881f18500a5277f2a9571d71bad500976949f73e873c0196cd29347723b382597c8d54e5961ff2751128f6591babb088a1efa60bcdb542da65468a
6
+ metadata.gz: 560eba41bcbcc66ee5036a91be810b3916575c08d9ee5f03fa22514f42e779932ad3c41ecaa6b073f626eae8c36a8fdc07da9dc360a71797ec4ac8845dca2063
7
+ data.tar.gz: bf6ea3797d64d7735917ae1d25de400db48222ef581d40358e0e921138298edad882d79cc8d358445a7287d9e08b692f7fe3ab287253a3ee9b2b5c78095ed75b
data/README.md CHANGED
@@ -35,7 +35,7 @@ period = Glicko2::RatingPeriod.from_objs [rating1, rating2]
35
35
  period.game([rating1, rating2], [1,2])
36
36
 
37
37
  # Generate the next rating period with updated players
38
- next_period = period.generate_next
38
+ next_period = period.generate_next(0.5)
39
39
 
40
40
  # Update all Glicko ratings
41
41
  next_period.players.each { |p| p.update_obj }
@@ -1,4 +1,4 @@
1
- # -*- encoding: utf-8 -*-
1
+ # coding: utf-8
2
2
  lib = File.expand_path('../lib', __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'glicko2/version'
@@ -17,5 +17,7 @@ Gem::Specification.new do |gem|
17
17
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
18
  gem.require_paths = ["lib"]
19
19
 
20
+ gem.add_development_dependency('bundler')
21
+ gem.add_development_dependency('rake')
20
22
  gem.add_development_dependency('minitest')
21
23
  end
@@ -2,38 +2,15 @@ module Glicko2
2
2
  DEFAULT_VOLATILITY = 0.06
3
3
  DEFAULT_GLICKO_RATING = 1500.0
4
4
  DEFAULT_GLICKO_RATING_DEVIATION = 350.0
5
- GLICKO_GRADIENT = 173.7178
6
5
 
7
- DEFAULT_CONFIG = {:volatility_change => 0.5}.freeze
6
+ TOLERANCE = 5.0e-15
7
+
8
+ class DuplicatePlayerError < StandardError; end
8
9
 
9
10
  # Collection of helper methods
10
11
  class Util
11
- GLICKO_INTERCEPT = DEFAULT_GLICKO_RATING
12
-
13
- # Convert from the original Glicko scale to Glicko2 scale
14
- #
15
- # @param [Numeric] r Glicko rating
16
- # @param [Numeric] rd Glicko rating deviation
17
- # @return [Array<Numeric>]
18
- def self.to_glicko2(r, rd)
19
- [(r - GLICKO_INTERCEPT) / GLICKO_GRADIENT, rd / GLICKO_GRADIENT]
20
- end
21
-
22
- # Convert from the Glicko2 scale to the original Glicko scale
23
- #
24
- # @param [Numeric] m Glicko2 mean
25
- # @param [Numeric] sd Glicko2 standard deviation
26
- # @return [Array<Numeric>]
27
- def self.to_glicko(m, sd)
28
- [GLICKO_GRADIENT * m + GLICKO_INTERCEPT, GLICKO_GRADIENT * sd]
29
- end
30
-
31
12
  # Convert from a rank, where lower numbers win against higher numbers,
32
13
  # into Glicko scores where wins are `1`, draws are `0.5` and losses are `0`.
33
- #
34
- # @param [Integer] rank players rank
35
- # @param [Integer] other opponents rank
36
- # @return [Numeric] Glicko score
37
14
  def self.ranks_to_score(rank, other)
38
15
  if rank < other
39
16
  1.0
@@ -43,10 +20,30 @@ module Glicko2
43
20
  0.0
44
21
  end
45
22
  end
23
+
24
+ # Illinois method is a version of the regula falsi method for solving an equation with one unknown
25
+ def self.illinois_method(a, b)
26
+ fa = yield a
27
+ fb = yield b
28
+ while (b - a).abs > TOLERANCE
29
+ c = a + (a - b) * fa / (fb - fa)
30
+ fc = yield c
31
+ if fc * fb < 0
32
+ a = b
33
+ fa = fb
34
+ else
35
+ fa /= 2.0
36
+ end
37
+ b = c
38
+ fb = fc
39
+ end
40
+ a
41
+ end
46
42
  end
47
43
  end
48
44
 
49
45
  require "glicko2/version"
50
- require "glicko2/normal_distribution"
46
+ require "glicko2/rating"
51
47
  require "glicko2/player"
48
+ require "glicko2/rater"
52
49
  require "glicko2/rating_period"
@@ -1,190 +1,45 @@
1
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 < NormalDistribution
26
- TOLERANCE = 0.0000001
27
- MIN_SD = DEFAULT_GLICKO_RATING_DEVIATION / GLICKO_GRADIENT
28
-
29
- attr_reader :volatility, :obj
2
+ # Player maps a seed object with a Glicko2 rating.
3
+ class Player
4
+ attr_reader :rating, :obj
30
5
 
31
6
  # Create a {Player} from a seed object, converting from Glicko
32
7
  # ratings to Glicko2.
33
8
  #
34
9
  # @param [#rating,#rating_deviation,#volatility] obj seed values object
35
10
  # @return [Player] constructed instance.
36
- def self.from_obj(obj, config=DEFAULT_CONFIG)
37
- mean, sd = Util.to_glicko2(obj.rating, obj.rating_deviation)
38
- new(mean, sd, obj.volatility, obj, config)
11
+ def self.from_obj(obj)
12
+ rating = Rating.from_glicko_rating(obj.rating, obj.rating_deviation, obj.volatility)
13
+ new(rating, obj)
39
14
  end
40
15
 
41
- # @param [Numeric] mean player mean
42
- # @param [Numeric] sd player standard deviation
43
- # @param [Numeric] volatility player volatility
44
- # @param [#rating,#rating_deviation,#volatility] obj seed values object
45
- def initialize(mean, sd, volatility, obj=nil, config=DEFAULT_CONFIG)
46
- super(mean, sd)
47
- @volatility = volatility
16
+ def initialize(rating, obj)
17
+ @rating = rating
48
18
  @obj = obj
49
- @config = config
50
- @e = {}
51
- end
52
-
53
- # Calculate `g(phi)` as defined in the Glicko2 paper
54
- #
55
- # @return [Numeric]
56
- def g
57
- @g ||= 1 / Math.sqrt(1 + 3 * variance / Math::PI ** 2)
58
- end
59
-
60
- # Calculate `E(mu, mu_j, phi_j)` as defined in the Glicko2 paper
61
- #
62
- # @param [Player] other the `j` player
63
- # @return [Numeric]
64
- def e(other)
65
- @e[other] ||= 1 / (1 + Math.exp(-other.g * (mean - other.mean)))
66
- end
67
-
68
- # Calculate the estimated variance of the team's/player's rating based only
69
- # on the game outcomes.
70
- #
71
- # @param [Array<Player>] others other participating players.
72
- # @return [Numeric]
73
- def estimated_variance(others)
74
- return 0.0 if others.length < 1
75
- others.reduce(0) do |v, other|
76
- e_other = e(other)
77
- v + other.g ** 2 * e_other * (1 - e_other)
78
- end ** -1
79
- end
80
-
81
- # Calculate the estimated improvement in rating by comparing the
82
- # pre-period rating to the performance rating based only on game outcomes.
83
- #
84
- # @param [Array<Player>] others list of opponent players
85
- # @param [Array<Numeric>] scores list of correlating scores (`0` for a loss,
86
- # `0.5` for a draw and `1` for a win).
87
- # @return [Numeric]
88
- def delta(others, scores)
89
- others.zip(scores).reduce(0) do |d, (other, score)|
90
- d + other.g * (score - e(other))
91
- end * estimated_variance(others)
92
- end
93
-
94
- # Calculate `f(x)` as defined in the Glicko2 paper
95
- #
96
- # @param [Numeric] x
97
- # @param [Numeric] d the result of calculating {#delta}
98
- # @param [Numeric] v the result of calculating {#estimated_variance}
99
- # @return [Numeric]
100
- def f(x, d, v)
101
- f_part1(x, d, v) - f_part2(x)
102
- end
103
-
104
- # Calculate the new value of the volatility
105
- #
106
- # @param [Numeric] d the result of calculating {#delta}
107
- # @param [Numeric] v the result of calculating {#estimated_variance}
108
- # @return [Numeric]
109
- def volatility1(d, v)
110
- a = Math::log(volatility ** 2)
111
- if d > variance + v
112
- b = Math.log(d - variance - v)
113
- else
114
- k = 1
115
- k += 1 while f(a - k * @config[:volatility_change], d, v) < 0
116
- b = a - k * @config[:volatility_change]
117
- end
118
- fa = f(a, d, v)
119
- fb = f(b, d, v)
120
- while (b - a).abs > TOLERANCE
121
- c = a + (a - b) * fa / (fb - fa)
122
- fc = f(c, d, v)
123
- if fc * fb < 0
124
- a = b
125
- fa = fb
126
- else
127
- fa /= 2.0
128
- end
129
- b = c
130
- fb = fc
131
- end
132
- Math.exp(a / 2.0)
133
- end
134
-
135
- # Create new {Player} with updated values.
136
- #
137
- # This method will not modify any objects that are passed into it.
138
- #
139
- # @param [Array<Player>] others list of opponent players
140
- # @param [Array<Numeric>] scores list of correlating scores (`0` for a loss,
141
- # `0.5` for a draw and `1` for a win).
142
- # @return [Player]
143
- def generate_next(others, scores)
144
- if others.length < 1
145
- generate_next_without_games
146
- else
147
- generate_next_with_games(others, scores)
148
- end
149
19
  end
150
20
 
151
21
  # Update seed object with this player's values
152
22
  def update_obj
153
- @obj.rating, @obj.rating_deviation = Util.to_glicko(mean, sd)
23
+ mean, sd = rating.to_glicko_rating
24
+ @obj.rating = mean
25
+ @obj.rating_deviation = sd
154
26
  @obj.volatility = volatility
155
27
  end
156
28
 
157
- def to_s
158
- "#<Player mean=#{mean}, sd=#{sd}, volatility=#{volatility}, obj=#{obj}>"
29
+ def mean
30
+ rating.mean
159
31
  end
160
32
 
161
- private
162
-
163
- def generate_next_without_games
164
- sd_pre = [Math.sqrt(variance + volatility ** 2), MIN_SD].min
165
- self.class.new(mean, sd_pre, volatility, obj)
33
+ def sd
34
+ rating.sd
166
35
  end
167
36
 
168
- def generate_next_with_games(others, scores)
169
- _v = estimated_variance(others)
170
- _d = delta(others, scores)
171
- _volatility = volatility1(_d, _v)
172
- sd_pre = [Math.sqrt(variance + volatility ** 2), MIN_SD].min
173
- _sd = 1 / Math.sqrt(1 / sd_pre ** 2 + 1 / _v)
174
- _mean = mean + _sd ** 2 * others.zip(scores).reduce(0) {
175
- |x, (other, score)| x + other.g * (score - e(other))
176
- }
177
- self.class.new(_mean, _sd, _volatility, obj)
37
+ def volatility
38
+ rating.volatility
178
39
  end
179
40
 
180
- def f_part1(x, d, v)
181
- exp_x = Math.exp(x)
182
- sd_sq = variance
183
- (exp_x * (d ** 2 - sd_sq - v - exp_x)) / (2 * (sd_sq + v + exp_x) ** 2)
184
- end
185
-
186
- def f_part2(x)
187
- (x - Math::log(volatility ** 2)) / @config[:volatility_change] ** 2
41
+ def to_s
42
+ "#<Player rating=#{rating}, obj=#{obj}>"
188
43
  end
189
44
  end
190
45
  end
@@ -0,0 +1,44 @@
1
+ module Glicko2
2
+ # A Rater calculates a new Rating based on actual game outcomes.
3
+ class Rater
4
+ attr_reader :rating
5
+
6
+ def initialize(rating)
7
+ @rating = rating
8
+ @v_pre = 0.0
9
+ @delta_pre = 0.0
10
+ end
11
+
12
+ # Add an outcome against the rating
13
+ def add(other_rating, score)
14
+ g, e = other_rating.gravity_expected_score(rating.mean)
15
+ @v_pre += g**2 * e * (1 - e)
16
+ @delta_pre += g * (score - e)
17
+ end
18
+
19
+ # Rate calculates Rating as at the start of the following period based on game outcomes
20
+ def rate(tau)
21
+ v = @v_pre**-1
22
+ delta2 = @delta_pre**2
23
+ sd2 = rating.sd**2
24
+ a = Math.log(rating.volatility**2)
25
+ f = lambda do |x|
26
+ expX = Math.exp(x)
27
+ (expX * (delta2 - sd2 - v - expX)) / (2 * (sd2 + v + expX)**2) - (x - a) / tau**2
28
+ end
29
+ if delta2 > sd2 + v
30
+ b = Math.log(delta2 - sd2 - v)
31
+ else
32
+ k = 1
33
+ k += 1 while f.call(a - k * tau) < 0
34
+ b = a - k * tau
35
+ end
36
+ a = Util.illinois_method(a, b, &f)
37
+ volatility = Math.exp(a / 2.0)
38
+ sd_pre = Math.sqrt(sd2 + volatility**2)
39
+ sd = 1 / Math.sqrt(1.0 / sd_pre**2 + 1 / v)
40
+ mean = rating.mean + sd**2 * @delta_pre
41
+ Rating.new(mean, sd, volatility)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,35 @@
1
+ module Glicko2
2
+ # A Rating of a player.
3
+ class Rating
4
+ GLICKO_GRADIENT = 173.7178
5
+ GLICKO_INTERCEPT = DEFAULT_GLICKO_RATING
6
+
7
+ attr_reader :mean, :sd, :volatility
8
+
9
+ def initialize(mean, sd, volatility=nil)
10
+ @mean = mean
11
+ @sd = sd
12
+ @volatility = volatility || DEFAULT_VOLATILITY
13
+ end
14
+
15
+ # Creates a Rating from the Glicko scale.
16
+ def self.from_glicko_rating(r, rd, volatility=nil)
17
+ new((r - GLICKO_INTERCEPT) / GLICKO_GRADIENT, rd / GLICKO_GRADIENT, volatility)
18
+ end
19
+
20
+ # Converts to the Glicko scale.
21
+ def to_glicko_rating
22
+ [GLICKO_GRADIENT * mean + GLICKO_INTERCEPT, GLICKO_GRADIENT * sd]
23
+ end
24
+
25
+ # Calculate `g(phi)` and `E(mu, mu_j, phi_j)` as defined in the Glicko2 paper
26
+ def gravity_expected_score(other_mean)
27
+ g = 1 / Math.sqrt(1 + 3 * sd**2 / Math::PI**2)
28
+ [g, 1 / (1 + Math.exp(-g * (other_mean - mean)))]
29
+ end
30
+
31
+ def to_s
32
+ "#<Rating mean=#{mean}, sd=#{sd}, volatility=#{volatility}>"
33
+ end
34
+ end
35
+ end
@@ -9,16 +9,20 @@ module Glicko2
9
9
  # @param [Array<Player>] players
10
10
  def initialize(players)
11
11
  @players = players
12
- @games = Hash.new { |h, k| h[k] = [] }
13
- @cache = players.reduce({}) { |memo, player| memo[player.obj] = player; memo }
12
+ @cache = players.reduce({}) do |memo, player|
13
+ raise DuplicatePlayerError unless memo[player.obj].nil?
14
+ memo[player.obj] = player
15
+ memo
16
+ end
17
+ @raters = players.map { |p| Rater.new(p.rating) }
14
18
  end
15
19
 
16
20
  # Create rating period from list of seed objects
17
21
  #
18
22
  # @param [Array<#rating,#rating_deviation,#volatility>] objs seed value objects
19
23
  # @return [RatingPeriod]
20
- def self.from_objs(objs, config=DEFAULT_CONFIG)
21
- new(objs.map { |obj| Player.from_obj(obj, config) })
24
+ def self.from_objs(objs)
25
+ new(objs.map { |obj| Player.from_obj(obj) })
22
26
  end
23
27
 
24
28
  # Register a game with this rating period
@@ -26,11 +30,10 @@ module Glicko2
26
30
  # @param [Array<#rating,#rating_deviation,#volatility>] game_seeds ratings participating in a game
27
31
  # @param [Array<Integer>] ranks corresponding ranks
28
32
  def game(game_seeds, ranks)
29
- game_seeds.zip(ranks).each do |seed, rank|
30
- game_seeds.zip(ranks).each do |other, other_rank|
31
- next if seed == other
32
- @games[player(seed)] << [player(other),
33
- Util.ranks_to_score(rank, other_rank)]
33
+ game_seeds.each_with_index do |iseed, i|
34
+ game_seeds.each_with_index do |jseed, j|
35
+ next if i == j
36
+ @raters[i].add(player(jseed).rating, Util.ranks_to_score(ranks[i], ranks[j]))
34
37
  end
35
38
  end
36
39
  end
@@ -38,15 +41,10 @@ module Glicko2
38
41
  # Generate a new {RatingPeriod} with a new list of updated {Player}
39
42
  #
40
43
  # @return [RatingPeriod]
41
- def generate_next
44
+ def generate_next(tau)
42
45
  p = []
43
- @players.each do |player|
44
- games = @games[player]
45
- if games.length > 0
46
- p << player.generate_next(*games.transpose)
47
- else
48
- p << player.generate_next([], [])
49
- end
46
+ @raters.each_with_index do |rater, i|
47
+ p << Player.new(rater.rate(tau), @players[i].obj)
50
48
  end
51
49
  self.class.new(p)
52
50
  end
@@ -1,3 +1,3 @@
1
1
  module Glicko2
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -1,3 +1,4 @@
1
- require 'minitest/benchmark'
2
1
  require 'minitest/autorun'
3
2
  require 'glicko2'
3
+
4
+ Rating = Struct.new(:rating, :rating_deviation, :volatility)
@@ -1,7 +1,5 @@
1
1
  require 'minitest_helper'
2
2
 
3
- Rating = Struct.new(:rating, :rating_deviation, :volatility)
4
-
5
3
  describe Glicko2::Player do
6
4
  before do
7
5
  @player = Glicko2::Player.from_obj(Rating.new(1500, 200, 0.06))
@@ -20,7 +18,7 @@ describe Glicko2::Player do
20
18
  end
21
19
 
22
20
  it "must create player from an object as example 1" do
23
- @player1.mean.must_be_close_to -0.5756, 0.0001
21
+ @player1.mean.must_be_close_to(-0.5756, 0.0001)
24
22
  @player1.sd.must_be_close_to 0.1727, 0.0001
25
23
  @player1.volatility.must_equal 0.06
26
24
  end
@@ -37,82 +35,4 @@ describe Glicko2::Player do
37
35
  @player3.volatility.must_equal 0.06
38
36
  end
39
37
  end
40
-
41
- describe "#g" do
42
- it "must be close to example 1" do
43
- @player1.g.must_be_close_to 0.9955, 0.0001
44
- end
45
-
46
- it "must be close to example 2" do
47
- @player2.g.must_be_close_to 0.9531, 0.0001
48
- end
49
-
50
- it "must be close to example 3" do
51
- @player3.g.must_be_close_to 0.7242, 0.0001
52
- end
53
- end
54
-
55
- describe "#e" do
56
- it "must be close to example 1" do
57
- @player.e(@player1).must_be_close_to 0.639
58
- end
59
-
60
- it "must be close to example 2" do
61
- @player.e(@player2).must_be_close_to 0.432
62
- end
63
-
64
- it "must be close to example 3" do
65
- @player.e(@player3).must_be_close_to 0.303
66
- end
67
- end
68
-
69
- describe "#estimated_variance" do
70
- it "must be close to example" do
71
- @player.estimated_variance(@others).must_be_close_to 1.7785
72
- end
73
- end
74
-
75
- describe "#delta" do
76
- it "must be close to example" do
77
- @player.delta(@others, @scores).must_be_close_to -0.4834
78
- end
79
- end
80
-
81
- describe "#generate_next" do
82
- it "must be close to example" do
83
- p = @player.generate_next(@others, @scores)
84
- p.mean.must_be_close_to -0.2069, 0.0001
85
- p.sd.must_be_close_to 0.8722, 0.0001
86
- p.volatility.must_be_close_to 0.05999, 0.00001
87
- end
88
-
89
- it "must allow players that did not play and games" do
90
- p = @player.generate_next([], [])
91
- p.mean.must_equal @player.mean
92
- p.volatility.must_equal @player.volatility
93
- p.sd.must_be_close_to Math.sqrt(@player.sd ** 2 + @player.volatility ** 2)
94
- end
95
-
96
- it "must not decay rating deviation above default" do
97
- @player = Glicko2::Player.from_obj(Rating.new(1500, Glicko2::DEFAULT_GLICKO_RATING_DEVIATION, 0.06))
98
- p = @player.generate_next([], [])
99
- p.update_obj
100
- p.obj.rating_deviation.must_equal Glicko2::DEFAULT_GLICKO_RATING_DEVIATION
101
- end
102
-
103
- bench_performance_linear "default" do |n|
104
- @player.generate_next(@others * n, @scores * n)
105
- end
106
- end
107
-
108
- describe "#update_obj" do
109
- it "must update object to be close to example" do
110
- p = @player.generate_next(@others, @scores)
111
- p.update_obj
112
- obj = p.obj
113
- obj.rating.must_be_close_to 1464.06, 0.01
114
- obj.rating_deviation.must_be_close_to 151.52, 0.01
115
- obj.volatility.must_be_close_to 0.05999, 0.00001
116
- end
117
- end
118
38
  end
@@ -0,0 +1,22 @@
1
+ require 'minitest_helper'
2
+
3
+ class TestRater < Minitest::Test
4
+ def setup
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
+ @rater = Glicko2::Rater.new(@rating)
10
+ end
11
+
12
+ def test_rate_matches_example
13
+ @rater.add(@rating1, 1.0)
14
+ @rater.add(@rating2, 0.0)
15
+ @rater.add(@rating3, 0.0)
16
+ rating = @rater.rate(0.5)
17
+
18
+ assert_in_delta(-0.2069, rating.mean, 0.00005)
19
+ assert_in_delta(0.8722, rating.sd, 0.00005)
20
+ assert_in_delta(0.05999, rating.volatility, 0.00005)
21
+ end
22
+ end
@@ -10,12 +10,20 @@ describe Glicko2::RatingPeriod do
10
10
  @period = Glicko2::RatingPeriod.from_objs(@players)
11
11
  end
12
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
+
13
21
  describe "#generate_next" do
14
22
  it "must be close to example" do
15
23
  @period.game([@player, @player1], [1, 2])
16
24
  @period.game([@player, @player2], [2, 1])
17
25
  @period.game([@player, @player3], [2, 1])
18
- @period.generate_next.players.each { |p| p.update_obj }
26
+ @period.generate_next(0.5).players.each(&:update_obj)
19
27
  obj = @player
20
28
  obj.rating.must_be_close_to 1464.06, 0.01
21
29
  obj.rating_deviation.must_be_close_to 151.52, 0.01
@@ -24,14 +32,7 @@ describe Glicko2::RatingPeriod do
24
32
 
25
33
  it "must process non-competing players" do
26
34
  @period.game([@player, @player1], [1, 2])
27
- @period.generate_next
28
- end
29
-
30
- bench_performance_linear "default" do |n|
31
- n.times do
32
- @period.game(@players.sample(2), [1, 2])
33
- end
34
- @period.generate_next
35
+ @period.generate_next 0.5
35
36
  end
36
37
  end
37
38
  end
@@ -0,0 +1,30 @@
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
+ end
10
+
11
+ describe "#gravity_expected_score" do
12
+ it "must be close to example 1" do
13
+ g, e = @rating1.gravity_expected_score(@rating.mean)
14
+ g.must_be_close_to 0.9955, 0.0001
15
+ e.must_be_close_to 0.639, 0.001
16
+ end
17
+
18
+ it "must be close to example 2" do
19
+ g, e = @rating2.gravity_expected_score(@rating.mean)
20
+ g.must_be_close_to 0.9531, 0.0001
21
+ e.must_be_close_to 0.432, 0.001
22
+ end
23
+
24
+ it "must be close to example 3" do
25
+ g, e = @rating3.gravity_expected_score(@rating.mean)
26
+ g.must_be_close_to 0.7242, 0.0001
27
+ e.must_be_close_to 0.303, 0.001
28
+ end
29
+ end
30
+ end
@@ -1,17 +1,16 @@
1
1
  require 'minitest_helper'
2
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
3
+ class TestUtil < Minitest::Test
4
+ def test_ranks_to_score
5
+ assert_equal 1.0, Glicko2::Util.ranks_to_score(1, 2)
6
+ assert_equal 0.5, Glicko2::Util.ranks_to_score(1, 1)
7
+ assert_equal 0.0, Glicko2::Util.ranks_to_score(2, 1)
8
+ end
12
9
 
13
- it "must return 0.0 when rank is more" do
14
- Glicko2::Util.ranks_to_score(2, 1).must_equal 0.0
10
+ def test_illinois_method
11
+ result = Glicko2::Util.illinois_method(0.0, 1.0) do |x|
12
+ Math.cos(x) - x**3
15
13
  end
14
+ assert_in_delta 0.865474033101614, result, Glicko2::TOLERANCE
16
15
  end
17
16
  end
metadata CHANGED
@@ -1,27 +1,55 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: glicko2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Fargher
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-03-04 00:00:00.000000000 Z
11
+ date: 2017-02-09 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
13
41
  - !ruby/object:Gem::Dependency
14
42
  name: minitest
15
43
  requirement: !ruby/object:Gem::Requirement
16
44
  requirements:
17
- - - '>='
45
+ - - ">="
18
46
  - !ruby/object:Gem::Version
19
47
  version: '0'
20
48
  type: :development
21
49
  prerelease: false
22
50
  version_requirements: !ruby/object:Gem::Requirement
23
51
  requirements:
24
- - - '>='
52
+ - - ">="
25
53
  - !ruby/object:Gem::Version
26
54
  version: '0'
27
55
  description: Implementation of Glicko2 ratings
@@ -31,21 +59,23 @@ executables: []
31
59
  extensions: []
32
60
  extra_rdoc_files: []
33
61
  files:
34
- - .gitignore
62
+ - ".gitignore"
35
63
  - Gemfile
36
64
  - LICENSE.txt
37
65
  - README.md
38
66
  - Rakefile
39
67
  - glicko2.gemspec
40
68
  - lib/glicko2.rb
41
- - lib/glicko2/normal_distribution.rb
42
69
  - lib/glicko2/player.rb
70
+ - lib/glicko2/rater.rb
71
+ - lib/glicko2/rating.rb
43
72
  - lib/glicko2/rating_period.rb
44
73
  - lib/glicko2/version.rb
45
74
  - spec/minitest_helper.rb
46
- - spec/normal_distribution_spec.rb
47
75
  - spec/player_spec.rb
76
+ - spec/rater_spec.rb
48
77
  - spec/rating_period_spec.rb
78
+ - spec/rating_spec.rb
49
79
  - spec/util_spec.rb
50
80
  homepage: https://github.com/proglottis/glicko2
51
81
  licenses: []
@@ -56,24 +86,24 @@ require_paths:
56
86
  - lib
57
87
  required_ruby_version: !ruby/object:Gem::Requirement
58
88
  requirements:
59
- - - '>='
89
+ - - ">="
60
90
  - !ruby/object:Gem::Version
61
91
  version: '0'
62
92
  required_rubygems_version: !ruby/object:Gem::Requirement
63
93
  requirements:
64
- - - '>='
94
+ - - ">="
65
95
  - !ruby/object:Gem::Version
66
96
  version: '0'
67
97
  requirements: []
68
98
  rubyforge_project:
69
- rubygems_version: 2.0.0
99
+ rubygems_version: 2.4.5.1
70
100
  signing_key:
71
101
  specification_version: 4
72
102
  summary: Implementation of Glicko2 ratings
73
103
  test_files:
74
104
  - spec/minitest_helper.rb
75
- - spec/normal_distribution_spec.rb
76
105
  - spec/player_spec.rb
106
+ - spec/rater_spec.rb
77
107
  - spec/rating_period_spec.rb
108
+ - spec/rating_spec.rb
78
109
  - spec/util_spec.rb
79
- has_rdoc:
@@ -1,58 +0,0 @@
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
- attr_reader :mean, :standard_deviation
7
- alias_method :sd, :standard_deviation
8
-
9
- def initialize(mean, standard_deviation)
10
- @mean = mean
11
- @standard_deviation = standard_deviation
12
- end
13
-
14
- # Calculate the distribution variance
15
- #
16
- # @return [Numeric]
17
- def variance
18
- standard_deviation ** 2.0
19
- end
20
-
21
- # Calculate the sum
22
- #
23
- # @param [NormalDistribution] other
24
- # @return [NormalDistribution]
25
- def +(other)
26
- self.class.new(mean + other.mean, Math.sqrt(variance + other.variance))
27
- end
28
-
29
- # Calculate the difference
30
- #
31
- # @param [NormalDistribution] other
32
- # @return [NormalDistribution]
33
- def -(other)
34
- self.class.new(mean - other.mean, Math.sqrt(variance + other.variance))
35
- end
36
-
37
- # Calculate the probability density at `x`
38
- #
39
- # @param [Numeric] x
40
- # @return [Numeric]
41
- def pdf(x)
42
- 1.0 / (sd * Math.sqrt(2.0 * Math::PI)) *
43
- Math.exp(-(x - mean) ** 2.0 / 2.0 * variance)
44
- end
45
-
46
- # Calculate the cumulative distribution at `x`
47
- #
48
- # @param [Numeric] x
49
- # @return [Numeric]
50
- def cdf(x)
51
- 0.5 * (1.0 + Math.erf((x - mean) / (sd * Math.sqrt(2.0))))
52
- end
53
-
54
- def to_s
55
- "#<NormalDistribution mean=#{mean}, sd=#{sd}>"
56
- end
57
- end
58
- end
@@ -1,69 +0,0 @@
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
- end