conductor 0.6.5 → 0.7.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 CHANGED
@@ -1 +1 @@
1
- 0.6.5
1
+ 0.7.0
data/lib/conductor.rb CHANGED
@@ -15,7 +15,8 @@ class Conductor
15
15
  DBG = false
16
16
 
17
17
  cattr_writer :cache
18
-
18
+ cattr_writer :days_till_weighting
19
+
19
20
  def self.cache
20
21
  @@cache || Rails.cache
21
22
  end
@@ -28,16 +29,29 @@ class Conductor
28
29
  def identity
29
30
  return (@conductor_identity || ActiveSupport::SecureRandom.hex(16))
30
31
  end
32
+
33
+ def minimum_launch_days
34
+ return (@@days_till_weighting || MINIMUM_LAUNCH_DAYS)
35
+ end
36
+
37
+ def attribute_for_weighting=(value)
38
+ raise "Conductor.attribute_for_weighting must be either :views, :conversions or :conversion_value (default)" unless [:views, :conversions, :conversion_value].include?(value)
39
+ @attribute_for_weighting = value
40
+ end
41
+
42
+ def attribute_for_weighting
43
+ return (@attribute_for_weighting || :conversion_value)
44
+ end
31
45
 
32
46
  def log(msg)
33
47
  puts msg if DBG
34
48
  end
35
-
49
+
36
50
  def sanitize(str)
37
51
  str.gsub(/\s/,'_').downcase
38
52
  end
39
53
  end
40
-
54
+
41
55
  end
42
56
 
43
57
 
@@ -45,4 +59,17 @@ class Array
45
59
  def sum_it(attribute)
46
60
  self.map {|x| x.send(attribute) }.compact.sum
47
61
  end
62
+
63
+ def weighted_mean_of_attribute(attribute)
64
+ self.map {|x| x.send(attribute) }.compact.weighted_mean
65
+ end
66
+
67
+ def weighted_mean
68
+ w_sum = sum(self)
69
+ return 0.00 if w_sum == 0.00
70
+
71
+ w_prod = 0
72
+ self.each_index {|i| w_prod += (i+1) * self[i].to_f}
73
+ w_prod.to_f / w_sum.to_f
74
+ end
48
75
  end
@@ -6,7 +6,7 @@ class Conductor
6
6
  # group do not match all the alternatives previously computed for the group, new weights are
7
7
  # generated. The cache is used to speed up this check
8
8
  def find_or_create(group_name, alternatives)
9
- weights_for_group = Conductor.cache.read("Conductor::Experiment::#{group_name}::Alternatives")
9
+ weights_for_group = Conductor.cache.read("Conductor::Experiment::#{Conductor.attribute_for_weighting}::#{group_name}::Alternatives")
10
10
 
11
11
  alternatives_array = weights_for_group.map(&:alternative).sort if weights_for_group
12
12
  if alternatives_array.eql?(alternatives.sort)
@@ -18,8 +18,8 @@ class Conductor
18
18
 
19
19
  # get the new weights
20
20
  weights_for_group = Conductor::Experiment::Weight.find(:all, :conditions => "group_name = '#{group_name}'")
21
- Conductor.cache.delete("Conductor::Experiment::#{group_name}::Alternatives")
22
- Conductor.cache.write("Conductor::Experiment::#{group_name}::Alternatives", weights_for_group)
21
+ Conductor.cache.delete("Conductor::Experiment::#{Conductor.attribute_for_weighting}::#{group_name}::Alternatives")
22
+ Conductor.cache.write("Conductor::Experiment::#{Conductor.attribute_for_weighting}::#{group_name}::Alternatives", weights_for_group)
23
23
  return weights_for_group
24
24
  end
25
25
  end
@@ -33,7 +33,7 @@ class Conductor
33
33
 
34
34
  unless group_rows.empty?
35
35
  Conductor::Experiment::Weight.delete_all(:group_name => group_name) # => remove all old data for group
36
- total = group_rows.sum_it(:conversion_value)
36
+ total = group_rows.sum_it(Conductor.attribute_for_weighting)
37
37
  data = total ? compute_weights_for_group(group_name, group_rows, total) : assign_equal_weights(group_rows)
38
38
  update_weights_in_db(group_name, data)
39
39
  end
@@ -46,46 +46,50 @@ class Conductor
46
46
  def compute_weights_for_group(group_name, group_rows, total)
47
47
  data = []
48
48
  recently_launched = []
49
- max_weight = 0
49
+
50
+ # calculate weights for each alternative
51
+ weighted_moving_avg, total, max_weight = compute_weighted_moving_stats(group_rows)
50
52
 
51
53
  group_rows.group_by(&:alternative).each do |alternative_name, alternatives|
52
- first_found_date = alternatives.map(&:activity_date).sort.first
53
- days_ago = Date.today - first_found_date
54
+ days_ago = compute_days_ago(alternatives)
54
55
 
55
- if days_ago >= MINIMUM_LAUNCH_DAYS
56
- data << compute_weight_for_alternative(alternative_name, alternatives, max_weight, total)
56
+ if days_ago >= Conductor.minimum_launch_days
57
+ data << {:name => alternative_name, :weight => (weighted_moving_avg[alternative_name] / total)}
57
58
  else
58
- Conductor.log("adding #{alternative_name} to recently launched array")
59
59
  recently_launched << {:name => alternative_name, :days_ago => days_ago}
60
60
  end
61
61
  end
62
62
 
