glicko2 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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