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 +1 -1
- data/lib/conductor.rb +9 -0
- data/lib/conductor/experiment.rb +14 -14
- data/lib/conductor/experiment/raw.rb +1 -1
- data/lib/conductor/roll_up.rb +8 -8
- data/lib/conductor/weights.rb +23 -23
- data/test/db/schema.rb +43 -0
- data/test/test_conductor.rb +166 -172
- data/test/test_helper.rb +29 -0
- metadata +7 -5
- data/test/helper.rb +0 -9
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
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
|
|
data/lib/conductor/experiment.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
class Conductor
|
2
2
|
class Experiment
|
3
3
|
class << self
|
4
|
-
# Selects the best
|
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 =
|
29
|
-
Conductor::
|
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::
|
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
|
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
|
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
|
-
|
98
|
+
alternative_filter = alternatives.inject([]) {|res,x| res << "alternative = '#{sanitize(x)}'"}.join(' OR ')
|
99
99
|
|
100
|
-
conditions = "group_name = '#{group_name}' AND (#{
|
100
|
+
conditions = "group_name = '#{group_name}' AND (#{alternative_filter})"
|
101
101
|
|
102
|
-
# get the
|
103
|
-
weights ||= Conductor::
|
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.
|
106
|
+
weighting_table = weights.inject({}) {|res, x| res.merge!({x.alternative => x.weight})}
|
107
107
|
|
108
108
|
# is anything missing?
|
109
|
-
|
110
|
-
missing = alternatives -
|
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
|
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, :
|
17
|
+
validates_presence_of :group_name, :alternative
|
18
18
|
|
19
19
|
def created_date
|
20
20
|
self.created_at.strftime('%Y-%m-%d')
|
data/lib/conductor/roll_up.rb
CHANGED
@@ -1,19 +1,19 @@
|
|
1
1
|
class Conductor
|
2
2
|
class RollUp
|
3
3
|
def self.process
|
4
|
-
Conductor::
|
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::
|
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(&:
|
11
|
-
conversion_value =
|
12
|
-
views =
|
13
|
-
conversions =
|
14
|
-
Conductor::
|
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
|
-
:
|
16
|
+
:alternative => alternative_name,
|
17
17
|
:conversion_value => conversion_value,
|
18
18
|
:views => views,
|
19
19
|
:conversions => conversions )
|
data/lib/conductor/weights.rb
CHANGED
@@ -2,10 +2,10 @@ class Conductor
|
|
2
2
|
class Weights
|
3
3
|
class << self
|
4
4
|
def compute
|
5
|
-
Conductor::
|
5
|
+
Conductor::Experiment::Weight.delete_all # => remove all old data
|
6
6
|
|
7
|
-
# loop through each group and determing weight of
|
8
|
-
Conductor::
|
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
|
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(&:
|
27
|
-
first_found_date =
|
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 <<
|
31
|
+
data << compute_weight_for_alternative(alternative_name, alternatives, max_weight, total)
|
32
32
|
else
|
33
|
-
Conductor.log("adding #{
|
34
|
-
recently_launched << {:name =>
|
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
|
43
|
-
Conductor.log("
|
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 =>
|
45
|
+
aggregates = {:name => alternative_name}
|
46
46
|
|
47
|
-
weight =
|
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 |
|
62
|
-
handicap = (
|
63
|
-
launch_window = (MINIMUM_LAUNCH_DAYS -
|
64
|
-
Conductor.log("Handicap for #{
|
65
|
-
data << {:name =>
|
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(&:
|
76
|
-
data << {:name =>
|
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 { |
|
84
|
-
Conductor::
|
85
|
-
Conductor::
|
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
|
data/test/test_conductor.rb
CHANGED
@@ -1,225 +1,219 @@
|
|
1
|
-
require '
|
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
|
-
|
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
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
17
|
-
|
45
|
+
context "a single site visitor" do
|
46
|
+
setup do
|
47
|
+
Conductor.identity = ActiveSupport::SecureRandom.hex(16)
|
48
|
+
end
|
18
49
|
|
19
|
-
|
20
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
54
|
+
(1..100).each do |x|
|
55
|
+
different = true if selected != Conductor::Experiment.pick('a_group', ["a", "b", "c"])
|
56
|
+
end
|
25
57
|
|
26
|
-
|
27
|
-
|
58
|
+
assert !different
|
59
|
+
end
|
28
60
|
|
29
|
-
|
30
|
-
|
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
|
-
|
64
|
+
Conductor::Experiment.track!
|
34
65
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
41
|
-
|
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
|
-
|
74
|
+
Conductor::Experiment.track!({:value => 12.34})
|
45
75
|
|
46
|
-
experiments =
|
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
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
99
|
+
Conductor::Experiment.track!({:goal => 'goal_1'})
|
74
100
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
-
|
99
|
-
|
100
|
-
|
121
|
+
should "correctly populate weighting table" do
|
122
|
+
Conductor::Weights.compute
|
123
|
+
end
|
124
|
+
end
|
101
125
|
|
102
|
-
|
103
|
-
|
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
|
-
|
106
|
-
|
107
|
-
seed_raw_data(100)
|
130
|
+
# rollup
|
131
|
+
Conductor::RollUp.process
|
108
132
|
|
109
|
-
|
110
|
-
|
133
|
+
# compute weights
|
134
|
+
Conductor::Weights.compute
|
111
135
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
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
|
-
|
120
|
-
|
121
|
-
|
143
|
+
context "conductor" do
|
144
|
+
setup do
|
145
|
+
seed_raw_data(100, 14);
|
122
146
|
|
123
|
-
|
124
|
-
|
147
|
+
# rollup
|
148
|
+
Conductor::RollUp.process
|
125
149
|
|
126
|
-
|
127
|
-
|
128
|
-
|
150
|
+
# compute weights
|
151
|
+
Conductor::Weights.compute
|
152
|
+
end
|
129
153
|
|
130
|
-
|
131
|
-
|
132
|
-
|
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
|
-
|
135
|
-
|
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
|
-
|
138
|
-
|
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
|
-
|
141
|
-
|
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
|
-
|
147
|
-
|
148
|
-
seed_raw_data(100, 14)
|
169
|
+
# compute weights
|
170
|
+
Conductor::Weights.compute
|
149
171
|
|
150
|
-
|
151
|
-
|
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
|
-
|
168
|
-
|
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
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
# rollup
|
178
|
-
VectorSixteen::RollUp.process
|
178
|
+
assert_equal weights['f'], (max_weight * 1.25)
|
179
|
+
end
|
180
|
+
end
|
179
181
|
|
180
|
-
|
181
|
-
|
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
|
-
|
188
|
-
|
189
|
-
seed_raw_data(100, 14)
|
186
|
+
# rollup
|
187
|
+
Conductor::RollUp.process
|
190
188
|
|
191
|
-
|
192
|
-
|
189
|
+
# compute weights
|
190
|
+
Conductor::Weights.compute
|
193
191
|
|
194
|
-
|
195
|
-
|
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
|
-
|
200
|
+
private
|
207
201
|
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
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
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
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
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
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
|
data/test/test_helper.rb
ADDED
@@ -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
|
-
-
|
8
|
-
-
|
9
|
-
version: 0.
|
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/
|
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/
|
82
|
+
- test/db/schema.rb
|
82
83
|
- test/test_conductor.rb
|
84
|
+
- test/test_helper.rb
|