trueskill 0.1.0

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.
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