conductor 0.8.3 → 0.9.3
Sign up to get free protection for your applications and to get access to all the features.
- data/{LICENSE → MIT-LICENSE} +1 -1
- data/README.rdoc +7 -0
- data/Rakefile +25 -45
- data/app/assets/javascripts/conductor/application.js +15 -0
- data/app/assets/javascripts/conductor/dashboard.js +2 -0
- data/app/assets/stylesheets/conductor/application.css +13 -0
- data/{generators/conductor/templates/conductor.css → app/assets/stylesheets/conductor/dashboard.css} +0 -0
- data/app/controllers/conductor/application_controller.rb +4 -0
- data/app/controllers/conductor/dashboard_controller.rb +11 -0
- data/app/helpers/conductor/application_helper.rb +4 -0
- data/{lib/conductor/rails/helpers → app/helpers/conductor}/dashboard_helper.rb +3 -1
- data/app/models/conductor/experiment/daily.rb +47 -0
- data/app/models/conductor/experiment/history.rb +9 -0
- data/app/models/conductor/experiment/raw.rb +20 -0
- data/app/models/conductor/experiment/weight.rb +11 -0
- data/{lib/conductor/rails/views → app/views/conductor}/dashboard/_current_weights.html.haml +0 -0
- data/{lib/conductor/rails/views → app/views/conductor}/dashboard/_daily_stats.html.haml +0 -0
- data/{lib/conductor/rails/views → app/views/conductor}/dashboard/_group_stats.html.haml +0 -0
- data/{lib/conductor/rails/views → app/views/conductor}/dashboard/_top_nav.html.haml +0 -0
- data/{lib/conductor/rails/views → app/views/conductor}/dashboard/_weight_history.html.haml +0 -0
- data/app/views/conductor/dashboard/index.html.haml +12 -0
- data/app/views/layouts/conductor/application.html.erb +14 -0
- data/config/routes.rb +4 -0
- data/db/migrate/20121010131323_create_conductor_daily_experiments.rb +16 -0
- data/db/migrate/20121010173406_create_conductor_experiment_weights.rb +13 -0
- data/db/migrate/20121010174835_create_conductor_raw_experiments.rb +13 -0
- data/db/migrate/20121010174950_create_conductor_weight_histories.rb +14 -0
- data/lib/conductor.rb +33 -57
- data/lib/conductor/core_ext.rb +18 -0
- data/lib/conductor/engine.rb +5 -0
- data/lib/conductor/experiment.rb +61 -60
- data/lib/conductor/roll_up.rb +2 -2
- data/lib/conductor/version.rb +3 -0
- data/lib/conductor/weights.rb +9 -8
- data/lib/tasks/conductor_tasks.rake +9 -0
- data/test/core_ext_test.rb +13 -0
- data/test/fixtures/conductor/conductor_daily_experiments.yml +17 -0
- data/test/functional/conductor/dashboard_controller_test.rb +11 -0
- data/test/integration/conductor/dashboard_test.rb +7 -0
- data/test/integration/navigation_test.rb +10 -0
- data/test/test_helper.rb +9 -23
- data/test/unit/conductor/conductor_daily_experiment_test.rb +9 -0
- data/test/unit/helpers/conductor/dashboard_helper_test.rb +6 -0
- metadata +240 -91
- data/VERSION +0 -1
- data/generators/conductor/conductor_generator.rb +0 -27
- data/generators/conductor/templates/conductor.rake +0 -6
- data/generators/conductor/templates/migration.rb +0 -54
- data/init.rb +0 -2
- data/lib/conductor/rails/controllers/dashboard.rb +0 -16
- data/lib/conductor/rails/models/daily.rb +0 -53
- data/lib/conductor/rails/models/history.rb +0 -14
- data/lib/conductor/rails/models/raw.rb +0 -27
- data/lib/conductor/rails/models/weight.rb +0 -18
- data/lib/conductor/rails/views/dashboard/index.html.haml +0 -29
- data/rails/init.rb +0 -7
- data/test/db/schema.rb +0 -43
- data/test/test_conductor.rb +0 -346
data/{LICENSE → MIT-LICENSE}
RENAMED
data/README.rdoc
CHANGED
data/Rakefile
CHANGED
@@ -1,57 +1,37 @@
|
|
1
|
-
|
2
|
-
require 'rake'
|
3
|
-
|
1
|
+
#!/usr/bin/env rake
|
4
2
|
begin
|
5
|
-
require '
|
6
|
-
Jeweler::Tasks.new do |gem|
|
7
|
-
gem.name = "conductor"
|
8
|
-
gem.summary = %Q{lets you just try things while always maximizing towards a goal (e.g. purchase, signups, etc)}
|
9
|
-
gem.description = %Q{Conductor is the bastard child of a/b testing and personalization. It throws everything you know about creating a web site our the window and lets you just "try stuff" without ever having to worry about not maximing your site's "purpose." Have a new landing page? Just throw it to the conductor. Want to try different price points - conductor. Different form designs? Conductor. Conductor will rotate all alternatives through the mix and eventually settle on the top performing of all, without you having to do anything other than just creating. Think "intelligent A/B testing" on steriods.}
|
10
|
-
gem.email = "jlippiner@noctivity.com"
|
11
|
-
gem.homepage = "http://github.com/noctivityinc/conductor"
|
12
|
-
gem.authors = ["Noctivity"]
|
13
|
-
gem.rubyforge_project = "conductor"
|
14
|
-
gem.files = FileList["[A-Z]*", "{generators,lib,tasks,rails}/**/*", "init.rb"]
|
15
|
-
gem.add_dependency 'googlecharts'
|
16
|
-
gem.add_dependency 'haml'
|
17
|
-
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
18
|
-
end
|
19
|
-
Jeweler::GemcutterTasks.new
|
3
|
+
require 'bundler/setup'
|
20
4
|
rescue LoadError
|
21
|
-
puts
|
22
|
-
end
|
23
|
-
|
24
|
-
require 'rake/testtask'
|
25
|
-
Rake::TestTask.new(:test) do |test|
|
26
|
-
test.libs << 'lib' << 'test'
|
27
|
-
test.pattern = 'test/**/test_*.rb'
|
28
|
-
test.verbose = true
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
29
6
|
end
|
30
|
-
|
31
7
|
begin
|
32
|
-
require '
|
33
|
-
Rcov::RcovTask.new do |test|
|
34
|
-
test.libs << 'test'
|
35
|
-
test.pattern = 'test/**/test_*.rb'
|
36
|
-
test.verbose = true
|
37
|
-
end
|
8
|
+
require 'rdoc/task'
|
38
9
|
rescue LoadError
|
39
|
-
|
40
|
-
|
41
|
-
|
10
|
+
require 'rdoc/rdoc'
|
11
|
+
require 'rake/rdoctask'
|
12
|
+
RDoc::Task = Rake::RDocTask
|
13
|
+
end
|
14
|
+
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
+
rdoc.rdoc_dir = 'rdoc'
|
17
|
+
rdoc.title = 'Conductor'
|
18
|
+
rdoc.options << '--line-numbers'
|
19
|
+
rdoc.rdoc_files.include('README.rdoc')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
42
21
|
end
|
43
22
|
|
44
|
-
|
23
|
+
APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
|
24
|
+
load 'rails/tasks/engine.rake'
|
45
25
|
|
46
|
-
|
26
|
+
Bundler::GemHelper.install_tasks
|
47
27
|
|
48
|
-
require 'rake/
|
49
|
-
Rake::RDocTask.new do |rdoc|
|
50
|
-
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
28
|
+
require 'rake/testtask'
|
51
29
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
30
|
+
Rake::TestTask.new(:test) do |t|
|
31
|
+
t.libs << 'lib'
|
32
|
+
t.libs << 'test'
|
33
|
+
t.pattern = 'test/**/*_test.rb'
|
34
|
+
t.verbose = false
|
56
35
|
end
|
57
36
|
|
37
|
+
task :default => :test
|
@@ -0,0 +1,15 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
2
|
+
// listed below.
|
3
|
+
//
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
5
|
+
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
|
6
|
+
//
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
8
|
+
// the compiled file.
|
9
|
+
//
|
10
|
+
// WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
|
11
|
+
// GO AFTER THE REQUIRES BELOW.
|
12
|
+
//
|
13
|
+
//= require jquery
|
14
|
+
//= require jquery_ujs
|
15
|
+
//= require_tree .
|
@@ -0,0 +1,13 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the top of the
|
9
|
+
* compiled file, but it's generally better to create a new file per style scope.
|
10
|
+
*
|
11
|
+
*= require_self
|
12
|
+
*= require_tree .
|
13
|
+
*/
|
data/{generators/conductor/templates/conductor.css → app/assets/stylesheets/conductor/dashboard.css}
RENAMED
File without changes
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require_dependency "conductor/application_controller"
|
2
|
+
|
3
|
+
module Conductor
|
4
|
+
class DashboardController < ApplicationController
|
5
|
+
def index
|
6
|
+
@weights = Conductor::Experiment::Weight.all
|
7
|
+
@weight_history = Conductor::Experiment::History.all
|
8
|
+
@dailies = Conductor::Experiment::Daily.all
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Conductor
|
2
|
+
class Experiment
|
3
|
+
|
4
|
+
class Daily < ActiveRecord::Base
|
5
|
+
self.table_name = "conductor_daily_experiments"
|
6
|
+
|
7
|
+
attr_accessible :activity_date, :conversion_value, :conversions, :group_name, :option_name, :views, :alternative
|
8
|
+
|
9
|
+
scope :since, lambda { |a_date| { :conditions => ['activity_date >= ?',a_date] }}
|
10
|
+
scope :for_group, lambda { |group_name| { :conditions => ['group_name = ?',group_name] }}
|
11
|
+
|
12
|
+
def self.find_equalization_period_stats_for(group_name, alternatives=nil)
|
13
|
+
alternative_filter = alternatives ? alternatives.inject([]) {|res,x| res << "alternative = '#{Conductor.sanitize(x)}'"}.join(' OR ') : 'true'
|
14
|
+
|
15
|
+
sql = "SELECT alternative, min(activity_date) AS activity_date
|
16
|
+
FROM conductor_daily_experiments
|
17
|
+
WHERE group_name = '#{group_name}'
|
18
|
+
AND (#{alternative_filter})
|
19
|
+
GROUP BY alternative
|
20
|
+
HAVING min(activity_date) > '#{Date.today - Conductor.equalization_period}'"
|
21
|
+
|
22
|
+
self.find_by_sql(sql)
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def self.find_post_equalization_period_stats_for(group_name, alternatives=nil)
|
27
|
+
alternative_filter = alternatives ? alternatives.inject([]) {|res,x| res << "alternative = '#{Conductor.sanitize(x)}'"}.join(' OR ') : 'true'
|
28
|
+
|
29
|
+
sql = "SELECT alternative, min(activity_date) AS activity_date, sum(views) AS views, sum(conversions) AS conversions, sum(conversion_value) AS conversion_value
|
30
|
+
FROM conductor_daily_experiments
|
31
|
+
WHERE group_name = '#{group_name}'
|
32
|
+
AND (#{alternative_filter})
|
33
|
+
AND activity_date >=
|
34
|
+
(SELECT max(min_date) FROM
|
35
|
+
(SELECT alternative, min(activity_date) AS min_date
|
36
|
+
FROM conductor_daily_experiments
|
37
|
+
WHERE activity_date >= '#{Date.today - Conductor.inclusion_period}'
|
38
|
+
GROUP BY alternative) AS a)
|
39
|
+
GROUP BY alternative
|
40
|
+
HAVING min(activity_date) <= '#{Date.today - Conductor.equalization_period}'"
|
41
|
+
|
42
|
+
self.find_by_sql(sql)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Conductor
|
2
|
+
class Experiment
|
3
|
+
class Raw < ActiveRecord::Base
|
4
|
+
self.table_name = 'conductor_raw_experiments'
|
5
|
+
|
6
|
+
attr_accessible :conversion_value, :group_name, :identity_id, :option_name, :alternative, :created_at, :updated_at, :goal
|
7
|
+
|
8
|
+
validates_presence_of :group_name, :alternative
|
9
|
+
scope :since, lambda { |a_date| { :conditions => ['created_at >= ?',a_date] }}
|
10
|
+
|
11
|
+
def created_date
|
12
|
+
self.created_at.strftime('%Y-%m-%d')
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.purge(days_old=30)
|
16
|
+
Conductor::Experiment::Raw.delete_all("created_at <= #{days_old.days.ago}")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Conductor
|
2
|
+
class Experiment
|
3
|
+
class Weight < ActiveRecord::Base
|
4
|
+
self.table_name = 'conductor_weighted_experiments'
|
5
|
+
attr_accessible :group_name, :option_name, :weight, :alternative, :created_at, :updated_at
|
6
|
+
|
7
|
+
scope :for_group, lambda { |group_name| { :conditions => ['group_name = ?',group_name] }}
|
8
|
+
scope :with_alternative, lambda { |alternative| { :conditions => ['alternative = ?',alternative] }}
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,12 @@
|
|
1
|
+
#conductor_dashboard
|
2
|
+
.title
|
3
|
+
%h1 Conductor Statistics
|
4
|
+
.generated= "Generated on #{Time.now}"
|
5
|
+
|
6
|
+
.top_nav= render :file => 'conductor/dashboard/_top_nav'
|
7
|
+
#weights= render :file => 'conductor/dashboard/_current_weights'
|
8
|
+
#group_stats= render :file => 'conductor/dashboard/_group_stats'
|
9
|
+
#dailies= render :file => 'conductor/dashboard/_daily_stats'
|
10
|
+
#history= render :file => 'conductor/dashboard/_weight_history'
|
11
|
+
|
12
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Conductor</title>
|
5
|
+
<%= stylesheet_link_tag "conductor/application", :media => "all" %>
|
6
|
+
<%= javascript_include_tag "conductor/application" %>
|
7
|
+
<%= csrf_meta_tags %>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
|
11
|
+
<%= yield %>
|
12
|
+
|
13
|
+
</body>
|
14
|
+
</html>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
class CreateConductorDailyExperiments < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table "conductor_daily_experiments", :force => true do |t|
|
4
|
+
t.date "activity_date"
|
5
|
+
t.string "group_name"
|
6
|
+
t.string "alternative"
|
7
|
+
t.decimal "conversion_value", :precision => 8, :scale => 2
|
8
|
+
t.integer "views"
|
9
|
+
t.integer "conversions"
|
10
|
+
end
|
11
|
+
|
12
|
+
add_index "conductor_daily_experiments", ["activity_date"], :name => "index_conductor_daily_experiments_on_activity_date"
|
13
|
+
add_index "conductor_daily_experiments", ["group_name"], :name => "index_conductor_daily_experiments_on_group_name"
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class CreateConductorExperimentWeights < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table "conductor_weighted_experiments", :force => true do |t|
|
4
|
+
t.string "group_name"
|
5
|
+
t.string "alternative"
|
6
|
+
t.decimal "weight", :precision => 8, :scale => 2
|
7
|
+
t.datetime "created_at"
|
8
|
+
t.datetime "updated_at"
|
9
|
+
end
|
10
|
+
|
11
|
+
add_index "conductor_weighted_experiments", ["group_name"], :name => "index_conductor_weighted_experiments_on_group_name"
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class CreateConductorRawExperiments < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table "conductor_raw_experiments", :force => true do |t|
|
4
|
+
t.string "identity_id"
|
5
|
+
t.string "group_name"
|
6
|
+
t.string "alternative"
|
7
|
+
t.decimal "conversion_value", :precision => 8, :scale => 2
|
8
|
+
t.datetime "created_at"
|
9
|
+
t.datetime "updated_at"
|
10
|
+
t.string "goal"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class CreateConductorWeightHistories < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table "conductor_weight_histories", :force => true do |t|
|
4
|
+
t.string "group_name"
|
5
|
+
t.string "alternative"
|
6
|
+
t.decimal "weight", :precision => 8, :scale => 2
|
7
|
+
t.datetime "computed_at"
|
8
|
+
t.integer "launch_window"
|
9
|
+
end
|
10
|
+
|
11
|
+
add_index "conductor_weight_histories", ["computed_at", "group_name"], :name => "conductor_wh_date_and_group_ndx"
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
data/lib/conductor.rb
CHANGED
@@ -1,27 +1,37 @@
|
|
1
|
-
|
1
|
+
require "conductor/engine"
|
2
|
+
require 'conductor/core_ext'
|
3
|
+
require 'conductor/experiment'
|
4
|
+
require 'conductor/roll_up'
|
5
|
+
require 'conductor/weights'
|
6
|
+
require 'haml'
|
7
|
+
|
8
|
+
module Conductor
|
2
9
|
MAX_WEIGHTING_FACTOR = 1.25
|
3
10
|
EQUALIZATION_PERIOD_DEFAULT = 7
|
4
|
-
MINIMUM_CONVERSIONS_PER_GROUP_DEFAULT = 10
|
5
11
|
DBG = false
|
6
12
|
|
7
|
-
|
8
|
-
|
13
|
+
# attr_accessor :cache
|
14
|
+
|
9
15
|
def self.cache
|
10
|
-
|
16
|
+
$cache || Rails.cache
|
11
17
|
end
|
12
18
|
|
13
19
|
class << self
|
14
|
-
# Specifies a unique identity for the current visitor. If no identity is specified
|
15
|
-
# then a random value is selected. Conductor makes sure that the same visitor
|
20
|
+
# Specifies a unique identity for the current visitor. If no identity is specified
|
21
|
+
# then a random value is selected. Conductor makes sure that the same visitor
|
16
22
|
# will always see the same alternative selections to reduce confusion.
|
17
23
|
def identity=(value)
|
18
24
|
@conductor_identity = value
|
19
25
|
end
|
20
26
|
|
21
27
|
def identity
|
22
|
-
return (@conductor_identity ||
|
28
|
+
return (@conductor_identity || SecureRandom.hex(16))
|
29
|
+
end
|
30
|
+
|
31
|
+
def reset_identity
|
32
|
+
@conductor_identity = SecureRandom.hex(16)
|
23
33
|
end
|
24
|
-
|
34
|
+
|
25
35
|
# The number of days to include when calculating weights
|
26
36
|
# The inclusion period MUST be higher than then equalization period
|
27
37
|
# The default is 14 days
|
@@ -30,51 +40,38 @@ class Conductor
|
|
30
40
|
raise "Conductor.inclusion_period must be greater than the equalization period" if value < equalization_period
|
31
41
|
@inclusion_period = value
|
32
42
|
end
|
33
|
-
|
43
|
+
|
34
44
|
def inclusion_period
|
35
45
|
return (@inclusion_period || 14)
|
36
46
|
end
|
37
|
-
|
38
|
-
# The
|
39
|
-
#
|
40
|
-
#
|
41
|
-
# TODO: trigger a notification if a post equalized group hits below this number
|
42
|
-
def minimum_conversions_per_group=(value)
|
43
|
-
raise "Conductor.minimum_conversions_per_group must be a positive number > 0" unless value.is_a?(Numeric) && value > 0
|
44
|
-
@minimum_conversions_per_group = value
|
45
|
-
end
|
46
|
-
|
47
|
-
def minimum_conversions_per_group
|
48
|
-
return (@minimum_conversions_per_group || MINIMUM_CONVERSIONS_PER_GROUP_DEFAULT)
|
49
|
-
end
|
50
|
-
|
51
|
-
# The equalization period is the initial amount of time, in days, that conductor
|
52
|
-
# should apply the max_weighting_factor towards a new alternative to ensure
|
47
|
+
|
48
|
+
# The equalization period is the initial amount of time, in days, that conductor
|
49
|
+
# should apply the max_weighting_factor towards a new alternative to ensure
|
53
50
|
# that it receives a far shot of performing.
|
54
51
|
#
|
55
|
-
# If an equalization period was not used then any new alternative would
|
56
|
-
# immediately be weighed very low since it has no conversions and would
|
52
|
+
# If an equalization period was not used then any new alternative would
|
53
|
+
# immediately be weighed very low since it has no conversions and would
|
57
54
|
# never have a chance of performing
|
58
55
|
def equalization_period=(value)
|
59
56
|
raise "Conductor.equalization_period must be a positive number > 0" unless value.is_a?(Numeric) && value > 0
|
60
57
|
@equalization_period = value
|
61
58
|
end
|
62
|
-
|
59
|
+
|
63
60
|
def equalization_period
|
64
61
|
return (@equalization_period || EQUALIZATION_PERIOD_DEFAULT)
|
65
62
|
end
|
66
|
-
|
67
|
-
# The attribute for weighting specifies if the conversion_value OR number of conversions
|
68
|
-
# should be used to calculate the weight. The default is
|
63
|
+
|
64
|
+
# The attribute for weighting specifies if the conversion_value OR number of conversions
|
65
|
+
# should be used to calculate the weight. The default is conversion_value.
|
69
66
|
#
|
70
|
-
# TODO:
|
67
|
+
# TODO: Allow of avg_conversion_value where acv = conversion_value / conversions
|
71
68
|
def attribute_for_weighting=(value)
|
72
|
-
raise "Conductor.attribute_for_weighting must be either :views, :conversions or :conversion_value (default)" unless [:views, :conversions, :conversion_value].include?(value)
|
69
|
+
raise "Conductor.attribute_for_weighting must be either :views, :conversions or :conversion_value (default)" unless [:views, :conversions, :conversion_value].include?(value)
|
73
70
|
@attribute_for_weighting = value
|
74
71
|
end
|
75
|
-
|
72
|
+
|
76
73
|
def attribute_for_weighting
|
77
|
-
return (@attribute_for_weighting || :
|
74
|
+
return (@attribute_for_weighting || :conversion_value)
|
78
75
|
end
|
79
76
|
|
80
77
|
def log(msg)
|
@@ -85,25 +82,4 @@ class Conductor
|
|
85
82
|
str.gsub(/\s/,'_').downcase
|
86
83
|
end
|
87
84
|
end
|
88
|
-
|
89
|
-
end
|
90
|
-
|
91
|
-
|
92
|
-
class Array
|
93
|
-
def sum_it(attribute)
|
94
|
-
self.map {|x| x.send(attribute) }.compact.sum
|
95
|
-
end
|
96
|
-
|
97
|
-
def weighted_mean_of_attribute(attribute)
|
98
|
-
self.map {|x| x.send(attribute) }.compact.weighted_mean
|
99
|
-
end
|
100
|
-
|
101
|
-
def weighted_mean
|
102
|
-
w_sum = sum(self)
|
103
|
-
return 0.00 if w_sum == 0.00
|
104
|
-
|
105
|
-
w_prod = 0
|
106
|
-
self.each_index {|i| w_prod += (i+1) * self[i].to_f}
|
107
|
-
w_prod.to_f / w_sum.to_f
|
108
|
-
end
|
109
85
|
end
|