ab-split 1.0.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.
- checksums.yaml +7 -0
- data/.codeclimate.yml +30 -0
- data/.csslintrc +2 -0
- data/.eslintignore +1 -0
- data/.eslintrc +213 -0
- data/.github/FUNDING.yml +1 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
- data/.rspec +1 -0
- data/.rubocop.yml +7 -0
- data/.rubocop_todo.yml +679 -0
- data/.travis.yml +60 -0
- data/Appraisals +19 -0
- data/CHANGELOG.md +696 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +62 -0
- data/Gemfile +7 -0
- data/LICENSE +22 -0
- data/README.md +955 -0
- data/Rakefile +9 -0
- data/ab-split.gemspec +44 -0
- data/gemfiles/4.2.gemfile +9 -0
- data/gemfiles/5.0.gemfile +9 -0
- data/gemfiles/5.1.gemfile +9 -0
- data/gemfiles/5.2.gemfile +9 -0
- data/gemfiles/6.0.gemfile +9 -0
- data/lib/split.rb +76 -0
- data/lib/split/algorithms/block_randomization.rb +23 -0
- data/lib/split/algorithms/weighted_sample.rb +18 -0
- data/lib/split/algorithms/whiplash.rb +38 -0
- data/lib/split/alternative.rb +191 -0
- data/lib/split/combined_experiments_helper.rb +37 -0
- data/lib/split/configuration.rb +255 -0
- data/lib/split/dashboard.rb +74 -0
- data/lib/split/dashboard/helpers.rb +45 -0
- data/lib/split/dashboard/pagination_helpers.rb +86 -0
- data/lib/split/dashboard/paginator.rb +16 -0
- data/lib/split/dashboard/public/dashboard-filtering.js +43 -0
- data/lib/split/dashboard/public/dashboard.js +24 -0
- data/lib/split/dashboard/public/jquery-1.11.1.min.js +4 -0
- data/lib/split/dashboard/public/reset.css +48 -0
- data/lib/split/dashboard/public/style.css +328 -0
- data/lib/split/dashboard/views/_controls.erb +18 -0
- data/lib/split/dashboard/views/_experiment.erb +155 -0
- data/lib/split/dashboard/views/_experiment_with_goal_header.erb +8 -0
- data/lib/split/dashboard/views/index.erb +26 -0
- data/lib/split/dashboard/views/layout.erb +27 -0
- data/lib/split/encapsulated_helper.rb +42 -0
- data/lib/split/engine.rb +15 -0
- data/lib/split/exceptions.rb +6 -0
- data/lib/split/experiment.rb +486 -0
- data/lib/split/experiment_catalog.rb +51 -0
- data/lib/split/extensions/string.rb +16 -0
- data/lib/split/goals_collection.rb +45 -0
- data/lib/split/helper.rb +165 -0
- data/lib/split/metric.rb +101 -0
- data/lib/split/persistence.rb +28 -0
- data/lib/split/persistence/cookie_adapter.rb +94 -0
- data/lib/split/persistence/dual_adapter.rb +85 -0
- data/lib/split/persistence/redis_adapter.rb +57 -0
- data/lib/split/persistence/session_adapter.rb +29 -0
- data/lib/split/redis_interface.rb +50 -0
- data/lib/split/trial.rb +117 -0
- data/lib/split/user.rb +69 -0
- data/lib/split/version.rb +7 -0
- data/lib/split/zscore.rb +57 -0
- data/spec/algorithms/block_randomization_spec.rb +32 -0
- data/spec/algorithms/weighted_sample_spec.rb +19 -0
- data/spec/algorithms/whiplash_spec.rb +24 -0
- data/spec/alternative_spec.rb +320 -0
- data/spec/combined_experiments_helper_spec.rb +57 -0
- data/spec/configuration_spec.rb +258 -0
- data/spec/dashboard/pagination_helpers_spec.rb +200 -0
- data/spec/dashboard/paginator_spec.rb +37 -0
- data/spec/dashboard_helpers_spec.rb +42 -0
- data/spec/dashboard_spec.rb +210 -0
- data/spec/encapsulated_helper_spec.rb +52 -0
- data/spec/experiment_catalog_spec.rb +53 -0
- data/spec/experiment_spec.rb +533 -0
- data/spec/goals_collection_spec.rb +80 -0
- data/spec/helper_spec.rb +1111 -0
- data/spec/metric_spec.rb +31 -0
- data/spec/persistence/cookie_adapter_spec.rb +106 -0
- data/spec/persistence/dual_adapter_spec.rb +194 -0
- data/spec/persistence/redis_adapter_spec.rb +90 -0
- data/spec/persistence/session_adapter_spec.rb +32 -0
- data/spec/persistence_spec.rb +34 -0
- data/spec/redis_interface_spec.rb +111 -0
- data/spec/spec_helper.rb +52 -0
- data/spec/split_spec.rb +43 -0
- data/spec/support/cookies_mock.rb +20 -0
- data/spec/trial_spec.rb +299 -0
- data/spec/user_spec.rb +87 -0
- metadata +322 -0
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Split
|
4
|
+
module Persistence
|
5
|
+
class DualAdapter
|
6
|
+
def self.with_config(options={})
|
7
|
+
self.config.merge!(options)
|
8
|
+
self
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.config
|
12
|
+
@config ||= {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(context)
|
16
|
+
if logged_in = self.class.config[:logged_in]
|
17
|
+
else
|
18
|
+
raise "Please configure :logged_in"
|
19
|
+
end
|
20
|
+
if logged_in_adapter = self.class.config[:logged_in_adapter]
|
21
|
+
else
|
22
|
+
raise "Please configure :logged_in_adapter"
|
23
|
+
end
|
24
|
+
if logged_out_adapter = self.class.config[:logged_out_adapter]
|
25
|
+
else
|
26
|
+
raise "Please configure :logged_out_adapter"
|
27
|
+
end
|
28
|
+
|
29
|
+
@fallback_to_logged_out_adapter =
|
30
|
+
self.class.config[:fallback_to_logged_out_adapter] || false
|
31
|
+
@logged_in = logged_in.call(context)
|
32
|
+
@logged_in_adapter = logged_in_adapter.new(context)
|
33
|
+
@logged_out_adapter = logged_out_adapter.new(context)
|
34
|
+
@active_adapter = @logged_in ? @logged_in_adapter : @logged_out_adapter
|
35
|
+
end
|
36
|
+
|
37
|
+
def keys
|
38
|
+
if @fallback_to_logged_out_adapter
|
39
|
+
(@logged_in_adapter.keys + @logged_out_adapter.keys).uniq
|
40
|
+
else
|
41
|
+
@active_adapter.keys
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def [](key)
|
46
|
+
if @fallback_to_logged_out_adapter
|
47
|
+
@logged_in && @logged_in_adapter[key] || @logged_out_adapter[key]
|
48
|
+
else
|
49
|
+
@active_adapter[key]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def []=(key, value)
|
54
|
+
if @fallback_to_logged_out_adapter
|
55
|
+
@logged_in_adapter[key] = value if @logged_in
|
56
|
+
old_value = @logged_out_adapter[key]
|
57
|
+
@logged_out_adapter[key] = value
|
58
|
+
|
59
|
+
decrement_participation(key, old_value) if decrement_participation?(old_value, value)
|
60
|
+
else
|
61
|
+
@active_adapter[key] = value
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def delete(key)
|
66
|
+
if @fallback_to_logged_out_adapter
|
67
|
+
@logged_in_adapter.delete(key)
|
68
|
+
@logged_out_adapter.delete(key)
|
69
|
+
else
|
70
|
+
@active_adapter.delete(key)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def decrement_participation?(old_value, value)
|
77
|
+
!old_value.nil? && !value.nil? && old_value != value
|
78
|
+
end
|
79
|
+
|
80
|
+
def decrement_participation(key, value)
|
81
|
+
Split.redis.hincrby("#{key}:#{value}", 'participant_count', -1)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Split
|
3
|
+
module Persistence
|
4
|
+
class RedisAdapter
|
5
|
+
DEFAULT_CONFIG = {:namespace => 'persistence'}.freeze
|
6
|
+
|
7
|
+
attr_reader :redis_key
|
8
|
+
|
9
|
+
def initialize(context, key = nil)
|
10
|
+
if key
|
11
|
+
@redis_key = "#{self.class.config[:namespace]}:#{key}"
|
12
|
+
elsif lookup_by = self.class.config[:lookup_by]
|
13
|
+
if lookup_by.respond_to?(:call)
|
14
|
+
key_frag = lookup_by.call(context)
|
15
|
+
else
|
16
|
+
key_frag = context.send(lookup_by)
|
17
|
+
end
|
18
|
+
@redis_key = "#{self.class.config[:namespace]}:#{key_frag}"
|
19
|
+
else
|
20
|
+
raise "Please configure lookup_by"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def [](field)
|
25
|
+
Split.redis.hget(redis_key, field)
|
26
|
+
end
|
27
|
+
|
28
|
+
def []=(field, value)
|
29
|
+
Split.redis.hset(redis_key, field, value)
|
30
|
+
expire_seconds = self.class.config[:expire_seconds]
|
31
|
+
Split.redis.expire(redis_key, expire_seconds) if expire_seconds
|
32
|
+
end
|
33
|
+
|
34
|
+
def delete(field)
|
35
|
+
Split.redis.hdel(redis_key, field)
|
36
|
+
end
|
37
|
+
|
38
|
+
def keys
|
39
|
+
Split.redis.hkeys(redis_key)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.with_config(options={})
|
43
|
+
self.config.merge!(options)
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.config
|
48
|
+
@config ||= DEFAULT_CONFIG.dup
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.reset_config!
|
52
|
+
@config = DEFAULT_CONFIG.dup
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Split
|
3
|
+
module Persistence
|
4
|
+
class SessionAdapter
|
5
|
+
|
6
|
+
def initialize(context)
|
7
|
+
@session = context.session
|
8
|
+
@session[:split] ||= {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def [](key)
|
12
|
+
@session[:split][key]
|
13
|
+
end
|
14
|
+
|
15
|
+
def []=(key, value)
|
16
|
+
@session[:split][key] = value
|
17
|
+
end
|
18
|
+
|
19
|
+
def delete(key)
|
20
|
+
@session[:split].delete(key)
|
21
|
+
end
|
22
|
+
|
23
|
+
def keys
|
24
|
+
@session[:split].keys
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Split
|
3
|
+
# Simplifies the interface to Redis.
|
4
|
+
class RedisInterface
|
5
|
+
def initialize
|
6
|
+
self.redis = Split.redis
|
7
|
+
end
|
8
|
+
|
9
|
+
def persist_list(list_name, list_values)
|
10
|
+
max_index = list_length(list_name) - 1
|
11
|
+
list_values.each_with_index do |value, index|
|
12
|
+
if index > max_index
|
13
|
+
add_to_list(list_name, value)
|
14
|
+
else
|
15
|
+
set_list_index(list_name, index, value)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
make_list_length(list_name, list_values.length)
|
19
|
+
list_values
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_to_list(list_name, value)
|
23
|
+
redis.rpush(list_name, value)
|
24
|
+
end
|
25
|
+
|
26
|
+
def set_list_index(list_name, index, value)
|
27
|
+
redis.lset(list_name, index, value)
|
28
|
+
end
|
29
|
+
|
30
|
+
def list_length(list_name)
|
31
|
+
redis.llen(list_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
def remove_last_item_from_list(list_name)
|
35
|
+
redis.rpop(list_name)
|
36
|
+
end
|
37
|
+
|
38
|
+
def make_list_length(list_name, new_length)
|
39
|
+
redis.ltrim(list_name, 0, new_length - 1)
|
40
|
+
end
|
41
|
+
|
42
|
+
def add_to_set(set_name, value)
|
43
|
+
redis.sadd(set_name, value) unless redis.sismember(set_name, value)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
attr_accessor :redis
|
49
|
+
end
|
50
|
+
end
|
data/lib/split/trial.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Split
|
3
|
+
class Trial
|
4
|
+
attr_accessor :experiment
|
5
|
+
attr_writer :metadata
|
6
|
+
|
7
|
+
def initialize(attrs = {})
|
8
|
+
self.experiment = attrs.delete(:experiment)
|
9
|
+
self.alternative = attrs.delete(:alternative)
|
10
|
+
self.metadata = attrs.delete(:metadata)
|
11
|
+
|
12
|
+
@user = attrs.delete(:user)
|
13
|
+
@options = attrs
|
14
|
+
|
15
|
+
@alternative_choosen = false
|
16
|
+
end
|
17
|
+
|
18
|
+
def metadata
|
19
|
+
@metadata ||= experiment.metadata[alternative.name] if experiment.metadata
|
20
|
+
end
|
21
|
+
|
22
|
+
def alternative
|
23
|
+
@alternative ||= if @experiment.has_winner?
|
24
|
+
@experiment.winner
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def alternative=(alternative)
|
29
|
+
@alternative = if alternative.kind_of?(Split::Alternative)
|
30
|
+
alternative
|
31
|
+
else
|
32
|
+
@experiment.alternatives.find{|a| a.name == alternative }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def complete!(goals=[], context = nil)
|
37
|
+
if alternative
|
38
|
+
if Array(goals).empty?
|
39
|
+
alternative.increment_completion
|
40
|
+
else
|
41
|
+
Array(goals).each {|g| alternative.increment_completion(g) }
|
42
|
+
end
|
43
|
+
|
44
|
+
run_callback context, Split.configuration.on_trial_complete
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Choose an alternative, add a participant, and save the alternative choice on the user. This
|
49
|
+
# method is guaranteed to only run once, and will skip the alternative choosing process if run
|
50
|
+
# a second time.
|
51
|
+
def choose!(context = nil)
|
52
|
+
@user.cleanup_old_experiments!
|
53
|
+
# Only run the process once
|
54
|
+
return alternative if @alternative_choosen
|
55
|
+
|
56
|
+
if override_is_alternative?
|
57
|
+
self.alternative = @options[:override]
|
58
|
+
if should_store_alternative? && !@user[@experiment.key]
|
59
|
+
self.alternative.increment_participation
|
60
|
+
end
|
61
|
+
elsif @options[:disabled] || Split.configuration.disabled?
|
62
|
+
self.alternative = @experiment.control
|
63
|
+
elsif @experiment.has_winner?
|
64
|
+
self.alternative = @experiment.winner
|
65
|
+
else
|
66
|
+
cleanup_old_versions
|
67
|
+
|
68
|
+
if exclude_user?
|
69
|
+
self.alternative = @experiment.control
|
70
|
+
else
|
71
|
+
self.alternative = @user[@experiment.key]
|
72
|
+
if alternative.nil?
|
73
|
+
self.alternative = @experiment.next_alternative
|
74
|
+
|
75
|
+
# Increment the number of participants since we are actually choosing a new alternative
|
76
|
+
self.alternative.increment_participation
|
77
|
+
|
78
|
+
run_callback context, Split.configuration.on_trial_choose
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
@user[@experiment.key] = alternative.name if !@experiment.has_winner? && should_store_alternative?
|
84
|
+
@alternative_choosen = true
|
85
|
+
run_callback context, Split.configuration.on_trial unless @options[:disabled] || Split.configuration.disabled?
|
86
|
+
alternative
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def run_callback(context, callback_name)
|
92
|
+
context.send(callback_name, self) if callback_name && context.respond_to?(callback_name, true)
|
93
|
+
end
|
94
|
+
|
95
|
+
def override_is_alternative?
|
96
|
+
@experiment.alternatives.map(&:name).include?(@options[:override])
|
97
|
+
end
|
98
|
+
|
99
|
+
def should_store_alternative?
|
100
|
+
if @options[:override] || @options[:disabled]
|
101
|
+
Split.configuration.store_override
|
102
|
+
else
|
103
|
+
!exclude_user?
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def cleanup_old_versions
|
108
|
+
if @experiment.version > 0
|
109
|
+
@user.cleanup_old_versions!(@experiment)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def exclude_user?
|
114
|
+
@options[:exclude] || @experiment.start_time.nil? || @user.max_experiments_reached?(@experiment.key)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/lib/split/user.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module Split
|
5
|
+
class User
|
6
|
+
extend Forwardable
|
7
|
+
def_delegators :@user, :keys, :[], :[]=, :delete
|
8
|
+
attr_reader :user
|
9
|
+
|
10
|
+
def initialize(context, adapter=nil)
|
11
|
+
@user = adapter || Split::Persistence.adapter.new(context)
|
12
|
+
@cleaned_up = false
|
13
|
+
end
|
14
|
+
|
15
|
+
def cleanup_old_experiments!
|
16
|
+
return if @cleaned_up
|
17
|
+
keys_without_finished(user.keys).each do |key|
|
18
|
+
experiment = ExperimentCatalog.find key_without_version(key)
|
19
|
+
if experiment.nil? || experiment.has_winner? || experiment.start_time.nil?
|
20
|
+
user.delete key
|
21
|
+
user.delete Experiment.finished_key(key)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
@cleaned_up = true
|
25
|
+
end
|
26
|
+
|
27
|
+
def max_experiments_reached?(experiment_key)
|
28
|
+
if Split.configuration.allow_multiple_experiments == 'control'
|
29
|
+
experiments = active_experiments
|
30
|
+
count_control = experiments.count {|k,v| k == experiment_key || v == 'control'}
|
31
|
+
experiments.size > count_control
|
32
|
+
else
|
33
|
+
!Split.configuration.allow_multiple_experiments &&
|
34
|
+
keys_without_experiment(user.keys, experiment_key).length > 0
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def cleanup_old_versions!(experiment)
|
39
|
+
keys = user.keys.select { |k| k.match(Regexp.new(experiment.name)) }
|
40
|
+
keys_without_experiment(keys, experiment.key).each { |key| user.delete(key) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def active_experiments
|
44
|
+
experiment_pairs = {}
|
45
|
+
keys_without_finished(user.keys).each do |key|
|
46
|
+
Metric.possible_experiments(key_without_version(key)).each do |experiment|
|
47
|
+
if !experiment.has_winner?
|
48
|
+
experiment_pairs[key_without_version(key)] = user[key]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
experiment_pairs
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def keys_without_experiment(keys, experiment_key)
|
58
|
+
keys.reject { |k| k.match(Regexp.new("^#{experiment_key}(:finished)?$")) }
|
59
|
+
end
|
60
|
+
|
61
|
+
def keys_without_finished(keys)
|
62
|
+
keys.reject { |k| k.include?(":finished") }
|
63
|
+
end
|
64
|
+
|
65
|
+
def key_without_version(key)
|
66
|
+
key.split(/\:\d(?!\:)/)[0]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/split/zscore.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Split
|
3
|
+
class Zscore
|
4
|
+
|
5
|
+
include Math
|
6
|
+
|
7
|
+
def self.calculate(p1, n1, p2, n2)
|
8
|
+
# p_1 = Pa = proportion of users who converted within the experiment split (conversion rate)
|
9
|
+
# p_2 = Pc = proportion of users who converted within the control split (conversion rate)
|
10
|
+
# n_1 = Na = the number of impressions within the experiment split
|
11
|
+
# n_2 = Nc = the number of impressions within the control split
|
12
|
+
# s_1 = SEa = standard error of p_1, the estiamte of the mean
|
13
|
+
# s_2 = SEc = standard error of p_2, the estimate of the control
|
14
|
+
# s_p = SEp = standard error of p_1 - p_2, assuming a pooled variance
|
15
|
+
# s_unp = SEunp = standard error of p_1 - p_2, assuming unpooled variance
|
16
|
+
|
17
|
+
p_1 = p1.to_f
|
18
|
+
p_2 = p2.to_f
|
19
|
+
|
20
|
+
n_1 = n1.to_f
|
21
|
+
n_2 = n2.to_f
|
22
|
+
|
23
|
+
# Perform checks on data to make sure we can validly run our confidence tests
|
24
|
+
if n_1 < 30 || n_2 < 30
|
25
|
+
error = "Needs 30+ participants."
|
26
|
+
return error
|
27
|
+
elsif p_1 * n_1 < 5 || p_2 * n_2 < 5
|
28
|
+
error = "Needs 5+ conversions."
|
29
|
+
return error
|
30
|
+
end
|
31
|
+
|
32
|
+
# Formula for standard error: root(pq/n) = root(p(1-p)/n)
|
33
|
+
s_1 = Math.sqrt((p_1)*(1-p_1)/(n_1))
|
34
|
+
s_2 = Math.sqrt((p_2)*(1-p_2)/(n_2))
|
35
|
+
|
36
|
+
# Formula for pooled error of the difference of the means: root(π*(1-π)*(1/na+1/nc)
|
37
|
+
# π = (xa + xc) / (na + nc)
|
38
|
+
pi = (p_1*n_1 + p_2*n_2)/(n_1 + n_2)
|
39
|
+
s_p = Math.sqrt(pi*(1-pi)*(1/n_1 + 1/n_2))
|
40
|
+
|
41
|
+
# Formula for unpooled error of the difference of the means: root(sa**2/na + sc**2/nc)
|
42
|
+
s_unp = Math.sqrt(s_1**2 + s_2**2)
|
43
|
+
|
44
|
+
# Boolean variable decides whether we can pool our variances
|
45
|
+
pooled = s_1/s_2 < 2 && s_2/s_1 < 2
|
46
|
+
|
47
|
+
# Assign standard error either the pooled or unpooled variance
|
48
|
+
se = pooled ? s_p : s_unp
|
49
|
+
|
50
|
+
# Calculate z-score
|
51
|
+
z_score = (p_1 - p_2)/(se)
|
52
|
+
|
53
|
+
return z_score
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|