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 CHANGED
@@ -1 +1 @@
1
- 0.7.2
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", '#daily_stats'
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
 
@@ -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 last two weeks.
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
- # create the conditions after sanitizing sql.
36
- alternative_filter = alternatives.inject([]) {|res,x| res << "alternative = '#{Conductor.sanitize(x)}'"}.join(' OR ')
37
-
38
- # pull daily data and recompute if daily data
39
- group_rows = Conductor::Experiment::Daily.since(14.days.ago).for_group(group_name).find(:all, :conditions => alternative_filter)
40
-
41
- unless group_rows.empty?
42
- Conductor::Experiment::Weight.delete_all(:group_name => group_name) # => remove all old data for group
43
- total = group_rows.sum_it(Conductor.attribute_for_weighting)
44
- data = total > 0 ? compute_weights_for_group(group_name, group_rows, total) : assign_equal_weights(group_rows)
45
- update_weights_in_db(group_name, data)
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
- # loops through all the alternatives for a given group and computes the weights for
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
- recently_launched = []
56
-
57
- # calculate weights for each alternative
58
- weighted_moving_avg, total, max_weight = compute_weighted_moving_stats(group_rows)
59
-
60
- group_rows.group_by(&:alternative).each do |alternative_name, alternatives|
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
- def weight_recently_launched(data, max_weight, recently_launched)
93
- # loop through recently_launched to create weights for table
94
- # the handicap sets a newly launched item to the max weight * MAX_WEIGHTING_FACTOR and then
95
- # slowly lowers its power until the launch period is over
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
- recently_launched.each do |alternative|
98
- handicap = (alternative[:days_ago].to_f / Conductor.equalization_period)
99
- launch_window = (Conductor.equalization_period - alternative[:days_ago]) if Conductor.equalization_period > alternative[:days_ago]
100
- Conductor.log("Handicap for #{alternative[:name]} is #{handicap} (#{alternative[:days_ago]} days ago)")
101
- data << {:name => alternative[:name], :weight => max_weight * MAX_WEIGHTING_FACTOR * (1 - handicap), :launch_window => launch_window}
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
- - 7
8
- - 2
9
- version: 0.7.2
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