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 CHANGED
@@ -1 +1 @@
1
- 0.7.0
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
- MINIMUM_LAUNCH_DAYS = 7
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
- def minimum_launch_days
34
- return (@@days_till_weighting || MINIMUM_LAUNCH_DAYS)
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
@@ -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.minimum_launch_days
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.minimum_launch_days)
92
- launch_window = (Conductor.minimum_launch_days - alternative[:days_ago]) if Conductor.minimum_launch_days > alternative[:days_ago]
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
@@ -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 minimum_launch_days to be configurable" do
27
- Conductor.days_till_weighting = 3
28
- assert_equal(3, Conductor.minimum_launch_days)
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 @@attribute_for_weighting" do
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
- should "populate the weighting table with equal weights if all new options are launched" do
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.days_till_weighting = 7
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.minimum_launch_days
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
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 7
8
- - 0
9
- version: 0.7.0
8
+ - 1
9
+ version: 0.7.1
10
10
  platform: ruby
11
11
  authors:
12
12
  - Noctivity