trueskill 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/.document +5 -0
  2. data/.gitignore +21 -0
  3. data/CHANGELOG +0 -0
  4. data/LICENSE +20 -0
  5. data/README.rdoc +17 -0
  6. data/Rakefile +46 -0
  7. data/VERSION +1 -0
  8. data/lib/saulabs/gauss.rb +3 -0
  9. data/lib/saulabs/gauss/distribution.rb +125 -0
  10. data/lib/saulabs/gauss/truncated_correction.rb +62 -0
  11. data/lib/saulabs/trueskill.rb +7 -0
  12. data/lib/saulabs/trueskill/factor_graph.rb +75 -0
  13. data/lib/saulabs/trueskill/factors/base.rb +50 -0
  14. data/lib/saulabs/trueskill/factors/greater_than.rb +45 -0
  15. data/lib/saulabs/trueskill/factors/likelihood.rb +45 -0
  16. data/lib/saulabs/trueskill/factors/prior.rb +35 -0
  17. data/lib/saulabs/trueskill/factors/weighted_sum.rb +80 -0
  18. data/lib/saulabs/trueskill/factors/within.rb +45 -0
  19. data/lib/saulabs/trueskill/layers/base.rb +32 -0
  20. data/lib/saulabs/trueskill/layers/iterated_team_performances.rb +72 -0
  21. data/lib/saulabs/trueskill/layers/performances_to_team_performances.rb +31 -0
  22. data/lib/saulabs/trueskill/layers/prior_to_skills.rb +32 -0
  23. data/lib/saulabs/trueskill/layers/skills_to_performances.rb +31 -0
  24. data/lib/saulabs/trueskill/layers/team_difference_comparision.rb +27 -0
  25. data/lib/saulabs/trueskill/layers/team_performance_differences.rb +22 -0
  26. data/lib/saulabs/trueskill/rating.rb +18 -0
  27. data/lib/saulabs/trueskill/schedules/base.rb +15 -0
  28. data/lib/saulabs/trueskill/schedules/loop.rb +26 -0
  29. data/lib/saulabs/trueskill/schedules/sequence.rb +23 -0
  30. data/lib/saulabs/trueskill/schedules/step.rb +20 -0
  31. data/spec/saulabs/gauss/distribution_spec.rb +162 -0
  32. data/spec/saulabs/gauss/truncated_correction_spec.rb +41 -0
  33. data/spec/saulabs/trueskill/factor_graph_spec.rb +29 -0
  34. data/spec/saulabs/trueskill/factors/greater_than_spec.rb +26 -0
  35. data/spec/saulabs/trueskill/factors/likelihood_spec.rb +32 -0
  36. data/spec/saulabs/trueskill/factors/prior_spec.rb +26 -0
  37. data/spec/saulabs/trueskill/factors/weighted_sum_spec.rb +67 -0
  38. data/spec/saulabs/trueskill/factors/within_spec.rb +26 -0
  39. data/spec/saulabs/trueskill/layers/prior_to_skills_spec.rb +39 -0
  40. data/spec/saulabs/trueskill/schedules_spec.rb +14 -0
  41. data/spec/spec.opts +1 -0
  42. data/spec/spec_helper.rb +29 -0
  43. data/trueskill.gemspec +99 -0
  44. 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