stat_power 0.1.0.alpha.1 → 0.1.0.alpha.2
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 +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +14 -4
- data/RELEASING.md +31 -23
- data/ROADMAP.md +9 -9
- data/docs/pwr_parity.md +9 -6
- data/docs/validation.md +60 -0
- data/lib/stat_power/anova.rb +308 -0
- data/lib/stat_power/anova_result.rb +41 -0
- data/lib/stat_power/chi_square.rb +232 -0
- data/lib/stat_power/chi_square_result.rb +20 -0
- data/lib/stat_power/distributions/chi_square.rb +117 -0
- data/lib/stat_power/distributions/noncentral_chi_square.rb +171 -0
- data/lib/stat_power/effect_size/chi_square.rb +125 -0
- data/lib/stat_power/f2.rb +247 -0
- data/lib/stat_power/f2_result.rb +27 -0
- data/lib/stat_power/power_curve.rb +85 -0
- data/lib/stat_power/power_curve_point.rb +6 -0
- data/lib/stat_power/special_functions/gamma.rb +119 -0
- data/lib/stat_power/version.rb +1 -1
- data/lib/stat_power.rb +12 -0
- data/sig/stat_power.rbs +147 -0
- metadata +15 -2
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StatPower
|
|
4
|
+
# Power analysis for general linear models using Cohen's f².
|
|
5
|
+
#
|
|
6
|
+
# The statistical parameterisation follows CRAN pwr.f2.test. Exactly one of
|
|
7
|
+
# numerator_df, denominator_df, effect_size, alpha, and power must be nil.
|
|
8
|
+
module F2
|
|
9
|
+
DF_LOWER = 1e-10
|
|
10
|
+
DF_UPPER = 1e9
|
|
11
|
+
EFFECT_SIZE_LOWER = 1e-10
|
|
12
|
+
EFFECT_SIZE_UPPER = 1e4
|
|
13
|
+
PROBABILITY_EPSILON = 1e-10
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# Solve one missing parameter of a general linear-model power analysis.
|
|
18
|
+
#
|
|
19
|
+
# @param numerator_df [Numeric, nil] numerator degrees of freedom, u
|
|
20
|
+
# @param denominator_df [Numeric, nil] denominator degrees of freedom, v
|
|
21
|
+
# @param effect_size [Numeric, Symbol, String, nil] Cohen's f²
|
|
22
|
+
# @param alpha [Numeric, nil] Type I error probability
|
|
23
|
+
# @param power [Numeric, nil] statistical power
|
|
24
|
+
# @return [StatPower::F2Result]
|
|
25
|
+
def solve(
|
|
26
|
+
numerator_df: nil,
|
|
27
|
+
denominator_df: nil,
|
|
28
|
+
effect_size: nil,
|
|
29
|
+
alpha: 0.05,
|
|
30
|
+
power: nil
|
|
31
|
+
)
|
|
32
|
+
ensure_one_missing!(
|
|
33
|
+
numerator_df,
|
|
34
|
+
denominator_df,
|
|
35
|
+
effect_size,
|
|
36
|
+
alpha,
|
|
37
|
+
power
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
numerator_df = optional_float(numerator_df)
|
|
41
|
+
denominator_df = optional_float(denominator_df)
|
|
42
|
+
effect_size = normalize_effect_size(effect_size)
|
|
43
|
+
alpha = optional_float(alpha)
|
|
44
|
+
power = optional_float(power)
|
|
45
|
+
|
|
46
|
+
validate_known_values!(
|
|
47
|
+
numerator_df:,
|
|
48
|
+
denominator_df:,
|
|
49
|
+
effect_size:,
|
|
50
|
+
alpha:,
|
|
51
|
+
power:
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
numerator_df, denominator_df, effect_size, alpha, power = solve_missing(
|
|
55
|
+
numerator_df:,
|
|
56
|
+
denominator_df:,
|
|
57
|
+
effect_size:,
|
|
58
|
+
alpha:,
|
|
59
|
+
power:
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
F2Result.new(
|
|
63
|
+
numerator_df:,
|
|
64
|
+
denominator_df:,
|
|
65
|
+
power:,
|
|
66
|
+
effect_size:,
|
|
67
|
+
alpha:,
|
|
68
|
+
analysis_method: "general linear model power calculation"
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def solve_missing(numerator_df:, denominator_df:, effect_size:, alpha:, power:)
|
|
73
|
+
if power.nil?
|
|
74
|
+
power = power_for(
|
|
75
|
+
numerator_df:,
|
|
76
|
+
denominator_df:,
|
|
77
|
+
effect_size:,
|
|
78
|
+
alpha:
|
|
79
|
+
)
|
|
80
|
+
elsif numerator_df.nil?
|
|
81
|
+
numerator_df = solve_numerator_df(
|
|
82
|
+
denominator_df:,
|
|
83
|
+
effect_size:,
|
|
84
|
+
alpha:,
|
|
85
|
+
power:
|
|
86
|
+
)
|
|
87
|
+
elsif denominator_df.nil?
|
|
88
|
+
denominator_df = solve_denominator_df(
|
|
89
|
+
numerator_df:,
|
|
90
|
+
effect_size:,
|
|
91
|
+
alpha:,
|
|
92
|
+
power:
|
|
93
|
+
)
|
|
94
|
+
elsif effect_size.nil?
|
|
95
|
+
effect_size = solve_effect_size(
|
|
96
|
+
numerator_df:,
|
|
97
|
+
denominator_df:,
|
|
98
|
+
alpha:,
|
|
99
|
+
power:
|
|
100
|
+
)
|
|
101
|
+
elsif alpha.nil?
|
|
102
|
+
alpha = solve_alpha(
|
|
103
|
+
numerator_df:,
|
|
104
|
+
denominator_df:,
|
|
105
|
+
effect_size:,
|
|
106
|
+
power:
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
[numerator_df, denominator_df, effect_size, alpha, power]
|
|
111
|
+
end
|
|
112
|
+
private_class_method :solve_missing
|
|
113
|
+
|
|
114
|
+
def power_for(numerator_df:, denominator_df:, effect_size:, alpha:)
|
|
115
|
+
noncentrality = effect_size * (numerator_df + denominator_df + 1.0)
|
|
116
|
+
|
|
117
|
+
critical = Distributions::FDistribution.quantile(
|
|
118
|
+
1.0 - alpha,
|
|
119
|
+
numerator_df:,
|
|
120
|
+
denominator_df:
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
Distributions::NoncentralF.survival(
|
|
124
|
+
critical,
|
|
125
|
+
numerator_df:,
|
|
126
|
+
denominator_df:,
|
|
127
|
+
noncentrality:
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
private_class_method :power_for
|
|
131
|
+
|
|
132
|
+
def solve_numerator_df(denominator_df:, effect_size:, alpha:, power:)
|
|
133
|
+
Solvers::Bisection.solve(
|
|
134
|
+
lower: DF_LOWER,
|
|
135
|
+
upper: DF_UPPER
|
|
136
|
+
) do |candidate|
|
|
137
|
+
power_for(
|
|
138
|
+
numerator_df: candidate,
|
|
139
|
+
denominator_df:,
|
|
140
|
+
effect_size:,
|
|
141
|
+
alpha:
|
|
142
|
+
) - power
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
private_class_method :solve_numerator_df
|
|
146
|
+
|
|
147
|
+
def solve_denominator_df(numerator_df:, effect_size:, alpha:, power:)
|
|
148
|
+
Solvers::Bisection.solve(
|
|
149
|
+
lower: DF_LOWER,
|
|
150
|
+
upper: DF_UPPER
|
|
151
|
+
) do |candidate|
|
|
152
|
+
power_for(
|
|
153
|
+
numerator_df:,
|
|
154
|
+
denominator_df: candidate,
|
|
155
|
+
effect_size:,
|
|
156
|
+
alpha:
|
|
157
|
+
) - power
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
private_class_method :solve_denominator_df
|
|
161
|
+
|
|
162
|
+
def solve_effect_size(numerator_df:, denominator_df:, alpha:, power:)
|
|
163
|
+
Solvers::Bisection.solve(
|
|
164
|
+
lower: EFFECT_SIZE_LOWER,
|
|
165
|
+
upper: EFFECT_SIZE_UPPER
|
|
166
|
+
) do |candidate|
|
|
167
|
+
power_for(
|
|
168
|
+
numerator_df:,
|
|
169
|
+
denominator_df:,
|
|
170
|
+
effect_size: candidate,
|
|
171
|
+
alpha:
|
|
172
|
+
) - power
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
private_class_method :solve_effect_size
|
|
176
|
+
|
|
177
|
+
def solve_alpha(numerator_df:, denominator_df:, effect_size:, power:)
|
|
178
|
+
Solvers::Bisection.solve(
|
|
179
|
+
lower: PROBABILITY_EPSILON,
|
|
180
|
+
upper: 1.0 - PROBABILITY_EPSILON
|
|
181
|
+
) do |candidate|
|
|
182
|
+
power_for(
|
|
183
|
+
numerator_df:,
|
|
184
|
+
denominator_df:,
|
|
185
|
+
effect_size:,
|
|
186
|
+
alpha: candidate
|
|
187
|
+
) - power
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
private_class_method :solve_alpha
|
|
191
|
+
|
|
192
|
+
def ensure_one_missing!(*values)
|
|
193
|
+
return if values.count(&:nil?) == 1
|
|
194
|
+
|
|
195
|
+
raise StatPower::DomainError,
|
|
196
|
+
"exactly one of numerator_df, denominator_df, effect_size, alpha, and power must be nil"
|
|
197
|
+
end
|
|
198
|
+
private_class_method :ensure_one_missing!
|
|
199
|
+
|
|
200
|
+
def normalize_effect_size(value)
|
|
201
|
+
return nil if value.nil?
|
|
202
|
+
return EffectSize::Conventional.resolve(test: :f2, size: value) if value.is_a?(String) || value.is_a?(Symbol)
|
|
203
|
+
|
|
204
|
+
Float(value)
|
|
205
|
+
rescue ArgumentError, TypeError
|
|
206
|
+
raise StatPower::DomainError, "effect_size must be numeric or a conventional size"
|
|
207
|
+
end
|
|
208
|
+
private_class_method :normalize_effect_size
|
|
209
|
+
|
|
210
|
+
def optional_float(value)
|
|
211
|
+
value.nil? ? nil : Float(value)
|
|
212
|
+
rescue ArgumentError, TypeError
|
|
213
|
+
raise StatPower::DomainError, "numeric parameters must be coercible to Float"
|
|
214
|
+
end
|
|
215
|
+
private_class_method :optional_float
|
|
216
|
+
|
|
217
|
+
def validate_known_values!(numerator_df:, denominator_df:, effect_size:, alpha:, power:)
|
|
218
|
+
validate_df!("numerator_df", numerator_df) if numerator_df
|
|
219
|
+
validate_df!("denominator_df", denominator_df) if denominator_df
|
|
220
|
+
validate_effect_size!(effect_size) if effect_size
|
|
221
|
+
validate_probability!("alpha", alpha) if alpha
|
|
222
|
+
validate_probability!("power", power) if power
|
|
223
|
+
end
|
|
224
|
+
private_class_method :validate_known_values!
|
|
225
|
+
|
|
226
|
+
def validate_df!(name, value)
|
|
227
|
+
return if value.finite? && value.positive?
|
|
228
|
+
|
|
229
|
+
raise StatPower::DomainError, "#{name} must be finite and positive"
|
|
230
|
+
end
|
|
231
|
+
private_class_method :validate_df!
|
|
232
|
+
|
|
233
|
+
def validate_effect_size!(effect_size)
|
|
234
|
+
return if effect_size.finite? && !effect_size.negative?
|
|
235
|
+
|
|
236
|
+
raise StatPower::DomainError, "effect_size must be finite and non-negative"
|
|
237
|
+
end
|
|
238
|
+
private_class_method :validate_effect_size!
|
|
239
|
+
|
|
240
|
+
def validate_probability!(name, value)
|
|
241
|
+
return if value.finite? && value.positive? && value < 1.0
|
|
242
|
+
|
|
243
|
+
raise StatPower::DomainError, "#{name} must lie strictly between 0 and 1"
|
|
244
|
+
end
|
|
245
|
+
private_class_method :validate_probability!
|
|
246
|
+
end
|
|
247
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StatPower
|
|
4
|
+
# Immutable result for general linear-model power analyses based on Cohen's f².
|
|
5
|
+
F2Result = Data.define(
|
|
6
|
+
:numerator_df,
|
|
7
|
+
:denominator_df,
|
|
8
|
+
:power,
|
|
9
|
+
:effect_size,
|
|
10
|
+
:alpha,
|
|
11
|
+
:analysis_method
|
|
12
|
+
) do
|
|
13
|
+
# Implied total sample size N = u + v + 1.
|
|
14
|
+
#
|
|
15
|
+
# @return [Float]
|
|
16
|
+
def total_sample_size
|
|
17
|
+
numerator_df + denominator_df + 1.0
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Smallest whole-number total sample size not below the continuous value.
|
|
21
|
+
#
|
|
22
|
+
# @return [Integer]
|
|
23
|
+
def required_total_sample_size
|
|
24
|
+
total_sample_size.ceil
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StatPower
|
|
4
|
+
# Data-first power-curve generation.
|
|
5
|
+
#
|
|
6
|
+
# A caller supplies the sample sizes and a block that evaluates an existing
|
|
7
|
+
# power solver at each sample size. The block may return either a numeric
|
|
8
|
+
# power value or any result object exposing a +power+ reader.
|
|
9
|
+
module PowerCurve
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Generate power-curve points.
|
|
13
|
+
#
|
|
14
|
+
# @param sample_sizes [Enumerable<Numeric>] sample sizes to evaluate
|
|
15
|
+
# @yieldparam sample_size [Float] current sample size
|
|
16
|
+
# @yieldreturn [Numeric, #power] achieved power or result object
|
|
17
|
+
# @return [Array<StatPower::PowerCurvePoint>]
|
|
18
|
+
def generate(sample_sizes:)
|
|
19
|
+
unless block_given?
|
|
20
|
+
raise StatPower::DomainError,
|
|
21
|
+
"a block evaluating power at each sample size is required"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
sizes = normalize_sample_sizes(sample_sizes)
|
|
25
|
+
|
|
26
|
+
sizes.map do |sample_size|
|
|
27
|
+
evaluated = yield(sample_size)
|
|
28
|
+
power = extract_power(evaluated)
|
|
29
|
+
|
|
30
|
+
PowerCurvePoint.new(
|
|
31
|
+
sample_size:,
|
|
32
|
+
power:
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def normalize_sample_sizes(values)
|
|
38
|
+
unless values.respond_to?(:map)
|
|
39
|
+
raise StatPower::DomainError,
|
|
40
|
+
"sample_sizes must be an enumerable collection"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
sizes = values.map do |value|
|
|
44
|
+
sample_size = Float(value)
|
|
45
|
+
|
|
46
|
+
unless sample_size.finite? && sample_size.positive?
|
|
47
|
+
raise StatPower::DomainError,
|
|
48
|
+
"sample sizes must be finite and positive"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
sample_size
|
|
52
|
+
rescue ArgumentError, TypeError
|
|
53
|
+
raise StatPower::DomainError,
|
|
54
|
+
"sample sizes must be numeric"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if sizes.empty?
|
|
58
|
+
raise StatPower::DomainError,
|
|
59
|
+
"sample_sizes must contain at least one value"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
sizes
|
|
63
|
+
end
|
|
64
|
+
private_class_method :normalize_sample_sizes
|
|
65
|
+
|
|
66
|
+
def extract_power(value)
|
|
67
|
+
power = if value.respond_to?(:power)
|
|
68
|
+
value.power
|
|
69
|
+
else
|
|
70
|
+
Float(value)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
power = Float(power)
|
|
74
|
+
|
|
75
|
+
return power if power.finite? && power >= 0.0 && power <= 1.0
|
|
76
|
+
|
|
77
|
+
raise StatPower::DomainError,
|
|
78
|
+
"power evaluations must lie in [0, 1]"
|
|
79
|
+
rescue ArgumentError, TypeError
|
|
80
|
+
raise StatPower::DomainError,
|
|
81
|
+
"power evaluations must be numeric or expose a numeric power value"
|
|
82
|
+
end
|
|
83
|
+
private_class_method :extract_power
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StatPower
|
|
4
|
+
module SpecialFunctions
|
|
5
|
+
# Numerical utilities for the regularized incomplete gamma functions.
|
|
6
|
+
module Gamma
|
|
7
|
+
MAX_ITERATIONS = 10_000
|
|
8
|
+
EPSILON = 3e-14
|
|
9
|
+
MIN_DENOMINATOR = 1e-300
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# Regularized lower incomplete gamma P(a, x).
|
|
14
|
+
#
|
|
15
|
+
# @param a [Numeric] positive shape parameter
|
|
16
|
+
# @param x [Numeric] non-negative evaluation point
|
|
17
|
+
# @return [Float]
|
|
18
|
+
def regularized_lower(a:, x:)
|
|
19
|
+
shape = Float(a)
|
|
20
|
+
value = Float(x)
|
|
21
|
+
validate_arguments!(shape, value)
|
|
22
|
+
|
|
23
|
+
return 0.0 if value.zero?
|
|
24
|
+
|
|
25
|
+
if value < shape + 1.0
|
|
26
|
+
lower_series(shape, value)
|
|
27
|
+
else
|
|
28
|
+
1.0 - upper_continued_fraction(shape, value)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Regularized upper incomplete gamma Q(a, x).
|
|
33
|
+
#
|
|
34
|
+
# @param a [Numeric] positive shape parameter
|
|
35
|
+
# @param x [Numeric] non-negative evaluation point
|
|
36
|
+
# @return [Float]
|
|
37
|
+
def regularized_upper(a:, x:)
|
|
38
|
+
shape = Float(a)
|
|
39
|
+
value = Float(x)
|
|
40
|
+
validate_arguments!(shape, value)
|
|
41
|
+
|
|
42
|
+
return 1.0 if value.zero?
|
|
43
|
+
|
|
44
|
+
if value < shape + 1.0
|
|
45
|
+
1.0 - lower_series(shape, value)
|
|
46
|
+
else
|
|
47
|
+
upper_continued_fraction(shape, value)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def lower_series(shape, value)
|
|
52
|
+
term = 1.0 / shape
|
|
53
|
+
sum = term
|
|
54
|
+
shifted_shape = shape
|
|
55
|
+
|
|
56
|
+
1.upto(MAX_ITERATIONS) do
|
|
57
|
+
shifted_shape += 1.0
|
|
58
|
+
term *= value / shifted_shape
|
|
59
|
+
sum += term
|
|
60
|
+
|
|
61
|
+
return sum * scale_factor(shape, value) if term.abs <= sum.abs * EPSILON
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
raise StatPower::ConvergenceError,
|
|
65
|
+
"regularized gamma series did not converge"
|
|
66
|
+
end
|
|
67
|
+
private_class_method :lower_series
|
|
68
|
+
|
|
69
|
+
def upper_continued_fraction(shape, value)
|
|
70
|
+
b = value + 1.0 - shape
|
|
71
|
+
c = 1.0 / MIN_DENOMINATOR
|
|
72
|
+
d = 1.0 / stabilize(b)
|
|
73
|
+
result = d
|
|
74
|
+
|
|
75
|
+
1.upto(MAX_ITERATIONS) do |iteration|
|
|
76
|
+
coefficient = -iteration * (iteration - shape)
|
|
77
|
+
b += 2.0
|
|
78
|
+
d = 1.0 / stabilize((coefficient * d) + b)
|
|
79
|
+
c = stabilize(b + (coefficient / c))
|
|
80
|
+
delta = d * c
|
|
81
|
+
result *= delta
|
|
82
|
+
|
|
83
|
+
return result * scale_factor(shape, value) if (delta - 1.0).abs <= EPSILON
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
raise StatPower::ConvergenceError,
|
|
87
|
+
"regularized gamma continued fraction did not converge"
|
|
88
|
+
end
|
|
89
|
+
private_class_method :upper_continued_fraction
|
|
90
|
+
|
|
91
|
+
def scale_factor(shape, value)
|
|
92
|
+
Math.exp(
|
|
93
|
+
(-value) +
|
|
94
|
+
(shape * Math.log(value)) -
|
|
95
|
+
Math.lgamma(shape).first
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
private_class_method :scale_factor
|
|
99
|
+
|
|
100
|
+
def stabilize(value)
|
|
101
|
+
return value if value.abs >= MIN_DENOMINATOR
|
|
102
|
+
|
|
103
|
+
value.negative? ? -MIN_DENOMINATOR : MIN_DENOMINATOR
|
|
104
|
+
end
|
|
105
|
+
private_class_method :stabilize
|
|
106
|
+
|
|
107
|
+
def validate_arguments!(shape, value)
|
|
108
|
+
unless shape.finite? && shape.positive?
|
|
109
|
+
raise StatPower::DomainError, "gamma shape parameter must be finite and positive"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
return if value.finite? && !value.negative?
|
|
113
|
+
|
|
114
|
+
raise StatPower::DomainError, "gamma evaluation point must be finite and non-negative"
|
|
115
|
+
end
|
|
116
|
+
private_class_method :validate_arguments!
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
data/lib/stat_power/version.rb
CHANGED
data/lib/stat_power.rb
CHANGED
|
@@ -4,22 +4,34 @@ require_relative "stat_power/version"
|
|
|
4
4
|
require_relative "stat_power/result"
|
|
5
5
|
require_relative "stat_power/power_result"
|
|
6
6
|
require_relative "stat_power/unequal_power_result"
|
|
7
|
+
require_relative "stat_power/anova_result"
|
|
8
|
+
require_relative "stat_power/f2_result"
|
|
9
|
+
require_relative "stat_power/chi_square_result"
|
|
10
|
+
require_relative "stat_power/power_curve_point"
|
|
7
11
|
require_relative "stat_power/errors"
|
|
8
12
|
require_relative "stat_power/special_functions/beta"
|
|
13
|
+
require_relative "stat_power/special_functions/gamma"
|
|
9
14
|
require_relative "stat_power/integration/adaptive_simpson"
|
|
10
15
|
require_relative "stat_power/distributions/normal"
|
|
11
16
|
require_relative "stat_power/distributions/student_t"
|
|
12
17
|
require_relative "stat_power/distributions/noncentral_t"
|
|
13
18
|
require_relative "stat_power/distributions/f_distribution"
|
|
14
19
|
require_relative "stat_power/distributions/noncentral_f"
|
|
20
|
+
require_relative "stat_power/distributions/chi_square"
|
|
21
|
+
require_relative "stat_power/distributions/noncentral_chi_square"
|
|
15
22
|
require_relative "stat_power/solvers/bisection"
|
|
16
23
|
require_relative "stat_power/effect_size/conventional"
|
|
17
24
|
require_relative "stat_power/effect_size/proportion"
|
|
25
|
+
require_relative "stat_power/effect_size/chi_square"
|
|
18
26
|
require_relative "stat_power/normal_mean"
|
|
19
27
|
require_relative "stat_power/proportion"
|
|
20
28
|
require_relative "stat_power/t_test"
|
|
21
29
|
require_relative "stat_power/t_test_unequal"
|
|
22
30
|
require_relative "stat_power/correlation"
|
|
31
|
+
require_relative "stat_power/anova"
|
|
32
|
+
require_relative "stat_power/f2"
|
|
33
|
+
require_relative "stat_power/chi_square"
|
|
34
|
+
require_relative "stat_power/power_curve"
|
|
23
35
|
|
|
24
36
|
module StatPower
|
|
25
37
|
end
|
data/sig/stat_power.rbs
CHANGED
|
@@ -69,6 +69,80 @@ module StatPower
|
|
|
69
69
|
def required_total_sample_size: () -> Integer
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
class AnovaResult
|
|
73
|
+
attr_reader groups: Float
|
|
74
|
+
attr_reader sample_size: Float
|
|
75
|
+
attr_reader power: Float
|
|
76
|
+
attr_reader effect_size: Float
|
|
77
|
+
attr_reader alpha: Float
|
|
78
|
+
attr_reader analysis_method: String
|
|
79
|
+
|
|
80
|
+
def initialize: (
|
|
81
|
+
groups: Float,
|
|
82
|
+
sample_size: Float,
|
|
83
|
+
power: Float,
|
|
84
|
+
effect_size: Float,
|
|
85
|
+
alpha: Float,
|
|
86
|
+
analysis_method: String
|
|
87
|
+
) -> void
|
|
88
|
+
|
|
89
|
+
def required_groups: () -> Integer
|
|
90
|
+
def required_sample_size: () -> Integer
|
|
91
|
+
def total_sample_size: () -> Float
|
|
92
|
+
def required_total_sample_size: () -> Integer
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
class F2Result
|
|
96
|
+
attr_reader numerator_df: Float
|
|
97
|
+
attr_reader denominator_df: Float
|
|
98
|
+
attr_reader power: Float
|
|
99
|
+
attr_reader effect_size: Float
|
|
100
|
+
attr_reader alpha: Float
|
|
101
|
+
attr_reader analysis_method: String
|
|
102
|
+
|
|
103
|
+
def initialize: (
|
|
104
|
+
numerator_df: Float,
|
|
105
|
+
denominator_df: Float,
|
|
106
|
+
power: Float,
|
|
107
|
+
effect_size: Float,
|
|
108
|
+
alpha: Float,
|
|
109
|
+
analysis_method: String
|
|
110
|
+
) -> void
|
|
111
|
+
|
|
112
|
+
def total_sample_size: () -> Float
|
|
113
|
+
def required_total_sample_size: () -> Integer
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
class ChiSquareResult
|
|
117
|
+
attr_reader degrees_of_freedom: Float
|
|
118
|
+
attr_reader sample_size: Float
|
|
119
|
+
attr_reader power: Float
|
|
120
|
+
attr_reader effect_size: Float
|
|
121
|
+
attr_reader alpha: Float
|
|
122
|
+
attr_reader analysis_method: String
|
|
123
|
+
|
|
124
|
+
def initialize: (
|
|
125
|
+
degrees_of_freedom: Float,
|
|
126
|
+
sample_size: Float,
|
|
127
|
+
power: Float,
|
|
128
|
+
effect_size: Float,
|
|
129
|
+
alpha: Float,
|
|
130
|
+
analysis_method: String
|
|
131
|
+
) -> void
|
|
132
|
+
|
|
133
|
+
def required_sample_size: () -> Integer
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
class PowerCurvePoint
|
|
137
|
+
attr_reader sample_size: Float
|
|
138
|
+
attr_reader power: Float
|
|
139
|
+
|
|
140
|
+
def initialize: (
|
|
141
|
+
sample_size: Float,
|
|
142
|
+
power: Float
|
|
143
|
+
) -> void
|
|
144
|
+
end
|
|
145
|
+
|
|
72
146
|
module EffectSize
|
|
73
147
|
module Conventional
|
|
74
148
|
def self.resolve: (test: Symbol | String, size: Symbol | String) -> Float
|
|
@@ -77,6 +151,17 @@ module StatPower
|
|
|
77
151
|
module Proportion
|
|
78
152
|
def self.cohen_h: (p1: Numeric, p2: Numeric) -> Float
|
|
79
153
|
end
|
|
154
|
+
|
|
155
|
+
module ChiSquare
|
|
156
|
+
def self.goodness_of_fit: (
|
|
157
|
+
null_probabilities: Array[Numeric],
|
|
158
|
+
alternative_probabilities: Array[Numeric]
|
|
159
|
+
) -> Float
|
|
160
|
+
|
|
161
|
+
def self.association: (
|
|
162
|
+
probabilities: Array[Array[Numeric]]
|
|
163
|
+
) -> Float
|
|
164
|
+
end
|
|
80
165
|
end
|
|
81
166
|
|
|
82
167
|
module NormalMean
|
|
@@ -161,10 +246,51 @@ module StatPower
|
|
|
161
246
|
) -> PowerResult
|
|
162
247
|
end
|
|
163
248
|
|
|
249
|
+
module Anova
|
|
250
|
+
def self.solve: (
|
|
251
|
+
?groups: Numeric | nil,
|
|
252
|
+
?sample_size: Numeric | nil,
|
|
253
|
+
?effect_size: Numeric | Symbol | String | nil,
|
|
254
|
+
?alpha: Numeric | nil,
|
|
255
|
+
?power: Numeric | nil
|
|
256
|
+
) -> AnovaResult
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
module F2
|
|
260
|
+
def self.solve: (
|
|
261
|
+
?numerator_df: Numeric | nil,
|
|
262
|
+
?denominator_df: Numeric | nil,
|
|
263
|
+
?effect_size: Numeric | Symbol | String | nil,
|
|
264
|
+
?alpha: Numeric | nil,
|
|
265
|
+
?power: Numeric | nil
|
|
266
|
+
) -> F2Result
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
module ChiSquare
|
|
270
|
+
def self.solve: (
|
|
271
|
+
degrees_of_freedom: Numeric,
|
|
272
|
+
?effect_size: Numeric | Symbol | String | nil,
|
|
273
|
+
?sample_size: Numeric | nil,
|
|
274
|
+
?alpha: Numeric | nil,
|
|
275
|
+
?power: Numeric | nil
|
|
276
|
+
) -> ChiSquareResult
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
module PowerCurve
|
|
280
|
+
def self.generate: (
|
|
281
|
+
sample_sizes: Enumerable[Numeric]
|
|
282
|
+
) { (Float) -> Numeric | PowerResult | UnequalPowerResult | AnovaResult | F2Result | ChiSquareResult } -> Array[PowerCurvePoint]
|
|
283
|
+
end
|
|
284
|
+
|
|
164
285
|
module SpecialFunctions
|
|
165
286
|
module Beta
|
|
166
287
|
def self.regularized: (Numeric x, a: Numeric, b: Numeric) -> Float
|
|
167
288
|
end
|
|
289
|
+
|
|
290
|
+
module Gamma
|
|
291
|
+
def self.regularized_lower: (a: Numeric, x: Numeric) -> Float
|
|
292
|
+
def self.regularized_upper: (a: Numeric, x: Numeric) -> Float
|
|
293
|
+
end
|
|
168
294
|
end
|
|
169
295
|
|
|
170
296
|
module Integration
|
|
@@ -248,6 +374,27 @@ module StatPower
|
|
|
248
374
|
noncentrality: Numeric
|
|
249
375
|
) -> Float
|
|
250
376
|
end
|
|
377
|
+
|
|
378
|
+
module ChiSquare
|
|
379
|
+
def self.pdf: (Numeric x, degrees_of_freedom: Numeric) -> Float
|
|
380
|
+
def self.cdf: (Numeric x, degrees_of_freedom: Numeric) -> Float
|
|
381
|
+
def self.survival: (Numeric x, degrees_of_freedom: Numeric) -> Float
|
|
382
|
+
def self.quantile: (Numeric probability, degrees_of_freedom: Numeric) -> Float
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
module NoncentralChiSquare
|
|
386
|
+
def self.cdf: (
|
|
387
|
+
Numeric x,
|
|
388
|
+
degrees_of_freedom: Numeric,
|
|
389
|
+
noncentrality: Numeric
|
|
390
|
+
) -> Float
|
|
391
|
+
|
|
392
|
+
def self.survival: (
|
|
393
|
+
Numeric x,
|
|
394
|
+
degrees_of_freedom: Numeric,
|
|
395
|
+
noncentrality: Numeric
|
|
396
|
+
) -> Float
|
|
397
|
+
end
|
|
251
398
|
end
|
|
252
399
|
|
|
253
400
|
module Solvers
|