trueskill 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/CHANGELOG +0 -0
- data/LICENSE +20 -0
- data/README.rdoc +17 -0
- data/Rakefile +46 -0
- data/VERSION +1 -0
- data/lib/saulabs/gauss.rb +3 -0
- data/lib/saulabs/gauss/distribution.rb +125 -0
- data/lib/saulabs/gauss/truncated_correction.rb +62 -0
- data/lib/saulabs/trueskill.rb +7 -0
- data/lib/saulabs/trueskill/factor_graph.rb +75 -0
- data/lib/saulabs/trueskill/factors/base.rb +50 -0
- data/lib/saulabs/trueskill/factors/greater_than.rb +45 -0
- data/lib/saulabs/trueskill/factors/likelihood.rb +45 -0
- data/lib/saulabs/trueskill/factors/prior.rb +35 -0
- data/lib/saulabs/trueskill/factors/weighted_sum.rb +80 -0
- data/lib/saulabs/trueskill/factors/within.rb +45 -0
- data/lib/saulabs/trueskill/layers/base.rb +32 -0
- data/lib/saulabs/trueskill/layers/iterated_team_performances.rb +72 -0
- data/lib/saulabs/trueskill/layers/performances_to_team_performances.rb +31 -0
- data/lib/saulabs/trueskill/layers/prior_to_skills.rb +32 -0
- data/lib/saulabs/trueskill/layers/skills_to_performances.rb +31 -0
- data/lib/saulabs/trueskill/layers/team_difference_comparision.rb +27 -0
- data/lib/saulabs/trueskill/layers/team_performance_differences.rb +22 -0
- data/lib/saulabs/trueskill/rating.rb +18 -0
- data/lib/saulabs/trueskill/schedules/base.rb +15 -0
- data/lib/saulabs/trueskill/schedules/loop.rb +26 -0
- data/lib/saulabs/trueskill/schedules/sequence.rb +23 -0
- data/lib/saulabs/trueskill/schedules/step.rb +20 -0
- data/spec/saulabs/gauss/distribution_spec.rb +162 -0
- data/spec/saulabs/gauss/truncated_correction_spec.rb +41 -0
- data/spec/saulabs/trueskill/factor_graph_spec.rb +29 -0
- data/spec/saulabs/trueskill/factors/greater_than_spec.rb +26 -0
- data/spec/saulabs/trueskill/factors/likelihood_spec.rb +32 -0
- data/spec/saulabs/trueskill/factors/prior_spec.rb +26 -0
- data/spec/saulabs/trueskill/factors/weighted_sum_spec.rb +67 -0
- data/spec/saulabs/trueskill/factors/within_spec.rb +26 -0
- data/spec/saulabs/trueskill/layers/prior_to_skills_spec.rb +39 -0
- data/spec/saulabs/trueskill/schedules_spec.rb +14 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +29 -0
- data/trueskill.gemspec +99 -0
- metadata +143 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
module Saulabs
|
2
|
+
module TrueSkill
|
3
|
+
module Factors
|
4
|
+
|
5
|
+
class GreaterThan < Base
|
6
|
+
|
7
|
+
def initialize(epsilon, variable)
|
8
|
+
super()
|
9
|
+
@epsilon = epsilon
|
10
|
+
bind(variable)
|
11
|
+
end
|
12
|
+
|
13
|
+
def log_normalization
|
14
|
+
msg = @variables[0] / @messages[0]
|
15
|
+
-Gauss::Distribution.log_product_normalization(msg, @messages[0]) +
|
16
|
+
Math.log(Gauss::Distribution.cdf((msg.mean - @epsilon) / msg.deviation))
|
17
|
+
end
|
18
|
+
|
19
|
+
def update_message_at(index)
|
20
|
+
raise "illegal message index: #{index}" if index < 0 || index > 0
|
21
|
+
message = @messages[index]
|
22
|
+
variable = @bindings[@messages[index]]
|
23
|
+
|
24
|
+
msg = variable / message
|
25
|
+
c = msg.precision
|
26
|
+
d = msg.precision_mean
|
27
|
+
sqrt_c = Math.sqrt(c)
|
28
|
+
d_sqrt_c = sqrt_c == 0 ? 0.0 : d / sqrt_c
|
29
|
+
e_sqrt_c = @epsilon * sqrt_c
|
30
|
+
denom = 1.0 - Gauss::TruncatedCorrection.w_exceeds_margin(d_sqrt_c, e_sqrt_c)
|
31
|
+
new_precision = c / denom
|
32
|
+
new_precision_mean = (d + sqrt_c * Gauss::TruncatedCorrection.v_exceeds_margin(d_sqrt_c, e_sqrt_c)) / denom
|
33
|
+
new_marginal = Gauss::Distribution.with_precision(new_precision_mean, new_precision)
|
34
|
+
new_message = message * new_marginal / variable
|
35
|
+
diff = new_marginal - variable
|
36
|
+
message.replace(new_message)
|
37
|
+
variable.replace(new_marginal)
|
38
|
+
return diff
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Saulabs
|
2
|
+
module TrueSkill
|
3
|
+
module Factors
|
4
|
+
|
5
|
+
class Likelihood < Base
|
6
|
+
|
7
|
+
def initialize(beta_squared, variable1, variable2)
|
8
|
+
super()
|
9
|
+
@precision = 1.0 / beta_squared
|
10
|
+
bind(variable1)
|
11
|
+
bind(variable2)
|
12
|
+
end
|
13
|
+
|
14
|
+
def update_message_at(index)
|
15
|
+
raise "illegal message index: #{index}" if index < 0 || index > 1
|
16
|
+
case index
|
17
|
+
when 0 then update_helper(@messages[0], @messages[1], @variables[0], @variables[1])
|
18
|
+
when 1 then update_helper(@messages[1], @messages[0], @variables[1], @variables[0])
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def log_normalization
|
23
|
+
Gauss::Distribution.log_ratio_normalization(@variables[0], @messages[0])
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def update_helper(message1, message2, variable1, variable2)
|
29
|
+
a = @precision / (@precision + variable2.precision - message2.precision)
|
30
|
+
new_message = Gauss::Distribution.with_precision(
|
31
|
+
a * (variable2.precision_mean - message2.precision_mean),
|
32
|
+
a * (variable2.precision - message2.precision)
|
33
|
+
)
|
34
|
+
new_marginal = (variable1 / message1) * new_message
|
35
|
+
diff = new_marginal - variable1
|
36
|
+
message1.replace(new_message)
|
37
|
+
variable1.replace(new_marginal)
|
38
|
+
return diff
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Saulabs
|
2
|
+
module TrueSkill
|
3
|
+
module Factors
|
4
|
+
|
5
|
+
class Prior < Base
|
6
|
+
|
7
|
+
def initialize(mean, variance, variable)
|
8
|
+
super()
|
9
|
+
@message = Gauss::Distribution.with_variance(mean, variance)
|
10
|
+
bind(variable)
|
11
|
+
end
|
12
|
+
|
13
|
+
def update_message_at(index)
|
14
|
+
raise "illegal message index: #{index}" if index < 0 || index > 0
|
15
|
+
message = @messages[index]
|
16
|
+
variable = @variables[index]
|
17
|
+
new_marginal = Gauss::Distribution.with_precision(
|
18
|
+
variable.precision_mean + @message.precision_mean - message.precision_mean,
|
19
|
+
variable.precision + @message.precision - message.precision
|
20
|
+
)
|
21
|
+
diff = variable - new_marginal
|
22
|
+
variable.replace(new_marginal)
|
23
|
+
message.replace(@message)
|
24
|
+
return diff
|
25
|
+
end
|
26
|
+
|
27
|
+
def log_normalization
|
28
|
+
0.0
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'pp'
|
2
|
+
|
3
|
+
module Saulabs
|
4
|
+
module TrueSkill
|
5
|
+
module Factors
|
6
|
+
|
7
|
+
class WeightedSum < Base
|
8
|
+
|
9
|
+
attr_reader :weights, :weights_squared, :index_order
|
10
|
+
|
11
|
+
def initialize(variable, ratings, weights)
|
12
|
+
super()
|
13
|
+
@weights = [weights]
|
14
|
+
@weights_squared = [@weights.first.map { |w| w**2 }]
|
15
|
+
@index_order = [(0..weights.size+1).to_a]
|
16
|
+
(1..weights.size).each do |idx|
|
17
|
+
dest_idx = 0
|
18
|
+
@weights[idx] = []
|
19
|
+
@weights_squared[idx] = []
|
20
|
+
@index_order << [idx]
|
21
|
+
(0..ratings.size-1).each do |src_idx|
|
22
|
+
next if src_idx == idx-1
|
23
|
+
weight = weights[idx-1] == 0 ? 0.0 : -weights[src_idx] / weights[idx-1]
|
24
|
+
@weights[idx][dest_idx] = weight
|
25
|
+
@weights_squared[idx][dest_idx] = weight**2
|
26
|
+
@index_order.last[dest_idx+1] = src_idx+1
|
27
|
+
dest_idx += 1
|
28
|
+
end
|
29
|
+
final_weight = weights[idx-1] == 0 ? 0.0 : 1.0 / weights[idx-1]
|
30
|
+
@weights[idx][dest_idx] = final_weight
|
31
|
+
@weights_squared[idx][dest_idx] = final_weight**2
|
32
|
+
@index_order.last[weights.size] = 0
|
33
|
+
end
|
34
|
+
bind(variable)
|
35
|
+
ratings.each { |v| bind(v) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def update_message_at(index)
|
39
|
+
raise "illegal message index: #{index}" if index < 0 || index >= @messages.count
|
40
|
+
indices = @index_order[index]
|
41
|
+
updated_messages = []
|
42
|
+
updated_variables = []
|
43
|
+
@messages.each_index do |i|
|
44
|
+
updated_messages << @messages[indices[i]]
|
45
|
+
updated_variables << @variables[indices[i]]
|
46
|
+
end
|
47
|
+
update_helper(@weights[index], @weights_squared[index], updated_messages, updated_variables)
|
48
|
+
end
|
49
|
+
|
50
|
+
def log_normalization
|
51
|
+
res = 0
|
52
|
+
(1..@variables.size-1).each do |i|
|
53
|
+
res += Gauss::Distribution.log_ratio_normalization(@variables[i], @messages[i])
|
54
|
+
end
|
55
|
+
res
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def update_helper(weights, weights_squared, messages, variables)
|
61
|
+
marginal0 = variables[0].clone
|
62
|
+
ips, wms = 0.0, 0.0
|
63
|
+
weights_squared.each_index do |i|
|
64
|
+
ips += weights_squared[i] / (variables[i+1].precision - messages[i+1].precision)
|
65
|
+
diff = variables[i+1] / messages[i+1]
|
66
|
+
wms += weights[i] * (variables[i+1].precision_mean - messages[i+1].precision_mean) / (variables[i+1].precision - messages[i+1].precision)
|
67
|
+
end
|
68
|
+
new_message = Gauss::Distribution.with_precision(1.0/ips*wms, 1.0/ips)
|
69
|
+
old_marginal = marginal0 / messages[0]
|
70
|
+
new_marginal = old_marginal * new_message
|
71
|
+
messages[0].replace(new_message)
|
72
|
+
variables[0].replace(new_marginal)
|
73
|
+
return new_marginal - marginal0
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Saulabs
|
2
|
+
module TrueSkill
|
3
|
+
module Factors
|
4
|
+
|
5
|
+
class Within < Base
|
6
|
+
|
7
|
+
def initialize(epsilon, variable)
|
8
|
+
super()
|
9
|
+
@epsilon = epsilon
|
10
|
+
bind(variable)
|
11
|
+
end
|
12
|
+
|
13
|
+
def log_normalization
|
14
|
+
msg = @variables[0] / @messages[0]
|
15
|
+
mean = msg.mean
|
16
|
+
dev = msg.deviation
|
17
|
+
z = Gauss::Distribution.cdf((@epsilon - mean) / dev) - Gauss::Distribution.cdf((-@epsilon - mean) / dev)
|
18
|
+
-Gauss::Distribution.log_product_normalization(msg, @messages[0]) + Math.log(z)
|
19
|
+
end
|
20
|
+
|
21
|
+
def update_message_at(index)
|
22
|
+
message = @messages[index]
|
23
|
+
variable = @variables[index]
|
24
|
+
msg = variable / message
|
25
|
+
c = msg.precision
|
26
|
+
d = msg.precision_mean
|
27
|
+
sqrt_c = Math.sqrt(c)
|
28
|
+
d_sqrt_c = sqrt_c == 0 ? 0.0 : d / sqrt_c
|
29
|
+
e_sqrt_c = @epsilon * sqrt_c
|
30
|
+
denom = 1.0 - Gauss::TruncatedCorrection.w_within_margin(d_sqrt_c, e_sqrt_c)
|
31
|
+
new_precision = c / denom
|
32
|
+
new_precision_mean = (d + sqrt_c * Gauss::TruncatedCorrection.v_within_margin(d_sqrt_c, e_sqrt_c)) / denom
|
33
|
+
new_marginal = Gauss::Distribution.with_precision(new_precision_mean, new_precision)
|
34
|
+
new_message = message * new_marginal / variable
|
35
|
+
diff = new_marginal - variable
|
36
|
+
message.replace(new_message)
|
37
|
+
variable.replace(new_marginal)
|
38
|
+
return diff
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Saulabs
|
2
|
+
module TrueSkill
|
3
|
+
module Layers
|
4
|
+
|
5
|
+
class Base
|
6
|
+
|
7
|
+
attr_accessor :graph, :factors, :output, :input
|
8
|
+
|
9
|
+
def initialize(graph)
|
10
|
+
@graph = graph
|
11
|
+
@factors = []
|
12
|
+
@output = []
|
13
|
+
@input = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def build
|
17
|
+
raise "Abstract method Layers::Base#build called"
|
18
|
+
end
|
19
|
+
|
20
|
+
def prior_schedule
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def posterior_schedule
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Saulabs
|
2
|
+
module TrueSkill
|
3
|
+
module Layers
|
4
|
+
|
5
|
+
class IteratedTeamPerformances < Base
|
6
|
+
|
7
|
+
def initialize(graph, team_perf_diff, team_diff_comp)
|
8
|
+
super(graph)
|
9
|
+
@tpd = team_perf_diff
|
10
|
+
@tdc = team_diff_comp
|
11
|
+
end
|
12
|
+
|
13
|
+
def build
|
14
|
+
@tpd.input = @input
|
15
|
+
@tpd.build
|
16
|
+
@tdc.input = @tpd.output
|
17
|
+
@tdc.build
|
18
|
+
end
|
19
|
+
|
20
|
+
def factors
|
21
|
+
@tpd.factors + @tdc.factors
|
22
|
+
end
|
23
|
+
|
24
|
+
def prior_schedule
|
25
|
+
loop_schedule = if @input.size == 2
|
26
|
+
two_team_loop_schedule
|
27
|
+
elsif @input.size > 2
|
28
|
+
multiple_team_loop_schedule
|
29
|
+
else
|
30
|
+
raise 'Illegal input'
|
31
|
+
end
|
32
|
+
team_diffs = @tpd.factors.size;
|
33
|
+
Schedules::Sequence.new([loop_schedule,
|
34
|
+
Schedules::Step.new(@tpd.factors[0], 1),
|
35
|
+
Schedules::Step.new(@tpd.factors[team_diffs-1], 2)])
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def two_team_loop_schedule
|
41
|
+
Schedules::Sequence.new([
|
42
|
+
Schedules::Step.new(@tpd.factors[0], 0),
|
43
|
+
Schedules::Step.new(@tdc.factors[0], 0)
|
44
|
+
])
|
45
|
+
end
|
46
|
+
|
47
|
+
def multiple_team_loop_schedule
|
48
|
+
team_diff = @tpd.factors.size
|
49
|
+
forward_schedule = Schedules::Sequence.new(
|
50
|
+
(0..team_diff-2).map { |i|
|
51
|
+
Schedules::Sequence.new([
|
52
|
+
Schedules::Step.new(@tpd.factors[i], 0),
|
53
|
+
Schedules::Step.new(@tdc.factors[i], 0),
|
54
|
+
Schedules::Step.new(@tpd.factors[i], 2)
|
55
|
+
])
|
56
|
+
})
|
57
|
+
backward_schedule = Schedules::Sequence.new(
|
58
|
+
(0..team_diff-2).map { |i|
|
59
|
+
Schedules::Sequence.new([
|
60
|
+
Schedules::Step.new(@tpd.factors[team_diff-1-i], 0),
|
61
|
+
Schedules::Step.new(@tdc.factors[team_diff-1-i], 0),
|
62
|
+
Schedules::Step.new(@tpd.factors[team_diff-1-i], 1)
|
63
|
+
])
|
64
|
+
})
|
65
|
+
Schedules::Loop.new(Schedules::Sequence.new([forward_schedule, backward_schedule]), 0.0001)
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Saulabs
|
2
|
+
module TrueSkill
|
3
|
+
module Layers
|
4
|
+
|
5
|
+
class PerformancesToTeamPerformances < Base
|
6
|
+
|
7
|
+
def build
|
8
|
+
@input.each do |ratings|
|
9
|
+
variable = Gauss::Distribution.new
|
10
|
+
@factors << Factors::WeightedSum.new(variable, ratings, ratings.map(&:activity))
|
11
|
+
@output << [variable]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def prior_schedule
|
16
|
+
Schedules::Sequence.new(@factors.map { |f| Schedules::Step.new(f, 0) })
|
17
|
+
end
|
18
|
+
|
19
|
+
def posterior_schedule
|
20
|
+
steps = []
|
21
|
+
@factors.each do |f|
|
22
|
+
(1..f.message_count-1).each { |i| steps << Schedules::Step.new(f, i) }
|
23
|
+
end
|
24
|
+
Schedules::Sequence.new(steps)
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Saulabs
|
2
|
+
module TrueSkill
|
3
|
+
module Layers
|
4
|
+
|
5
|
+
class PriorToSkills < Base
|
6
|
+
|
7
|
+
def initialize(graph, teams)
|
8
|
+
super(graph)
|
9
|
+
@teams = teams
|
10
|
+
end
|
11
|
+
|
12
|
+
def build
|
13
|
+
@teams.each do |team|
|
14
|
+
team_skills = []
|
15
|
+
team.each do |rating|
|
16
|
+
variable = TrueSkill::Rating.new(0.0, 0.0, rating.activity, rating.tau)
|
17
|
+
@factors << Factors::Prior.new(rating.mean, rating.variance + rating.tau_squared, variable)
|
18
|
+
team_skills << variable
|
19
|
+
end
|
20
|
+
@output << team_skills
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def prior_schedule
|
25
|
+
Schedules::Sequence.new(@factors.map { |f| Schedules::Step.new(f, 0) })
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Saulabs
|
2
|
+
module TrueSkill
|
3
|
+
module Layers
|
4
|
+
|
5
|
+
class SkillsToPerformances < Base
|
6
|
+
|
7
|
+
def build
|
8
|
+
@input.each do |team|
|
9
|
+
team_performances = []
|
10
|
+
team.each do |rating|
|
11
|
+
variable = TrueSkill::Rating.new(0.0, 0.0, rating.activity, rating.tau)
|
12
|
+
@factors << Factors::Likelihood.new(@graph.beta_squared, variable, rating)
|
13
|
+
team_performances << variable
|
14
|
+
end
|
15
|
+
@output << team_performances
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def prior_schedule
|
20
|
+
Schedules::Sequence.new(@factors.map { |f| Schedules::Step.new(f, 0) })
|
21
|
+
end
|
22
|
+
|
23
|
+
def posterior_schedule
|
24
|
+
Schedules::Sequence.new(@factors.map { |f| Schedules::Step.new(f, 1) })
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|