conductor 0.2.16 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.16
1
+ 0.3.0
data/lib/conductor.rb CHANGED
@@ -4,6 +4,7 @@ require 'conductor/weights'
4
4
  require 'conductor/experiment/raw'
5
5
  require 'conductor/experiment/daily'
6
6
  require 'conductor/experiment/weight'
7
+ require 'conductor/experiment/history'
7
8
 
8
9
  class Conductor
9
10
  MAX_WEIGHTING_FACTOR = 1.25
@@ -34,6 +35,14 @@ class Conductor
34
35
  puts msg if DBG
35
36
  end
36
37
  end
38
+
39
+ # class Rails
40
+ # cattr_writer :cache
41
+ #
42
+ # def self.cache
43
+ # []
44
+ # end
45
+ # end
37
46
  end
38
47
 
39
48
 
@@ -1,7 +1,7 @@
1
1
  class Conductor
2
2
  class Experiment
3
3
  class << self
4
- # Selects the best option for a given group
4
+ # Selects the best alternative for a given group
5
5
  #
6
6
  # Method also saves the selection to the
7
7
  # database so everything happens in one move
@@ -25,8 +25,8 @@ class Conductor
25
25
  selection = Conductor.cache.read("Conductor::#{Conductor.identity}::Experience::#{group_name}")
26
26
 
27
27
  unless selection
28
- selection = select_option_for_group(group_name, alternatives)
29
- Conductor::RawExperiment.create!({:identity_id => Conductor.identity.to_s, :group_name => group_name, :option_name => selection}.merge!(options))
28
+ selection = select_alternative_for_group(group_name, alternatives)
29
+ Conductor::Experiment::Raw.create!({:identity_id => Conductor.identity.to_s, :group_name => group_name, :alternative => selection}.merge!(options))
30
30
  Conductor.cache.write("Conductor::#{Conductor.identity}::Experience::#{group_name}", selection)
31
31
  end
32
32
 
@@ -69,13 +69,13 @@ class Conductor
69
69
  #
70
70
  def track!(options={})
71
71
  value = (options.delete(:value) || 1) # => pull the conversion value and remove from hash or set value to 1
72
- experiments = Conductor::RawExperiment.find(:all, :conditions => {:identity_id => Conductor.identity}.merge!(options))
72
+ experiments = Conductor::Experiment::Raw.find(:all, :conditions => {:identity_id => Conductor.identity}.merge!(options))
73
73
  experiments.each {|x| x.update_attributes(:conversion_value => value)} if experiments
74
74
  end
75
75
 
76
76
  private
77
77
 
78
- def select_option_for_group(group_name, alternatives)
78
+ def select_alternative_for_group(group_name, alternatives)
79
79
  # create weighting table
80
80
  weighting_table = generate_weighting_table(group_name, alternatives)
81
81
 
@@ -88,26 +88,26 @@ class Conductor
88
88
  #
89
89
  # Note: We create sql where statement that includes the list of
90
90
  # alternatives to select from in case an existing group
91
- # has an option you no longer want to include in the result set
91
+ # has an alternative you no longer want to include in the result set
92
92
  #
93
93
  # TODO: store all weights for a group in cache and then weed out
94
94
  # those not in the alternatives list
95
95
  #
96
96
  def generate_weighting_table(group_name, alternatives)
97
97
  # create the conditions after sanitizing sql.
98
- option_conditions = alternatives.inject([]) {|res,x| res << "option_name = '#{sanitize(x)}'"}.join(' OR ')
98
+ alternative_filter = alternatives.inject([]) {|res,x| res << "alternative = '#{sanitize(x)}'"}.join(' OR ')
99
99
 
100
- conditions = "group_name = '#{group_name}' AND (#{option_conditions})"
100
+ conditions = "group_name = '#{group_name}' AND (#{alternative_filter})"
101
101
 
102
- # get the options from the database
103
- weights ||= Conductor::WeightedExperiment.find(:all, :conditions => conditions)
102
+ # get the alternatives from the database
103
+ weights ||= Conductor::Experiment::Weight.find(:all, :conditions => conditions)
104
104
 
105
105
  # create selection hash
106
- weighting_table = weights.inject({}) {|res, x| res.merge!({x.option_name => x.weight})}
106
+ weighting_table = weights.inject({}) {|res, x| res.merge!({x.alternative => x.weight})}
107
107
 
108
108
  # is anything missing?
