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 +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
|