ab-split 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|