compact-glicko2 0.1.3

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