conductor 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -52,3 +52,4 @@ Rake::RDocTask.new do |rdoc|
52
52
  rdoc.rdoc_files.include('README*')
53
53
  rdoc.rdoc_files.include('lib/**/*.rb')
54
54
  end
55
+
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.0
1
+ 0.5.0
data/lib/conductor.rb CHANGED
@@ -34,15 +34,12 @@ class Conductor
34
34
  def log(msg)
35
35
  puts msg if DBG
36
36
  end
37
+
38
+ def sanitize(str)
39
+ str.gsub(/\s/,'_').downcase
40
+ end
37
41
  end
38
42
 
39
- # class Rails
40
- # cattr_writer :cache
41
- #
42
- # def self.cache
43
- # []
44
- # end
45
- # end
46
43
  end
47
44
 
48
45
 
@@ -0,0 +1,15 @@
1
+ class Conductor
2
+ module Controller
3
+ module Dashboard
4
+
5
+ ActionController::Base.view_paths.unshift File.join(File.dirname(__FILE__), "../views")
6
+
7
+ def index
8
+ @weights = Conductor::Experiment::Weight.all
9
+ @weight_history = Conductor::Experiment::History.all
10
+ @dailies = Conductor::Experiment::Daily.all
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -19,7 +19,7 @@ class Conductor
19
19
  # specific goal.
20
20
  #
21
21
  def pick(group_name, alternatives, options={})
22
- group_name = sanitize(group_name) # => clean up and standardize
22
+ group_name = Conductor.sanitize(group_name) # => clean up and standardize
23
23
 
24
24
  # check for previous selection
25
25
  selection = Conductor.cache.read("Conductor::#{Conductor.identity}::Experience::#{group_name}")
@@ -35,7 +35,7 @@ class Conductor
35
35
 
36
36
  # returns the raw weighting table for all alternatives for a specified group
37
37
  def weights(group_name, alternatives)
38
- group_name = sanitize(group_name) # => clean up and standardize
38
+ group_name = Conductor.sanitize(group_name) # => clean up and standardize
39
39
  return generate_weighting_table(group_name, alternatives)
40
40
  end
41
41
 
@@ -94,14 +94,8 @@ class Conductor
94
94
  # those not in the alternatives list
95
95
  #
96
96
  def generate_weighting_table(group_name, alternatives)
97
- # create the conditions after sanitizing sql.
98
- alternative_filter = alternatives.inject([]) {|res,x| res << "alternative = '#{sanitize(x)}'"}.join(' OR ')
99
-
100
- conditions = "group_name = '#{group_name}' AND (#{alternative_filter})"
101
-
102
- # get the alternatives from the database
103
- weights ||= Conductor::Experiment::Weight.find(:all, :conditions => conditions)
104
-
97
+ weights = Conductor::Weights.find_or_create(group_name, alternatives)
98
+
105
99
  # create selection hash
106
100
  weighting_table = weights.inject({}) {|res, x| res.merge!({x.alternative => x.weight})}
107
101
 
@@ -119,7 +113,7 @@ class Conductor
119
113
 
120
114
  return weighting_table
121
115
  end
122
-
116
+
123
117
  # selects a random float
124
118
  def float_rand(start_num, end_num=0)
125
119
  width = end_num-start_num
@@ -146,11 +140,6 @@ class Conductor
146
140
  sum = sum.to_f
147
141
  weighted.each { |item, weight| weighted[item] = weight/sum }
148
142
  end
149
-
150
- def sanitize(str)
151
- str.gsub(/\s/,'_').downcase
152
- end
153
-
154
143
  end
155
144
  end
156
145
  end
@@ -14,4 +14,6 @@
14
14
  class Conductor::Experiment::Daily < ActiveRecord::Base
15
15
  set_table_name "conductor_daily_experiments"
16
16
  named_scope :since, lambda { |a_date| { :conditions => ['activity_date >= ?',a_date] }}
17
+ named_scope :for_group, lambda { |group_name| { :conditions => ['group_name = ?',group_name] }}
18
+
17
19
  end
@@ -12,12 +12,15 @@ class Conductor
12
12
  views = alternatives.count
13
13
  conversions = alternatives.count {|x| !x.conversion_value.nil?}
14
14
  Conductor::Experiment::Daily.create!(:activity_date => day,
15
- :group_name => group_name,
16
- :alternative => alternative_name,
17
- :conversion_value => conversion_value,
18
- :views => views,
19
- :conversions => conversions )
15
+ :group_name => group_name,
16
+ :alternative => alternative_name,
17
+ :conversion_value => conversion_value,
18
+ :views => views,
19
+ :conversions => conversions )
20
20
  end
21
+
22
+ # delete the cache for this group so it can be recreated with the new values
23
+ Conductor.cache.delete("Conductor::Experiment::#{group_name}::Alternatives")
21
24
  end
22
25
  end
23
26
  end
