From a0d1b2206e1ab2e07d1fbabe17b5892de8f31aac Mon Sep 17 00:00:00 2001 From: liujin Date: Sat, 5 Jan 2013 09:41:39 +0800 Subject: [PATCH 1/8] DRY spec --- spec/alternative_spec.rb | 57 +++++++++++--------- spec/dashboard_spec.rb | 36 +++++++++---- spec/experiment_spec.rb | 114 ++++++++++++++++++--------------------- spec/helper_spec.rb | 23 ++------ 4 files changed, 113 insertions(+), 117 deletions(-) diff --git a/spec/alternative_spec.rb b/spec/alternative_spec.rb index b7ef1bad..b7077c5c 100644 --- a/spec/alternative_spec.rb +++ b/spec/alternative_spec.rb @@ -3,6 +3,23 @@ describe Split::Alternative do + let(:alternative) { + Split::Alternative.new('Basket', 'basket_text') + } + + let(:alternative2) { + Split::Alternative.new('Cart', 'basket_text') + } + + let(:experiment) { + Split::Experiment.find_or_create('basket_text', 'Basket', "Cart") + } + + # setup experiment + before do + experiment + end + it "should have a name" do experiment = Split::Experiment.new('basket_text', :alternative_names => ['Basket', "Cart"]) alternative = Split::Alternative.new('Basket', 'basket_text') @@ -14,20 +31,20 @@ alternative = Split::Alternative.new('Basket', 'basket_text') alternative.name.should eql('Basket') end - + describe 'weights' do - + it "should set the weights" do experiment = Split::Experiment.new('basket_text', :alternative_names => [{'Basket' => 0.6}, {"Cart" => 0.4}]) first = experiment.alternatives[0] first.name.should == 'Basket' first.weight.should == 0.6 - + second = experiment.alternatives[1] second.name.should == 'Cart' - second.weight.should == 0.4 + second.weight.should == 0.4 end - + it "accepts probability on alternatives" do Split.configuration.experiments = { :my_experiment => { @@ -42,12 +59,12 @@ first = experiment.alternatives[0] first.name.should == 'control_opt' first.weight.should == 0.67 - + second = experiment.alternatives[1] second.name.should == 'second_opt' second.weight.should == 0.1 end - + # it "accepts probability on some alternatives" do # Split.configuration.experiments[:my_experiment] = { # :alternatives => [ @@ -60,7 +77,7 @@ # should start_experiment(:my_experiment).with({"control_opt" => 0.34}, {"second_opt" => 0.215}, {"third_opt" => 0.23}, {"fourth_opt" => 0.215}) # ab_test :my_experiment # end - # + # # it "allows name param without probability" do # Split.configuration.experiments[:my_experiment] = { # :alternatives => [ @@ -72,19 +89,17 @@ # should start_experiment(:my_experiment).with({"control_opt" => 0.18}, {"second_opt" => 0.18}, {"third_opt" => 0.64}) # ab_test :my_experiment # end - + it "should set the weights from a configuration file" do - + end end it "should have a default participation count of 0" do - alternative = Split::Alternative.new('Basket', 'basket_text') alternative.participant_count.should eql(0) end it "should have a default completed count of 0" do - alternative = Split::Alternative.new('Basket', 'basket_text') alternative.completed_count.should eql(0) end @@ -96,7 +111,6 @@ end it "should save to redis" do - alternative = Split::Alternative.new('Basket', 'basket_text') alternative.save Split.redis.exists('basket_text:Basket').should be true end @@ -109,7 +123,7 @@ alternative.increment_participation alternative.participant_count.should eql(old_participant_count+1) - Split::Alternative.new('Basket', 'basket_text').participant_count.should eql(old_participant_count+1) + alternative.participant_count.should eql(old_participant_count+1) end it "should increment completed count" do @@ -120,11 +134,10 @@ alternative.increment_completion alternative.completed_count.should eql(old_completed_count+1) - Split::Alternative.new('Basket', 'basket_text').completed_count.should eql(old_completed_count+1) + alternative.completed_count.should eql(old_completed_count+1) end it "can be reset" do - alternative = Split::Alternative.new('Basket', 'basket_text') alternative.participant_count = 10 alternative.completed_count = 4 alternative.reset @@ -137,8 +150,7 @@ experiment.save alternative = Split::Alternative.new('Basket', 'basket_text') alternative.control?.should be_true - alternative = Split::Alternative.new('Cart', 'basket_text') - alternative.control?.should be_false + alternative2.control?.should be_false end describe 'unfinished_count' do @@ -153,13 +165,11 @@ describe 'conversion rate' do it "should be 0 if there are no conversions" do - alternative = Split::Alternative.new('Basket', 'basket_text') alternative.completed_count.should eql(0) alternative.conversion_rate.should eql(0) end it "calculate conversion rate" do - alternative = Split::Alternative.new('Basket', 'basket_text') alternative.stub(:participant_count).and_return(10) alternative.stub(:completed_count).and_return(4) alternative.conversion_rate.should eql(0.4) @@ -168,15 +178,10 @@ describe 'z score' do it 'should be zero when the control has no conversions' do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') - - alternative = Split::Alternative.new('red', 'link_color') - alternative.z_score.should eql(0) + alternative2.z_score.should eql(0) end it "should be N/A for the control" do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') - control = experiment.control control.z_score.should eql('N/A') end diff --git a/spec/dashboard_spec.rb b/spec/dashboard_spec.rb index 2d63a3df..0e8c75ad 100644 --- a/spec/dashboard_spec.rb +++ b/spec/dashboard_spec.rb @@ -5,6 +5,22 @@ describe Split::Dashboard do include Rack::Test::Methods + let(:link_color) { + Split::Experiment.find_or_create('link_color', 'blue', 'red') + } + + def link(color) + Split::Alternative.new(color, 'link_color') + end + + let(:red_link) { + link("red") + } + + let(:blue_link) { + link("blue") + } + def app @app ||= Split::Dashboard end @@ -15,33 +31,31 @@ def app end it "should reset an experiment" do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') + experiment = link_color - red = Split::Alternative.new('red', 'link_color') - blue = Split::Alternative.new('blue', 'link_color') - red.participant_count = 5 - blue.participant_count = 6 + red_link.participant_count = 5 + blue_link.participant_count = 6 post '/reset/link_color' last_response.should be_redirect - new_red_count = Split::Alternative.new('red', 'link_color').participant_count - new_blue_count = Split::Alternative.new('blue', 'link_color').participant_count + new_red_count = red_link.participant_count + new_blue_count = blue_link.participant_count new_blue_count.should eql(0) new_red_count.should eql(0) end it "should delete an experiment" do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') + experiment = link_color delete '/link_color' last_response.should be_redirect Split::Experiment.find('link_color').should be_nil end it "should mark an alternative as the winner" do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') + experiment = link_color experiment.winner.should be_nil post '/link_color', :alternative => 'red' @@ -53,7 +67,7 @@ def app it "should display the start date" do experiment_start_time = Time.parse('2011-07-07') Time.stub(:now => experiment_start_time) - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') + experiment = link_color get '/' @@ -63,7 +77,7 @@ def app it "should handle experiments without a start date" do experiment_start_time = Time.parse('2011-07-07') Time.stub(:now => experiment_start_time) - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') + experiment = link_color Split.redis.hdel(:experiment_start_times, experiment.name) diff --git a/spec/experiment_spec.rb b/spec/experiment_spec.rb index 2ea72075..f70bda44 100644 --- a/spec/experiment_spec.rb +++ b/spec/experiment_spec.rb @@ -6,7 +6,7 @@ describe Split::Experiment do context "with an experiment" do let(:experiment) { Split::Experiment.new('basket_text', :alternative_names => ['Basket', "Cart"]) } - + it "should have a name" do experiment.name.should eql('basket_text') end @@ -14,15 +14,15 @@ it "should have alternatives" do experiment.alternatives.length.should be 2 end - + it "should have alternatives with correct names" do experiment.alternatives.collect{|a| a.name}.should == ['Basket', 'Cart'] end - + it "should be resettable by default" do experiment.resettable.should be_true end - + it "should save to redis" do experiment.save Split.redis.exists('basket_text').should be true @@ -35,10 +35,10 @@ Split::Experiment.find('basket_text').start_time.should == experiment_start_time end - + it "should save the selected algorithm to redis" do experiment_algorithm = Split::Algorithms::Whiplash - experiment.algorithm = experiment_algorithm + experiment.algorithm = experiment_algorithm experiment.save Split::Experiment.find('basket_text').algorithm.should == experiment_algorithm @@ -60,7 +60,7 @@ Split.redis.exists('basket_text').should be true Split.redis.lrange('basket_text', 0, -1).should eql(['Basket', "Cart"]) end - + describe 'new record?' do it "should know if it hasn't been saved yet" do experiment.new_record?.should be_true @@ -71,7 +71,7 @@ experiment.new_record?.should be_false end end - + describe 'find' do it "should return an existing experiment" do experiment.save @@ -82,7 +82,7 @@ Split::Experiment.find('non_existent_experiment').should be_nil end end - + describe 'control' do it 'should be the first alternative' do experiment.save @@ -95,75 +95,71 @@ experiment = Split::Experiment.new('basket_text', :alternative_names => ['Basket', "Cart"], :algorithm => Split::Algorithms::Whiplash) experiment.algorithm.should == Split::Algorithms::Whiplash end - + it "should be possible to make an experiment not resettable" do experiment = Split::Experiment.new('basket_text', :alternative_names => ['Basket', "Cart"], :resettable => false) - experiment.resettable.should be_false + experiment.resettable.should be_false end end - + describe 'persistent configuration' do - + it "should persist resettable in redis" do experiment = Split::Experiment.new('basket_text', :alternative_names => ['Basket', "Cart"], :resettable => false) experiment.save - + e = Split::Experiment.find('basket_text') e.should == experiment e.resettable.should be_false - + end - + it "should persist algorithm in redis" do experiment = Split::Experiment.new('basket_text', :alternative_names => ['Basket', "Cart"], :algorithm => Split::Algorithms::Whiplash) experiment.save - + e = Split::Experiment.find('basket_text') e.should == experiment e.algorithm.should == Split::Algorithms::Whiplash end end - - - + + + describe 'deleting' do it 'should delete itself' do experiment = Split::Experiment.new('basket_text', :alternative_names => [ 'Basket', "Cart"]) experiment.save experiment.delete - Split.redis.exists('basket_text').should be false - Split::Experiment.find('basket_text').should be_nil + Split.redis.exists('link_color').should be false + Split::Experiment.find('link_color').should be_nil end it "should increment the version" do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green') experiment.version.should eql(0) experiment.delete experiment.version.should eql(1) end end + describe 'winner' do it "should have no winner initially" do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') experiment.winner.should be_nil end it "should allow you to specify a winner" do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') + experiment.save experiment.winner = 'red' - - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') experiment.winner.name.should == 'red' end end describe 'reset' do + before { experiment.save } it 'should reset all alternatives' do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green') - green = Split::Alternative.new('green', 'link_color') experiment.winner = 'green' experiment.next_alternative.name.should eql('green') @@ -171,14 +167,11 @@ experiment.reset - reset_green = Split::Alternative.new('green', 'link_color') - reset_green.participant_count.should eql(0) - reset_green.completed_count.should eql(0) + green.participant_count.should eql(0) + green.completed_count.should eql(0) end it 'should reset the winner' do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green') - green = Split::Alternative.new('green', 'link_color') experiment.winner = 'green' experiment.next_alternative.name.should eql('green') @@ -190,29 +183,28 @@ end it "should increment the version" do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green') experiment.version.should eql(0) experiment.reset experiment.version.should eql(1) end end - + describe 'algorithm' do let(:experiment) { Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green') } - + it 'should use the default algorithm if none is specified' do experiment.algorithm.should == Split.configuration.algorithm end - + it 'should use the user specified algorithm for this experiment if specified' do experiment.algorithm = Split::Algorithms::Whiplash experiment.algorithm.should == Split::Algorithms::Whiplash end end - + describe 'next_alternative' do let(:experiment) { Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green') } - + it "should always return the winner if one exists" do green = Split::Alternative.new('green', 'link_color') experiment.winner = 'green' @@ -220,42 +212,42 @@ experiment.next_alternative.name.should eql('green') green.increment_participation - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green') experiment.next_alternative.name.should eql('green') end - + it "should use the specified algorithm if a winner does not exist" do Split.configuration.algorithm.should_receive(:choose_alternative).and_return(Split::Alternative.new('green', 'link_color')) experiment.next_alternative.name.should eql('green') end end - + describe 'single alternative' do let(:experiment) { Split::Experiment.find_or_create('link_color', 'blue') } - + it "should always return the color blue" do experiment.next_alternative.name.should eql('blue') - end + end end describe 'changing an existing experiment' do + def same_but_different + Split::Experiment.find_or_create('link_color', 'blue', 'yellow', 'orange') + end + it "should reset an experiment if it is loaded with different alternatives" do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green') - blue = Split::Alternative.new('blue', 'link_color') + experiment.save blue.participant_count = 5 - blue.save - same_experiment = Split::Experiment.find_or_create('link_color', 'blue', 'yellow', 'orange') + same_experiment = same_but_different same_experiment.alternatives.map(&:name).should eql(['blue', 'yellow', 'orange']) - new_blue = Split::Alternative.new('blue', 'link_color') - new_blue.participant_count.should eql(0) + blue.participant_count.should eql(0) end it "should only reset once" do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green') + experiment.save experiment.version.should eql(0) - same_experiment = Split::Experiment.find_or_create('link_color', 'blue', 'yellow', 'orange') + same_experiment = same_but_different same_experiment.version.should eql(1) - same_experiment_again = Split::Experiment.find_or_create('link_color', 'blue', 'yellow', 'orange') + same_experiment_again = same_but_different same_experiment_again.version.should eql(1) end end @@ -268,18 +260,18 @@ end describe 'specifying weights' do - it "should work for a new experiment" do - experiment = Split::Experiment.find_or_create('link_color', {'blue' => 1}, {'red' => 2 }) + let(:experiment_with_weight) { + Split::Experiment.find_or_create('link_color', {'blue' => 1}, {'red' => 2 }) + } - experiment.alternatives.map(&:weight).should == [1, 2] + it "should work for a new experiment" do + experiment_with_weight.alternatives.map(&:weight).should == [1, 2] end it "should work for an existing experiment" do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') experiment.save - - same_experiment = Split::Experiment.find_or_create('link_color', {'blue' => 1}, {'red' => 2 }) - same_experiment.alternatives.map(&:weight).should == [1, 2] + experiment_with_weight.alternatives.map(&:weight).should == [1, 2] end end + end diff --git a/spec/helper_spec.rb b/spec/helper_spec.rb index 05b9720a..ede73ab5 100755 --- a/spec/helper_spec.rb +++ b/spec/helper_spec.rb @@ -5,6 +5,10 @@ describe Split::Helper do include Split::Helper + let(:experiment) { + Split::Experiment.find_or_create('link_color', 'blue', 'red') + } + describe "ab_test" do it "should not raise an error when passed strings for alternatives" do @@ -25,7 +29,6 @@ end it "should increment the participation counter after assignment to a new user" do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') previous_red_count = Split::Alternative.new('red', 'link_color').participant_count previous_blue_count = Split::Alternative.new('blue', 'link_color').participant_count @@ -39,21 +42,18 @@ end it "should return the given alternative for an existing user" do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') alternative = ab_test('link_color', 'blue', 'red') repeat_alternative = ab_test('link_color', 'blue', 'red') alternative.should eql repeat_alternative end it 'should always return the winner if one is present' do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') experiment.winner = "orange" ab_test('link_color', 'blue', 'red').should == 'orange' end it "should allow the alternative to be force by passing it in the params" do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') @params = {'link_color' => 'blue'} alternative = ab_test('link_color', 'blue', 'red') alternative.should eql('blue') @@ -107,7 +107,6 @@ end it "should not over-write a finished key when an experiment is on a later version" do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') experiment.increment_version ab_user = { experiment.key => 'blue', experiment.finished_key => true } finshed_session = ab_user.dup @@ -262,7 +261,6 @@ def should_finish_experiment(experiment_name, should_finish=true) describe 'conversions' do it 'should return a conversion rate for an alternative' do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') alternative_name = ab_test('link_color', 'blue', 'red') previous_convertion_rate = Split::Alternative.new(alternative_name, 'link_color').conversion_rate @@ -282,13 +280,11 @@ def should_finish_experiment(experiment_name, should_finish=true) describe 'ab_test' do it 'should return the control' do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') alternative = ab_test('link_color', 'blue', 'red') alternative.should eql experiment.control.name end it "should not increment the participation count" do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') previous_red_count = Split::Alternative.new('red', 'link_color').participant_count previous_blue_count = Split::Alternative.new('blue', 'link_color').participant_count @@ -304,7 +300,6 @@ def should_finish_experiment(experiment_name, should_finish=true) describe 'finished' do it "should not increment the completed count" do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') alternative_name = ab_test('link_color', 'blue', 'red') previous_completion_count = Split::Alternative.new(alternative_name, 'link_color').completed_count @@ -328,13 +323,11 @@ def should_finish_experiment(experiment_name, should_finish=true) describe 'ab_test' do it 'should return the control' do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') alternative = ab_test('link_color', 'blue', 'red') alternative.should eql experiment.control.name end it "should not increment the participation count" do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') previous_red_count = Split::Alternative.new('red', 'link_color').participant_count previous_blue_count = Split::Alternative.new('blue', 'link_color').participant_count @@ -350,7 +343,6 @@ def should_finish_experiment(experiment_name, should_finish=true) describe 'finished' do it "should not increment the completed count" do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') alternative_name = ab_test('link_color', 'blue', 'red') previous_completion_count = Split::Alternative.new(alternative_name, 'link_color').completed_count @@ -366,14 +358,12 @@ def should_finish_experiment(experiment_name, should_finish=true) describe 'versioned experiments' do it "should use version zero if no version is present" do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') alternative_name = ab_test('link_color', 'blue', 'red') experiment.version.should eql(0) ab_user.should eql({'link_color' => alternative_name}) end it "should save the version of the experiment to the session" do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') experiment.reset experiment.version.should eql(1) alternative_name = ab_test('link_color', 'blue', 'red') @@ -381,7 +371,6 @@ def should_finish_experiment(experiment_name, should_finish=true) end it "should load the experiment even if the version is not 0" do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') experiment.reset experiment.version.should eql(1) alternative_name = ab_test('link_color', 'blue', 'red') @@ -391,7 +380,6 @@ def should_finish_experiment(experiment_name, should_finish=true) end it "should reset the session of a user on an older version of the experiment" do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') alternative_name = ab_test('link_color', 'blue', 'red') ab_user.should eql({'link_color' => alternative_name}) alternative = Split::Alternative.new(alternative_name, 'link_color') @@ -409,7 +397,6 @@ def should_finish_experiment(experiment_name, should_finish=true) end it "should cleanup old versions of experiments from the session" do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') alternative_name = ab_test('link_color', 'blue', 'red') ab_user.should eql({'link_color' => alternative_name}) alternative = Split::Alternative.new(alternative_name, 'link_color') @@ -425,7 +412,6 @@ def should_finish_experiment(experiment_name, should_finish=true) end it "should only count completion of users on the current version" do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') alternative_name = ab_test('link_color', 'blue', 'red') ab_user.should eql({'link_color' => alternative_name}) alternative = Split::Alternative.new(alternative_name, 'link_color') @@ -667,7 +653,6 @@ def should_finish_experiment(experiment_name, should_finish=true) end it 'should handle multiple experiments correctly' do - experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red') experiment2 = Split::Experiment.find_or_create('link_color2', 'blue', 'red') alternative_name = ab_test('link_color', 'blue', 'red') alternative_name2 = ab_test('link_color2', 'blue', 'red') From 36c32d1603d99a32fb2b05e628c55046d4d6a6e4 Mon Sep 17 00:00:00 2001 From: liujin Date: Sat, 5 Jan 2013 16:55:49 +0800 Subject: [PATCH 2/8] Implement multiple goals for an experiment --- lib/split/alternative.rb | 59 +++++++++++++++-- lib/split/dashboard/public/style.css | 12 +++- lib/split/dashboard/views/_experiment.erb | 53 +++++++++------ lib/split/dashboard/views/index.erb | 9 ++- lib/split/experiment.rb | 81 ++++++++++++++++------- lib/split/helper.rb | 10 +++ spec/alternative_spec.rb | 45 +++++++++++-- spec/experiment_spec.rb | 36 ++++++++-- spec/helper_spec.rb | 67 ++++++++++++++++--- 9 files changed, 298 insertions(+), 74 deletions(-) diff --git a/lib/split/alternative.rb b/lib/split/alternative.rb index e50e976a..f3b0c581 100644 --- a/lib/split/alternative.rb +++ b/lib/split/alternative.rb @@ -19,6 +19,10 @@ def to_s name end + def goals + self.experiment.goals + end + def participant_count @participant_count ||= Split.redis.hget(key, 'participant_count').to_i end @@ -28,41 +32,75 @@ def participant_count=(count) Split.redis.hset(key, 'participant_count', count.to_i) end +<<<<<<< HEAD def completed_count @completed_count ||= Split.redis.hget(key, 'completed_count').to_i +======= + def completed_count(goal = nil) + field = set_field(goal) + Split.redis.hget(key, field).to_i + end + + def all_completed_count + if goals.empty? + completed_count + else + goals.inject(completed_count) do |sum, g| + sum + completed_count(g) + end + end +>>>>>>> Implement multiple goals for an experiment end - + def unfinished_count - participant_count - completed_count + participant_count - all_completed_count + end + + def set_field(goal) + field = "completed_count" + field += ":" + goal unless goal.nil? + return field end +<<<<<<< HEAD def completed_count=(count) @completed_count = count Split.redis.hset(key, 'completed_count', count.to_i) +======= + def set_completed_count (count, goal = nil) + field = set_field(goal) + Split.redis.hset(key, field, count.to_i) +>>>>>>> Implement multiple goals for an experiment end def increment_participation @participant_count = Split.redis.hincrby key, 'participant_count', 1 end +<<<<<<< HEAD def increment_completion @completed_count = Split.redis.hincrby key, 'completed_count', 1 +======= + def increment_completion(goal = nil) + field = set_field(goal) + Split.redis.hincrby(key, field, 1) +>>>>>>> Implement multiple goals for an experiment end def control? experiment.control.name == self.name end - def conversion_rate + def conversion_rate(goal = nil) return 0 if participant_count.zero? - (completed_count.to_f/participant_count.to_f) + (completed_count(goal).to_f)/participant_count.to_f end def experiment Split::Experiment.find(experiment_name) end - def z_score + def z_score(goal = nil) # CTR_E = the CTR within the experiment split # CTR_C = the CTR within the control split # E = the number of impressions within the experiment split @@ -74,8 +112,9 @@ def z_score return 'N/A' if control.name == alternative.name - ctr_e = alternative.conversion_rate - ctr_c = control.conversion_rate + ctr_e = alternative.conversion_rate(goal) + ctr_c = control.conversion_rate(goal) + e = alternative.participant_count c = control.participant_count @@ -96,6 +135,12 @@ def reset @participant_count = nil @completed_count = nil Split.redis.hmset key, 'participant_count', 0, 'completed_count', 0 + unless goals.empty? + goals.each do |g| + field = "completed_count:#{g}" + Split.redis.hset key, field, 0 + end + end end def delete diff --git a/lib/split/dashboard/public/style.css b/lib/split/dashboard/public/style.css index 2afce084..7cb46da4 100644 --- a/lib/split/dashboard/public/style.css +++ b/lib/split/dashboard/public/style.css @@ -160,6 +160,10 @@ body { margin:30px 0; } +.experiment_with_goal { + margin: -32px 0 30px 0; +} + .experiment .experiment-header { background: #f4f4f4; background: -webkit-gradient(linear, left top, left bottom, @@ -177,14 +181,18 @@ body { .experiment h2 { color:#888; - margin: 12px 0 0; + margin: 12px 0 12px 0; font-size: 1em; font-weight:bold; float:left; text-shadow:0 1px 0 rgba(255,255,255,0.8); } -.experiment h2 .version{ +.experiment h2 .goal { + font-style: italic; +} + +.experiment h2 .version { font-style:italic; font-size:0.8em; color:#bbb; diff --git a/lib/split/dashboard/views/_experiment.erb b/lib/split/dashboard/views/_experiment.erb index 1af6686c..3a6c9547 100644 --- a/lib/split/dashboard/views/_experiment.erb +++ b/lib/split/dashboard/views/_experiment.erb @@ -1,20 +1,28 @@ -
+<% unless goal.nil? %> + <% experiment_class = "experiment experiment_with_goal" %> +<% else %> + <% experiment_class = "experiment" %> +<% end %> +

