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 +1 -0
- data/VERSION +1 -1
- data/lib/conductor.rb +4 -7
- data/lib/conductor/controller/dashboard.rb +15 -0
- data/lib/conductor/experiment.rb +5 -16
- data/lib/conductor/experiment/daily.rb +2 -0
- data/lib/conductor/roll_up.rb +8 -5
- data/lib/conductor/views/index.html.haml +33 -0
- data/lib/conductor/weights.rb +31 -6
- data/test/test_conductor.rb +48 -18
- data/test/test_helper.rb +1 -1
- metadata +5 -3
data/Rakefile
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
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
|
data/lib/conductor/experiment.rb
CHANGED
@@ -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
|
-
|
98
|
-
|
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
|
data/lib/conductor/roll_up.rb
CHANGED
@@ -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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
+
|
data/lib/conductor/weights.rb
CHANGED
@@ -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
|
-
|
8
|
-
|
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
|
data/test/test_conductor.rb
CHANGED
@@ -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
|
-
|
122
|
-
Conductor::
|
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
|
-
#
|
134
|
-
Conductor::
|
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
|
-
#
|
151
|
-
Conductor::
|
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
|
-
#
|
190
|
-
Conductor::
|
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
|
-
-
|
8
|
-
-
|
9
|
-
version: 0.
|
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
|