diff --git a/lib/saulabs/trueskill/factor_graph.rb b/lib/saulabs/trueskill/factor_graph.rb index 2c8abf7..8d9a21a 100644 --- a/lib/saulabs/trueskill/factor_graph.rb +++ b/lib/saulabs/trueskill/factor_graph.rb @@ -1,38 +1,38 @@ # -*- encoding : utf-8 -*- module Saulabs module TrueSkill - + class FactorGraph - + # @return [Array>] attr_reader :teams - + # @return [Float] attr_reader :beta - + # @return [Float] attr_reader :beta_squared - + # @return [Float] attr_reader :draw_probability - + # @return [Float] attr_reader :epsilon - + # @private attr_reader :layers # @return [Boolean] attr_reader :skills_additive - - + + # Creates a new trueskill factor graph for calculating the new skills based on the given game parameters # - # @param [Array>] teams + # @param [Array>] teams # player-ratings grouped in Arrays by teams - # @param [Array] ranks + # @param [Array] ranks # team rankings, example: [2,1,3] first team in teams finished 2nd, second team 1st and third team 3rd - # @param [Hash] options + # @param [Hash] options # the options hash to configure the factor graph constants beta, draw_probability and skills_additive # # @option options [Float] :beta (4.166667) @@ -44,30 +44,31 @@ class FactorGraph # true is valid for games like Halo etc, where skill is additive (2 players are better than 1), # false for card games like Skat, Doppelkopf, Bridge where skills are not additive, # two players dont make the team stronger, skills averaged) - # + # # @example Calculating new skills of a two team game, where one team has one player and the other two # # require 'rubygems' # require 'saulabs/trueskill' - # + # # include Saulabs::TrueSkill - # + # # # team 1 has just one player with a mean skill of 27.1, a skill-deviation of 2.13 # # and a play activity of 100 % # team1 = [Rating.new(27.1, 2.13, 1.0)] - # + # # # team 2 has two players # team2 = [Rating.new(22.0, 0.98, 0.8), Rating.new(31.1, 5.33, 0.9)] - # + # # # team 1 finished first and team 2 second # graph = FactorGraph.new([team1, team2], [1,2]) - # + # # # update the Ratings # graph.update_skills # - def initialize(teams, ranks, options = {}) - @teams = teams - @ranks = ranks + def initialize(ranks_teams_hash, options = {}) + @teams = ranks_teams_hash.keys + @ranks = ranks_teams_hash.values + opts = { :beta => 25/6.0, :draw_probability => 0.1, @@ -79,7 +80,7 @@ def initialize(teams, ranks, options = {}) @beta_squared = @beta**2 @epsilon = -Math.sqrt(2.0 * @beta_squared) * Gauss::Distribution.inv_cdf((1.0 - @draw_probability) / 2.0) @skills_additive = opts[:skills_additive] - + @prior_layer = Layers::PriorToSkills.new(self, @teams) @layers = [ @prior_layer, @@ -87,17 +88,17 @@ def initialize(teams, ranks, options = {}) Layers::PerformancesToTeamPerformances.new(self, @skills_additive), Layers::IteratedTeamPerformances.new(self, Layers::TeamPerformanceDifferences.new(self), - Layers::TeamDifferenceComparision.new(self, ranks) + Layers::TeamDifferenceComparision.new(self, @ranks) ) ] end - + def draw_margin Gauss::Distribution.inv_cdf(0.5*(@draw_probability + 1)) * Math.sqrt(1 + 1) * @beta end - + # Updates the skills of the players inplace - # + # # @return [Float] the probability of the games outcome def update_skills build_layers @@ -109,9 +110,9 @@ def update_skills end ranking_probability end - + private - + def ranking_probability # factor_list = [] # sum_log_z, sum_log_s = 0.0 @@ -125,11 +126,11 @@ def ranking_probability # Math.exp(sum_log_z + sum_log_s) 0.0 end - + def updated_skills @prior_layer.output end - + def build_layers output = nil @layers.each do |layer| @@ -138,13 +139,13 @@ def build_layers output = layer.output end end - + def run_schedule schedules = @layers.map(&:prior_schedule) + @layers.reverse.map(&:posterior_schedule) Schedules::Sequence.new(schedules.compact).visit end - + end - + end end diff --git a/lib/saulabs/trueskill/layers/prior_to_skills.rb b/lib/saulabs/trueskill/layers/prior_to_skills.rb index 65de32a..8c9cb04 100644 --- a/lib/saulabs/trueskill/layers/prior_to_skills.rb +++ b/lib/saulabs/trueskill/layers/prior_to_skills.rb @@ -3,15 +3,15 @@ module Saulabs module TrueSkill # @private module Layers - + # @private class PriorToSkills < Base - + def initialize(graph, teams) super(graph) @teams = teams end - + def build @teams.each do |team| team_skills = [] @@ -23,13 +23,13 @@ def build @output << team_skills end end - + def prior_schedule Schedules::Sequence.new(@factors.map { |f| Schedules::Step.new(f, 0) }) end - + end - + end end end diff --git a/spec/saulabs/trueskill/factor_graph_spec.rb b/spec/saulabs/trueskill/factor_graph_spec.rb index 09698c4..d0d4c4b 100644 --- a/spec/saulabs/trueskill/factor_graph_spec.rb +++ b/spec/saulabs/trueskill/factor_graph_spec.rb @@ -2,137 +2,129 @@ require File.expand_path('spec/spec_helper.rb') describe Saulabs::TrueSkill::FactorGraph do - + before :each do @teams = create_teams - @skill = @teams.first.first - @graph = TrueSkill::FactorGraph.new(@teams, [1,2,3]) + @skill = @teams[0][0] + @results = { @team1 => 1, @team2 => 2, @team3 => 3 } + @graph = TrueSkill::FactorGraph.new(@results) end - + describe "#update_skills" do - + it "should update the mean of the first player in team1 to 30.38345" do @graph.update_skills @skill.mean.should be_within(tolerance).of(30.38345) end - + it "should update the deviation of the first player in team1 to 3.46421" do @graph.update_skills @skill.deviation.should be_within(tolerance).of(3.46421) end - + end - + describe "#draw_margin" do - + it "should be -0.998291 for diff 0.740466" do @graph.draw_margin.should be_within(tolerance).of(0.740466) end - + end - + end describe Saulabs::TrueSkill::FactorGraph, "two players" do - + before :each do - @teams = [ - [TrueSkill::Rating.new(25.0, 25.0/3.0, 1.0, 25.0/300.0)], - [TrueSkill::Rating.new(25.0, 25.0/3.0, 1.0, 25.0/300.0)] - ] + team1 = [TrueSkill::Rating.new(25.0, 25.0/3.0, 1.0, 25.0/300.0)] + team2 = [TrueSkill::Rating.new(25.0, 25.0/3.0, 1.0, 25.0/300.0)] + @teams = [team1,team2] + @results = { team1 => 1, team2 => 2 } end - + describe 'win with standard rating' do - + before :each do - TrueSkill::FactorGraph.new(@teams, [1,2]).update_skills + TrueSkill::FactorGraph.new(@results).update_skills end - + it "should change first players rating to [29.395832, 7.1714755]" do @teams[0][0].should eql_rating(29.395832, 7.1714755) end - + it "should change second players rating to [20.6041679, 7.1714755]" do @teams[1][0].should eql_rating(20.6041679, 7.1714755) end - + end - + describe 'draw with standard rating' do - + before :each do - TrueSkill::FactorGraph.new(@teams, [1,1]).update_skills + team1 = [TrueSkill::Rating.new(25.0, 25.0/3.0, 1.0, 25.0/300.0)] + team2 = [TrueSkill::Rating.new(25.0, 25.0/3.0, 1.0, 25.0/300.0)] + @teams = [ team1, team2] + results = { team1 => 1, team2 => 1 } + TrueSkill::FactorGraph.new(results).update_skills end - + it "should change first players rating to [25.0, 6.4575196]" do @teams[0][0].should eql_rating(25.0, 6.4575196) end - + it "should change second players rating to [25.0, 6.4575196]" do @teams[1][0].should eql_rating(25.0, 6.4575196) end - + end - + describe 'draw with different ratings' do - + before :each do - @teams[1][0] = TrueSkill::Rating.new(50.0, 12.5, 1.0, 25.0/300.0) - TrueSkill::FactorGraph.new(@teams, [1,1]).update_skills + team1 = [TrueSkill::Rating.new(25.0, 25.0/3.0, 1.0, 25.0/300.0)] + team2 = [TrueSkill::Rating.new(50.0, 12.5, 1.0, 25.0/300.0)] + @teams = [team1,team2] + results = { team1 => 1, team2 => 1 } + TrueSkill::FactorGraph.new(results).update_skills end - + it "should change first players rating to [31.6623, 7.1374]" do @teams[0][0].should eql_rating(31.662301, 7.1374459) end - + it "should change second players mean to [35.0107, 7.9101]" do @teams[1][0].should eql_rating(35.010653, 7.910077) end - + end - + end describe Saulabs::TrueSkill::FactorGraph, "1 vs 2, skills are additive, standard rating" do + before :each do + team1 = [ TrueSkill::Rating.new(25.0, 25.0/3.0, 1.0, 25.0/300.0), ] + team2 = [ + TrueSkill::Rating.new(25.0, 25.0/3.0, 1.0, 25.0/300.0), + TrueSkill::Rating.new(25.0, 25.0/3.0, 1.0, 25.0/300.0) + ] - - before :each do - @teams = [ - [ - TrueSkill::Rating.new(25.0, 25.0/3.0, 1.0, 25.0/300.0), - - ], - [ - TrueSkill::Rating.new(25.0, 25.0/3.0, 1.0, 25.0/300.0), - TrueSkill::Rating.new(25.0, 25.0/3.0, 1.0, 25.0/300.0) - + @teams = [team1, team2] + @results = { team1 => 1, team2 => 1 } + end - ] - ] + describe "#@skill_update" do + it "should have a Boolean @skills_additive = false" do + @graph = TrueSkill::FactorGraph.new(@results, {:skills_additive => false}) + @graph.skills_additive.should be_false end - describe "#@skill_update" do - - - it "should have a Boolean @skills_additive = false" do - @graph = TrueSkill::FactorGraph.new(@teams, [1,1], {:skills_additive => false}) - @graph.skills_additive.should be_false - end - - - - it "should update the mean of the first player in team1 to 25.0 after draw" do - @graph = TrueSkill::FactorGraph.new(@teams, [1,1], {:skills_additive => false}) - - @graph.update_skills - @teams[0][0].mean.should be_within(tolerance).of(25.0) - - end - - - - + it "should update the mean of the first player in team1 to 25.0 after draw" do + @graph = TrueSkill::FactorGraph.new(@results, {:skills_additive => false}) + @graph.update_skills + @teams[0][0].mean.should be_within(tolerance).of(25.0) + end end end diff --git a/spec/saulabs/trueskill/layers/prior_to_skills_spec.rb b/spec/saulabs/trueskill/layers/prior_to_skills_spec.rb index 19f3649..9d638a4 100644 --- a/spec/saulabs/trueskill/layers/prior_to_skills_spec.rb +++ b/spec/saulabs/trueskill/layers/prior_to_skills_spec.rb @@ -2,39 +2,40 @@ require File.expand_path('spec/spec_helper.rb') describe TrueSkill::Layers::PriorToSkills do - - before :each do + + before :each do @teams = create_teams - @graph = TrueSkill::FactorGraph.new(@teams, [1,2,3]) + @results = {@team1 => 1, @team2 => 2, @team3 => 3} + @graph = TrueSkill::FactorGraph.new(@results) @layer = TrueSkill::Layers::PriorToSkills.new(@graph, @teams) end - + describe "#build" do - + it "should add 4 factors" do lambda { @layer.build }.should change(@layer.factors, :size).by(4) end - + it "should add 3 output variables" do lambda { @layer.build }.should change(@layer.output, :size).by(3) end - + end - + describe "#prior_schedule" do - - before :each do + + before :each do @layer.build end - + it "should return a sequence-schedule" do @layer.prior_schedule.should be_kind_of(TrueSkill::Schedules::Sequence) end - + end - + end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0142064..a0bfb50 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -15,16 +15,8 @@ def tolerance end def create_teams - [ - [ - TrueSkill::Rating.new(25, 4.1) - ], - [ - TrueSkill::Rating.new(27, 3.1), - TrueSkill::Rating.new(10, 1.0) - ], - [ - TrueSkill::Rating.new(32, 0.2) - ] - ] + @team1 = [ TrueSkill::Rating.new(25, 4.1) ] + @team2 = [ TrueSkill::Rating.new(27, 3.1), TrueSkill::Rating.new(10, 1.0) ] + @team3 = [ TrueSkill::Rating.new(32, 0.2) ] + [@team1, @team2, @team3] end