Experiment: <%= experiment.name %> <% if experiment.version > 1 %>v<%= experiment.version %><% end %> + <% unless goal.nil? %>Goal:<%= goal %><% end %>

-
- <%= experiment.start_time ? experiment.start_time.strftime('%Y-%m-%d') : 'Unknown' %> -
" method='post' onclick="return confirmReset()"> - -
-
" method='post' onclick="return confirmDelete()"> - - -
-
+ <% if goal.nil? %> +
+ <%= experiment.start_time ? experiment.start_time.strftime('%Y-%m-%d') : 'Unknown' %> +
" method='post' onclick="return confirmReset()"> + +
+
" method='post' onclick="return confirmDelete()"> + + +
+
+ <% end %>
@@ -27,7 +35,7 @@ - <% total_participants = total_completed = 0 %> + <% total_participants = total_completed = total_unfinished = 0 %> <% experiment.alternatives.each do |alternative| %> - + <% total_participants += alternative.participant_count %> - <% total_completed += alternative.completed_count %> + <% total_unfinished += alternative.unfinished_count %> + <% total_completed += alternative.completed_count(goal) %> <% end %> - + diff --git a/lib/split/dashboard/views/index.erb b/lib/split/dashboard/views/index.erb index e7804203..e7d12b14 100644 --- a/lib/split/dashboard/views/index.erb +++ b/lib/split/dashboard/views/index.erb @@ -2,7 +2,14 @@

The list below contains all the registered experiments along with the number of test participants, completed and conversion rate currently in the system.

<% @experiments.each do |experiment| %> - <%= erb :_experiment, :locals => {:experiment => experiment} %> + <% if experiment.goals.empty? %> + <%= erb :_experiment, :locals => {:goal => nil, :experiment => experiment} %> + <% else %> + <%= erb :_experiment, :locals => {:goal => nil, :experiment => experiment} %> + <% experiment.goals.each do |g| %> + <%= erb :_experiment, :locals => {:goal => g, :experiment => experiment} %> + <% end %> + <% end %> <% end %> <% else %>

No experiments have started yet, you need to define them in your code and introduce them to your users.

diff --git a/lib/split/experiment.rb b/lib/split/experiment.rb index 66034ef0..380eb475 100644 --- a/lib/split/experiment.rb +++ b/lib/split/experiment.rb @@ -4,31 +4,31 @@ class Experiment attr_writer :algorithm attr_accessor :resettable - def initialize(name, options = {}) + def initialize(name, options = {}) options = { :resettable => true, }.merge(options) - - @name = name.to_s + + @name = name.to_s @alternatives = options[:alternatives] if !options[:alternatives].nil? - - if !options[:algorithm].nil? + + if !options[:algorithm].nil? @algorithm = options[:algorithm].is_a?(String) ? options[:algorithm].constantize : options[:algorithm] end - + if !options[:resettable].nil? @resettable = options[:resettable].is_a?(String) ? options[:resettable] == 'true' : options[:resettable] end - - if !options[:alternative_names].nil? + + if !options[:alternative_names].nil? @alternatives = options[:alternative_names].map do |alternative| Split::Alternative.new(alternative, name) end end - - + + end - + def algorithm @algorithm ||= Split.configuration.algorithm end @@ -44,7 +44,7 @@ def winner nil end end - + def participant_count alternatives.inject(0){|sum,a| sum + a.participant_count} end @@ -65,9 +65,9 @@ def start_time t = Split.redis.hget(:experiment_start_times, @name) Time.parse(t) if t end - + def [](name) - alternatives.find{|a| a.name == name} + alternatives.find{|a| a.name == name} end def alternatives @@ -106,6 +106,14 @@ def key end end + def self.goals_key(name) + "#{name}:goals" + end + + def goals_key + self.class.goals_key(self.name) + end + def finished_key "#{key}:finished" end @@ -117,6 +125,7 @@ def resettable? def reset alternatives.each(&:reset) reset_winner + Split.redis.del(goals_key) increment_version end @@ -125,6 +134,7 @@ def delete reset_winner Split.redis.srem(:experiments, name) Split.redis.del(name) + Split.redis.del(goals_key) increment_version end @@ -136,10 +146,12 @@ def save if new_record? Split.redis.sadd(:experiments, name) Split.redis.hset(:experiment_start_times, @name, Time.now) - @alternatives.reverse.each {|a| Split.redis.lpush(name, a.name) } + @alternatives.reverse.each {|a| Split.redis.lpush(name, a.name)} + @goals.reverse.each {|a| Split.redis.lpush(goals_key, a)} unless @goals.nil? else Split.redis.del(name) - @alternatives.reverse.each {|a| Split.redis.lpush(name, a.name) } + @alternatives.reverse.each {|a| Split.redis.lpush(name, a.name)} + @goals.reverse.each {|a| Split.redis.lpush(goals_key, a)} unless @goals.nil? end config_key = Split::Experiment.experiment_config_key(name) Split.redis.hset(config_key, :resettable, resettable) @@ -179,18 +191,18 @@ def self.load_alternatives_from_redis_for(name) def self.load_from_configuration(name) exp_config = Split.configuration.experiment_for(name) || {} - self.new(name, :alternative_names => load_alternatives_for(name), - :resettable => exp_config[:resettable], + self.new(name, :alternative_names => load_alternatives_for(name), + :resettable => exp_config[:resettable], :algorithm => exp_config[:algorithm]) end def self.load_from_redis(name) exp_config = Split.redis.hgetall(experiment_config_key(name)) self.new(name, :alternative_names => load_alternatives_for(name), - :resettable => exp_config['resettable'], - :algorithm => exp_config['algorithm']) + :resettable => exp_config['resettable'], + :algorithm => exp_config['algorithm']) end - + def self.experiment_config_key(name) "experiment_configurations/#{name}" end @@ -219,8 +231,10 @@ def self.find(name) obj end - def self.find_or_create(key, *alternatives) - name = key.to_s.split(':')[0] + def self.find_or_create(name, *alternatives) + experiment_name_with_version, goals = normalize_experiment_name(name) + name = experiment_name_with_version.to_s.split(':')[0] + experiment_label = {name => goals} # KLUDGE: sneak in goals along with name to maintain backward compatibility if alternatives.length == 1 if alternatives[0].is_a? Hash @@ -229,6 +243,7 @@ def self.find_or_create(key, *alternatives) end alts = initialize_alternatives(alternatives, name) + gls = initialize_goals(goals) if Split.redis.exists(name) existing_alternatives = load_alternatives_for(name) @@ -249,6 +264,17 @@ def self.find_or_create(key, *alternatives) end + def self.normalize_experiment_name(name) + if Hash === name + experiment_name = name.keys.first + goals = name.values.first + else + experiment_name = name + goals = [] + end + return experiment_name, goals + end + def self.initialize_alternatives(alternatives, name) unless alternatives.all? { |a| Split::Alternative.valid?(a) } @@ -259,5 +285,14 @@ def self.initialize_alternatives(alternatives, name) Split::Alternative.new(alternative, name) end end + + def self.initialize_goals(goals) + raise ArgumentError, 'Goals must be an array' unless valid_goals?(goals) + goals + end + + def self.valid_goals?(goals) + Array === goals rescue false + end end end diff --git a/lib/split/helper.rb b/lib/split/helper.rb index b0a250c1..e384e751 100644 --- a/lib/split/helper.rb +++ b/lib/split/helper.rb @@ -132,6 +132,16 @@ def is_ignored_ip_address? end protected + def normalize_experiment(experiment) + if Hash === experiment + experiment_name = experiment.keys.first + goals = experiment.values.first + else + experiment_name = experiment + goals = [] + end + return experiment_name, goals + end def control_variable(control) Hash === control ? control.keys.first : control diff --git a/spec/alternative_spec.rb b/spec/alternative_spec.rb index b7077c5c..017b7103 100644 --- a/spec/alternative_spec.rb +++ b/spec/alternative_spec.rb @@ -12,14 +12,21 @@ } let(:experiment) { - Split::Experiment.find_or_create('basket_text', 'Basket', "Cart") + Split::Experiment.find_or_create({"basket_text" => ["purchase", "refund"]}, "Basket", "Cart") } + let(:goal1) { "purchase" } + let(:goal2) { "refund" } + # setup experiment before do experiment end + it "should have goals" do + alternative.goals.should eql(["purchase", "refund"]) + end + it "should have a name" do experiment = Split::Experiment.new('basket_text', :alternative_names => ['Basket', "Cart"]) alternative = Split::Alternative.new('Basket', 'basket_text') @@ -99,8 +106,10 @@ alternative.participant_count.should eql(0) end - it "should have a default completed count of 0" do + it "should have a default completed count of 0 for each goal" do alternative.completed_count.should eql(0) + alternative.completed_count(goal1).should eql(0) + alternative.completed_count(goal2).should eql(0) end it "should belong to an experiment" do @@ -132,16 +141,23 @@ alternative = Split::Alternative.new('Basket', 'basket_text') old_completed_count = alternative.participant_count alternative.increment_completion - alternative.completed_count.should eql(old_completed_count+1) + alternative.increment_completion(goal1) + alternative.increment_completion(goal2) - alternative.completed_count.should eql(old_completed_count+1) + alternative.completed_count.should eql(old_default_completed_count+1) + alternative.completed_count(goal1).should eql(old_completed_count_for_goal1+1) + alternative.completed_count(goal2).should eql(old_completed_count_for_goal2+1) end it "can be reset" do alternative.participant_count = 10 - alternative.completed_count = 4 + alternative.set_completed_count(4, goal1) + alternative.set_completed_count(5, goal2) + alternative.set_completed_count(6) alternative.reset alternative.participant_count.should eql(0) + alternative.completed_count(goal1).should eql(0) + alternative.completed_count(goal2).should eql(0) alternative.completed_count.should eql(0) end @@ -161,6 +177,15 @@ alternative.increment_participation alternative.unfinished_count.should eql(alternative.participant_count) end + + it "should return the correct unfinished_count" do + alternative.participant_count = 10 + alternative.set_completed_count(4, goal1) + alternative.set_completed_count(3, goal2) + alternative.set_completed_count(2) + + alternative.unfinished_count.should eql(1) + end end describe 'conversion rate' do @@ -173,17 +198,27 @@ alternative.stub(:participant_count).and_return(10) alternative.stub(:completed_count).and_return(4) alternative.conversion_rate.should eql(0.4) + + alternative.stub(:completed_count).with(goal1).and_return(5) + alternative.conversion_rate(goal1).should eql(0.5) + + alternative.stub(:completed_count).with(goal2).and_return(6) + alternative.conversion_rate(goal2).should eql(0.6) end end describe 'z score' do it 'should be zero when the control has no conversions' do alternative2.z_score.should eql(0) + alternative2.z_score(goal1).should eql(0) + alternative2.z_score(goal2).should eql(0) end it "should be N/A for the control" do control = experiment.control control.z_score.should eql('N/A') + control.z_score(goal1).should eql('N/A') + control.z_score(goal2).should eql('N/A') end end end diff --git a/spec/experiment_spec.rb b/spec/experiment_spec.rb index f70bda44..b4de617e 100644 --- a/spec/experiment_spec.rb +++ b/spec/experiment_spec.rb @@ -230,14 +230,14 @@ end describe 'changing an existing experiment' do - def same_but_different + def same_but_different_alternative Split::Experiment.find_or_create('link_color', 'blue', 'yellow', 'orange') end it "should reset an experiment if it is loaded with different alternatives" do experiment.save blue.participant_count = 5 - same_experiment = same_but_different + same_experiment = same_but_different_alternative same_experiment.alternatives.map(&:name).should eql(['blue', 'yellow', 'orange']) blue.participant_count.should eql(0) end @@ -245,9 +245,9 @@ def same_but_different it "should only reset once" do experiment.save experiment.version.should eql(0) - same_experiment = same_but_different + same_experiment = same_but_different_alternative same_experiment.version.should eql(1) - same_experiment_again = same_but_different + same_experiment_again = same_but_different_alternative same_experiment_again.version.should eql(1) end end @@ -274,4 +274,32 @@ def same_but_different end end + describe "specifying goals" do + let(:experiment) { + new_experiment(["purchase"]) + } + + context "saving experiment" do + def same_but_different_goals + Split::Experiment.find_or_create({'link_color' => ["purchase", "refund"]}, 'blue', 'red', 'green') + end + + before { experiment.save } + + it "can find existing experiment" do + Split::Experiment.find("link_color").name.should eql("link_color") + end + + it "should reset an experiment if it is loaded with different goals" do + same_experiment = same_but_different_goals + Split::Experiment.load_goals_for("link_color").should == ["purchase", "refund"] + end + + end + + it "should have goals" do + experiment.goals.should eql(["purchase"]) + end + end + end diff --git a/spec/helper_spec.rb b/spec/helper_spec.rb index ede73ab5..aeef54b7 100755 --- a/spec/helper_spec.rb +++ b/spec/helper_spec.rb @@ -23,6 +23,14 @@ lambda { ab_test('xyz', :a, :b, :c) }.should raise_error(ArgumentError) end + it "should not raise error when passed an array for experiment" do + lambda { ab_test({'link_color' => ["purchase", "refund"]}, 'blue', 'red') }.should_not raise_error + end + + it "should raise the appropriate error when passed string for experiment" do + lambda { ab_test({'link_color' => "purchase"}, 'blue', 'red') }.should raise_error(ArgumentError) + end + it "should assign a random alternative to a new user when there are an equal number of alternatives assigned" do ab_test('link_color', 'blue', 'red') ['red', 'blue'].should include(ab_user['link_color']) @@ -131,12 +139,12 @@ end it "should set experiment's finished key if reset is false" do - finished(@experiment_name, :reset => false) + finished(@experiment_name, {:reset => false}) ab_user.should eql(@experiment.key => @alternative_name, @experiment.finished_key => true) end it 'should not increment the counter if reset is false and the experiment has been already finished' do - 2.times { finished(@experiment_name, :reset => false) } + 2.times { finished(@experiment_name, {:reset => false}) } new_completion_count = Split::Alternative.new(@alternative_name, @experiment_name).completed_count new_completion_count.should eql(@previous_completion_count + 1) end @@ -149,7 +157,7 @@ it "should not clear out the users session if reset is false" do ab_user.should eql(@experiment.key => @alternative_name) - finished(@experiment_name, :reset => false) + finished(@experiment_name, {:reset => false}) ab_user.should eql(@experiment.key => @alternative_name, @experiment.finished_key => true) end @@ -563,7 +571,7 @@ def should_finish_experiment(experiment_name, should_finish=true) context "with preloaded config" do before { Split.configuration.experiments = {}} - + it "pulls options from config file" do Split.configuration.experiments[:my_experiment] = { :alternatives => [ "control_opt", "other_opt" ], @@ -571,7 +579,7 @@ def should_finish_experiment(experiment_name, should_finish=true) ab_test :my_experiment Split::Experiment.find(:my_experiment).alternative_names.should == [ "control_opt", "other_opt" ] end - + it "can be called multiple times" do Split.configuration.experiments[:my_experiment] = { :alternatives => [ "control_opt", "other_opt" ], @@ -581,7 +589,7 @@ def should_finish_experiment(experiment_name, should_finish=true) experiment.alternative_names.should == [ "control_opt", "other_opt" ] experiment.participant_count.should == 1 end - + it "accepts multiple alternatives" do Split.configuration.experiments[:my_experiment] = { :alternatives => [ "control_opt", "second_opt", "third_opt" ], @@ -590,7 +598,7 @@ def should_finish_experiment(experiment_name, should_finish=true) experiment = Split::Experiment.find(:my_experiment) experiment.alternative_names.should == [ "control_opt", "second_opt", "third_opt" ] end - + it "accepts probability on alternatives" do Split.configuration.experiments[:my_experiment] = { :alternatives => [ @@ -602,9 +610,9 @@ def should_finish_experiment(experiment_name, should_finish=true) ab_test :my_experiment experiment = Split::Experiment.find(:my_experiment) experiment.alternatives.collect{|a| [a.name, a.weight]}.should == [['control_opt', 0.67], ['second_opt', 0.1], ['third_opt', 0.23]] - + end - + it "accepts probability on some alternatives" do Split.configuration.experiments[:my_experiment] = { :alternatives => [ @@ -620,7 +628,7 @@ def should_finish_experiment(experiment_name, should_finish=true) names_and_weights.should == [['control_opt', 0.34], ['second_opt', 0.215], ['third_opt', 0.23], ['fourth_opt', 0.215]] names_and_weights.inject(0){|sum, nw| sum + nw[1]}.should == 1.0 end - + it "allows name param without probability" do Split.configuration.experiments[:my_experiment] = { :alternatives => [ @@ -662,4 +670,43 @@ def should_finish_experiment(experiment_name, should_finish=true) alt.unfinished_count.should eq(0) end end + + context "with goals" do + before do + @experiment = {'link_color' => ["purchase", "refund"]} + @alternatives = ['blue', 'red'] + @experiment_name, @goals = normalize_experiment(@experiment) + @goal1 = @goals[0] + @goal2 = @goals[1] + end + + it "should normalize experiment" do + @experiment_name.should eql("link_color") + @goals.should eql(["purchase", "refund"]) + end + + describe "ab_test" do + it "should allow experiment goals interface as a singel hash" do + ab_test(@experiment, *@alternatives) + experiment = Split::Experiment.find('link_color') + experiment.goals.should eql(['purchase', "refund"]) + end + end + + describe "finished" do + before do + @alternative_name = ab_test(@experiment, *@alternatives) + end + + it "should increment the counter for the specified-goal completed alternative" do + @previous_completion_count_for_goal1 = Split::Alternative.new(@alternative_name, @experiment_name).completed_count(@goal1) + @previous_completion_count_for_goal2 = Split::Alternative.new(@alternative_name, @experiment_name).completed_count(@goal2) + finished(@experiment) + new_completion_count_for_goal1 = Split::Alternative.new(@alternative_name, @experiment_name).completed_count(@goal1) + new_completion_count_for_goal1.should eql(@previous_completion_count_for_goal1 + 1) + new_completion_count_for_goal2 = Split::Alternative.new(@alternative_name, @experiment_name).completed_count(@goal2) + new_completion_count_for_goal2.should eql(@previous_completion_count_for_goal2 + 1) + end + end + end end From 004badad78df4369eab6219bcd2a7525517e0734 Mon Sep 17 00:00:00 2001 From: liujin Date: Mon, 14 Jan 2013 17:22:39 +0800 Subject: [PATCH 3/8] rebase from upstream/master --- lib/split/alternative.rb | 16 ----------- lib/split/experiment.rb | 58 ++++++++++++++++++++++++++++------------ lib/split/helper.rb | 21 ++++++++------- lib/split/trial.rb | 10 ++++--- 4 files changed, 60 insertions(+), 45 deletions(-) diff --git a/lib/split/alternative.rb b/lib/split/alternative.rb index f3b0c581..39ce2344 100644 --- a/lib/split/alternative.rb +++ b/lib/split/alternative.rb @@ -32,10 +32,6 @@ def participant_count=(count) Split.redis.hset(key, 'participant_count', count.to_i) end -<<<<<<< HEAD - def completed_count - @completed_count ||= Split.redis.hget(key, 'completed_count').to_i -======= def completed_count(goal = nil) field = set_field(goal) Split.redis.hget(key, field).to_i @@ -49,7 +45,6 @@ def all_completed_count sum + completed_count(g) end end ->>>>>>> Implement multiple goals for an experiment end def unfinished_count @@ -62,29 +57,18 @@ def set_field(goal) return field end -<<<<<<< HEAD - def completed_count=(count) - @completed_count = count - Split.redis.hset(key, 'completed_count', count.to_i) -======= def set_completed_count (count, goal = nil) field = set_field(goal) Split.redis.hset(key, field, count.to_i) ->>>>>>> Implement multiple goals for an experiment end def increment_participation @participant_count = Split.redis.hincrby key, 'participant_count', 1 end -<<<<<<< HEAD - def increment_completion - @completed_count = Split.redis.hincrby key, 'completed_count', 1 -======= def increment_completion(goal = nil) field = set_field(goal) Split.redis.hincrby(key, field, 1) ->>>>>>> Implement multiple goals for an experiment end def control? diff --git a/lib/split/experiment.rb b/lib/split/experiment.rb index 380eb475..2bdcca52 100644 --- a/lib/split/experiment.rb +++ b/lib/split/experiment.rb @@ -3,6 +3,7 @@ class Experiment attr_accessor :name attr_writer :algorithm attr_accessor :resettable + attr_accessor :goals def initialize(name, options = {}) options = { @@ -12,6 +13,10 @@ def initialize(name, options = {}) @name = name.to_s @alternatives = options[:alternatives] if !options[:alternatives].nil? + if !options[:goals].nil? + @goals = options[:goals] + end + if !options[:algorithm].nil? @algorithm = options[:algorithm].is_a?(String) ? options[:algorithm].constantize : options[:algorithm] end @@ -159,6 +164,23 @@ def save self end + def self.load_goals_for(name) + if Split.configuration.experiment_for(name) + load_goals_from_configuration_for(name) + else + load_goals_from_redis_for(name) + end + end + + def self.load_goals_from_configuration_for(name) + goals = Split.configuration.experiment_for(name)[:goals] + goals.flatten + end + + def self.load_goals_from_redis_for(name) + Split.redis.lrange(goals_key(name), 0, -1) + end + def self.load_alternatives_for(name) if Split.configuration.experiment_for(name) load_alternatives_from_configuration_for(name) @@ -192,15 +214,17 @@ def self.load_alternatives_from_redis_for(name) def self.load_from_configuration(name) exp_config = Split.configuration.experiment_for(name) || {} self.new(name, :alternative_names => load_alternatives_for(name), - :resettable => exp_config[:resettable], - :algorithm => exp_config[:algorithm]) + :goals => load_goals_for(name), + :resettable => exp_config[:resettable], + :algorithm => exp_config[:algorithm]) end def self.load_from_redis(name) exp_config = Split.redis.hgetall(experiment_config_key(name)) self.new(name, :alternative_names => load_alternatives_for(name), - :resettable => exp_config['resettable'], - :algorithm => exp_config['algorithm']) + :goals => load_goals_for(name), + :resettable => exp_config['resettable'], + :algorithm => exp_config['algorithm']) end def self.experiment_config_key(name) @@ -231,10 +255,9 @@ def self.find(name) obj end - def self.find_or_create(name, *alternatives) - experiment_name_with_version, goals = normalize_experiment_name(name) + def self.find_or_create(label, *alternatives) + experiment_name_with_version, goals = normalize_experiment(label) name = experiment_name_with_version.to_s.split(':')[0] - experiment_label = {name => goals} # KLUDGE: sneak in goals along with name to maintain backward compatibility if alternatives.length == 1 if alternatives[0].is_a? Hash @@ -247,29 +270,30 @@ def self.find_or_create(name, *alternatives) if Split.redis.exists(name) existing_alternatives = load_alternatives_for(name) - if existing_alternatives == alts.map(&:name) - experiment = self.new(name, :alternative_names => alternatives) + existing_goals = load_goals_for(name) + if existing_alternatives == alts.map(&:name) && existing_goals == gls + experiment = self.new(name, :alternative_names => alternatives, :goals => goals) else - exp = self.new(name, :alternative_names => existing_alternatives) + exp = self.new(name, :alternative_names => existing_alternatives, :goals => goals) exp.reset exp.alternatives.each(&:delete) - experiment = self.new(name, :alternative_names =>alternatives) + experiment = self.new(name, :alternative_names =>alternatives, :goals => goals) experiment.save end else - experiment = self.new(name, :alternative_names => alternatives) + experiment = self.new(name, :alternative_names => alternatives, :goals => goals) experiment.save end return experiment end - def self.normalize_experiment_name(name) - if Hash === name - experiment_name = name.keys.first - goals = name.values.first + def self.normalize_experiment(label) + if Hash === label + experiment_name = label.keys.first + goals = label.values.first else - experiment_name = name + experiment_name = label goals = [] end return experiment_name, goals diff --git a/lib/split/helper.rb b/lib/split/helper.rb index e384e751..c5badcf4 100644 --- a/lib/split/helper.rb +++ b/lib/split/helper.rb @@ -1,14 +1,16 @@ module Split module Helper - def ab_test(experiment_name, control=nil, *alternatives) + def ab_test(experiment_label, control=nil, *alternatives) if RUBY_VERSION.match(/1\.8/) && alternatives.length.zero? && ! control.nil? puts 'WARNING: You should always pass the control alternative through as the second argument with any other alternatives as the third because the order of the hash is not preserved in ruby 1.8' end + experiment_name, goals = normalize_experiment(experiment_label) + begin ret = if Split.configuration.enabled - load_and_start_trial(experiment_name, control, alternatives) + load_and_start_trial(experiment_label, control, alternatives) else control_variable(control) end @@ -132,12 +134,12 @@ def is_ignored_ip_address? end protected - def normalize_experiment(experiment) - if Hash === experiment - experiment_name = experiment.keys.first - goals = experiment.values.first + def normalize_experiment(experiment_label) + if Hash === experiment_label + experiment_name = experiment_label.keys.first + goals = experiment_label.values.first else - experiment_name = experiment + experiment_name = experiment_label goals = [] end return experiment_name, goals @@ -147,13 +149,14 @@ def control_variable(control) Hash === control ? control.keys.first : control end - def load_and_start_trial(experiment_name, control, alternatives) + def load_and_start_trial(experiment_label, control, alternatives) + experiment_name, goals = normalize_experiment(experiment_label) if control.nil? && alternatives.length.zero? experiment = Experiment.find(experiment_name) raise ExperimentNotFound.new("#{experiment_name} not found") if experiment.nil? else - experiment = Split::Experiment.find_or_create(experiment_name, *([control] + alternatives)) + experiment = Split::Experiment.find_or_create(experiment_label, *([control] + alternatives)) end start_trial( Trial.new(:experiment => experiment) ) diff --git a/lib/split/trial.rb b/lib/split/trial.rb index b5f1b68b..94ac3221 100644 --- a/lib/split/trial.rb +++ b/lib/split/trial.rb @@ -16,14 +16,18 @@ def alternative end def complete! - alternative.increment_completion if alternative + if experiment.goals.empty? + alternative.increment_completion + else + experiment.goals.each {|g| alternative.increment_completion(g)} + end end def choose! choose record! end - + def record! alternative.increment_participation end @@ -35,7 +39,7 @@ def choose self.alternative = experiment.next_alternative end end - + def alternative_name=(name) self.alternative= self.experiment.alternatives.find{|a| a.name == name } end From 9854bf612c79f015728e2bc2bb73035870465016 Mon Sep 17 00:00:00 2001 From: liujin Date: Tue, 15 Jan 2013 12:01:13 +0800 Subject: [PATCH 4/8] DRY spec --- lib/split/alternative.rb | 7 ++----- lib/split/configuration.rb | 8 ++++---- lib/split/experiment.rb | 2 +- lib/split/metric.rb | 14 ++++++++++++++ spec/alternative_spec.rb | 30 ++++++------------------------ spec/dashboard_spec.rb | 2 +- spec/experiment_spec.rb | 31 +++++++++++++++++++++++++++---- 7 files changed, 55 insertions(+), 39 deletions(-) diff --git a/lib/split/alternative.rb b/lib/split/alternative.rb index 39ce2344..ad6e6a7c 100644 --- a/lib/split/alternative.rb +++ b/lib/split/alternative.rb @@ -24,11 +24,10 @@ def goals end def participant_count - @participant_count ||= Split.redis.hget(key, 'participant_count').to_i + Split.redis.hget(key, 'participant_count').to_i end def participant_count=(count) - @participant_count = count Split.redis.hset(key, 'participant_count', count.to_i) end @@ -63,7 +62,7 @@ def set_completed_count (count, goal = nil) end def increment_participation - @participant_count = Split.redis.hincrby key, 'participant_count', 1 + Split.redis.hincrby key, 'participant_count', 1 end def increment_completion(goal = nil) @@ -116,8 +115,6 @@ def save end def reset - @participant_count = nil - @completed_count = nil Split.redis.hmset key, 'participant_count', 0, 'completed_count', 0 unless goals.empty? goals.each do |g| diff --git a/lib/split/configuration.rb b/lib/split/configuration.rb index 7b4b8b50..5ba9aed6 100644 --- a/lib/split/configuration.rb +++ b/lib/split/configuration.rb @@ -48,7 +48,7 @@ def metrics end @metrics end - + def normalized_experiments if @experiments.nil? nil @@ -58,13 +58,13 @@ def normalized_experiments experiment_config[name] = {} end @experiments.each do | experiment_name, settings| - experiment_config[experiment_name][:alternatives] = normalize_alternatives(settings[:alternatives]) if settings[:alternatives] + experiment_config[experiment_name][:alternatives] = normalize_alternatives(settings[:alternatives]) if settings[:alternatives] + experiment_config[experiment_name][:goals] = settings[:goals] if settings[:goals] end experiment_config end end - - + def normalize_alternatives(alternatives) given_probability, num_with_probability = alternatives.inject([0,0]) do |a,v| p, n = a diff --git a/lib/split/experiment.rb b/lib/split/experiment.rb index 2bdcca52..b15664f9 100644 --- a/lib/split/experiment.rb +++ b/lib/split/experiment.rb @@ -174,7 +174,7 @@ def self.load_goals_for(name) def self.load_goals_from_configuration_for(name) goals = Split.configuration.experiment_for(name)[:goals] - goals.flatten + goals.flatten unless goals.nil? end def self.load_goals_from_redis_for(name) diff --git a/lib/split/metric.rb b/lib/split/metric.rb index 5841a60c..03f40718 100644 --- a/lib/split/metric.rb +++ b/lib/split/metric.rb @@ -43,6 +43,7 @@ def self.find(name) end def self.possible_experiments(metric_name) + metric_name, goals = normalize_metric(metric_name) experiments = [] metric = Split::Metric.find(metric_name) if metric @@ -64,5 +65,18 @@ def complete! experiment.complete! end end + + private + + def self.normalize_metric(label) + if Hash === label + metric_name = label.keys.first + goals = label.values.first + else + metric_name = label + goals = [] + end + return metric_name, goals + end end end \ No newline at end of file diff --git a/spec/alternative_spec.rb b/spec/alternative_spec.rb index 017b7103..8c25d654 100644 --- a/spec/alternative_spec.rb +++ b/spec/alternative_spec.rb @@ -28,14 +28,10 @@ end it "should have a name" do - experiment = Split::Experiment.new('basket_text', :alternative_names => ['Basket', "Cart"]) - alternative = Split::Alternative.new('Basket', 'basket_text') alternative.name.should eql('Basket') end it "return only the name" do - experiment = Split::Experiment.new('basket_text', :alternative_names => [{'Basket' => 0.6}, {"Cart" => 0.4}]) - alternative = Split::Alternative.new('Basket', 'basket_text') alternative.name.should eql('Basket') end @@ -59,7 +55,7 @@ { :name => "control_opt", :percent => 67 }, { :name => "second_opt", :percent => 10 }, { :name => "third_opt", :percent => 23 }, - ], + ] } } experiment = Split::Experiment.find(:my_experiment) @@ -113,9 +109,6 @@ end it "should belong to an experiment" do - experiment = Split::Experiment.new('basket_text', :alternative_names => ['Basket', "Cart"]) - experiment.save - alternative = Split::Alternative.new('Basket', 'basket_text') alternative.experiment.name.should eql(experiment.name) end @@ -125,21 +118,16 @@ end it "should increment participation count" do - experiment = Split::Experiment.new('basket_text', :alternative_names => ['Basket', "Cart"]) - experiment.save - alternative = Split::Alternative.new('Basket', 'basket_text') old_participant_count = alternative.participant_count alternative.increment_participation alternative.participant_count.should eql(old_participant_count+1) - - alternative.participant_count.should eql(old_participant_count+1) end - it "should increment completed count" do - experiment = Split::Experiment.new('basket_text', :alternative_names => ['Basket', "Cart"]) - experiment.save - alternative = Split::Alternative.new('Basket', 'basket_text') - old_completed_count = alternative.participant_count + it "should increment completed count for each goal" do + old_default_completed_count = alternative.completed_count + old_completed_count_for_goal1 = alternative.completed_count(goal1) + old_completed_count_for_goal2 = alternative.completed_count(goal2) + alternative.increment_completion alternative.increment_completion(goal1) alternative.increment_completion(goal2) @@ -162,18 +150,12 @@ end it "should know if it is the control of an experiment" do - experiment = Split::Experiment.new('basket_text', :alternative_names => ['Basket', "Cart"]) - experiment.save - alternative = Split::Alternative.new('Basket', 'basket_text') alternative.control?.should be_true alternative2.control?.should be_false end describe 'unfinished_count' do it "should be difference between participant and completed counts" do - experiment = Split::Experiment.new('basket_text', :alternative_names => ['Basket', "Cart"]) - experiment.save - alternative = Split::Alternative.new('Basket', 'basket_text') alternative.increment_participation alternative.unfinished_count.should eql(alternative.participant_count) end diff --git a/spec/dashboard_spec.rb b/spec/dashboard_spec.rb index 0e8c75ad..d79c56b2 100644 --- a/spec/dashboard_spec.rb +++ b/spec/dashboard_spec.rb @@ -34,7 +34,7 @@ def app experiment = link_color red_link.participant_count = 5 - blue_link.participant_count = 6 + blue_link.participant_count = 7 post '/reset/link_color' diff --git a/spec/experiment_spec.rb b/spec/experiment_spec.rb index b4de617e..7396dfec 100644 --- a/spec/experiment_spec.rb +++ b/spec/experiment_spec.rb @@ -4,6 +4,21 @@ require 'time' describe Split::Experiment do + def new_experiment(goals=[]) + Split::Experiment.new('link_color', :alternative_names => ['blue', 'red', 'green'], :goals => goals) + end + + def alternative(color) + Split::Alternative.new(color, 'link_color') + end + + let(:experiment) { + new_experiment + } + + let(:blue) { alternative("blue") } + let(:green) { alternative("green") } + context "with an experiment" do let(:experiment) { Split::Experiment.new('basket_text', :alternative_names => ['Basket', "Cart"]) } @@ -75,7 +90,8 @@ describe 'find' do it "should return an existing experiment" do experiment.save - Split::Experiment.find('basket_text').name.should eql('basket_text') + experiment = Split::Experiment.find('basket_text') + experiment.name.should eql('basket_text') end it "should return an existing experiment" do @@ -90,6 +106,7 @@ end end end + describe 'initialization' do it "should set the algorithm when passed as an option to the initializer" do experiment = Split::Experiment.new('basket_text', :alternative_names => ['Basket', "Cart"], :algorithm => Split::Algorithms::Whiplash) @@ -124,9 +141,6 @@ end end - - - describe 'deleting' do it 'should delete itself' do experiment = Split::Experiment.new('basket_text', :alternative_names => [ 'Basket', "Cart"]) @@ -300,6 +314,15 @@ def same_but_different_goals it "should have goals" do experiment.goals.should eql(["purchase"]) end + + context "find or create experiment" do + it "should have correct goals" do + experiment = Split::Experiment.find_or_create({'link_color3' => ["purchase", "refund"]}, 'blue', 'red', 'green') + experiment.goals.should == ["purchase", "refund"] + experiment = Split::Experiment.find_or_create('link_color3', 'blue', 'red', 'green') + experiment.goals.should == [] + end + end end end From 7a853b10b64fb6ef0c853e37693fcda0d8db9d40 Mon Sep 17 00:00:00 2001 From: liujin Date: Tue, 15 Jan 2013 18:24:45 +0800 Subject: [PATCH 5/8] fix some bugs --- lib/split/experiment.rb | 14 +++++++++++--- lib/split/helper.rb | 5 +++-- lib/split/metric.rb | 1 - lib/split/trial.rb | 12 ++++++++---- spec/dashboard_spec.rb | 2 ++ spec/helper_spec.rb | 27 +++++++++++++++++++++++++-- 6 files changed, 49 insertions(+), 12 deletions(-) diff --git a/lib/split/experiment.rb b/lib/split/experiment.rb index b15664f9..c68c2fcd 100644 --- a/lib/split/experiment.rb +++ b/lib/split/experiment.rb @@ -130,7 +130,6 @@ def resettable? def reset alternatives.each(&:reset) reset_winner - Split.redis.del(goals_key) increment_version end @@ -139,10 +138,14 @@ def delete reset_winner Split.redis.srem(:experiments, name) Split.redis.del(name) - Split.redis.del(goals_key) + delete_goals increment_version end + def delete_goals + Split.redis.del(goals_key) + end + def new_record? !Split.redis.exists(name) end @@ -174,7 +177,11 @@ def self.load_goals_for(name) def self.load_goals_from_configuration_for(name) goals = Split.configuration.experiment_for(name)[:goals] - goals.flatten unless goals.nil? + if goals.nil? + goals = [] + else + goals.flatten + end end def self.load_goals_from_redis_for(name) @@ -277,6 +284,7 @@ def self.find_or_create(label, *alternatives) exp = self.new(name, :alternative_names => existing_alternatives, :goals => goals) exp.reset exp.alternatives.each(&:delete) + exp.delete_goals experiment = self.new(name, :alternative_names =>alternatives, :goals => goals) experiment.save end diff --git a/lib/split/helper.rb b/lib/split/helper.rb index c5badcf4..ed8a76e0 100644 --- a/lib/split/helper.rb +++ b/lib/split/helper.rb @@ -51,7 +51,7 @@ def finish_experiment(experiment, options = {:reset => true}) return true else alternative_name = ab_user[experiment.key] - trial = Trial.new(:experiment => experiment, :alternative_name => alternative_name) + trial = Trial.new(:experiment => experiment, :alternative_name => alternative_name, :goals => options[:goals]) trial.complete! if should_reset reset!(experiment) @@ -64,11 +64,12 @@ def finish_experiment(experiment, options = {:reset => true}) def finished(metric_name, options = {:reset => true}) return if exclude_visitor? || Split.configuration.disabled? + metric_name, goals = normalize_experiment(metric_name) experiments = Metric.possible_experiments(metric_name) if experiments.any? experiments.each do |experiment| - finish_experiment(experiment, options) + finish_experiment(experiment, options.merge(:goals => goals)) end end rescue => e diff --git a/lib/split/metric.rb b/lib/split/metric.rb index 03f40718..6ab76628 100644 --- a/lib/split/metric.rb +++ b/lib/split/metric.rb @@ -43,7 +43,6 @@ def self.find(name) end def self.possible_experiments(metric_name) - metric_name, goals = normalize_metric(metric_name) experiments = [] metric = Split::Metric.find(metric_name) if metric diff --git a/lib/split/trial.rb b/lib/split/trial.rb index 94ac3221..4f024f99 100644 --- a/lib/split/trial.rb +++ b/lib/split/trial.rb @@ -2,11 +2,13 @@ module Split class Trial attr_accessor :experiment attr_writer :alternative + attr_accessor :goals def initialize(attrs = {}) self.experiment = attrs[:experiment] if !attrs[:experiment].nil? self.alternative = attrs[:alternative] if !attrs[:alternative].nil? self.alternative_name = attrs[:alternative_name] if !attrs[:alternative_name].nil? + self.goals = attrs[:goals] if !attrs[:goals].nil? end def alternative @@ -16,10 +18,12 @@ def alternative end def complete! - if experiment.goals.empty? - alternative.increment_completion - else - experiment.goals.each {|g| alternative.increment_completion(g)} + if alternative + if self.goals.empty? + alternative.increment_completion + else + self.goals.each {|g| alternative.increment_completion(g)} + end end end diff --git a/spec/dashboard_spec.rb b/spec/dashboard_spec.rb index d79c56b2..5e8c9697 100644 --- a/spec/dashboard_spec.rb +++ b/spec/dashboard_spec.rb @@ -35,6 +35,7 @@ def app red_link.participant_count = 5 blue_link.participant_count = 7 + experiment.winner = 'blue' post '/reset/link_color' @@ -45,6 +46,7 @@ def app new_blue_count.should eql(0) new_red_count.should eql(0) + experiment.winner.should be_nil end it "should delete an experiment" do diff --git a/spec/helper_spec.rb b/spec/helper_spec.rb index aeef54b7..84f0d82e 100755 --- a/spec/helper_spec.rb +++ b/spec/helper_spec.rb @@ -264,6 +264,7 @@ def should_finish_experiment(experiment_name, should_finish=true) finished :my_metric, :reset => false ab_user.should eql(@experiment.key => @alternative_name, @experiment.finished_key => true) end + end end @@ -575,21 +576,43 @@ def should_finish_experiment(experiment_name, should_finish=true) it "pulls options from config file" do Split.configuration.experiments[:my_experiment] = { :alternatives => [ "control_opt", "other_opt" ], + :goals => ["goal1", "goal2"] } ab_test :my_experiment Split::Experiment.find(:my_experiment).alternative_names.should == [ "control_opt", "other_opt" ] + Split::Experiment.find(:my_experiment).goals.should == [ "goal1", "goal2" ] end it "can be called multiple times" do Split.configuration.experiments[:my_experiment] = { :alternatives => [ "control_opt", "other_opt" ], + :goals => ["goal1", "goal2"] } 5.times { ab_test :my_experiment } experiment = Split::Experiment.find(:my_experiment) experiment.alternative_names.should == [ "control_opt", "other_opt" ] + experiment.goals.should == [ "goal1", "goal2" ] experiment.participant_count.should == 1 end + it "accepts multiple goals" do + Split.configuration.experiments[:my_experiment] = { + :variants => [ "control_opt", "other_opt" ], + :goals => [ "goal1", "goal2", "goal3" ] + } + ab_test :my_experiment + experiment = Split::Experiment.find(:my_experiment) + experiment.goals.should == [ "goal1", "goal2", "goal3" ] + end + + it "allow specifying goals to be optional" do + Split.configuration.experiments[:my_experiment] = { + :variants => [ "control_opt", "other_opt" ] + } + experiment = Split::Experiment.find(:my_experiment) + experiment.goals.should == [] + end + it "accepts multiple alternatives" do Split.configuration.experiments[:my_experiment] = { :alternatives => [ "control_opt", "second_opt", "third_opt" ], @@ -701,11 +724,11 @@ def should_finish_experiment(experiment_name, should_finish=true) it "should increment the counter for the specified-goal completed alternative" do @previous_completion_count_for_goal1 = Split::Alternative.new(@alternative_name, @experiment_name).completed_count(@goal1) @previous_completion_count_for_goal2 = Split::Alternative.new(@alternative_name, @experiment_name).completed_count(@goal2) - finished(@experiment) + finished({"link_color" => ["purchase"]}) new_completion_count_for_goal1 = Split::Alternative.new(@alternative_name, @experiment_name).completed_count(@goal1) new_completion_count_for_goal1.should eql(@previous_completion_count_for_goal1 + 1) new_completion_count_for_goal2 = Split::Alternative.new(@alternative_name, @experiment_name).completed_count(@goal2) - new_completion_count_for_goal2.should eql(@previous_completion_count_for_goal2 + 1) + new_completion_count_for_goal2.should eql(@previous_completion_count_for_goal2) end end end From 5a0eb0c84f6e4b4cf5eb48efe8f00ccadb31ba51 Mon Sep 17 00:00:00 2001 From: liujin Date: Tue, 29 Jan 2013 14:34:53 +0800 Subject: [PATCH 6/8] remove default goal --- .../views/_experiment_with_goal_header.erb | 14 ++++++++++++++ lib/split/dashboard/views/index.erb | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 lib/split/dashboard/views/_experiment_with_goal_header.erb diff --git a/lib/split/dashboard/views/_experiment_with_goal_header.erb b/lib/split/dashboard/views/_experiment_with_goal_header.erb new file mode 100644 index 00000000..24d09bcb --- /dev/null +++ b/lib/split/dashboard/views/_experiment_with_goal_header.erb @@ -0,0 +1,14 @@ +
+
+
+ <%= experiment.start_time ? experiment.start_time.strftime('%Y-%m-%d') : 'Unknown' %> +
" method='post' onclick="return confirmReset()"> + + +
" method='post' onclick="return confirmDelete()"> + + + +
+
+
\ No newline at end of file diff --git a/lib/split/dashboard/views/index.erb b/lib/split/dashboard/views/index.erb index e7d12b14..b0cf5a60 100644 --- a/lib/split/dashboard/views/index.erb +++ b/lib/split/dashboard/views/index.erb @@ -5,7 +5,7 @@ <% if experiment.goals.empty? %> <%= erb :_experiment, :locals => {:goal => nil, :experiment => experiment} %> <% else %> - <%= erb :_experiment, :locals => {:goal => nil, :experiment => experiment} %> + <%= erb :_experiment_with_goal_header, :locals => {:experiment => experiment} %> <% experiment.goals.each do |g| %> <%= erb :_experiment, :locals => {:goal => g, :experiment => experiment} %> <% end %> From a5cce75b3e1e770a06db1b2aa662c9a97dd4790a Mon Sep 17 00:00:00 2001 From: liujin Date: Tue, 29 Jan 2013 14:50:53 +0800 Subject: [PATCH 7/8] update readme --- README.mdown | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/README.mdown b/README.mdown index 85cd62e4..6c7ee5d8 100644 --- a/README.mdown +++ b/README.mdown @@ -314,12 +314,34 @@ Your code may then track a completion using the metric instead of the experiment name: finished(:conversion) - -You can also create a new metric by instantiating and saving a new Metric object. + +You can also create a new metric by instantiating and saving a new Metric object. Split::Metric.new(:conversion) Split::Metric.save +#### Goals + +You might wish to allow an experiment to have multiple goals. +The API to define goals for an experiment is this: + + ab_test({"link_color" => ["purchase", "refund"]}, "red", "blue") + +or you can you can define them in a configuration file: + + Split.configure do |config| + config.experiments = { + :link_color => { + :alternatives => ["red", "blue"], + :goals => ["purchase", "refund"] + } + } + end + +To complete a goal conversion, you do it like: + + finished({"link_color" => ["purchase"]}) + ### DB failover solution Due to the fact that Redis has no autom. failover mechanism, it's @@ -388,7 +410,7 @@ is configured. ## Outside of a Web Session -Split provides the Helper module to facilitate running experiments inside web sessions. +Split provides the Helper module to facilitate running experiments inside web sessions. Alternatively, you can access the underlying Metric, Trial, Experiment and Alternative objects to conduct experiments that are not tied to a web session. @@ -412,11 +434,11 @@ end ## Algorithms -By default, Split ships with an algorithm that randomly selects from possible alternatives for a traditional a/b test. +By default, Split ships with an algorithm that randomly selects from possible alternatives for a traditional a/b test. -An implementation of a bandit algorithm is also provided. +An implementation of a bandit algorithm is also provided. -Users may also write their own algorithms. The default algorithm may be specified globally in the configuration file, or on a per experiment basis using the experiments hash of the configuration file. +Users may also write their own algorithms. The default algorithm may be specified globally in the configuration file, or on a per experiment basis using the experiments hash of the configuration file. ## Extensions From b4f7e2d46fcadb4f606d4b126ee9eaa9d5c93d0b Mon Sep 17 00:00:00 2001 From: liujin Date: Tue, 29 Jan 2013 14:58:56 +0800 Subject: [PATCH 8/8] rename variants to alternatives --- spec/helper_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/helper_spec.rb b/spec/helper_spec.rb index 84f0d82e..a14c7369 100755 --- a/spec/helper_spec.rb +++ b/spec/helper_spec.rb @@ -597,7 +597,7 @@ def should_finish_experiment(experiment_name, should_finish=true) it "accepts multiple goals" do Split.configuration.experiments[:my_experiment] = { - :variants => [ "control_opt", "other_opt" ], + :alternatives => [ "control_opt", "other_opt" ], :goals => [ "goal1", "goal2", "goal3" ] } ab_test :my_experiment @@ -607,7 +607,7 @@ def should_finish_experiment(experiment_name, should_finish=true) it "allow specifying goals to be optional" do Split.configuration.experiments[:my_experiment] = { - :variants => [ "control_opt", "other_opt" ] + :alternatives => [ "control_opt", "other_opt" ] } experiment = Split::Experiment.find(:my_experiment) experiment.goals.should == []
Finish
@@ -38,23 +46,23 @@ <%= alternative.participant_count %> <%= alternative.unfinished_count %><%= alternative.completed_count %><%= alternative.completed_count(goal) %> - <%= number_to_percentage(alternative.conversion_rate) %>% - <% if experiment.control.conversion_rate > 0 && !alternative.control? %> - <% if alternative.conversion_rate > experiment.control.conversion_rate %> + <%= number_to_percentage(alternative.conversion_rate(goal)) %>% + <% if experiment.control.conversion_rate(goal) > 0 && !alternative.control? %> + <% if alternative.conversion_rate(goal) > experiment.control.conversion_rate(goal) %> - +<%= number_to_percentage((alternative.conversion_rate/experiment.control.conversion_rate)-1) %>% + +<%= number_to_percentage((alternative.conversion_rate(goal)/experiment.control.conversion_rate(goal))-1) %>% - <% elsif alternative.conversion_rate < experiment.control.conversion_rate %> + <% elsif alternative.conversion_rate(goal) < experiment.control.conversion_rate(goal) %> - <%= number_to_percentage((alternative.conversion_rate/experiment.control.conversion_rate)-1) %>% + <%= number_to_percentage((alternative.conversion_rate(goal)/experiment.control.conversion_rate(goal))-1) %>% <% end %> <% end %> - <%= confidence_level(alternative.z_score) %> + <%= confidence_level(alternative.z_score(goal)) %> <% if experiment.winner %> @@ -73,13 +81,14 @@
Totals <%= total_participants %><%= total_participants - total_completed %><%= total_unfinished %> <%= total_completed %> N/A N/A