@@ -0,0 +1,33 @@
1
+ #conductor_dashboard
2
+ #weights
3
+ - @weights.group_by(&:group_name).each do |group_name, group|
4
+ .group
5
+ %fieldset
6
+ %legend= group_name
7
+ = group.each do |row|
8
+ .name= row.alternative
9
+ .weight= row.weight
10
+
11
+ #dailies
12
+ - @dailies.group_by(&:group_name).each do |group_name, group|
13
+ .group
14
+ %fieldset
15
+ %legend= group_name
16
+ = group.sort_by(&:activity_date).each do |row|
17
+ .date= row.activity_date
18
+ .name= row.alternative
19
+ .views= row.views
20
+ .conversions= row.conversions
21
+ .value= row.conversion_value
22
+
23
+ #history
24
+ - @weight_history.group_by(&:group_name).each do |group_name, group|
25
+ .group
26
+ %fieldset
27
+ %legend= group_name
28
+ = group.sort_by(&:computed_at).reverse.each do |row|
29
+ .date= row.computed_at
30
+ .name= row.alternative
31
+ .weight= row.weight
32
+
33
+
@@ -1,11 +1,38 @@
1
1
  class Conductor
2
2
  class Weights
3
3
  class << self
4
- def compute
5
- Conductor::Experiment::Weight.delete_all # => remove all old data
6
4
 
7
- # loop through each group and determing weight of alternatives
8
- Conductor::Experiment::Daily.since(14.days.ago).group_by(&:group_name).each do |group_name, group_rows|
5
+ # Returns all the weights for a given group. In the event that the alternatives specified for the
6
+ # group do not match all the alternatives previously computed for the group, new weights are
7
+ # generated. The cache is used to speed up this check
8
+ def find_or_create(group_name, alternatives)
9
+ weights_for_group = Conductor.cache.read("Conductor::Experiment::#{group_name}::Alternatives")
10
+
11
+ alternatives_array = weights_for_group.map(&:alternative).sort if weights_for_group
12
+ if alternatives_array.eql?(alternatives.sort)
13
+ Conductor.log('alternatives equal to cache')
14
+ return weights_for_group
15
+ else
16
+ # Conductor.log('alternatives NOT equal to cache. Need to recompute')
17
+ compute(group_name, alternatives)
18
+
19
+ # get the new weights
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)
23
+ return weights_for_group
24
+ end
25
+ end
26
+
27
+ def compute(group_name, alternatives)
28
+ # create the conditions after sanitizing sql.
29
+ alternative_filter = alternatives.inject([]) {|res,x| res << "alternative = '#{Conductor.sanitize(x)}'"}.join(' OR ')
30
+
31
+ # pull daily data and recompute if daily data
32
+ group_rows = Conductor::Experiment::Daily.since(14.days.ago).for_group(group_name).find(:all, :conditions => alternative_filter)
33
+
34
+ unless group_rows.empty?
35
+ Conductor::Experiment::Weight.delete_all(:group_name => group_name) # => remove all old data for group
9
36
  total = group_rows.sum_it(:conversion_value)
10
37
  data = total ? compute_weights_for_group(group_name, group_rows, total) : assign_equal_weights(group_rows)
11
38
  update_weights_in_db(group_name, data)
@@ -17,8 +44,6 @@ class Conductor
17
44
  # loops through all the alternatives for a given group and computes the weights for
18
45
  # each alternative
19
46
  def compute_weights_for_group(group_name, group_rows, total)
20
- Conductor.log('compute_weights_for_group')
21
-
22
47
  data = []
23
48
  recently_launched = []
24
49
  max_weight = 0
@@ -17,6 +17,12 @@ class TestConductor < Test::Unit::TestCase
17
17
  assert ["a", "b", "c"].include? selected
18
18
  end
19
19
 
20
+ should "use the cache if working" do
21
+ Conductor.cache.write('testing','value')
22
+ x = Conductor.cache.read('testing')
23
+ assert_equal x, 'value'
24
+ end
25
+
20
26
  should "almost equally select each option if no weights exist" do
21
27
  a = 0
22
28
  b = 0
@@ -109,6 +115,7 @@ class TestConductor < Test::Unit::TestCase
109
115
  setup do
110
116
  seed_raw_data(100)
111
117
  Conductor::RollUp.process
118
+ Conductor.identity = ActiveSupport::SecureRandom.hex(16)
112
119
  end
113
120
 
114
121
  should "correctly RollUp daily data" do
@@ -117,9 +124,39 @@ class TestConductor < Test::Unit::TestCase
117
124
  assert Conductor::Experiment::Daily.all.detect {|x| x.views > 0}
118
125
  assert Conductor::Experiment::Daily.all.detect {|x| x.conversion_value > 0}
119
126
  end
127
+
128
+ should "correctly populate weighting table when selecting a value" do
129
+ selected = Conductor::Experiment.pick('a_group', ["a", "b", "c"])
130
+ assert_equal 3, Conductor::Experiment::Weight.count
131
+ end
132
+
133
+ should "pull weights from the cache" do
134
+ Conductor::Experiment.pick('a_group', ["a", "b", "c"])
135
+
136
+ (1..100).each do |x|
137
+ Conductor.identity = ActiveSupport::SecureRandom.hex(16)
138
+ Conductor::Experiment.pick('a_group', ["a", "b", "c"])
139
+ end
140
+
141
+ # => if this works the history table should have only been updated one time not 101 so there should
142
+ # => be three records (one for a, b and c)
143
+ assert_equal 3, Conductor::Experiment::History.count
144
+ end
145
+
146
+ should "pull weights from the cache and then recreate weights when the alternative list changes" do
147
+ Conductor::Experiment.pick('a_group', ["a", "b", "c"])
148
+
149
+ (1..100).each do |x|
150
+ Conductor.identity = ActiveSupport::SecureRandom.hex(16)
151
+ Conductor::Experiment.pick('a_group', ["a", "b", "c"])
152
+ end
120
153
 
