statcpp
C++17 Header-Only Statistics Library
Loading...
Searching...
No Matches
power_analysis.hpp
Go to the documentation of this file.
1
19#pragma once
20
23#include "statcpp/parametric_tests.hpp" // for alternative_hypothesis enum
24
25#include <algorithm>
26#include <cmath>
27#include <cstddef>
28#include <stdexcept>
29#include <string>
30
31namespace statcpp {
32
33// ============================================================================
34// Power Analysis Result Structure
35// ============================================================================
36
41 double power;
42 double sample_size;
43 double effect_size;
44 double alpha;
45};
46
47// ============================================================================
48// Helper Functions
49// ============================================================================
50
54namespace detail {
55
63inline double noncentrality_parameter_t(double effect_size, double n) {
64 return effect_size * std::sqrt(n);
65}
66
75inline double noncentrality_parameter_t_two_sample(double effect_size, double n1, double n2) {
76 return effect_size * std::sqrt((n1 * n2) / (n1 + n2));
77}
78
86inline double critical_t_two_sided(double alpha, double df) {
87 return t_quantile(1.0 - alpha / 2.0, df);
88}
89
97inline double critical_t_one_sided(double alpha, double df) {
98 return t_quantile(1.0 - alpha, df);
99}
100
107inline double critical_z_two_sided(double alpha) {
108 return norm_quantile(1.0 - alpha / 2.0);
109}
110
117inline double critical_z_one_sided(double alpha) {
118 return norm_quantile(1.0 - alpha);
119}
120
128 switch (alt) {
129 case alternative_hypothesis::two_sided: return "two.sided";
130 case alternative_hypothesis::greater: return "greater";
131 case alternative_hypothesis::less: return "less";
132 default: return "two.sided";
133 }
134}
135
136} // namespace detail
137
138// ============================================================================
139// One-sample t-test power analysis
140// ============================================================================
141
156inline double power_t_test_one_sample(double effect_size, std::size_t n,
157 double alpha = 0.05,
158 const std::string& alternative = "two.sided")
159{
160 if (n == 0) {
161 throw std::invalid_argument("statcpp::power_t_test_one_sample: sample size must be positive");
162 }
163 if (alpha <= 0.0 || alpha >= 1.0) {
164 throw std::invalid_argument("statcpp::power_t_test_one_sample: alpha must be in (0, 1)");
165 }
166
167 double ncp = detail::noncentrality_parameter_t(effect_size, static_cast<double>(n));
168
169 if (alternative == "two.sided") {
170 // Two-sided test: P(|T| > t_crit | ncp)
171 // Simple approximation: approximate with normal distribution
172 double z_crit = detail::critical_z_two_sided(alpha);
173 return 1.0 - norm_cdf(z_crit - ncp) + norm_cdf(-z_crit - ncp);
174 } else if (alternative == "greater") {
175 double z_crit = detail::critical_z_one_sided(alpha);
176 return 1.0 - norm_cdf(z_crit - ncp);
177 } else if (alternative == "less") {
178 double z_crit = detail::critical_z_one_sided(alpha);
179 return norm_cdf(-z_crit - ncp);
180 } else {
181 throw std::invalid_argument("statcpp::power_t_test_one_sample: alternative must be 'two.sided', 'greater', or 'less'");
182 }
183}
184
195inline std::size_t sample_size_t_test_one_sample(double effect_size, double power = 0.80,
196 double alpha = 0.05,
197 const std::string& alternative = "two.sided")
198{
199 if (effect_size == 0.0) {
200 throw std::invalid_argument("statcpp::sample_size_t_test_one_sample: effect size must be non-zero");
201 }
202 if (power <= 0.0 || power >= 1.0) {
203 throw std::invalid_argument("statcpp::sample_size_t_test_one_sample: power must be in (0, 1)");
204 }
205 if (alpha <= 0.0 || alpha >= 1.0) {
206 throw std::invalid_argument("statcpp::sample_size_t_test_one_sample: alpha must be in (0, 1)");
207 }
208
209 // Initial estimate using normal approximation
210 double z_alpha, z_beta;
211 if (alternative == "two.sided") {
212 z_alpha = detail::critical_z_two_sided(alpha);
213 } else {
214 z_alpha = detail::critical_z_one_sided(alpha);
215 }
216 z_beta = norm_quantile(power);
217
218 // Initial estimate: n = ((z_alpha + z_beta) / d)^2
219 double n_approx = std::pow((z_alpha + z_beta) / std::abs(effect_size), 2.0);
220
221 // Minimum sample size
222 std::size_t n = std::max(2.0, std::ceil(n_approx));
223
224 // Refine with iteration (maximum 100 iterations)
225 for (int iter = 0; iter < 100; ++iter) {
226 double current_power = power_t_test_one_sample(effect_size, n, alpha, alternative);
227 if (current_power >= power) {
228 break;
229 }
230 n++;
231 }
232
233 return n;
234}
235
236// ============================================================================
237// Two-sample t-test power analysis
238// ============================================================================
239
255inline double power_t_test_two_sample(double effect_size, std::size_t n1, std::size_t n2,
256 double alpha = 0.05,
257 const std::string& alternative = "two.sided")
258{
259 if (n1 == 0 || n2 == 0) {
260 throw std::invalid_argument("statcpp::power_t_test_two_sample: sample sizes must be positive");
261 }
262 if (alpha <= 0.0 || alpha >= 1.0) {
263 throw std::invalid_argument("statcpp::power_t_test_two_sample: alpha must be in (0, 1)");
264 }
265
266 double ncp = detail::noncentrality_parameter_t_two_sample(effect_size,
267 static_cast<double>(n1),
268 static_cast<double>(n2));
269
270 if (alternative == "two.sided") {
271 double z_crit = detail::critical_z_two_sided(alpha);
272 return 1.0 - norm_cdf(z_crit - ncp) + norm_cdf(-z_crit - ncp);
273 } else if (alternative == "greater") {
274 double z_crit = detail::critical_z_one_sided(alpha);
275 return 1.0 - norm_cdf(z_crit - ncp);
276 } else if (alternative == "less") {
277 double z_crit = detail::critical_z_one_sided(alpha);
278 return norm_cdf(-z_crit - ncp);
279 } else {
280 throw std::invalid_argument("statcpp::power_t_test_two_sample: alternative must be 'two.sided', 'greater', or 'less'");
281 }
282}
283
295inline std::size_t sample_size_t_test_two_sample(double effect_size, double power = 0.80,
296 double alpha = 0.05, double ratio = 1.0,
297 const std::string& alternative = "two.sided")
298{
299 if (effect_size == 0.0) {
300 throw std::invalid_argument("statcpp::sample_size_t_test_two_sample: effect size must be non-zero");
301 }
302 if (power <= 0.0 || power >= 1.0) {
303 throw std::invalid_argument("statcpp::sample_size_t_test_two_sample: power must be in (0, 1)");
304 }
305 if (alpha <= 0.0 || alpha >= 1.0) {
306 throw std::invalid_argument("statcpp::sample_size_t_test_two_sample: alpha must be in (0, 1)");
307 }
308 if (ratio <= 0.0) {
309 throw std::invalid_argument("statcpp::sample_size_t_test_two_sample: ratio must be positive");
310 }
311
312 // Normal approximation
313 double z_alpha, z_beta;
314 if (alternative == "two.sided") {
315 z_alpha = detail::critical_z_two_sided(alpha);
316 } else {
317 z_alpha = detail::critical_z_one_sided(alpha);
318 }
319 z_beta = norm_quantile(power);
320
321 // Initial estimate: n1 = ((z_alpha + z_beta) / d)^2 * (1 + 1/r)
322 double n1_approx = std::pow((z_alpha + z_beta) / std::abs(effect_size), 2.0) * (1.0 + 1.0 / ratio);
323
324 std::size_t n1 = std::max(2.0, std::ceil(n1_approx));
325 std::size_t n2 = std::max(2.0, std::ceil(n1 * ratio));
326
327 // Refine with iteration
328 for (int iter = 0; iter < 100; ++iter) {
329 double current_power = power_t_test_two_sample(effect_size, n1, n2, alpha, alternative);
330 if (current_power >= power) {
331 break;
332 }
333 n1++;
334 n2 = std::max(2.0, std::ceil(n1 * ratio));
335 }
336
337 return n1;
338}
339
340// ============================================================================
341// Proportion test power analysis
342// ============================================================================
343
355inline double power_prop_test(double p1, double p2, std::size_t n,
356 double alpha = 0.05,
357 const std::string& alternative = "two.sided")
358{
359 if (p1 < 0.0 || p1 > 1.0 || p2 < 0.0 || p2 > 1.0) {
360 throw std::invalid_argument("statcpp::power_prop_test: proportions must be in [0, 1]");
361 }
362 if (n == 0) {
363 throw std::invalid_argument("statcpp::power_prop_test: sample size must be positive");
364 }
365 if (alpha <= 0.0 || alpha >= 1.0) {
366 throw std::invalid_argument("statcpp::power_prop_test: alpha must be in (0, 1)");
367 }
368
369 // Standard error under null hypothesis (pooled proportion)
370 double p_pool = (p1 + p2) / 2.0;
371 double se_null = std::sqrt(2.0 * p_pool * (1.0 - p_pool) / static_cast<double>(n));
372
373 // Standard error under alternative hypothesis
374 double se_alt = std::sqrt((p1 * (1.0 - p1) + p2 * (1.0 - p2)) / static_cast<double>(n));
375
376 double diff = std::abs(p1 - p2);
377
378 if (alternative == "two.sided") {
379 double z_crit = detail::critical_z_two_sided(alpha);
380 double boundary = z_crit * se_null;
381 double z_power = (diff - boundary) / se_alt;
382 return 1.0 - norm_cdf(-z_power);
383 } else if (alternative == "greater") {
384 double z_crit = detail::critical_z_one_sided(alpha);
385 double boundary = z_crit * se_null;
386 double z_power = ((p1 - p2) - boundary) / se_alt;
387 return 1.0 - norm_cdf(-z_power);
388 } else if (alternative == "less") {
389 double z_crit = detail::critical_z_one_sided(alpha);
390 double boundary = z_crit * se_null;
391 double z_power = ((p2 - p1) - boundary) / se_alt;
392 return 1.0 - norm_cdf(-z_power);
393 } else {
394 throw std::invalid_argument("statcpp::power_prop_test: alternative must be 'two.sided', 'greater', or 'less'");
395 }
396}
397
409inline std::size_t sample_size_prop_test(double p1, double p2, double power = 0.80,
410 double alpha = 0.05,
411 const std::string& alternative = "two.sided")
412{
413 if (p1 < 0.0 || p1 > 1.0 || p2 < 0.0 || p2 > 1.0) {
414 throw std::invalid_argument("statcpp::sample_size_prop_test: proportions must be in [0, 1]");
415 }
416 if (std::abs(p1 - p2) < 1e-10) {
417 throw std::invalid_argument("statcpp::sample_size_prop_test: proportions must be different");
418 }
419 if (power <= 0.0 || power >= 1.0) {
420 throw std::invalid_argument("statcpp::sample_size_prop_test: power must be in (0, 1)");
421 }
422 if (alpha <= 0.0 || alpha >= 1.0) {
423 throw std::invalid_argument("statcpp::sample_size_prop_test: alpha must be in (0, 1)");
424 }
425
426 double z_alpha, z_beta;
427 if (alternative == "two.sided") {
428 z_alpha = detail::critical_z_two_sided(alpha);
429 } else {
430 z_alpha = detail::critical_z_one_sided(alpha);
431 }
432 z_beta = norm_quantile(power);
433
434 double p_pooled = (p1 + p2) / 2.0;
435 double diff = std::abs(p1 - p2);
436
437 // Initial estimate
438 double n_approx = (std::pow(z_alpha * std::sqrt(2.0 * p_pooled * (1.0 - p_pooled)) +
439 z_beta * std::sqrt(p1 * (1.0 - p1) + p2 * (1.0 - p2)), 2.0)) /
440 std::pow(diff, 2.0);
441
442 std::size_t n = std::max(2.0, std::ceil(n_approx));
443
444 // Refine with iteration
445 for (int iter = 0; iter < 100; ++iter) {
446 double current_power = power_prop_test(p1, p2, n, alpha, alternative);
447 if (current_power >= power) {
448 break;
449 }
450 n++;
451 }
452
453 return n;
454}
455
456// ============================================================================
457// Power analysis wrapper functions (returning structs)
458// ============================================================================
459
469inline power_result power_analysis_t_one_sample(double effect_size, std::size_t n,
470 double alpha = 0.05,
471 const std::string& alternative = "two.sided")
472{
473 power_result result;
474 result.effect_size = effect_size;
475 result.sample_size = static_cast<double>(n);
476 result.alpha = alpha;
477 result.power = power_t_test_one_sample(effect_size, n, alpha, alternative);
478 return result;
479}
480
490inline power_result power_analysis_t_one_sample_n(double effect_size, double power = 0.80,
491 double alpha = 0.05,
492 const std::string& alternative = "two.sided")
493{
494 power_result result;
495 result.effect_size = effect_size;
496 result.power = power;
497 result.alpha = alpha;
498 result.sample_size = static_cast<double>(sample_size_t_test_one_sample(effect_size, power, alpha, alternative));
499 return result;
500}
501
502// ============================================================================
503// Enum overloads (type-safe alternative to string-based API)
504// ============================================================================
505
515inline double power_t_test_one_sample(double effect_size, std::size_t n,
516 double alpha,
518{
519 return power_t_test_one_sample(effect_size, n, alpha, detail::alternative_to_string(alt));
520}
521
531inline std::size_t sample_size_t_test_one_sample(double effect_size, double power,
532 double alpha,
534{
535 return sample_size_t_test_one_sample(effect_size, power, alpha, detail::alternative_to_string(alt));
536}
537
548inline double power_t_test_two_sample(double effect_size, std::size_t n1, std::size_t n2,
549 double alpha,
551{
552 return power_t_test_two_sample(effect_size, n1, n2, alpha, detail::alternative_to_string(alt));
553}
554
565inline std::size_t sample_size_t_test_two_sample(double effect_size, double power,
566 double alpha, double ratio,
568{
569 return sample_size_t_test_two_sample(effect_size, power, alpha, ratio, detail::alternative_to_string(alt));
570}
571
582inline double power_prop_test(double p1, double p2, std::size_t n,
583 double alpha,
585{
586 return power_prop_test(p1, p2, n, alpha, detail::alternative_to_string(alt));
587}
588
599inline std::size_t sample_size_prop_test(double p1, double p2, double power,
600 double alpha,
602{
603 return sample_size_prop_test(p1, p2, power, alpha, detail::alternative_to_string(alt));
604}
605
615inline power_result power_analysis_t_one_sample(double effect_size, std::size_t n,
616 double alpha,
618{
619 return power_analysis_t_one_sample(effect_size, n, alpha, detail::alternative_to_string(alt));
620}
621
631inline power_result power_analysis_t_one_sample_n(double effect_size, double power,
632 double alpha,
634{
635 return power_analysis_t_one_sample_n(effect_size, power, alpha, detail::alternative_to_string(alt));
636}
637
638} // namespace statcpp
Continuous distribution functions.
double noncentrality_parameter_t(double effect_size, double n)
Calculate noncentrality parameter for t-distribution.
double critical_t_two_sided(double alpha, double df)
Calculate critical value for two-sided t-test.
double critical_t_one_sided(double alpha, double df)
Calculate critical value for one-sided t-test.
const char * alternative_to_string(alternative_hypothesis alt)
Convert alternative_hypothesis enum to string.
double critical_z_one_sided(double alpha)
Calculate critical value for one-sided normal test.
double noncentrality_parameter_t_two_sample(double effect_size, double n1, double n2)
Calculate noncentrality parameter for two-sample case.
double critical_z_two_sided(double alpha)
Calculate critical value for two-sided normal test.
alternative_hypothesis
Enumeration representing the type of alternative hypothesis.
@ greater
One-sided test (greater than)
@ less
One-sided test (less than)
power_result power_analysis_t_one_sample(double effect_size, std::size_t n, double alpha=0.05, const std::string &alternative="two.sided")
Power analysis for one-sample t-test (calculate power)
std::size_t sample_size_t_test_one_sample(double effect_size, double power=0.80, double alpha=0.05, const std::string &alternative="two.sided")
Calculate required sample size for one-sample t-test.
double power_t_test_one_sample(double effect_size, std::size_t n, double alpha=0.05, const std::string &alternative="two.sided")
Calculate power for one-sample t-test.
double norm_cdf(double x)
Standard normal CDF.
double norm_quantile(double p)
Standard normal quantile function.
double power_t_test_two_sample(double effect_size, std::size_t n1, std::size_t n2, double alpha=0.05, const std::string &alternative="two.sided")
Calculate power for two-sample t-test.
power_result power_analysis_t_one_sample_n(double effect_size, double power=0.80, double alpha=0.05, const std::string &alternative="two.sided")
Power analysis for one-sample t-test (calculate sample size)
double t_quantile(double p, double df)
t-distribution quantile function (Newton-Raphson method)
std::vector< double > diff(Iterator first, Iterator last, std::size_t order=1)
Difference series (first-order or d-th order differencing)
std::size_t sample_size_prop_test(double p1, double p2, double power=0.80, double alpha=0.05, const std::string &alternative="two.sided")
Calculate required sample size for two-sample proportion test.
std::size_t sample_size_t_test_two_sample(double effect_size, double power=0.80, double alpha=0.05, double ratio=1.0, const std::string &alternative="two.sided")
Calculate required sample size for two-sample t-test (per group)
double power_prop_test(double p1, double p2, std::size_t n, double alpha=0.05, const std::string &alternative="two.sided")
Calculate power for two-sample proportion test.
Parametric test functions.
Special mathematical functions implementation.
Power analysis result.
double effect_size
Effect size.
double power
Statistical power (1-beta)
double sample_size
Sample size.
double alpha
Significance level.