109
- options_names = weights.map(&:option_name)
110
- missing = alternatives - options_names
109
+ alternative_names = weights.map(&:alternative)
110
+ missing = alternatives - alternative_names
111
111
 
112
112
  # if anything is missing, add it to the weighted list
113
113
  unless missing.empty?
@@ -126,7 +126,7 @@ class Conductor
126
126
  return (rand*width)+start_num
127
127
  end
128
128
 
129
- # choose a random option based on weights
129
+ # choose a random alternative based on weights
130
130
  # from recipe 5.11 in ruby cookbook
131
131
  def choose_weighted(weighted)
132
132
  sum = weighted.inject(0) do |sum, item_and_weight|
@@ -14,7 +14,7 @@
14
14
  class Conductor::Experiment::Raw < ActiveRecord::Base
15
15
  set_table_name "conductor_raw_experiments"
16
16
 
17
- validates_presence_of :group_name, :option_name
17
+ validates_presence_of :group_name, :alternative
18
18
 
19
19
  def created_date
20
20
  self.created_at.strftime('%Y-%m-%d')
@@ -1,19 +1,19 @@
1
1
  class Conductor
2
2
  class RollUp
3
3
  def self.process
4
- Conductor::RawExperiment.all.group_by(&:created_date).each do |day, daily_rows|
4
+ Conductor::Experiment::Raw.all.group_by(&:created_date).each do |day, daily_rows|
5
5
 
6
6
  # remove all the existing data for that day
7
- Conductor::DailyExperiment.delete_all(:activity_date => day)
7
+ Conductor::Experiment::Daily.delete_all(:activity_date => day)
8
8
 
9
9
  daily_rows.group_by(&:group_name).each do |group_name, group_rows|
