conductor 0.6.5 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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