121
- should "correctly populate weighting table" do
122
- Conductor::Weights.compute
154
+ Conductor.identity = ActiveSupport::SecureRandom.hex(16)
155
+ Conductor::Experiment.pick('a_group', ["a", "c"])
156
+
157
+ # => if this works the history table should have only been updated one time not 101 so there should
158
+ # => be FIVE records (one for a, b and c and then one for a and c)
159
+ assert_equal 5, Conductor::Experiment::History.count
123
160
  end
124
161
  end
125
162
 
@@ -130,8 +167,9 @@ class TestConductor < Test::Unit::TestCase
130
167
  # rollup
131
168
  Conductor::RollUp.process
132
169
 
133
- # compute weights
134
- Conductor::Weights.compute
170
+ # hit after rollup to populare weight table
171
+ Conductor.identity = ActiveSupport::SecureRandom.hex(16)
172
+ selected = Conductor::Experiment.pick('a_group', ["a", "b", "c"])
135
173
 
136
174
  # this makes the following assumptions:
137
175
  # MINIMUM_LAUNCH_DAYS = 7
@@ -147,8 +185,9 @@ class TestConductor < Test::Unit::TestCase
147
185
  # rollup
148
186
  Conductor::RollUp.process
149
187
 
150
- # compute weights
151
- Conductor::Weights.compute
188
+ # hit after rollup to populare weight table
189
+ Conductor.identity = ActiveSupport::SecureRandom.hex(16)
190
+ selected = Conductor::Experiment.pick('a_group', ["a", "b", "c"])
152
191
  end
153
192
 
154
193
  should "populate the weighting table with different weights" do
@@ -161,14 +200,6 @@ class TestConductor < Test::Unit::TestCase
161
200
  end
162
201
 
163
202
  should "return a weight 1.25 times higher than the highest weight for a newly launched and non-recorded alernative" do
164
- seed_raw_data(100, 14)
165
-
166
- # rollup
167
- Conductor::RollUp.process
168
-
169
- # compute weights
170
- Conductor::Weights.compute
171
-
172
203
  # get the highest weight
173
204
  max_weight = Conductor::Experiment::Weight.maximum(:weight)
174
205
 
@@ -186,8 +217,9 @@ class TestConductor < Test::Unit::TestCase
186
217
  # rollup
187
218
  Conductor::RollUp.process
188
219
 
189
- # compute weights
190
- Conductor::Weights.compute
220
+ # hit after rollup to populare weight table
221
+ Conductor.identity = ActiveSupport::SecureRandom.hex(16)
222
+ selected = Conductor::Experiment.pick('a_group', ["a", "b", "c"])
191
223
 
192
224
  # make sure that launch_window values can be detected
193
225
  assert_not_nil Conductor::Experiment::History.find(:all, :conditions => 'launch_window > 0')
@@ -195,8 +227,6 @@ class TestConductor < Test::Unit::TestCase
195
227
  end
196
228
 
197
229
 
198
-
199
-
200
230
  private
201
231
 
202
232
  def wipe
data/test/test_helper.rb CHANGED
@@ -17,7 +17,7 @@ require File.dirname(__FILE__) + '/../init.rb'
17
17
 
18
18
 
19
19
  config = YAML::load(IO.read(File.dirname(__FILE__) + '/db/database.yml'))
20
- ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
20
+ # ActiveRecord::Base.logger = Logger.new(STDOUT) # Logger.new(File.dirname(__FILE__) + "/debug.log")
21
21
  ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'postgresql'])
22
22
  ActiveRecord::Migration.verbose = false
23
23
  load(File.dirname(__FILE__) + "/db/schema.rb")
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 4
8
- - 1
9
- version: 0.4.1
7
+ - 5
8
+ - 0
9
+ version: 0.5.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Noctivity
@@ -36,12 +36,14 @@ files:
36
36
  - generators/conductor_migration/templates/migration.rb
37
37
  - init.rb
38
38
  - lib/conductor.rb
39
+ - lib/conductor/controller/dashboard.rb
39
40
  - lib/conductor/experiment.rb
40
41
  - lib/conductor/experiment/daily.rb
41
42
  - lib/conductor/experiment/history.rb
42
43
  - lib/conductor/experiment/raw.rb
43
44
  - lib/conductor/experiment/weight.rb
44
45
  - lib/conductor/roll_up.rb
46
+ - lib/conductor/views/index.html.haml
45
47
  - lib/conductor/weights.rb
46
48
  - test/db/schema.rb
47
49
  - test/test_conductor.rb