conductor 0.7.2 → 0.8.1
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 +13 -0
- data/lib/conductor/experiment/daily.rb +35 -1
- data/lib/conductor/experiment/weight.rb +3 -0
- data/lib/conductor/helpers/dashboard_helper.rb +5 -0
- data/lib/conductor/views/dashboard/_group_stats.html.haml +23 -0
- data/lib/conductor/views/dashboard/_top_nav.html.haml +2 -1
- data/lib/conductor/views/dashboard/index.html.haml +1 -0
- data/lib/conductor/weights.rb +38 -62
- metadata +4 -3
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.8.1
|
data/lib/conductor.rb
CHANGED
@@ -32,6 +32,19 @@ class Conductor
|
|
32
32
|
return (@conductor_identity || ActiveSupport::SecureRandom.hex(16))
|
33
33
|
end
|
34
34
|
|
35
|
+
# The number of days to include when calculating weights
|
36
|
+
# The inclusion period MUST be higher than then equalization period
|
37
|
+
# The default is 14 days
|
38
|
+
def inclusion_period=(value)
|
39
|
+
raise "Conductor.inclusion_period must be a positive number > 0" unless value.is_a?(Numeric) && value > 0
|
40
|
+
raise "Conductor.inclusion_period must be greater than the equalization period" if value < equalization_period
|
41
|
+
@inclusion_period = value
|
42
|
+
end
|
43
|
+
|
44
|
+
def inclusion_period
|
45
|
+
return (@inclusion_period || 14)
|
46
|
+
end
|
47
|
+
|
35
48
|
# The equalization period is the initial amount of time, in days, that conductor
|
36
49
|
# should apply the max_weighting_factor towards a new alternative to ensure
|
37
50
|
# that it receives a far shot of performing.
|
@@ -13,7 +13,41 @@
|
|
13
13
|
|
14
14
|
class Conductor::Experiment::Daily < ActiveRecord::Base
|
15
15
|
set_table_name "conductor_daily_experiments"
|
16
|
+
|
16
17
|
named_scope :since, lambda { |a_date| { :conditions => ['activity_date >= ?',a_date] }}
|
17
18
|
named_scope :for_group, lambda { |group_name| { :conditions => ['group_name = ?',group_name] }}
|
18
|
-
|
19
|
+
|
20
|
+
def self.find_equalization_period_stats_for(group_name, alternatives=nil)
|
21
|
+
alternative_filter = alternatives ? alternatives.inject([]) {|res,x| res << "alternative = '#{Conductor.sanitize(x)}'"}.join(' OR ') : 'true'
|
22
|
+
|
23
|
+
sql = "SELECT alternative, min(activity_date) AS activity_date
|
24
|
+
FROM conductor_daily_experiments
|
25
|
+
WHERE group_name = '#{group_name}'
|
26
|
+
AND (#{alternative_filter})
|
27
|
+
GROUP BY alternative
|
28
|
+
HAVING min(activity_date) > '#{Date.today - Conductor.equalization_period}'"
|
29
|
+
|
30
|
+
self.find_by_sql(sql)
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def self.find_post_equalization_period_stats_for(group_name, alternatives=nil)
|
35
|
+
alternative_filter = alternatives ? alternatives.inject([]) {|res,x| res << "alternative = '#{Conductor.sanitize(x)}'"}.join(' OR ') : 'true'
|
36
|
+
|
37
|
+
sql = "SELECT alternative, min(activity_date) AS activity_date, sum(views) AS views, sum(conversions) AS conversions, sum(conversion_value) AS conversion_value
|
38
|
+
FROM conductor_daily_experiments
|
39
|
+
WHERE group_name = '#{group_name}'
|
40
|
+
AND (#{alternative_filter})
|
41
|
+
AND activity_date >=
|
42
|
+
(SELECT max(min_date) FROM
|
43
|
+
(SELECT alternative, min(activity_date) AS min_date
|
44
|
+
FROM conductor_daily_experiments
|
45
|
+
WHERE activity_date >= '#{Date.today - Conductor.inclusion_period}'
|
46
|
+
GROUP BY alternative) AS a)
|
47
|
+
GROUP BY alternative
|
48
|
+
HAVING min(activity_date) <= '#{Date.today - Conductor.equalization_period}'"
|
49
|
+
|
50
|
+
self.find_by_sql(sql)
|
51
|
+
end
|
52
|
+
|
19
53
|
end
|
@@ -12,4 +12,7 @@
|
|
12
12
|
|
13
13
|
class Conductor::Experiment::Weight < ActiveRecord::Base
|
14
14
|
set_table_name "conductor_weighted_experiments"
|
15
|
+
|
16
|
+
named_scope :for_group, lambda { |group_name| { :conditions => ['group_name = ?',group_name] }}
|
17
|
+
named_scope :with_alternative, lambda { |alternative| { :conditions => ['alternative = ?',alternative] }}
|
15
18
|
end
|
@@ -10,6 +10,11 @@ module DashboardHelper
|
|
10
10
|
return Gchart.pie(:data => data, :legend => legend, :size => '600x200')
|
11
11
|
end
|
12
12
|
|
13
|
+
def get_weight(group_name, alternative_name)
|
14
|
+
itm = Conductor::Experiment::Weight.for_group(group_name).with_alternative(alternative_name)
|
15
|
+
return itm.first.weight if itm && itm.first
|
16
|
+
end
|
17
|
+
|
13
18
|
def daily_stats(group_name, group)
|
14
19
|
data = []
|
15
20
|
legend = []
|
@@ -0,0 +1,23 @@
|
|
1
|
+
%a{:name => "group_stats"}
|
2
|
+
%h2 Experiment Statistics
|
3
|
+
- @dailies.group_by(&:group_name).each do |group_name, group|
|
4
|
+
.group
|
5
|
+
%fieldset
|
6
|
+
%legend= group_name
|
7
|
+
%table
|
8
|
+
%tr
|
9
|
+
%th Alternative
|
10
|
+
%th Views
|
11
|
+
%th Conversions
|
12
|
+
%th Value
|
13
|
+
%th Value per Conv
|
14
|
+
%th Weight
|
15
|
+
- group.group_by(&:alternative).sort.each do |alternative_name, alternative_data|
|
16
|
+
%tr
|
17
|
+
%td.name= alternative_name
|
18
|
+
%td.views= alternative_data.sum_it(:views)
|
19
|
+
%td.conversions= alternative_data.sum_it(:conversions)
|
20
|
+
%td.value= alternative_data.sum_it(:conversion_value)
|
21
|
+
%td.norm= ("%0.2f" % (alternative_data.sum_it(:conversion_value).to_f / alternative_data.sum_it(:conversions).to_f))
|
22
|
+
%td.weight
|
23
|
+
%b= get_weight(group_name, alternative_name)
|
@@ -1,3 +1,4 @@
|
|
1
1
|
.nav= link_to "Current Experiment Weights", '#current_weights'
|
2
|
-
.nav= link_to "Experiment Statistics", '#
|
2
|
+
.nav= link_to "Experiment Statistics", '#group_stats'
|
3
|
+
.nav= link_to "Daily Statistics", '#daily_stats'
|
3
4
|
.nav.last= link_to "Experiment Weight History", '#weight_history'
|
@@ -22,6 +22,7 @@
|
|
22
22
|
|
23
23
|
.top_nav= render :file => 'dashboard/_top_nav'
|
24
24
|
#weights= render :file => 'dashboard/_current_weights'
|
25
|
+
#group_stats= render :file => 'dashboard/_group_stats'
|
25
26
|
#dailies= render :file => 'dashboard/_daily_stats'
|
26
27
|
#history= render :file => 'dashboard/_weight_history'
|
27
28
|
|
data/lib/conductor/weights.rb
CHANGED
@@ -24,83 +24,59 @@ class Conductor
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
-
# Computes the weights for a group based on the attribute for weighting and
|
28
|
-
# activity for the
|
27
|
+
# Computes the weights for a group based on the attribute for weighting and
|
28
|
+
# activity for the inclusion period.
|
29
29
|
#
|
30
|
-
# If no conversions have taken place yet for a group, all alternatives are weighted
|
30
|
+
# If no conversions have taken place yet for a group, all alternatives are weighted
|
31
31
|
# equally.
|
32
32
|
#
|
33
33
|
# TODO: add notification table and all notification if there are no conversions and we are out of the equalization period
|
34
34
|
def compute(group_name, alternatives)
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
35
|
+
Conductor::Experiment::Weight.delete_all(:group_name => group_name)
|
36
|
+
|
37
|
+
data = []
|
38
|
+
equalization_period_data = Conductor::Experiment::Daily.find_equalization_period_stats_for(group_name, alternatives)
|
39
|
+
post_equalization_data = Conductor::Experiment::Daily.find_post_equalization_period_stats_for(group_name, alternatives)
|
40
|
+
|
41
|
+
# handle all post_equalization_data
|
42
|
+
max_weight = 0
|
43
|
+
unless post_equalization_data.empty?
|
44
|
+
total = post_equalization_data.sum_it(Conductor.attribute_for_weighting)
|
45
|
+
data = (total > 0) ? compute_weights(post_equalization_data, total, max_weight) : assign_equal_weights(post_equalization_data)
|
46
46
|
end
|
47
|
+
|
48
|
+
# add weights for recently launched
|
49
|
+
weight_recently_launched(data, max_weight, equalization_period_data) unless equalization_period_data.empty?
|
50
|
+
|
51
|
+
# add to database
|
52
|
+
update_weights_in_db(group_name, data)
|
47
53
|
end
|
48
54
|
|
49
55
|
private
|
50
56
|
|
51
|
-
|
52
|
-
# each alternative
|
53
|
-
def compute_weights_for_group(group_name, group_rows, total)
|
57
|
+
def compute_weights(post_equalization_data, total, max_weight)
|
54
58
|
data = []
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
days_ago = compute_days_ago(alternatives)
|
62
|
-
|
63
|
-
if days_ago >= Conductor.equalization_period
|
64
|
-
data << {:name => alternative_name, :weight => (weighted_moving_avg[alternative_name] / total)}
|
65
|
-
else
|
66
|
-
recently_launched << {:name => alternative_name, :days_ago => days_ago}
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
weight_recently_launched(data, max_weight, recently_launched)
|
71
|
-
return data
|
72
|
-
end
|
73
|
-
|
74
|
-
def compute_weighted_moving_stats(group_rows)
|
75
|
-
weighted_moving_avg = {}
|
76
|
-
group_rows.group_by(&:alternative).each do |alternative_name, alternatives|
|
77
|
-
weighted_moving_avg.merge!({"#{alternative_name}" => alternatives.sort_by(&:activity_date).weighted_mean_of_attribute(Conductor.attribute_for_weighting)})
|
78
|
-
end
|
79
|
-
|
80
|
-
# calculate total and max_weight for weighted moving averages
|
81
|
-
total = weighted_moving_avg.values.sum
|
82
|
-
max_weight = weighted_moving_avg.values.max
|
83
|
-
|
84
|
-
return weighted_moving_avg, total, max_weight
|
85
|
-
end
|
86
|
-
|
87
|
-
def compute_days_ago(alternatives)
|
88
|
-
first_found_date = alternatives.map(&:activity_date).min
|
89
|
-
(Date.today - first_found_date.to_date).to_f
|
59
|
+
post_equalization_data.each {|x|
|
60
|
+
weight = (x.send(Conductor.attribute_for_weighting).to_f / total.to_f)
|
61
|
+
max_weight = weight if weight > max_weight
|
62
|
+
data << {:name => x.alternative, :weight => weight}
|
63
|
+
}
|
64
|
+
data
|
90
65
|
end
|
91
66
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
67
|
+
# loop through recently_launched to create weights for table
|
68
|
+
# the handicap sets a newly launched item to the max weight * MAX_WEIGHTING_FACTOR and then
|
69
|
+
# slowly lowers its power until the launch period is over
|
70
|
+
def weight_recently_launched(data, max_weight, equalization_period_data)
|
96
71
|
max_weight = 1 if data.empty?
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
Conductor.
|
101
|
-
|
72
|
+
equalization_period_data.each do |x|
|
73
|
+
days_ago = (Date.today - x.activity_date)
|
74
|
+
handicap = (days_ago.to_f / Conductor.equalization_period)
|
75
|
+
launch_window = (Conductor.equalization_period - days_ago)
|
76
|
+
|
77
|
+
Conductor.log("Handicap for #{x.alternative} is #{handicap} (#{days_ago} days ago)")
|
78
|
+
data << {:name => x.alternative, :weight => max_weight * MAX_WEIGHTING_FACTOR * (1 - handicap), :launch_window => launch_window}
|
102
79
|
end
|
103
|
-
data
|
104
80
|
end
|
105
81
|
|
106
82
|
def assign_equal_weights(group_rows)
|
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
|
+
- 8
|
8
|
+
- 1
|
9
|
+
version: 0.8.1
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Noctivity
|
@@ -73,6 +73,7 @@ files:
|
|
73
73
|
- lib/conductor/roll_up.rb
|
74
74
|
- lib/conductor/views/dashboard/_current_weights.html.haml
|
75
75
|
- lib/conductor/views/dashboard/_daily_stats.html.haml
|
76
|
+
- lib/conductor/views/dashboard/_group_stats.html.haml
|
76
77
|
- lib/conductor/views/dashboard/_top_nav.html.haml
|
77
78
|
- lib/conductor/views/dashboard/_weight_history.html.haml
|
78
79
|
- lib/conductor/views/dashboard/index.html.haml
|