rating-system 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/rating_system/contestant.rb +12 -0
- data/lib/rating_system/rating_calculator.rb +190 -0
- data/lib/rating_system/standings_row.rb +11 -0
- data/lib/rating_system.rb +9 -0
- metadata +48 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f6c7bc812ebc0d2ce970dd4b57183fafd0407892950a8f749d01d9d9d5c9a0dc
|
4
|
+
data.tar.gz: 11a8e8e6e0dc3c778e1142049c905bb5b22fb01f729e5ce15fd1561f93b1322a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2dbd68b9ee8a3e9366436932e2a308f7f97f65b4f944ca16fc925a542b7dcefdb377ff5025c68e3c5dbab072dc89aa5bfaa2c48cd7db803ca8f9de0ea925efec
|
7
|
+
data.tar.gz: 07b206ba45014cf5f8aafb5e9ecc5bb453f8dc3def14b434129fa1526283628343fd51d36468b3a244b6508c64aa949836084552ccd59242ee41c4c6dadc70d7
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module RatingSystem
|
2
|
+
class Contestant
|
3
|
+
attr_accessor :user_id, :rank, :points, :rating, :delta, :seed, :need_rating
|
4
|
+
|
5
|
+
def initialize(user_id, rank, points, rating)
|
6
|
+
@user_id = user_id
|
7
|
+
@rank = rank
|
8
|
+
@points = points
|
9
|
+
@rating = rating
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
module RatingSystem
|
2
|
+
class RatingCalculator
|
3
|
+
class << self
|
4
|
+
def get_new_ratings(contestants)
|
5
|
+
# previous_ratings
|
6
|
+
previous_ratings = {}
|
7
|
+
contestants.each { |c| previous_ratings[c[:user_id]] = c[:previous_rating] }
|
8
|
+
|
9
|
+
# standings_rows
|
10
|
+
standings_rows = []
|
11
|
+
total = 1_000_000
|
12
|
+
|
13
|
+
contestants.each do |c|
|
14
|
+
points = total - c[:position]
|
15
|
+
if points.negative?
|
16
|
+
raise StandardError, "Position of contestant is higher than length:\n ' #{c[:position]}', '#{c[:user_id]}"
|
17
|
+
end
|
18
|
+
|
19
|
+
standings_rows << RatingSystem::StandingsRow.new(c[:user_id], points)
|
20
|
+
end
|
21
|
+
|
22
|
+
rating_calculator = RatingSystem::RatingCalculator.new
|
23
|
+
rating_changes = rating_calculator.calculate_rating_changes(previous_ratings, standings_rows)
|
24
|
+
|
25
|
+
contestants.each do |c|
|
26
|
+
c[:delta] = rating_changes[c[:user_id]]
|
27
|
+
c[:new_rating] = c[:previous_rating].to_i + c[:delta].to_i
|
28
|
+
end
|
29
|
+
|
30
|
+
contestants
|
31
|
+
end
|
32
|
+
|
33
|
+
def run_benchmarks(contestant_count = 100)
|
34
|
+
start = Time.now
|
35
|
+
contestants = []
|
36
|
+
(1..contestant_count).each do |i|
|
37
|
+
h = {
|
38
|
+
position: i,
|
39
|
+
user_id: i,
|
40
|
+
previous_rating: 1500
|
41
|
+
}
|
42
|
+
contestants << h
|
43
|
+
end
|
44
|
+
|
45
|
+
RatingSystem::RatingCalculator.get_new_ratings(contestants)
|
46
|
+
finish = Time.now
|
47
|
+
|
48
|
+
diff = finish - start
|
49
|
+
p "total execution time: #{diff} seconds"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def calculate_rating_changes(previous_ratings, standings_rows)
|
54
|
+
contestants = []
|
55
|
+
|
56
|
+
standings_rows.each do |standings_row|
|
57
|
+
rank = standings_row.rank
|
58
|
+
user_id = standings_row.user_id
|
59
|
+
contestants << RatingSystem::Contestant.new(user_id, rank, standings_row.points, previous_ratings[user_id])
|
60
|
+
end
|
61
|
+
|
62
|
+
contestants = process(contestants)
|
63
|
+
|
64
|
+
rating_changes = {}
|
65
|
+
contestants.each { |contestant| rating_changes[contestant.user_id] = contestant.delta }
|
66
|
+
|
67
|
+
rating_changes
|
68
|
+
end
|
69
|
+
|
70
|
+
def process(contestants)
|
71
|
+
return if contestants.length.zero?
|
72
|
+
|
73
|
+
contestants = reassign_ranks(contestants)
|
74
|
+
|
75
|
+
contestants.each do |contestant|
|
76
|
+
contestant.seed = get_seed(contestants, contestant, contestant.rating)
|
77
|
+
# calculating geometric mean of rank and seed
|
78
|
+
mid_rank = Math.sqrt(contestant.rank * contestant.seed)
|
79
|
+
contestant.need_rating = get_rating_to_rank(contestants, contestant, mid_rank)
|
80
|
+
contestant.delta = ((contestant.need_rating - contestant.rating) / 2.00).truncate
|
81
|
+
end
|
82
|
+
|
83
|
+
contestants = adjust_inflation(contestants)
|
84
|
+
validate_deltas(contestants)
|
85
|
+
contestants
|
86
|
+
end
|
87
|
+
|
88
|
+
def adjust_inflation(contestants)
|
89
|
+
contestants = sort_by_rating_desc(contestants)
|
90
|
+
|
91
|
+
# Total sum should not be more than zero.
|
92
|
+
sum = 0
|
93
|
+
contestants.each { |c| sum += c.delta }
|
94
|
+
|
95
|
+
inc = (-sum / contestants.length - 1)
|
96
|
+
contestants.each { |c| c.delta += inc }
|
97
|
+
|
98
|
+
# Sum of top-4*sqrt should be adjusted to zero.
|
99
|
+
sum = 0
|
100
|
+
zero_sum_count = [4 * Math.sqrt(contestants.length).round, contestants.length].min.truncate
|
101
|
+
|
102
|
+
(0..zero_sum_count - 1).each { |i| sum += contestants[i].delta }
|
103
|
+
|
104
|
+
inc = [[(-sum / zero_sum_count).truncate, -10].max, 0].min
|
105
|
+
contestants.each { |c| c.delta += inc }
|
106
|
+
contestants
|
107
|
+
end
|
108
|
+
|
109
|
+
def reassign_ranks(contestants)
|
110
|
+
contestants = sort_by_points_desc(contestants)
|
111
|
+
|
112
|
+
contestants.each do |contestant|
|
113
|
+
contestant.rank = 0
|
114
|
+
contestant.delta = 0
|
115
|
+
end
|
116
|
+
|
117
|
+
first = 0
|
118
|
+
points = contestants.first.points
|
119
|
+
|
120
|
+
(1..contestants.length - 1).each do |i|
|
121
|
+
next if contestants[i].points >= points
|
122
|
+
|
123
|
+
(first..i).each { |j| contestants[j].rank = i }
|
124
|
+
first = i
|
125
|
+
points = contestants[i].points
|
126
|
+
end
|
127
|
+
|
128
|
+
rank = contestants.length
|
129
|
+
(first..contestants.length - 1).each { |j| contestants[j].rank = rank }
|
130
|
+
|
131
|
+
contestants
|
132
|
+
end
|
133
|
+
|
134
|
+
def sort_by_points_desc(contestants)
|
135
|
+
contestants.sort! { |a, b| b.points <=> a.points }
|
136
|
+
end
|
137
|
+
|
138
|
+
def get_elo_win_probability(ra, rb)
|
139
|
+
1.0 / (1 + (10**((rb - ra) / 400.0)))
|
140
|
+
end
|
141
|
+
|
142
|
+
def get_rating_to_rank(contestants, contestant, rank)
|
143
|
+
left = 1
|
144
|
+
right = 8000
|
145
|
+
|
146
|
+
while (right - left) > 1
|
147
|
+
mid_rating = ((left + right) / 2.00).truncate
|
148
|
+
|
149
|
+
if get_seed(contestants, contestant, mid_rating) < rank
|
150
|
+
right = mid_rating
|
151
|
+
else
|
152
|
+
left = mid_rating
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
left
|
157
|
+
end
|
158
|
+
|
159
|
+
# get seed for a contestant if the constant has a particular rating
|
160
|
+
def get_seed(contestants, contestant, rating)
|
161
|
+
result = 1
|
162
|
+
contestants.each do |other|
|
163
|
+
result += get_elo_win_probability(other.rating, rating) if other.user_id != contestant.user_id
|
164
|
+
end
|
165
|
+
result
|
166
|
+
end
|
167
|
+
|
168
|
+
def sort_by_rating_desc(contestants)
|
169
|
+
contestants.sort! { |a, b| b.rating <=> a.rating }
|
170
|
+
end
|
171
|
+
|
172
|
+
def validate_deltas(contestants)
|
173
|
+
contestants = sort_by_points_desc(contestants)
|
174
|
+
(0..contestants.length - 1).each do |i|
|
175
|
+
(i + 1..contestants.length - 1).each do |j|
|
176
|
+
# Contestant i has better place than j
|
177
|
+
if contestants[i].rating > contestants[j].rating && (contestants[i].rating + contestants[i].delta < contestants[j].rating + contestants[j].delta)
|
178
|
+
raise StandardError,
|
179
|
+
"First rating invariant failed #{contestants[i].user_id} vs. #{contestants[j].user_id.user_id}"
|
180
|
+
end
|
181
|
+
|
182
|
+
if contestants[i].rating < contestants[j].rating && (contestants[i].delta < contestants[j].delta)
|
183
|
+
raise StandardError,
|
184
|
+
"Second rating invariant failed #{contestants[i].user_id} vs. #{contestants[j].user_id}"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
metadata
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rating-system
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ramit Koul
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-06-16 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: A simple rating system for multiplayer games
|
14
|
+
email: ramitkaul@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/rating_system.rb
|
20
|
+
- lib/rating_system/contestant.rb
|
21
|
+
- lib/rating_system/rating_calculator.rb
|
22
|
+
- lib/rating_system/standings_row.rb
|
23
|
+
homepage: https://github.com/rkwap/rating-system
|
24
|
+
licenses:
|
25
|
+
- MIT
|
26
|
+
metadata:
|
27
|
+
allowed_push_host: https://rubygems.org
|
28
|
+
source_code_uri: https://github.com/rkwap/rating-system
|
29
|
+
post_install_message:
|
30
|
+
rdoc_options: []
|
31
|
+
require_paths:
|
32
|
+
- lib
|
33
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - ">="
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 2.7.4
|
38
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
requirements: []
|
44
|
+
rubygems_version: 3.2.33
|
45
|
+
signing_key:
|
46
|
+
specification_version: 4
|
47
|
+
summary: A simple rating system
|
48
|
+
test_files: []
|