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 +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
|