glicko2 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ 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,58 @@
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
+ # Create players based on Glicko ratings
32
+ player1 = Glicko2::Player.from_obj(rating1)
33
+ player2 = Glicko2::Player.from_obj(rating2)
34
+
35
+ # Rating period with all participating players
36
+ period = Glicko2::RatingPeriod.new [player1, player2]
37
+
38
+ # Register a game in this rating period
39
+ period.game([player1, player2], [1,2])
40
+
41
+ # Generate the next rating period with updated players
42
+ next_period = period.generate_next
43
+
44
+ # Update all Glicko ratings
45
+ next_period.players.each { |p| p.update_obj }
46
+
47
+ # Output updated Glicko ratings
48
+ puts rating1
49
+ puts rating2
50
+ ```
51
+
52
+ ## Contributing
53
+
54
+ 1. Fork it
55
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
56
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
57
+ 4. Push to the branch (`git push origin my-new-feature`)
58
+ 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,21 @@
1
+ # -*- encoding: 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 = "glicko2"
8
+ gem.version = Glicko2::VERSION
9
+ gem.authors = ["James Fargher"]
10
+ gem.email = ["proglottis@gmail.com"]
11
+ gem.description = %q{Implementation of Glicko2 ratings}
12
+ gem.summary = %q{Implementation of Glicko2 ratings}
13
+ gem.homepage = "https://github.com/proglottis/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('minitest')
21
+ end
@@ -0,0 +1,153 @@
1
+ require "glicko2/version"
2
+
3
+ module Glicko2
4
+ TOLERANCE = 0.0000001
5
+ DEFAULT_VOLATILITY = 0.06
6
+ DEFAULT_GLICKO_RATING = 1500.0
7
+ DEFAULT_GLICKO_RATING_DEVIATION = 350.0
8
+
9
+ GLICKO_GRADIENT = 173.7178
10
+ GLICKO_INTERCEPT = DEFAULT_GLICKO_RATING
11
+
12
+ VOLATILITY_CHANGE = 0.5
13
+
14
+ class Player
15
+ attr_reader :mean, :sd, :volatility, :obj
16
+
17
+ def self.from_obj(obj)
18
+ mean = (obj.rating - GLICKO_INTERCEPT) / GLICKO_GRADIENT
19
+ sd = obj.rating_deviation / GLICKO_GRADIENT
20
+ new(mean, sd, obj.volatility, obj)
21
+ end
22
+
23
+ def initialize(mean, sd, volatility, obj)
24
+ @mean = mean
25
+ @sd = sd
26
+ @volatility = volatility
27
+ @obj = obj
28
+ end
29
+
30
+ def g
31
+ 1 / Math.sqrt(1 + 3 * sd ** 2 / Math::PI ** 2)
32
+ end
33
+
34
+ def e(other)
35
+ 1 / (1 + Math.exp(-other.g * (mean - other.mean)))
36
+ end
37
+
38
+ def variance(others)
39
+ return 0.0 if others.length < 1
40
+ others.reduce(0) do |v, other|
41
+ v + other.g ** 2 * e(other) * (1 - e(other))
42
+ end ** -1
43
+ end
44
+
45
+ def delta(others, scores)
46
+ others.zip(scores).reduce(0) do |d, (other, score)|
47
+ d + other.g * (score - e(other))
48
+ end * variance(others)
49
+ end
50
+
51
+ def f_part1(x, others, scores)
52
+ sd_sq = sd ** 2
53
+ v = variance(others)
54
+ _x = Math.exp(x)
55
+ (_x * (delta(others, scores) ** 2 - sd_sq - v - _x)) / (2 * (sd_sq + v + _x) ** 2)
56
+ end
57
+
58
+ def f_part2(x)
59
+ (x - Math::log(volatility ** 2)) / VOLATILITY_CHANGE ** 2
60
+ end
61
+
62
+ def f(x, others, scores)
63
+ f_part1(x, others, scores) - f_part2(x)
64
+ end
65
+
66
+ def generate_next(others, scores)
67
+ if others.length < 1
68
+ sd_pre = Math.sqrt(sd ** 2 + volatility ** 2)
69
+ return self.class.new(mean, sd_pre, volatility, obj) if others.length < 1
70
+ end
71
+ _v = variance(others)
72
+ a = Math::log(volatility ** 2)
73
+ if delta(others, scores) > sd ** 2 + _v
74
+ b = Math.log(_delta - sd ** 2 - _v)
75
+ else
76
+ k = 1
77
+ k += 1 while f(a - k * VOLATILITY_CHANGE, others, scores) < 0
78
+ b = a - k * VOLATILITY_CHANGE
79
+ end
80
+ fa = f(a, others, scores)
81
+ fb = f(b, others, scores)
82
+ while (b - a).abs > TOLERANCE
83
+ c = a + (a - b) * fa / (fb - fa)
84
+ fc = f(c, others, scores)
85
+ if fc * fb < 0
86
+ a = b
87
+ fa = fb
88
+ else
89
+ fa /= 2.0
90
+ end
91
+ b = c
92
+ fb = fc
93
+ end
94
+ volatility1 = Math.exp(a / 2.0)
95
+ sd_pre = Math.sqrt(sd ** 2 + volatility1 ** 2)
96
+ sd1 = 1 / Math.sqrt(1 / sd_pre ** 2 + 1 / _v)
97
+ mean1 = mean + sd1 ** 2 * others.zip(scores).reduce(0) {|x, (other, score)| x + other.g * (score - e(other)) }
98
+ self.class.new(mean1, sd1, volatility1, obj)
99
+ end
100
+
101
+ def update_obj
102
+ @obj.rating = GLICKO_GRADIENT * mean + GLICKO_INTERCEPT
103
+ @obj.rating_deviation = GLICKO_GRADIENT * sd
104
+ @obj.volatility = volatility
105
+ end
106
+
107
+ def to_s
108
+ "#<Player mean=#{mean}, sd=#{sd}, volatility=#{volatility}, obj=#{obj}>"
109
+ end
110
+ end
111
+
112
+ class RatingPeriod
113
+ def initialize(players)
114
+ @players = players.reduce({}) { |memo, player| memo[player] = []; memo }
115
+ end
116
+
117
+ def self.ranks_to_score(rank, other)
118
+ if rank < other
119
+ 1.0
120
+ elsif rank == other
121
+ 0.5
122
+ else
123
+ 0.0
124
+ end
125
+ end
126
+
127
+ def game(game_players, ranks)
128
+ game_players.zip(ranks).each do |player, rank|
129
+ game_players.zip(ranks).each do |other, other_rank|
130
+ next if player == other
131
+ @players[player] << [other, self.class.ranks_to_score(rank, other_rank)]
132
+ end
133
+ end
134
+ end
135
+
136
+ def generate_next
137
+ p = []
138
+ @players.each do |player, games|
139
+ p << player.generate_next(*games.transpose)
140
+ end
141
+ self.class.new(p)
142
+ end
143
+
144
+ def players
145
+ @players.keys
146
+ end
147
+
148
+ def to_s
149
+ "#<RatingPeriod players=#{@players.keys}"
150
+ end
151
+ end
152
+
153
+ end
@@ -0,0 +1,3 @@
1
+ module Glicko2
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,2 @@
1
+ require 'minitest/autorun'
2
+ require 'glicko2'
@@ -0,0 +1,107 @@
1
+ require 'minitest_helper'
2
+
3
+ Rating = Struct.new(:rating, :rating_deviation, :volatility)
4
+
5
+ describe Glicko2::Player do
6
+ before do
7
+ @player = Glicko2::Player.from_obj(Rating.new(1500, 200, 0.06))
8
+ @player1 = Glicko2::Player.from_obj(Rating.new(1400, 30, 0.06))
9
+ @player2 = Glicko2::Player.from_obj(Rating.new(1550, 100, 0.06))
10
+ @player3 = Glicko2::Player.from_obj(Rating.new(1700, 300, 0.06))
11
+ @others = [@player1, @player2, @player3]
12
+ @scores = [1, 0, 0]
13
+ end
14
+
15
+ describe ".from_obj" do
16
+ it "must create player from an object as example" do
17
+ @player.mean.must_be_close_to 0, 0.0001
18
+ @player.sd.must_be_close_to 1.1513, 0.0001
19
+ @player.volatility.must_equal 0.06
20
+ end
21
+
22
+ it "must create player from an object as example 1" do
23
+ @player1.mean.must_be_close_to -0.5756, 0.0001
24
+ @player1.sd.must_be_close_to 0.1727, 0.0001
25
+ @player1.volatility.must_equal 0.06
26
+ end
27
+
28
+ it "must create player from an object as example 2" do
29
+ @player2.mean.must_be_close_to 0.2878, 0.0001
30
+ @player2.sd.must_be_close_to 0.5756, 0.0001
31
+ @player2.volatility.must_equal 0.06
32
+ end
33
+
34
+ it "must create player from an object as example 3" do
35
+ @player3.mean.must_be_close_to 1.1513, 0.0001
36
+ @player3.sd.must_be_close_to 1.7269, 0.0001
37
+ @player3.volatility.must_equal 0.06
38
+ end
39
+ 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 "#variance" do
70
+ it "must be close to example" do
71
+ @player.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
+ end
96
+
97
+ describe "#update_obj" do
98
+ it "must update object to be close to example" do
99
+ p = @player.generate_next(@others, @scores)
100
+ p.update_obj
101
+ obj = p.obj
102
+ obj.rating.must_be_close_to 1464.06, 0.01
103
+ obj.rating_deviation.must_be_close_to 151.52, 0.01
104
+ obj.volatility.must_be_close_to 0.05999, 0.00001
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,48 @@
1
+ require 'minitest_helper'
2
+
3
+ describe Glicko2::RatingPeriod 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
+ @players = [@player, @player1, @player2, @player3]
10
+ @period = Glicko2::RatingPeriod.new(@players)
11
+ end
12
+
13
+ describe ".new" do
14
+ it "must assign players" do
15
+ @period.players.must_include @player
16
+ @period.players.must_include @player1
17
+ @period.players.must_include @player2
18
+ @period.players.must_include @player3
19
+ end
20
+ end
21
+
22
+ describe ".ranks_to_score" do
23
+ it "must return 1.0 when rank is less" do
24
+ Glicko2::RatingPeriod.ranks_to_score(1, 2).must_equal 1.0
25
+ end
26
+
27
+ it "must return 0.5 when rank is equal" do
28
+ Glicko2::RatingPeriod.ranks_to_score(1, 1).must_equal 0.5
29
+ end
30
+
31
+ it "must return 0.0 when rank is more" do
32
+ Glicko2::RatingPeriod.ranks_to_score(2, 1).must_equal 0.0
33
+ end
34
+ end
35
+
36
+ describe "complete rating period" do
37
+ it "must be close to example" do
38
+ @period.game([@player, @player1], [1, 2])
39
+ @period.game([@player, @player2], [2, 1])
40
+ @period.game([@player, @player3], [2, 1])
41
+ @period.generate_next.players.each { |p| p.update_obj }
42
+ obj = @player.obj
43
+ obj.rating.must_be_close_to 1464.06, 0.01
44
+ obj.rating_deviation.must_be_close_to 151.52, 0.01
45
+ obj.volatility.must_be_close_to 0.05999, 0.00001
46
+ end
47
+ end
48
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: glicko2
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - James Fargher
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-12-12 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: minitest
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: Implementation of Glicko2 ratings
31
+ email:
32
+ - proglottis@gmail.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - .gitignore
38
+ - Gemfile
39
+ - LICENSE.txt
40
+ - README.md
41
+ - Rakefile
42
+ - glicko2.gemspec
43
+ - lib/glicko2.rb
44
+ - lib/glicko2/version.rb
45
+ - spec/minitest_helper.rb
46
+ - spec/player_spec.rb
47
+ - spec/rating_period_spec.rb
48
+ homepage: https://github.com/proglottis/glicko2
49
+ licenses: []
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ! '>='
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubyforge_project:
68
+ rubygems_version: 1.8.23
69
+ signing_key:
70
+ specification_version: 3
71
+ summary: Implementation of Glicko2 ratings
72
+ test_files:
73
+ - spec/minitest_helper.rb
74
+ - spec/player_spec.rb
75
+ - spec/rating_period_spec.rb