63
- data += weight_recently_launched(max_weight, recently_launched)
63
+ weight_recently_launched(data, max_weight, recently_launched)
64
64
  return data
65
65
  end
66
66
 
67
- def compute_weight_for_alternative(alternative_name, alternatives, max_weight, total)
68
- Conductor.log("compute_weight_for_alternative for #{alternative_name}")
69
-
70
- aggregates = {:name => alternative_name}
71
-
72
- weight = alternatives.sum_it(:conversion_value) / total
73
- max_weight = weight if weight > max_weight
74
- aggregates.merge!({:weight => weight})
75
-
76
- return aggregates
67
+ def compute_weighted_moving_stats(group_rows)
68
+ weighted_moving_avg = {}
69
+ group_rows.group_by(&:alternative).each do |alternative_name, alternatives|
70
+ weighted_moving_avg.merge!({"#{alternative_name}" => alternatives.sort_by(&:activity_date).weighted_mean_of_attribute(Conductor.attribute_for_weighting)})
71
+ end
72
+
73
+ # calculate total and max_weight for weighted moving averages
74
+ total = weighted_moving_avg.values.sum
75
+ max_weight = weighted_moving_avg.values.max
76
+
77
+ return weighted_moving_avg, total, max_weight
78
+ end
79
+
80
+ def compute_days_ago(alternatives)
81
+ first_found_date = alternatives.map(&:activity_date).min
82
+ (Date.today - first_found_date.to_date).to_f
77
83
  end
78
84
 
79
- def weight_recently_launched(max_weight, recently_launched)
85
+ def weight_recently_launched(data, max_weight, recently_launched)
80
86
  # loop through recently_launched to create weights for table
81
87
  # the handicap sets a newly launched item to the max weight * MAX_WEIGHTING_FACTOR and then
82
88
  # slowly lowers its power until the launch period is over
83
- data = []
84
- max_weight = 0 ? 1 : max_weight # => if a max weight could not be computed, set it to 1
85
- Conductor.log("max weight: #{max_weight}")
89
+ max_weight = 1 if data.empty?
86
90
  recently_launched.each do |alternative|
87
- handicap = (alternative[:days_ago].to_f / MINIMUM_LAUNCH_DAYS)
88
- launch_window = (MINIMUM_LAUNCH_DAYS - alternative[:days_ago]) if MINIMUM_LAUNCH_DAYS > alternative[:days_ago]
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]
89
93
  Conductor.log("Handicap for #{alternative[:name]} is #{handicap} (#{alternative[:days_ago]} days ago)")
90
94
  data << {:name => alternative[:name], :weight => max_weight * MAX_WEIGHTING_FACTOR * (1 - handicap), :launch_window => launch_window}
91
95
  end
@@ -22,6 +22,15 @@ 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)
29
+ end
30
+
31
+ should "raise an error if an improper attribute is specified for @@attribute_for_weighting" do
32
+ assert_raise(RuntimeError, LoadError) { Conductor.attribute_for_weighting = :random}
33
+ end
25
34
 
26
35
  should "almost equally select each option if no weights exist" do
27
36
  a = 0
@@ -169,11 +178,11 @@ class TestConductor < Test::Unit::TestCase
169
178
 
170
179
  # hit after rollup to populare weight table
171
180
  Conductor.identity = ActiveSupport::SecureRandom.hex(16)
181
+ Conductor.days_till_weighting = 7
172
182
  selected = Conductor::Experiment.pick('a_group', ["a", "b", "c"])
173
183
 
174
- # this makes the following assumptions:
175
- # MINIMUM_LAUNCH_DAYS = 7
176
184
  # each weight will be equal to 0.18
185
+ assert_equal 7, Conductor.minimum_launch_days
177
186
  assert_equal 0.54, Conductor::Experiment::Weight.all.sum_it(:weight).to_f
178
187
  end
179
188
  end
@@ -225,6 +234,30 @@ class TestConductor < Test::Unit::TestCase
225
234
  assert_not_nil Conductor::Experiment::History.find(:all, :conditions => 'launch_window > 0')
226
235
  end
227
236
  end
237
+
238
+ context "conductor" do
239
+ setup do
240
+ seed_raw_data(500, 30)
241
+
242
+ # rollup
243
+ Conductor::RollUp.process
244
+ end
245
+
246
+ should "allow for the number of conversions to be used for weighting instead of conversion_value" do
247
+ Conductor.identity = ActiveSupport::SecureRandom.hex(16)
248
+ Conductor::Experiment.pick('a_group', ["a", "b", "c"])
249
+ weights_cv = Conductor::Experiment::Weight.all.map(&:weight).sort
250
+
251
+ Conductor.identity = ActiveSupport::SecureRandom.hex(16)
252
+ Conductor.attribute_for_weighting = :conversions
253
+ Conductor::Experiment.pick('a_group', ["a", "b", "c"])
254
+ weights_c = Conductor::Experiment::Weight.all.map(&:weight).sort
255
+
256
+ # since one is using conversion_value and the other is using conversions, they two weight arrays should be different
257
+ assert_equal :conversions, Conductor.attribute_for_weighting
258
+ assert_not_equal weights_cv, weights_c
259
+ end
260
+ end
228
261
 
229
262
 
230
263
  private
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 6
8
- - 5
9
- version: 0.6.5
7
+ - 7
8
+ - 0
9
+ version: 0.7.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Noctivity
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-10-01 00:00:00 -04:00
17
+ date: 2010-10-03 00:00:00 -04:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency