1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# frozen_string_literal: true
module OpenSkill
class Model
attr_reader :mu, :sigma, :beta, :beta_sq, :two_beta_sq, :epsilon, :z, :alpha, :target #: Float
attr_reader :tau #: Float?
attr_reader :limit_sigma #: bool
# Parameters are taken as written, 25 or 8.33, and kept as Floats.
#
# @rbs mu: number
# @rbs z: number
# @rbs sigma: number
# @rbs beta: number
# @rbs epsilon: number
# @rbs alpha: number
# @rbs target: number
# @rbs tau: number?
# @rbs prevent_sigma_increase: bool
# @rbs limit_sigma: bool
# @rbs return: void
def initialize(mu: 25, z: 3, sigma: mu.to_f / z, beta: sigma.to_f / 2,
epsilon: 0.0001, alpha: 1, target: 0, tau: nil,
prevent_sigma_increase: false, limit_sigma: prevent_sigma_increase)
@mu = mu.to_f
@sigma = sigma.to_f
@beta = beta.to_f
@beta_sq = @beta**2
@two_beta_sq = 2 * @beta_sq
@epsilon = epsilon.to_f
@z = z.to_f
@alpha = alpha.to_f
@target = target.to_f
@tau = tau&.to_f
@limit_sigma = limit_sigma
@rating_model = PlackettLuce.new(self)
end
#: () -> Rating
def rating
Rating.new(mu, sigma)
end
# Rate teams given a Game — placements (first to last), each containing
# one or more tied teams, each containing player Ratings.
#
# model.rate(Game.new([[winner]], [[loser]]))
# model.rate(Game.new([[first]], [[tied_a], [tied_b]], [[last]]))
#
# Returns a flat array of updated teams in the same (flattened) order.
#: (Game game) -> Array[Array[Rating]]
def rate(game)
processed_game = game
tau = self.tau
# If tau is provided, use additive dynamics factor to keep ratings pliable
if tau
tau_sq = tau**2
inflated_groups = game.rank_groups.map { |group|
group.map { |team|
team.map { |p| Rating.new(p.mu, Math.sqrt(p.sigma**2 + tau_sq)) }
}
}
processed_game = Game.new(*inflated_groups)
end
new_ratings = @rating_model.rate(processed_game)
# Cap sigma at pre-tau value so ordinal never drops after a win
if tau && limit_sigma
new_ratings = new_ratings.map.with_index { |team, i|
team.map.with_index { |p, j|
Rating.new(p.mu, [p.sigma, game.teams[i][j].sigma].min)
}
}
end
new_ratings
end
#: (Rating rating) -> Float
def ordinal(rating)
target + alpha * (rating.mu - z * rating.sigma)
end
end
end