stat_power 0.1.0.alpha.1 → 0.1.0.alpha.3
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 +54 -0
- data/README.md +62 -13
- 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/distributions/noncentral_f.rb +1 -1
- data/lib/stat_power/distributions/noncentral_t.rb +1 -1
- data/lib/stat_power/effect_size/chi_square.rb +118 -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 +20 -5
- data/RELEASING.md +0 -105
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2c69ef6117ff1e0bf73f795f855383722810e4e0153d33fe65e5baf803c6554b
|
|
4
|
+
data.tar.gz: 68e88eb27fff0e23651c8ae4f9a954eb14718703287b9aaf596b251dd0d742af
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d8882a7378d4e07735877624fdb21ac512b9e54f5bb42a0342374fcd18e922e85b36710fe9e05fcf92a8b2a5abea3711198bc3df678a7d5796735581bfa9b108
|
|
7
|
+
data.tar.gz: b5f799b202dc11cc3a7b0290a2d90ff1061771f43cc021e7626e6c256213ff95a8a597f09f5034ec9b1e7f9b070aef0ef8f940fec7734b52263f589349ac86d8
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,60 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.1.0.alpha.3 - 2026-09-20
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Contribution guide, code of conduct, and security policy
|
|
12
|
+
- Issue templates, including a dedicated numerical-discrepancy report
|
|
13
|
+
- Pull request template requiring independent numerical validation
|
|
14
|
+
- `CODEOWNERS` and Dependabot configuration for Bundler and GitHub Actions
|
|
15
|
+
- CodeQL analysis workflow
|
|
16
|
+
- `bin/setup` and `bin/console` developer scripts
|
|
17
|
+
- `rake verify` task running every check that CI runs
|
|
18
|
+
- Steep type checking of `lib/` against `sig/`, now enforced in CI
|
|
19
|
+
- SimpleCov line and branch coverage with an enforced minimum
|
|
20
|
+
- `.gitattributes`, `.editorconfig`, and `.ruby-version` for reproducible
|
|
21
|
+
formatting and tooling across platforms
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- CI now runs lint, type check, and coverage as dedicated jobs, declares
|
|
26
|
+
least-privilege permissions, and cancels superseded runs
|
|
27
|
+
- RuboCop runs with `NewCops: enable`
|
|
28
|
+
- Gem metadata now includes a contact address, bug tracker, and versioned
|
|
29
|
+
source URI; maintainer-only files are no longer packaged
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **`sig/stat_power.rbs` shipped in 0.1.0.alpha.2 was not valid RBS.** The
|
|
34
|
+
block signature for `PowerCurve.generate` used an unparenthesized union
|
|
35
|
+
return type, so any consumer feeding the packaged signatures to `rbs` or
|
|
36
|
+
Steep hit a parse error at line 282. Runtime behaviour was unaffected;
|
|
37
|
+
0.1.0.alpha.1 was not affected, because it predates `PowerCurve`. `rbs
|
|
38
|
+
validate` accepts the malformed form, which is why it reached a release;
|
|
39
|
+
`steep check` now runs in CI and in the release gate.
|
|
40
|
+
- `EffectSize::ChiSquare.association` computed column marginals by mutating an
|
|
41
|
+
accumulator array; it now uses `transpose`
|
|
42
|
+
|
|
43
|
+
## 0.1.0.alpha.2 - 2026-09-20
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- Balanced one-way ANOVA power analysis compatible with `pwr.anova.test`
|
|
48
|
+
- Dedicated ANOVA result object with per-group and total sample-size helpers
|
|
49
|
+
- General linear-model power analysis compatible with `pwr.f2.test`
|
|
50
|
+
- Dedicated `F2Result` with implied total sample-size helpers
|
|
51
|
+
- Regularized incomplete gamma functions
|
|
52
|
+
- Central and noncentral chi-square distribution utilities
|
|
53
|
+
- Cohen's `w` effect-size helpers for goodness-of-fit and association tests
|
|
54
|
+
- Chi-square power analysis compatible with `pwr.chisq.test`
|
|
55
|
+
- Dedicated `ChiSquareResult` with total sample-size helper
|
|
56
|
+
- Data-first power-curve generation via `StatPower::PowerCurve.generate`
|
|
57
|
+
- Immutable `PowerCurvePoint` values for plotting-library-independent output
|
|
58
|
+
- Fixture-driven CRAN `pwr` parity validation across all implemented solver families
|
|
59
|
+
- Documented numerical tolerance and reference-provenance policy
|
|
60
|
+
|
|
7
61
|
## 0.1.0.alpha.1 - 2026-09-19
|
|
8
62
|
|
|
9
63
|
### Added
|
data/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# stat_power
|
|
2
2
|
|
|
3
|
+
[](https://github.com/DiogoRibeiro7/stat_power/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/stat_power)
|
|
5
|
+
[](https://rubygems.org/gems/stat_power)
|
|
6
|
+
[](https://www.ruby-lang.org/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
3
9
|
Native Ruby statistical power analysis and sample-size determination.
|
|
4
10
|
|
|
5
11
|
> **Alpha release:** `stat_power` is under active development. The public API
|
|
@@ -17,13 +23,13 @@ gem install stat_power --prerelease
|
|
|
17
23
|
or pin the exact alpha:
|
|
18
24
|
|
|
19
25
|
```bash
|
|
20
|
-
gem install stat_power -v 0.1.0.alpha.
|
|
26
|
+
gem install stat_power -v 0.1.0.alpha.3
|
|
21
27
|
```
|
|
22
28
|
|
|
23
29
|
With Bundler:
|
|
24
30
|
|
|
25
31
|
```ruby
|
|
26
|
-
gem "stat_power", "0.1.0.alpha.
|
|
32
|
+
gem "stat_power", "0.1.0.alpha.3"
|
|
27
33
|
```
|
|
28
34
|
|
|
29
35
|
## Status
|
|
@@ -41,9 +47,16 @@ Implemented compatibility currently includes:
|
|
|
41
47
|
- `pwr.t.test`
|
|
42
48
|
- `pwr.t2n.test`
|
|
43
49
|
- `pwr.r.test`
|
|
50
|
+
- `pwr.anova.test`
|
|
51
|
+
- `pwr.f2.test`
|
|
52
|
+
- `pwr.chisq.test`
|
|
53
|
+
- `ES.h`
|
|
54
|
+
- `ES.w1`
|
|
55
|
+
- `ES.w2`
|
|
56
|
+
- power-curve data generation equivalent in purpose to `plot.power.htest`
|
|
44
57
|
|
|
45
|
-
The library also contains the numerical distribution machinery required
|
|
46
|
-
|
|
58
|
+
The library also contains the numerical distribution machinery required by
|
|
59
|
+
these methods, including central/noncentral t, F, and chi-square distributions.
|
|
47
60
|
|
|
48
61
|
See [docs/pwr_parity.md](docs/pwr_parity.md) for the full compatibility matrix.
|
|
49
62
|
|
|
@@ -105,29 +118,39 @@ freeze candidate.
|
|
|
105
118
|
|
|
106
119
|
## Development
|
|
107
120
|
|
|
108
|
-
|
|
121
|
+
Set up the checkout:
|
|
109
122
|
|
|
110
123
|
```bash
|
|
111
|
-
|
|
124
|
+
bin/setup
|
|
112
125
|
```
|
|
113
126
|
|
|
114
|
-
|
|
127
|
+
Open a console with the library loaded:
|
|
115
128
|
|
|
116
129
|
```bash
|
|
117
|
-
|
|
130
|
+
bin/console
|
|
118
131
|
```
|
|
119
132
|
|
|
120
|
-
Run
|
|
133
|
+
Run everything CI runs:
|
|
121
134
|
|
|
122
135
|
```bash
|
|
123
|
-
bundle exec
|
|
124
|
-
bundle exec rbs validate
|
|
136
|
+
bundle exec rake verify
|
|
125
137
|
```
|
|
126
138
|
|
|
127
|
-
|
|
139
|
+
That is the same as running each check individually:
|
|
128
140
|
|
|
129
141
|
```bash
|
|
130
|
-
|
|
142
|
+
bundle exec rspec # specs
|
|
143
|
+
bundle exec rubocop # lint
|
|
144
|
+
bundle exec rbs validate # signature syntax
|
|
145
|
+
bundle exec steep check # lib/ type checked against sig/
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Measure test coverage. Instrumentation roughly doubles the runtime, so it is
|
|
149
|
+
opt-in; CI enforces a minimum in a dedicated job.
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
COVERAGE=1 bundle exec rspec
|
|
153
|
+
open coverage/index.html
|
|
131
154
|
```
|
|
132
155
|
|
|
133
156
|
Build the gem locally:
|
|
@@ -136,10 +159,36 @@ Build the gem locally:
|
|
|
136
159
|
gem build stat_power.gemspec
|
|
137
160
|
```
|
|
138
161
|
|
|
162
|
+
## Contributing
|
|
163
|
+
|
|
164
|
+
Contributions are welcome. Because this is a numerical library, the bar is
|
|
165
|
+
reproducibility: every statistical result must be justified by a published
|
|
166
|
+
formula or an independently generated reference value.
|
|
167
|
+
|
|
168
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the workflow, the shape a new
|
|
169
|
+
power-analysis family is expected to take, and how to supply reference values.
|
|
170
|
+
|
|
171
|
+
Participation is governed by the [Code of Conduct](CODE_OF_CONDUCT.md).
|
|
172
|
+
|
|
173
|
+
### Found a wrong number?
|
|
174
|
+
|
|
175
|
+
If `stat_power` disagrees with `pwr`, R, G*Power or a textbook, that is the
|
|
176
|
+
highest-priority class of bug here. Open an issue using the **Numerical
|
|
177
|
+
discrepancy** template.
|
|
178
|
+
|
|
179
|
+
## Security
|
|
180
|
+
|
|
181
|
+
See [SECURITY.md](SECURITY.md) for supported versions and how to report a
|
|
182
|
+
vulnerability privately. A numerically incorrect result is a bug, not a
|
|
183
|
+
vulnerability; report those as ordinary issues.
|
|
184
|
+
|
|
139
185
|
## Mathematical conventions
|
|
140
186
|
|
|
141
187
|
See [docs/mathematical_conventions.md](docs/mathematical_conventions.md).
|
|
142
188
|
|
|
189
|
+
For numerical reference fixtures and tolerance rules, see
|
|
190
|
+
[docs/validation.md](docs/validation.md).
|
|
191
|
+
|
|
143
192
|
## Roadmap
|
|
144
193
|
|
|
145
194
|
See [ROADMAP.md](ROADMAP.md).
|
data/ROADMAP.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
- [x] Normal mean power analysis equivalent to `pwr.norm.test`
|
|
12
12
|
- [x] Student t distribution utilities
|
|
13
13
|
- [x] Noncentral t distribution utilities
|
|
14
|
-
- [
|
|
14
|
+
- [x] Chi-square and noncentral chi-square utilities
|
|
15
15
|
- [x] F and noncentral F utilities
|
|
16
16
|
|
|
17
17
|
### pwr compatibility
|
|
@@ -22,24 +22,24 @@
|
|
|
22
22
|
- [x] `pwr.2p2n.test`
|
|
23
23
|
- [x] `pwr.t.test`
|
|
24
24
|
- [x] `pwr.t2n.test`
|
|
25
|
-
- [
|
|
25
|
+
- [x] `pwr.anova.test`
|
|
26
26
|
- [x] `pwr.r.test`
|
|
27
|
-
- [
|
|
28
|
-
- [
|
|
27
|
+
- [x] `pwr.chisq.test`
|
|
28
|
+
- [x] `pwr.f2.test`
|
|
29
29
|
- [x] `ES.h`
|
|
30
|
-
- [
|
|
31
|
-
- [
|
|
32
|
-
- [
|
|
30
|
+
- [x] `ES.w1`
|
|
31
|
+
- [x] `ES.w2`
|
|
32
|
+
- [x] power-curve data equivalent to `plot.power.htest`
|
|
33
33
|
|
|
34
34
|
Each migrated family must support the inverse problems exposed by the reference
|
|
35
35
|
method and include numerical parity tests.
|
|
36
36
|
|
|
37
37
|
## 0.2.x — Validation and usability
|
|
38
38
|
|
|
39
|
-
- Automated parity fixtures
|
|
39
|
+
- [x] Automated parity fixtures derived from CRAN `pwr` references
|
|
40
40
|
- Published Cohen examples
|
|
41
41
|
- Cross-validation against statsmodels where applicable
|
|
42
|
-
- Documented numerical tolerance policy
|
|
42
|
+
- [x] Documented numerical tolerance policy
|
|
43
43
|
- Stable result objects
|
|
44
44
|
- User-facing method documentation
|
|
45
45
|
- Benchmarks
|
data/docs/pwr_parity.md
CHANGED
|
@@ -18,14 +18,14 @@ reference points.
|
|
|
18
18
|
| `pwr.2p2n.test` | `StatPower::Proportion.two_sample_unequal` | Implemented |
|
|
19
19
|
| `pwr.t.test` | `StatPower::TTest.one_sample`, `.paired`, `.two_sample` | Implemented |
|
|
20
20
|
| `pwr.t2n.test` | `StatPower::TTest.two_sample_unequal` | Implemented |
|
|
21
|
-
| `pwr.anova.test` |
|
|
21
|
+
| `pwr.anova.test` | `StatPower::Anova.solve` | Implemented |
|
|
22
22
|
| `pwr.r.test` | `StatPower::Correlation.solve` | Implemented |
|
|
23
|
-
| `pwr.chisq.test` |
|
|
24
|
-
| `pwr.f2.test` |
|
|
23
|
+
| `pwr.chisq.test` | `StatPower::ChiSquare.solve` | Implemented |
|
|
24
|
+
| `pwr.f2.test` | `StatPower::F2.solve` | Implemented |
|
|
25
25
|
| `ES.h` | `StatPower::EffectSize::Proportion.cohen_h` | Implemented |
|
|
26
|
-
| `ES.w1` |
|
|
27
|
-
| `ES.w2` |
|
|
28
|
-
| `plot.power.htest` |
|
|
26
|
+
| `ES.w1` | `StatPower::EffectSize::ChiSquare.goodness_of_fit` | Implemented |
|
|
27
|
+
| `ES.w2` | `StatPower::EffectSize::ChiSquare.association` | Implemented |
|
|
28
|
+
| `plot.power.htest` | `StatPower::PowerCurve.generate` | Implemented as data generation |
|
|
29
29
|
|
|
30
30
|
## Solver convention
|
|
31
31
|
|
|
@@ -60,3 +60,6 @@ Each migrated family should include:
|
|
|
60
60
|
Parity means agreement in the statistical model and numerical result within a
|
|
61
61
|
documented tolerance. Ruby naming and object design remain idiomatic rather
|
|
62
62
|
than cloning R syntax.
|
|
63
|
+
|
|
64
|
+
See [validation.md](validation.md) for the fixture suite, reference provenance,
|
|
65
|
+
and numerical tolerance policy.
|
data/docs/validation.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Numerical validation
|
|
2
|
+
|
|
3
|
+
`stat_power` treats numerical validation as part of the public contract.
|
|
4
|
+
|
|
5
|
+
## CRAN pwr reference fixtures
|
|
6
|
+
|
|
7
|
+
The first compatibility target is CRAN `pwr` 1.3-0. Reference cases live in
|
|
8
|
+
`spec/fixtures/pwr_parity.yml` and are exercised by
|
|
9
|
+
`spec/parity/pwr_parity_spec.rb`.
|
|
10
|
+
|
|
11
|
+
The fixture set deliberately uses public examples from the `pwr` reference
|
|
12
|
+
manual and Cohen (1988) where available. It covers every power-analysis family
|
|
13
|
+
implemented by `stat_power`, including both direct power calculations and
|
|
14
|
+
representative inverse problems.
|
|
15
|
+
|
|
16
|
+
The fixture file records:
|
|
17
|
+
|
|
18
|
+
- the corresponding `pwr` function,
|
|
19
|
+
- the source/example label,
|
|
20
|
+
- the Ruby public API call,
|
|
21
|
+
- the input arguments,
|
|
22
|
+
- the output field being checked,
|
|
23
|
+
- the expected numeric value,
|
|
24
|
+
- the accepted absolute tolerance.
|
|
25
|
+
|
|
26
|
+
Detailed unit tests remain alongside each implementation. The fixture suite is
|
|
27
|
+
an additional end-to-end compatibility layer, not a replacement for
|
|
28
|
+
distribution-level, solver-level, domain, or round-trip tests.
|
|
29
|
+
|
|
30
|
+
## Tolerance policy
|
|
31
|
+
|
|
32
|
+
Numerical tolerances are explicit per fixture.
|
|
33
|
+
|
|
34
|
+
As a default policy:
|
|
35
|
+
|
|
36
|
+
- direct probabilities and distribution values use absolute tolerances around
|
|
37
|
+
`1e-8` or tighter when the reference is stable,
|
|
38
|
+
- inverse root solutions generally use `1e-5` because independent numerical
|
|
39
|
+
solvers can stop at slightly different points while agreeing statistically,
|
|
40
|
+
- looser tolerances are used only when the published reference itself is
|
|
41
|
+
reported with fewer digits.
|
|
42
|
+
|
|
43
|
+
A tolerance should not be widened merely to make a failing test pass. A change
|
|
44
|
+
must first be traced to one of:
|
|
45
|
+
|
|
46
|
+
1. a documented difference in the statistical model,
|
|
47
|
+
2. a known precision limit of the numerical method,
|
|
48
|
+
3. a reference value with limited published precision,
|
|
49
|
+
4. an implementation defect.
|
|
50
|
+
|
|
51
|
+
## Reproducibility
|
|
52
|
+
|
|
53
|
+
The fixture values are versioned with the code so changes are reviewable in
|
|
54
|
+
pull requests. Adding a new statistical family requires adding at least one
|
|
55
|
+
forward reference case and one inverse reference case when the reference API
|
|
56
|
+
supports inverse solving.
|
|
57
|
+
|
|
58
|
+
The project does not execute R during normal gem CI. This keeps the Ruby test
|
|
59
|
+
suite self-contained and deterministic while preserving explicit provenance
|
|
60
|
+
for the reference numbers.
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StatPower
|
|
4
|
+
# Power analysis for balanced one-way analysis of variance.
|
|
5
|
+
#
|
|
6
|
+
# The statistical parameterisation follows CRAN pwr.anova.test. Exactly one
|
|
7
|
+
# of groups, sample_size, effect_size, alpha, and power must be nil.
|
|
8
|
+
module Anova
|
|
9
|
+
GROUPS_LOWER = 2.0 + 1e-10
|
|
10
|
+
GROUPS_UPPER = 100.0
|
|
11
|
+
SAMPLE_SIZE_LOWER = 2.0 + 1e-10
|
|
12
|
+
SAMPLE_SIZE_MAX = 1e9
|
|
13
|
+
EFFECT_SIZE_LOWER = 1e-7
|
|
14
|
+
EFFECT_SIZE_MAX = 1e4
|
|
15
|
+
PROBABILITY_EPSILON = 1e-10
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# Solve one missing parameter of a balanced one-way ANOVA power analysis.
|
|
20
|
+
#
|
|
21
|
+
# @param groups [Numeric, nil] number of groups
|
|
22
|
+
# @param sample_size [Numeric, nil] observations per group
|
|
23
|
+
# @param effect_size [Numeric, Symbol, String, nil] Cohen's f
|
|
24
|
+
# @param alpha [Numeric, nil] Type I error probability
|
|
25
|
+
# @param power [Numeric, nil] statistical power
|
|
26
|
+
# @return [StatPower::AnovaResult]
|
|
27
|
+
def solve(
|
|
28
|
+
groups: nil,
|
|
29
|
+
sample_size: nil,
|
|
30
|
+
effect_size: nil,
|
|
31
|
+
alpha: 0.05,
|
|
32
|
+
power: nil
|
|
33
|
+
)
|
|
34
|
+
ensure_one_missing!(groups, sample_size, effect_size, alpha, power)
|
|
35
|
+
|
|
36
|
+
groups = optional_float(groups)
|
|
37
|
+
sample_size = optional_float(sample_size)
|
|
38
|
+
effect_size = normalize_effect_size(effect_size)
|
|
39
|
+
alpha = optional_float(alpha)
|
|
40
|
+
power = optional_float(power)
|
|
41
|
+
|
|
42
|
+
validate_known_values!(
|
|
43
|
+
groups:,
|
|
44
|
+
sample_size:,
|
|
45
|
+
effect_size:,
|
|
46
|
+
alpha:,
|
|
47
|
+
power:
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
groups, sample_size, effect_size, alpha, power = solve_missing(
|
|
51
|
+
groups:,
|
|
52
|
+
sample_size:,
|
|
53
|
+
effect_size:,
|
|
54
|
+
alpha:,
|
|
55
|
+
power:
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
AnovaResult.new(
|
|
59
|
+
groups:,
|
|
60
|
+
sample_size:,
|
|
61
|
+
power:,
|
|
62
|
+
effect_size:,
|
|
63
|
+
alpha:,
|
|
64
|
+
analysis_method: "balanced one-way analysis of variance power calculation"
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def solve_missing(groups:, sample_size:, effect_size:, alpha:, power:)
|
|
69
|
+
if power.nil?
|
|
70
|
+
power = power_for(
|
|
71
|
+
groups:,
|
|
72
|
+
sample_size:,
|
|
73
|
+
effect_size:,
|
|
74
|
+
alpha:
|
|
75
|
+
)
|
|
76
|
+
elsif groups.nil?
|
|
77
|
+
groups = solve_groups(
|
|
78
|
+
sample_size:,
|
|
79
|
+
effect_size:,
|
|
80
|
+
alpha:,
|
|
81
|
+
power:
|
|
82
|
+
)
|
|
83
|
+
elsif sample_size.nil?
|
|
84
|
+
sample_size = solve_sample_size(
|
|
85
|
+
groups:,
|
|
86
|
+
effect_size:,
|
|
87
|
+
alpha:,
|
|
88
|
+
power:
|
|
89
|
+
)
|
|
90
|
+
elsif effect_size.nil?
|
|
91
|
+
effect_size = solve_effect_size(
|
|
92
|
+
groups:,
|
|
93
|
+
sample_size:,
|
|
94
|
+
alpha:,
|
|
95
|
+
power:
|
|
96
|
+
)
|
|
97
|
+
elsif alpha.nil?
|
|
98
|
+
alpha = solve_alpha(
|
|
99
|
+
groups:,
|
|
100
|
+
sample_size:,
|
|
101
|
+
effect_size:,
|
|
102
|
+
power:
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
[groups, sample_size, effect_size, alpha, power]
|
|
107
|
+
end
|
|
108
|
+
private_class_method :solve_missing
|
|
109
|
+
|
|
110
|
+
def power_for(groups:, sample_size:, effect_size:, alpha:)
|
|
111
|
+
numerator_df = groups - 1.0
|
|
112
|
+
denominator_df = (sample_size - 1.0) * groups
|
|
113
|
+
noncentrality = groups * sample_size * effect_size * effect_size
|
|
114
|
+
|
|
115
|
+
critical = Distributions::FDistribution.quantile(
|
|
116
|
+
1.0 - alpha,
|
|
117
|
+
numerator_df:,
|
|
118
|
+
denominator_df:
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
Distributions::NoncentralF.survival(
|
|
122
|
+
critical,
|
|
123
|
+
numerator_df:,
|
|
124
|
+
denominator_df:,
|
|
125
|
+
noncentrality:
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
private_class_method :power_for
|
|
129
|
+
|
|
130
|
+
def solve_groups(sample_size:, effect_size:, alpha:, power:)
|
|
131
|
+
Solvers::Bisection.solve(
|
|
132
|
+
lower: GROUPS_LOWER,
|
|
133
|
+
upper: GROUPS_UPPER
|
|
134
|
+
) do |candidate|
|
|
135
|
+
power_for(
|
|
136
|
+
groups: candidate,
|
|
137
|
+
sample_size:,
|
|
138
|
+
effect_size:,
|
|
139
|
+
alpha:
|
|
140
|
+
) - power
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
private_class_method :solve_groups
|
|
144
|
+
|
|
145
|
+
def solve_sample_size(groups:, effect_size:, alpha:, power:)
|
|
146
|
+
upper = bracket_sample_size(
|
|
147
|
+
groups:,
|
|
148
|
+
effect_size:,
|
|
149
|
+
alpha:,
|
|
150
|
+
power:
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
Solvers::Bisection.solve(
|
|
154
|
+
lower: SAMPLE_SIZE_LOWER,
|
|
155
|
+
upper:,
|
|
156
|
+
absolute_tolerance: 1e-7,
|
|
157
|
+
relative_tolerance: 1e-9
|
|
158
|
+
) do |candidate|
|
|
159
|
+
power_for(
|
|
160
|
+
groups:,
|
|
161
|
+
sample_size: candidate,
|
|
162
|
+
effect_size:,
|
|
163
|
+
alpha:
|
|
164
|
+
) - power
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
private_class_method :solve_sample_size
|
|
168
|
+
|
|
169
|
+
def bracket_sample_size(groups:, effect_size:, alpha:, power:)
|
|
170
|
+
upper = 4.0
|
|
171
|
+
|
|
172
|
+
while upper < SAMPLE_SIZE_MAX
|
|
173
|
+
achieved = power_for(
|
|
174
|
+
groups:,
|
|
175
|
+
sample_size: upper,
|
|
176
|
+
effect_size:,
|
|
177
|
+
alpha:
|
|
178
|
+
)
|
|
179
|
+
return upper if achieved >= power
|
|
180
|
+
|
|
181
|
+
upper *= 2.0
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
raise StatPower::DomainError,
|
|
185
|
+
"target power cannot be bracketed within the supported sample-size range"
|
|
186
|
+
end
|
|
187
|
+
private_class_method :bracket_sample_size
|
|
188
|
+
|
|
189
|
+
def solve_effect_size(groups:, sample_size:, alpha:, power:)
|
|
190
|
+
upper = bracket_effect_size(
|
|
191
|
+
groups:,
|
|
192
|
+
sample_size:,
|
|
193
|
+
alpha:,
|
|
194
|
+
power:
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
Solvers::Bisection.solve(
|
|
198
|
+
lower: EFFECT_SIZE_LOWER,
|
|
199
|
+
upper:
|
|
200
|
+
) do |candidate|
|
|
201
|
+
power_for(
|
|
202
|
+
groups:,
|
|
203
|
+
sample_size:,
|
|
204
|
+
effect_size: candidate,
|
|
205
|
+
alpha:
|
|
206
|
+
) - power
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
private_class_method :solve_effect_size
|
|
210
|
+
|
|
211
|
+
def bracket_effect_size(groups:, sample_size:, alpha:, power:)
|
|
212
|
+
upper = 0.5
|
|
213
|
+
|
|
214
|
+
while upper < EFFECT_SIZE_MAX
|
|
215
|
+
achieved = power_for(
|
|
216
|
+
groups:,
|
|
217
|
+
sample_size:,
|
|
218
|
+
effect_size: upper,
|
|
219
|
+
alpha:
|
|
220
|
+
)
|
|
221
|
+
return upper if achieved >= power
|
|
222
|
+
|
|
223
|
+
upper *= 2.0
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
raise StatPower::DomainError,
|
|
227
|
+
"target power cannot be bracketed within the supported effect-size range"
|
|
228
|
+
end
|
|
229
|
+
private_class_method :bracket_effect_size
|
|
230
|
+
|
|
231
|
+
def solve_alpha(groups:, sample_size:, effect_size:, power:)
|
|
232
|
+
Solvers::Bisection.solve(
|
|
233
|
+
lower: PROBABILITY_EPSILON,
|
|
234
|
+
upper: 1.0 - PROBABILITY_EPSILON
|
|
235
|
+
) do |candidate|
|
|
236
|
+
power_for(
|
|
237
|
+
groups:,
|
|
238
|
+
sample_size:,
|
|
239
|
+
effect_size:,
|
|
240
|
+
alpha: candidate
|
|
241
|
+
) - power
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
private_class_method :solve_alpha
|
|
245
|
+
|
|
246
|
+
def ensure_one_missing!(*values)
|
|
247
|
+
return if values.count(&:nil?) == 1
|
|
248
|
+
|
|
249
|
+
raise StatPower::DomainError,
|
|
250
|
+
"exactly one of groups, sample_size, effect_size, alpha, and power must be nil"
|
|
251
|
+
end
|
|
252
|
+
private_class_method :ensure_one_missing!
|
|
253
|
+
|
|
254
|
+
def normalize_effect_size(value)
|
|
255
|
+
return nil if value.nil?
|
|
256
|
+
return EffectSize::Conventional.resolve(test: :anov, size: value) if value.is_a?(String) || value.is_a?(Symbol)
|
|
257
|
+
|
|
258
|
+
Float(value)
|
|
259
|
+
rescue ArgumentError, TypeError
|
|
260
|
+
raise StatPower::DomainError, "effect_size must be numeric or a conventional size"
|
|
261
|
+
end
|
|
262
|
+
private_class_method :normalize_effect_size
|
|
263
|
+
|
|
264
|
+
def optional_float(value)
|
|
265
|
+
value.nil? ? nil : Float(value)
|
|
266
|
+
rescue ArgumentError, TypeError
|
|
267
|
+
raise StatPower::DomainError, "numeric parameters must be coercible to Float"
|
|
268
|
+
end
|
|
269
|
+
private_class_method :optional_float
|
|
270
|
+
|
|
271
|
+
def validate_known_values!(groups:, sample_size:, effect_size:, alpha:, power:)
|
|
272
|
+
validate_groups!(groups) if groups
|
|
273
|
+
validate_sample_size!(sample_size) if sample_size
|
|
274
|
+
validate_effect_size!(effect_size) if effect_size
|
|
275
|
+
validate_probability!("alpha", alpha) if alpha
|
|
276
|
+
validate_probability!("power", power) if power
|
|
277
|
+
end
|
|
278
|
+
private_class_method :validate_known_values!
|
|
279
|
+
|
|
280
|
+
def validate_groups!(groups)
|
|
281
|
+
return if groups.finite? && groups >= 2.0
|
|
282
|
+
|
|
283
|
+
raise StatPower::DomainError, "groups must be finite and at least 2"
|
|
284
|
+
end
|
|
285
|
+
private_class_method :validate_groups!
|
|
286
|
+
|
|
287
|
+
def validate_sample_size!(sample_size)
|
|
288
|
+
return if sample_size.finite? && sample_size >= 2.0
|
|
289
|
+
|
|
290
|
+
raise StatPower::DomainError, "sample_size must be finite and at least 2"
|
|
291
|
+
end
|
|
292
|
+
private_class_method :validate_sample_size!
|
|
293
|
+
|
|
294
|
+
def validate_effect_size!(effect_size)
|
|
295
|
+
return if effect_size.finite? && !effect_size.negative?
|
|
296
|
+
|
|
297
|
+
raise StatPower::DomainError, "effect_size must be finite and non-negative"
|
|
298
|
+
end
|
|
299
|
+
private_class_method :validate_effect_size!
|
|
300
|
+
|
|
301
|
+
def validate_probability!(name, value)
|
|
302
|
+
return if value.finite? && value.positive? && value < 1.0
|
|
303
|
+
|
|
304
|
+
raise StatPower::DomainError, "#{name} must lie strictly between 0 and 1"
|
|
305
|
+
end
|
|
306
|
+
private_class_method :validate_probability!
|
|
307
|
+
end
|
|
308
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StatPower
|
|
4
|
+
# Immutable result for balanced one-way ANOVA power analyses.
|
|
5
|
+
AnovaResult = Data.define(
|
|
6
|
+
:groups,
|
|
7
|
+
:sample_size,
|
|
8
|
+
:power,
|
|
9
|
+
:effect_size,
|
|
10
|
+
:alpha,
|
|
11
|
+
:analysis_method
|
|
12
|
+
) do
|
|
13
|
+
# Smallest whole-number group count not below the continuous solution.
|
|
14
|
+
#
|
|
15
|
+
# @return [Integer]
|
|
16
|
+
def required_groups
|
|
17
|
+
groups.ceil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Smallest whole-number per-group sample size not below the solution.
|
|
21
|
+
#
|
|
22
|
+
# @return [Integer]
|
|
23
|
+
def required_sample_size
|
|
24
|
+
sample_size.ceil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Continuous total sample size.
|
|
28
|
+
#
|
|
29
|
+
# @return [Float]
|
|
30
|
+
def total_sample_size
|
|
31
|
+
groups * sample_size
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Practical total after independently rounding groups and per-group n up.
|
|
35
|
+
#
|
|
36
|
+
# @return [Integer]
|
|
37
|
+
def required_total_sample_size
|
|
38
|
+
required_groups * required_sample_size
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|