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