conductor 0.7.0 → 0.7.1
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 +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
|