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