conductor 0.7.0 → 0.7.1
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 +22 -4
- data/lib/conductor/weights.rb +3 -3
- data/test/test_conductor.rb +32 -24
- metadata +2 -2
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.7.
|
1
|
+
0.7.1
|
data/lib/conductor.rb
CHANGED
@@ -11,17 +11,19 @@ require 'conductor/helpers/dashboard_helper'
|
|
11
11
|
|
12
12
|
class Conductor
|
13
13
|
MAX_WEIGHTING_FACTOR = 1.25
|
14
|
-
|
14
|
+
EQUALIZATION_PERIOD_DEFAULT = 7
|
15
15
|
DBG = false
|
16
16
|
|
17
17
|
cattr_writer :cache
|
18
|
-
cattr_writer :days_till_weighting
|
19
18
|
|
20
19
|
def self.cache
|
21
20
|
@@cache || Rails.cache
|
22
21
|
end
|
23
22
|
|
24
23
|
class << self
|
24
|
+
# Specifies a unique identity for the current visitor. If no identity is specified
|
25
|
+
# then a random value is selected. Conductor makes sure that the same visitor
|
26
|
+
# will always see the same alternative selections to reduce confusion.
|
25
27
|
def identity=(value)
|
26
28
|
@conductor_identity = value
|
27
29
|
end
|
@@ -30,10 +32,26 @@ class Conductor
|
|
30
32
|
return (@conductor_identity || ActiveSupport::SecureRandom.hex(16))
|
31
33
|
end
|
32
34
|
|
33
|
-
|
34
|
-
|
35
|
+
# The equalization period is the initial amount of time, in days, that conductor
|
36
|
+
# should apply the max_weighting_factor towards a new alternative to ensure
|
37
|
+
# that it receives a far shot of performing.
|
38
|
+
#
|
39
|
+
# If an equalization period was not used then any new alternative would
|
40
|
+
# immediately be weighed very low since it has no conversions and would
|
41
|
+
# never have a chance of performing
|
42
|
+
def equalization_period=(value)
|
43
|
+
raise "Conductor.equalization_period must be a positive number > 0" unless value.is_a?(Numeric) && value > 0
|
44
|
+
@equalization_period = value
|
35
45
|
end
|
36
46
|
|
47
|
+
def equalization_period
|
48
|
+
return (@equalization_period || EQUALIZATION_PERIOD_DEFAULT)
|
49
|
+
end
|
50
|
+
|
51
|
+
# The attribute for weighting specifies if the conversion_value OR number of conversions
|
52
|
+
# should be used to calculate the weight. The default is conversion_value.
|
53
|
+
#
|
54
|
+
# TODO: Allow of avg_conversion_value where acv = conversion_value / conversions
|
37
55
|
def attribute_for_weighting=(value)
|
38
56
|
raise "Conductor.attribute_for_weighting must be either :views, :conversions or :conversion_value (default)" unless [:views, :conversions, :conversion_value].include?(value)
|
39
57
|
@attribute_for_weighting = value
|
data/lib/conductor/weights.rb
CHANGED
@@ -53,7 +53,7 @@ class Conductor
|
|
53
53
|
group_rows.group_by(&:alternative).each do |alternative_name, alternatives|
|
54
54
|
days_ago = compute_days_ago(alternatives)
|
55
55
|
|
56
|
-
if days_ago >= Conductor.
|
56
|
+
if days_ago >= Conductor.equalization_period
|
57
57
|
data << {:name => alternative_name, :weight => (weighted_moving_avg[alternative_name] / total)}
|
58
58
|
else
|
59
59
|
recently_launched << {:name => alternative_name, :days_ago => days_ago}
|
@@ -88,8 +88,8 @@ class Conductor
|
|
88
88
|
# slowly lowers its power until the launch period is over
|
89
89
|
max_weight = 1 if data.empty?
|
90
90
|
recently_launched.each do |alternative|
|
91
|
-
handicap = (alternative[:days_ago].to_f / Conductor.
|
92
|
-
launch_window = (Conductor.
|
91
|
+
handicap = (alternative[:days_ago].to_f / Conductor.equalization_period)
|
92
|
+
launch_window = (Conductor.equalization_period - alternative[:days_ago]) if Conductor.equalization_period > alternative[:days_ago]
|
93
93
|
Conductor.log("Handicap for #{alternative[:name]} is #{handicap} (#{alternative[:days_ago]} days ago)")
|
94
94
|
data << {:name => alternative[:name], :weight => max_weight * MAX_WEIGHTING_FACTOR * (1 - handicap), :launch_window => launch_window}
|
95
95
|
end
|
data/test/test_conductor.rb
CHANGED
@@ -22,13 +22,20 @@ class TestConductor < Test::Unit::TestCase
|
|
22
22
|
x = Conductor.cache.read('testing')
|
23
23
|
assert_equal x, 'value'
|
24
24
|
end
|
25
|
-
|
26
|
-
should "allow for the
|
27
|
-
Conductor.
|
28
|
-
assert_equal(3, Conductor.
|
25
|
+
|
26
|
+
should "allow for the equalization_period to be configurable" do
|
27
|
+
Conductor.equalization_period = 3
|
28
|
+
assert_equal(3, Conductor.equalization_period)
|
29
|
+
end
|
30
|
+
|
31
|
+
should "raise an error if a non-numeric value, negative or 0 value is specified for the equalization_period" do
|
32
|
+
assert_raise(RuntimeError, LoadError) { Conductor.equalization_period = 'junk'}
|
33
|
+
assert_raise(RuntimeError, LoadError) { Conductor.equalization_period = -1.0}
|
34
|
+
assert_raise(RuntimeError, LoadError) { Conductor.equalization_period = 0}
|
35
|
+
assert_nothing_raised(RuntimeError, LoadError) { Conductor.equalization_period = 3}
|
29
36
|
end
|
30
|
-
|
31
|
-
should "raise an error if an improper attribute is specified for
|
37
|
+
|
38
|
+
should "raise an error if an improper attribute is specified for @attribute_for_weighting" do
|
32
39
|
assert_raise(RuntimeError, LoadError) { Conductor.attribute_for_weighting = :random}
|
33
40
|
end
|
34
41
|
|
@@ -133,7 +140,7 @@ class TestConductor < Test::Unit::TestCase
|
|
133
140
|
assert Conductor::Experiment::Daily.all.detect {|x| x.views > 0}
|
134
141
|
assert Conductor::Experiment::Daily.all.detect {|x| x.conversion_value > 0}
|
135
142
|
end
|
136
|
-
|
143
|
+
|
137
144
|
should "correctly populate weighting table when selecting a value" do
|
138
145
|
selected = Conductor::Experiment.pick('a_group', ["a", "b", "c"])
|
139
146
|
assert_equal 3, Conductor::Experiment::Weight.count
|
@@ -141,48 +148,49 @@ class TestConductor < Test::Unit::TestCase
|
|
141
148
|
|
142
149
|
should "pull weights from the cache" do
|
143
150
|
Conductor::Experiment.pick('a_group', ["a", "b", "c"])
|
144
|
-
|
151
|
+
|
145
152
|
(1..100).each do |x|
|
146
153
|
Conductor.identity = ActiveSupport::SecureRandom.hex(16)
|
147
154
|
Conductor::Experiment.pick('a_group', ["a", "b", "c"])
|
148
155
|
end
|
149
|
-
|
150
|
-
# => if this works the history table should have only been updated one time not 101 so there should
|
156
|
+
|
157
|
+
# => if this works the history table should have only been updated one time not 101 so there should
|
151
158
|
# => be three records (one for a, b and c)
|
152
159
|
assert_equal 3, Conductor::Experiment::History.count
|
153
160
|
end
|
154
|
-
|
161
|
+
|
155
162
|
should "pull weights from the cache and then recreate weights when the alternative list changes" do
|
156
163
|
Conductor::Experiment.pick('a_group', ["a", "b", "c"])
|
157
|
-
|
164
|
+
|
158
165
|
(1..100).each do |x|
|
159
166
|
Conductor.identity = ActiveSupport::SecureRandom.hex(16)
|
160
167
|
Conductor::Experiment.pick('a_group', ["a", "b", "c"])
|
161
168
|
end
|
162
169
|
|
163
|
-
Conductor.identity = ActiveSupport::SecureRandom.hex(16)
|
170
|
+
Conductor.identity = ActiveSupport::SecureRandom.hex(16)
|
164
171
|
Conductor::Experiment.pick('a_group', ["a", "c"])
|
165
|
-
|
166
|
-
# => if this works the history table should have only been updated one time not 101 so there should
|
172
|
+
|
173
|
+
# => if this works the history table should have only been updated one time not 101 so there should
|
167
174
|
# => be FIVE records (one for a, b and c and then one for a and c)
|
168
175
|
assert_equal 5, Conductor::Experiment::History.count
|
169
176
|
end
|
170
177
|
end
|
171
178
|
|
172
179
|
context "conductor" do
|
173
|
-
|
180
|
+
setup do
|
181
|
+
wipe
|
174
182
|
seed_raw_data(100, 7)
|
175
|
-
|
176
|
-
# rollup
|
177
183
|
Conductor::RollUp.process
|
184
|
+
end
|
178
185
|
|
186
|
+
should "populate the weighting table with equal weights if all new options are launched" do
|
179
187
|
# hit after rollup to populare weight table
|
180
188
|
Conductor.identity = ActiveSupport::SecureRandom.hex(16)
|
181
|
-
Conductor.
|
189
|
+
Conductor.equalization_period = 7
|
182
190
|
selected = Conductor::Experiment.pick('a_group', ["a", "b", "c"])
|
183
191
|
|
184
192
|
# each weight will be equal to 0.18
|
185
|
-
assert_equal 7, Conductor.
|
193
|
+
assert_equal 7, Conductor.equalization_period
|
186
194
|
assert_equal 0.54, Conductor::Experiment::Weight.all.sum_it(:weight).to_f
|
187
195
|
end
|
188
196
|
end
|
@@ -234,7 +242,7 @@ class TestConductor < Test::Unit::TestCase
|
|
234
242
|
assert_not_nil Conductor::Experiment::History.find(:all, :conditions => 'launch_window > 0')
|
235
243
|
end
|
236
244
|
end
|
237
|
-
|
245
|
+
|
238
246
|
context "conductor" do
|
239
247
|
setup do
|
240
248
|
seed_raw_data(500, 30)
|
@@ -242,7 +250,7 @@ class TestConductor < Test::Unit::TestCase
|
|
242
250
|
# rollup
|
243
251
|
Conductor::RollUp.process
|
244
252
|
end
|
245
|
-
|
253
|
+
|
246
254
|
should "allow for the number of conversions to be used for weighting instead of conversion_value" do
|
247
255
|
Conductor.identity = ActiveSupport::SecureRandom.hex(16)
|
248
256
|
Conductor::Experiment.pick('a_group', ["a", "b", "c"])
|
@@ -252,12 +260,12 @@ class TestConductor < Test::Unit::TestCase
|
|
252
260
|
Conductor.attribute_for_weighting = :conversions
|
253
261
|
Conductor::Experiment.pick('a_group', ["a", "b", "c"])
|
254
262
|
weights_c = Conductor::Experiment::Weight.all.map(&:weight).sort
|
255
|
-
|
263
|
+
|
256
264
|
# since one is using conversion_value and the other is using conversions, they two weight arrays should be different
|
257
265
|
assert_equal :conversions, Conductor.attribute_for_weighting
|
258
266
|
assert_not_equal weights_cv, weights_c
|
259
267
|
end
|
260
|
-
end
|
268
|
+
end
|
261
269
|
|
262
270
|
|
263
271
|
private
|