10
- group_rows.group_by(&:option_name).each do |option_name, option_rows|
11
- conversion_value = option_rows.select {|x| !x.conversion_value.nil?}.inject(0) {|res, x| res += x.conversion_value}
12
- views = option_rows.count
13
- conversions = option_rows.count {|x| !x.conversion_value.nil?}
14
- Conductor::DailyExperiment.create!(:activity_date => day,
10
+ group_rows.group_by(&:alternative).each do |alternative_name, alternatives|
11
+ conversion_value = alternatives.select {|x| !x.conversion_value.nil?}.inject(0) {|res, x| res += x.conversion_value}
12
+ views = alternatives.count
13
+ conversions = alternatives.count {|x| !x.conversion_value.nil?}
14
+ Conductor::Experiment::Daily.create!(:activity_date => day,
15
15
  :group_name => group_name,
16
- :option_name => option_name,
16
+ :alternative => alternative_name,
17
17
  :conversion_value => conversion_value,
18
18
  :views => views,
19
19
  :conversions => conversions )
@@ -2,10 +2,10 @@ class Conductor
2
2
  class Weights
3
3
  class << self
4
4
  def compute
5
- Conductor::WeightedExperiment.delete_all # => remove all old data
5
+ Conductor::Experiment::Weight.delete_all # => remove all old data
6
6
 
7
- # loop through each group and determing weight of options
8
- Conductor::DailyExperiment.since(14.days.ago).group_by(&:group_name).each do |group_name, group_rows|
7
+ # loop through each group and determing weight of alternatives
8
+ Conductor::Experiment::Daily.since(14.days.ago).group_by(&:group_name).each do |group_name, group_rows|
9
9
  total = group_rows.sum_it(:conversion_value)
10
10
  data = total ? compute_weights_for_group(group_name, group_rows, total) : assign_equal_weights(group_rows)
11
11
  update_weights_in_db(group_name, data)
@@ -14,7 +14,7 @@ class Conductor
14
14
 
15
15
  private
16
16
 
17
- # loops through all the options for a given group and computes the weights for
17
+ # loops through all the alternatives for a given group and computes the weights for
18
18
  # each alternative
19
19
  def compute_weights_for_group(group_name, group_rows, total)
20
20
  Conductor.log('compute_weights_for_group')
@@ -23,15 +23,15 @@ class Conductor
23
23
  recently_launched = []
24
24
  max_weight = 0
25
25
 
26
- group_rows.group_by(&:option_name).each do |option_name, option_rows|
27
- first_found_date = option_rows.map(&:activity_date).sort.first
26
+ group_rows.group_by(&:alternative).each do |alternative_name, alternatives|
27
+ first_found_date = alternatives.map(&:activity_date).sort.first
28
28
  days_ago = Date.today - first_found_date
29
29
 
30
30
  if days_ago >= MINIMUM_LAUNCH_DAYS
31
- data << compute_weight_for_option(option_name, option_rows, max_weight, total)
31
+ data << compute_weight_for_alternative(alternative_name, alternatives, max_weight, total)
32
32
  else
33
- Conductor.log("adding #{option_name} to recently launched array")
34
- recently_launched << {:name => option_name, :days_ago => days_ago}
33
+ Conductor.log("adding #{alternative_name} to recently launched array")
34
+ recently_launched << {:name => alternative_name, :days_ago => days_ago}
35
35
  end
36
36
  end
37
37
 
@@ -39,12 +39,12 @@ class Conductor
39
39
  return data
40
40
  end
41
41
 
42
- def compute_weight_for_option(option_name, option_rows, max_weight, total)
43
- Conductor.log("compute_weight_for_option for #{option_name}")
42
+ def compute_weight_for_alternative(alternative_name, alternatives, max_weight, total)
43
+ Conductor.log("compute_weight_for_alternative for #{alternative_name}")
44
44
 
45
- aggregates = {:name => option_name}
45
+ aggregates = {:name => alternative_name}
46
46
 
47
- weight = option_rows.sum_it(:conversion_value) / total
47
+ weight = alternatives.sum_it(:conversion_value) / total
48
48
  max_weight = weight if weight > max_weight
49
49
  aggregates.merge!({:weight => weight})
50
50
 
@@ -58,11 +58,11 @@ class Conductor
58
58
  data = []
59
59
  max_weight = 0 ? 1 : max_weight # => if a max weight could not be computed, set it to 1
60
60
  Conductor.log("max weight: #{max_weight}")
61
- recently_launched.each do |option|
62
- handicap = (option[:days_ago].to_f / MINIMUM_LAUNCH_DAYS)
63
- launch_window = (MINIMUM_LAUNCH_DAYS - option[:days_ago]) if MINIMUM_LAUNCH_DAYS > option[:days_ago]
64
- Conductor.log("Handicap for #{option[:name]} is #{handicap} (#{option[:days_ago]} days ago)")
65
- data << {:name => option[:name], :weight => max_weight * MAX_WEIGHTING_FACTOR * (1 - handicap), :launch_window => launch_window}
61
+ recently_launched.each do |alternative|
62
+ handicap = (alternative[:days_ago].to_f / MINIMUM_LAUNCH_DAYS)
63
+ launch_window = (MINIMUM_LAUNCH_DAYS - alternative[:days_ago]) if MINIMUM_LAUNCH_DAYS > alternative[:days_ago]
64
+ Conductor.log("Handicap for #{alternative[:name]} is #{handicap} (#{alternative[:days_ago]} days ago)")
65
+ data << {:name => alternative[:name], :weight => max_weight * MAX_WEIGHTING_FACTOR * (1 - handicap), :launch_window => launch_window}
66
66
  end
67
67
  data
68
68
  end
@@ -72,17 +72,17 @@ class Conductor
72
72
 
73
73
  # weight everything the same since there were no conversions
74
74
  data = []
75
- group_rows.group_by(&:option_name).each do |option_name, option_rows|
76
- data << {:name => option_name, :weight => 1}
75
+ group_rows.group_by(&:alternative).each do |alternative_name, alternatives|
76
+ data << {:name => alternative_name, :weight => 1}
77
77
  end
78
78
  data
79
79
  end
80
80
 
81
81
  # creates new records in weights table and adds weights to weight history for reporting
82
82
  def update_weights_in_db(group_name, data)
83
- data.each { |option|
84
- Conductor::WeightedExperiment.create!(:group_name => group_name, :option_name => option[:name], :weight => option[:weight])
85
- Conductor::WeightHistory.create!(:group_name => group_name, :option_name => option[:name], :weight => option[:weight], :launch_window => option[:launch_window], :computed_at => Time.now)
83
+ data.each { |alternative|
84
+ Conductor::Experiment::Weight.create!(:group_name => group_name, :alternative => alternative[:name], :weight => alternative[:weight])
85
+ Conductor::Experiment::History.create!(:group_name => group_name, :alternative => alternative[:name], :weight => alternative[:weight], :launch_window => alternative[:launch_window], :computed_at => Time.now)
86
86
  }
87
87
  end
88
88
 
data/test/db/schema.rb ADDED
@@ -0,0 +1,43 @@
1
+ ActiveRecord::Schema.define(:version => 0) do
2
+ create_table "conductor_daily_experiments", :force => true do |t|
3
+ t.date "activity_date"
4
+ t.string "group_name"
5
+ t.string "alternative"
6
+ t.decimal "conversion_value", :precision => 8, :scale => 2
7
+ t.integer "views"
8
+ t.integer "conversions"
9
+ end
10
+
11
+ add_index "conductor_daily_experiments", ["activity_date"], :name => "index_conductor_daily_experiments_on_activity_date"
12
+ add_index "conductor_daily_experiments", ["group_name"], :name => "index_conductor_daily_experiments_on_group_name"
13
+
14
+ create_table "conductor_raw_experiments", :force => true do |t|
15
+ t.string "identity_id"
16
+ t.string "group_name"
17
+ t.string "alternative"
18
+ t.decimal "conversion_value", :precision => 8, :scale => 2
19
+ t.datetime "created_at"
20
+ t.datetime "updated_at"
21
+ t.string "goal"
22
+ end
23
+
24
+ create_table "conductor_weight_histories", :force => true do |t|
25
+ t.string "group_name"
26
+ t.string "alternative"
27
+ t.decimal "weight", :precision => 8, :scale => 2
28
+ t.datetime "computed_at"
29
+ t.integer "launch_window"
30
+ end
31
+
32
+ add_index "conductor_weight_histories", ["computed_at", "group_name"], :name => "conductor_wh_date_and_group_ndx"
33
+
34
+ create_table "conductor_weighted_experiments", :force => true do |t|
35
+ t.string "group_name"
36
+ t.string "alternative"
37
+ t.decimal "weight", :precision => 8, :scale => 2
38
+ t.datetime "created_at"
39
+ t.datetime "updated_at"
40
+ end
41
+
42
+ add_index "conductor_weighted_experiments", ["group_name"], :name => "index_conductor_weighted_experiments_on_group_name"
43
+ end
@@ -1,225 +1,219 @@
1
- require 'helper'
1
+ require File.expand_path(File.dirname(__FILE__) + '/test_helper')
2
2
 
3
3
  class TestConductor < Test::Unit::TestCase
4
- #Wipes cache, D/B prior to doing a test run.
5
- VectorSixteen.cache.clear
4
+ # Wipes cache, D/B prior to doing a test run.
5
+ def setup
6
+ Conductor.cache.clear
7
+ wipe
8
+ end
9
+
10
+ context "conductor" do
11
+ should "assign an identity if none is specified" do
12
+ assert Conductor.identity != nil
13
+ end
6
14
 
7
- test "will automatically assign an identity if none is specified" do
8
- assert VectorSixteen.identity != nil
9
- end
15
+ should "select one of the specified options randomly" do
16
+ selected = Conductor::Experiment.pick('a_group', ["a", "b", "c"]) # => value must be unique
17
+ assert ["a", "b", "c"].include? selected
18
+ end
10
19
 
11
- test "will select one of the specified options randomly" do
12
- selected = VectorSixteen::Experiment.pick('a_group', ["a", "b", "c"]) # => value must be unique
13
- assert ["a", "b", "c"].include? selected
14
- end
20
+ should "almost equally select each option if no weights exist" do
21
+ a = 0
22
+ b = 0
23
+ c = 0
24
+ (1..1000).each do |x|
25
+ Conductor.identity = ActiveSupport::SecureRandom.hex(16)
26
+ selected_lander = Conductor::Experiment.pick('a_group', ["a", "b", "c"]) # => value must be unique
27
+ case selected_lander
28
+ when 'a' then
29
+ a += 1
30
+ when 'b' then
31
+ b += 1
32
+ when 'c' then
33
+ c += 1
34
+ end
35
+ end
36
+
37
+ nums = [] << a << b << c
38
+ nums.sort!
39
+ range = nums.last - nums.first
40
+
41
+ assert (nums.first * 0.20) >= range
42
+ end
43
+ end
15
44
 
16
- test "will always select the same option using the cache" do
17
- VectorSixteen.identity = ActiveSupport::SecureRandom.hex(16)
45
+ context "a single site visitor" do
46
+ setup do
47
+ Conductor.identity = ActiveSupport::SecureRandom.hex(16)
48
+ end
18
49
 
19
- selected = VectorSixteen::Experiment.pick('a_group', ["a", "b", "c"]) # => value must be unique
20
- different = false
50
+ should "always select the same alternative when using the cache" do
51
+ selected = Conductor::Experiment.pick('a_group', ["a", "b", "c"]) # => value must be unique
52
+ different = false
21
53
 
22
- (1..100).each do |x|
23
- different = true if selected != VectorSixteen::Experiment.pick('a_group', ["a", "b", "c"])
24
- end
54
+ (1..100).each do |x|
55
+ different = true if selected != Conductor::Experiment.pick('a_group', ["a", "b", "c"])
56
+ end
25
57
 
26
- assert !different
27
- end
58
+ assert !different
59
+ end
28
60
 
29
- test "will select a lander and then successfully record a conversion" do
30
- VectorSixteen.identity = ActiveSupport::SecureRandom.hex(16)
31
- selected = VectorSixteen::Experiment.pick('a_group', ["a", "b", "c"]) # => value must be unique
61
+ should "select a lander and then successfully record a conversion" do
62
+ selected = Conductor::Experiment.pick('a_group', ["a", "b", "c"]) # => value must be unique
32
63
 
33
- VectorSixteen::Experiment.track!
64
+ Conductor::Experiment.track!
34
65
 
35
- experiments = V16::RawExperiment.find_all_by_identity_id(VectorSixteen.identity)
36
- assert_equal 1, experiments.count
37
- assert_equal 1, experiments.first.conversion_value
38
- end
66
+ experiments = Conductor::Experiment::Raw.find_all_by_identity_id(Conductor.identity)
67
+ assert_equal 1, experiments.count
68
+ assert_equal 1, experiments.first.conversion_value
69
+ end
39
70
 
40
- test "will select a lander and then successfully record custom conversion value" do
41
- VectorSixteen.identity = ActiveSupport::SecureRandom.hex(16)
42
- selected = VectorSixteen::Experiment.pick('a_group', ["a", "b", "c"]) # => value must be unique
71
+ should "select a lander and then successfully record custom conversion value" do
72
+ selected = Conductor::Experiment.pick('a_group', ["a", "b", "c"]) # => value must be unique
43
73
 
44
- VectorSixteen::Experiment.track!({:value => 12.34})
74
+ Conductor::Experiment.track!({:value => 12.34})
45
75
 
46
- experiments = V16::RawExperiment.find_all_by_identity_id(VectorSixteen.identity)
76
+ experiments = Conductor::Experiment::Raw.find_all_by_identity_id(Conductor.identity)
47
77
  assert_equal 1, experiments.count
48
78
  assert_equal 12.34, experiments.first.conversion_value
49
79
  end
50
80
 
81
+ should "record three different experiments with two goals but a single conversion for all goals for the same identity" do
82
+ first = Conductor::Experiment.pick('a_group', ["a", "b", "c"], {:goal => 'goal_1'}) # => value must be unique
83
+ second = Conductor::Experiment.pick('b_group', ["1", "2", "3"], {:goal => 'goal_2'}) # => value must be unique
84
+ third = Conductor::Experiment.pick('c_group', ["zz", "xx", "yy"], {:goal => 'goal_1'}) # => value must be unique
51
85
 
86
+ Conductor::Experiment.track!
52
87
 
53
- test "will record three different experiments with two goals but a single conversion for all goals for the same identity" do
54
- VectorSixteen.identity = ActiveSupport::SecureRandom.hex(16)
55
- first = VectorSixteen::Experiment.pick('a_group', ["a", "b", "c"], {:goal => 'goal_1'}) # => value must be unique
56
- second = VectorSixteen::Experiment.pick('b_group', ["1", "2", "3"], {:goal => 'goal_2'}) # => value must be unique
57
- third = VectorSixteen::Experiment.pick('c_group', ["zz", "xx", "yy"], {:goal => 'goal_1'}) # => value must be unique
58
-
59
- VectorSixteen::Experiment.track!
60
-
61
- experiments = V16::RawExperiment.find_all_by_identity_id(VectorSixteen.identity)
62
- assert_equal 3, experiments.count
63
- assert_equal 2, experiments.count {|x| x.goal == 'goal_1'}
64
- assert_equal 3, experiments.sum_it(:conversion_value)
65
- end
88
+ experiments = Conductor::Experiment::Raw.find_all_by_identity_id(Conductor.identity)
89
+ assert_equal 3, experiments.count
90
+ assert_equal 2, experiments.count {|x| x.goal == 'goal_1'}
91
+ assert_equal 3, experiments.sum_it(:conversion_value)
92
+ end
66
93
 
67
- test "will record three different experiments with two goals but only track a conversion for goal_1" do
68
- VectorSixteen.identity = ActiveSupport::SecureRandom.hex(16)
69
- first = VectorSixteen::Experiment.pick('a_group', ["a", "b", "c"], {:goal => 'goal_1'}) # => value must be unique
70
- second = VectorSixteen::Experiment.pick('b_group', ["1", "2", "3"], {:goal => 'goal_2'}) # => value must be unique
71
- third = VectorSixteen::Experiment.pick('c_group', ["zz", "xx", "yy"], {:goal => 'goal_1'}) # => value must be unique
94
+ should "record three different experiments with two goals but only track a conversion for goal_1" do
95
+ first = Conductor::Experiment.pick('a_group', ["a", "b", "c"], {:goal => 'goal_1'}) # => value must be unique
96
+ second = Conductor::Experiment.pick('b_group', ["1", "2", "3"], {:goal => 'goal_2'}) # => value must be unique
97
+ third = Conductor::Experiment.pick('c_group', ["zz", "xx", "yy"], {:goal => 'goal_1'}) # => value must be unique
72
98
 
73
- VectorSixteen::Experiment.track!({:goal => 'goal_1'})
99
+ Conductor::Experiment.track!({:goal => 'goal_1'})
74
100
 
75
- experiments = V16::RawExperiment.find_all_by_identity_id(VectorSixteen.identity)
76
- assert_equal 3, experiments.count
77
- assert_equal 2, experiments.count {|x| x.goal == 'goal_1'}
78
- assert_equal 2, experiments.sum_it(:conversion_value)
79
- end
101
+ experiments = Conductor::Experiment::Raw.find_all_by_identity_id(Conductor.identity)
102
+ assert_equal 3, experiments.count
103
+ assert_equal 2, experiments.count {|x| x.goal == 'goal_1'}
104
+ assert_equal 2, experiments.sum_it(:conversion_value)
105
+ end
106
+ end
80
107
 
108
+ context "conductor" do
109
+ setup do
110
+ seed_raw_data(100)
111
+ Conductor::RollUp.process
112
+ end
81
113
 
82
- test "will almost equally select each option if no weights exist" do
83
- a = 0
84
- b = 0
85
- c = 0
86
- (1..1000).each do |x|
87
- selected_lander = VectorSixteen::Experiment.pick('a_group', ["a", "b", "c"]) # => value must be unique
88
- case selected_lander
89
- when 'a' then
90
- a += 1
91
- when 'b' then
92
- b += 1
93
- when 'c' then
94
- c += 1
95
- end
96
- end
114
+ should "correctly RollUp daily data" do
115
+ assert Conductor::Experiment::Daily.count > 2
116
+ assert Conductor::Experiment::Daily.all.detect {|x| x.conversions > 0}
117
+ assert Conductor::Experiment::Daily.all.detect {|x| x.views > 0}
118
+ assert Conductor::Experiment::Daily.all.detect {|x| x.conversion_value > 0}
119
+ end
97
120
 
98
- nums = [] << a << b << c
99
- nums.sort!
100
- range = nums.last - nums.first
121
+ should "correctly populate weighting table" do
122
+ Conductor::Weights.compute
123
+ end
124
+ end
101
125
 
102
- assert (nums.first * 0.20) >= range
103
- end
126
+ context "conductor" do
127
+ should "populate the weighting table with equal weights if all new options are launched" do
128
+ seed_raw_data(100, 7)
104
129
 
105
- test "will correctly RollUp daily data" do
106
- # seed
107
- seed_raw_data(100)
130
+ # rollup
131
+ Conductor::RollUp.process
108
132
 
109
- # rollup
110
- VectorSixteen::RollUp.process
133
+ # compute weights
134
+ Conductor::Weights.compute
111
135
 
112
- # do some checks
113
- assert V16::DailyExperiment.count > 2
114
- assert V16::DailyExperiment.all.detect {|x| x.conversions > 0}
115
- assert V16::DailyExperiment.all.detect {|x| x.views > 0}
116
- assert V16::DailyExperiment.all.detect {|x| x.conversion_value > 0}
117
- end
136
+ # this makes the following assumptions:
137
+ # MINIMUM_LAUNCH_DAYS = 7
138
+ # each weight will be equal to 0.18
139
+ assert_equal 0.54, Conductor::Experiment::Weight.all.sum_it(:weight).to_f
140
+ end
141
+ end
118
142
 
119
- test "will correctly populate weighting table" do
120
- # seed
121
- seed_raw_data(100)
143
+ context "conductor" do
144
+ setup do
145
+ seed_raw_data(100, 14);
122
146
 
123
- # rollup
124
- VectorSixteen::RollUp.process
147
+ # rollup
148
+ Conductor::RollUp.process
125
149
 
126
- # compute weights
127
- VectorSixteen::Weights.compute
128
- end
150
+ # compute weights
151
+ Conductor::Weights.compute
152
+ end
129
153
 
130
- test "will populate the weighting table with equal weights if all new options are launched" do
131
- wipe
132
- seed_raw_data(100, 7)
154
+ should "populate the weighting table with different weights" do
155
+ # if this DOES NOT work then each weight will be equal to 0.18
156
+ assert_not_equal 0.54, Conductor::Experiment::Weight.all.sum_it(:weight).to_f
157
+ end
133
158
 
134
- # rollup
135
- VectorSixteen::RollUp.process
159
+ should "record the new weights in the weight history table in database" do
160
+ assert Conductor::Experiment::History.count > 1
161
+ end
136
162
 
137
- # compute weights
138
- VectorSixteen::Weights.compute
163
+ should "return a weight 1.25 times higher than the highest weight for a newly launched and non-recorded alernative" do
164
+ seed_raw_data(100, 14)
139
165
 
140
- # this makes the following assumptions:
141
- # MINIMUM_LAUNCH_DAYS = 7
142
- # each weight will be equal to 0.18
143
- assert_equal 0.54, V16::WeightedExperiment.all.sum_it(:weight).to_f
144
- end
166
+ # rollup
167
+ Conductor::RollUp.process
145
168
 
146
- test "will populate the weighting table with different weights" do
147
- wipe
148
- seed_raw_data(100, 14)
169
+ # compute weights
170
+ Conductor::Weights.compute
149
171
 
150
- # rollup
151
- VectorSixteen::RollUp.process
152
-
153
- # compute weights
154
- VectorSixteen::Weights.compute
155
-
156
- # if this DOES NOT work then each weight will be equal to 0.18
157
- assert_not_equal 0.54, V16::WeightedExperiment.all.sum_it(:weight).to_f
158
- end
159
-
160
- test "will record the new weights in the weight history table in database" do
161
- wipe
162
- seed_raw_data(100, 14)
163
-
164
- # rollup
165
- VectorSixteen::RollUp.process
172
+ # get the highest weight
173
+ max_weight = Conductor::Experiment::Weight.maximum(:weight)
166
174
 
167
- # compute weights
168
- VectorSixteen::Weights.compute
169
-
170
- assert V16::WeightHistory.count > 1
171
- end
175
+ # pick something
176
+ weights = Conductor::Experiment.weights('a_group', ["a", "b", "c", "f"]) # => value must be unique
172
177
 
173
- test "will correctly record the launch window in the weight histories table" do
174
- wipe
175
- seed_raw_data(10, 6)
176
-
177
- # rollup
178
- VectorSixteen::RollUp.process
178
+ assert_equal weights['f'], (max_weight * 1.25)
179
+ end
180
+ end
179
181
 
180
- # compute weights
181
- VectorSixteen::Weights.compute
182
-
183
- # make sure that launch_window values can be detected
184
- assert_not_nil V16::WeightHistory.find(:all, :conditions => 'launch_window > 0')
185
- end
182
+ context "conductor" do
183
+ should "correctly record the launch window in the weight histories table" do
184
+ seed_raw_data(10, 6)
186
185
 
187
- test "will return a weight 1.25 times higher than the highest weight for a newly launched and non-recorded alernative" do
188
- wipe
189
- seed_raw_data(100, 14)
186
+ # rollup
187
+ Conductor::RollUp.process
190
188
 
191
- # rollup
192
- VectorSixteen::RollUp.process
189
+ # compute weights
190
+ Conductor::Weights.compute
193
191
 
194
- # compute weights
195
- VectorSixteen::Weights.compute
192
+ # make sure that launch_window values can be detected
193
+ assert_not_nil Conductor::Experiment::History.find(:all, :conditions => 'launch_window > 0')
194
+ end
195
+ end
196
196
 
197
- # get the highest weight
198
- max_weight = V16::WeightedExperiment.maximum(:weight)
199
197
 
200
- # pick something
201
- weights = VectorSixteen::Experiment.weights('a_group', ["a", "b", "c", "f"]) # => value must be unique
202
198
 
203
- assert_equal weights['f'], (max_weight * 1.25)
204
- end
205
199
 
206
- private
200
+ private
207
201
 
208
- def wipe
209
- V16::DailyExperiment.delete_all
210
- V16::RawExperiment.delete_all
211
- V16::WeightedExperiment.delete_all
212
- V16::WeightHistory.delete_all
213
- end
202
+ def wipe
203
+ Conductor::Experiment::Daily.delete_all
204
+ Conductor::Experiment::Raw.delete_all
205
+ Conductor::Experiment::Weight.delete_all
206
+ Conductor::Experiment::History.delete_all
207
+ end
214
208
 
215
- def seed_raw_data(num, days_ago=14)
216
- # seed the raw data
217
- (1..num).each do |x|
218
- VectorSixteen.identity = ActiveSupport::SecureRandom.hex(16)
209
+ def seed_raw_data(num, days_ago=14)
210
+ # seed the raw data
211
+ (1..num).each do |x|
212
+ Conductor.identity = ActiveSupport::SecureRandom.hex(16)
219
213
 
220
- options = {:created_at => rand(days_ago).days.ago}
221
- options.merge!({:conversion_value => rand(100)}) if rand() < 0.20 # => convert 20% of traffic
222
- selected_lander = VectorSixteen::Experiment.pick('a_group', ["a", "b", "c"], options) # => value must be unique
223
- end
224
- end
214
+ options = {:created_at => rand(days_ago).days.ago}
215
+ options.merge!({:conversion_value => rand(100)}) if rand() < 0.20 # => convert 20% of traffic
216
+ selected_lander = Conductor::Experiment.pick('a_group', ["a", "b", "c"], options) # => value must be unique
217
+ end
218
+ end
225
219
  end
@@ -0,0 +1,29 @@
1
+ ENV["RAILS_ENV"] = "test"
2
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
3
+
4
+ require 'rubygems'
5
+ require 'active_record'
6
+ require 'active_record/version'
7
+ require 'active_record/fixtures'
8
+ require 'action_controller'
9
+ require 'action_controller/test_process'
10
+ require 'action_view'
11
+ require 'active_support'
12
+ require 'test/unit'
13
+ require 'conductor'
14
+ require 'shoulda'
15
+
16
+ require File.dirname(__FILE__) + '/../init.rb'
17
+
18
+
19
+ config = YAML::load(IO.read(File.dirname(__FILE__) + '/db/database.yml'))
20
+ ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
21
+ ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'postgresql'])
22
+ ActiveRecord::Migration.verbose = false
23
+ load(File.dirname(__FILE__) + "/db/schema.rb")
24
+
25
+ @@cache = ActiveSupport::Cache::MemoryStore.new
26
+
27
+ class Test::Unit::TestCase
28
+
29
+ end
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 2
8
- - 16
9
- version: 0.2.16
7
+ - 3
8
+ - 0
9
+ version: 0.3.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Noctivity
@@ -43,8 +43,9 @@ files:
43
43
  - lib/conductor/experiment/weight.rb
44
44
  - lib/conductor/roll_up.rb
45
45
  - lib/conductor/weights.rb
46
- - test/helper.rb
46
+ - test/db/schema.rb
47
47
  - test/test_conductor.rb
48
+ - test/test_helper.rb
48
49
  has_rdoc: true
49
50
  homepage: http://github.com/noctivityinc/conductor
50
51
  licenses: []
@@ -78,5 +79,6 @@ signing_key:
78
79
  specification_version: 3
79
80
  summary: lets you just try things while always maximizing towards a goal (e.g. purchase, signups, etc)
80
81
  test_files:
81
- - test/helper.rb
82
+ - test/db/schema.rb
82
83
  - test/test_conductor.rb
84
+ - test/test_helper.rb
data/test/helper.rb DELETED
@@ -1,9 +0,0 @@
1
- require 'rubygems'
2
- require 'test/unit'
3
-
4
- $LOAD_PATH.unshift(File.dirname(__FILE__))
5
- $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
- require 'conductor'
7
-
8
- class Test::Unit::TestCase
9
- end