conductor 0.7.2 → 0.8.1
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 +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
|