conductor 0.4.1 → 0.5.0

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