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 +1 -1
- data/lib/conductor.rb +30 -3
- data/lib/conductor/weights.rb +31 -27
- data/test/test_conductor.rb +35 -2
- metadata +4 -4
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
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
|
data/lib/conductor/weights.rb
CHANGED
@@ -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(
|
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
|
-
|
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
|
-
|
53
|
-
days_ago = Date.today - first_found_date
|
54
|
+
days_ago = compute_days_ago(alternatives)
|
54
55
|
|
55
|
-
if days_ago >=
|
56
|
-
data <<
|
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
|
63
|
+
weight_recently_launched(data, max_weight, recently_launched)
|
64
64
|
return data
|
65
65
|
end
|
66
66
|
|
67
|
-
def
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
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 /
|
88
|
-
launch_window = (
|
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
|
data/test/test_conductor.rb
CHANGED
@@ -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
|
-
-
|
8
|
-
-
|
9
|
-
version: 0.
|
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-
|
17
|
+
date: 2010-10-03 00:00:00 -04:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|