conductor 0.2.16 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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