A RetroSearch Logo

Home - News ( United States | United Kingdom | Italy | Germany ) - Football scores

Search Query:

Showing content from http://philchalmers.github.io/SimDesign/html/06-Build_a_power-curve.html below:

Power-curve estimates

Power-curves are useful when researchers are interested in the range of power effects when varying different population and sample properties. For instance, researchers may be interested in how power improves as the magnitude of the population mean difference for an independent samples \(t\)-test increases, or how quickly power improves when increasing the sample size (\(N\)). The following simulation assumes that the population variances are equal.

Define the functions
library(SimDesign)
#SimFunctions(comments = FALSE)

### Define design conditions. Here we want a finer range of values for plotting
Design <- createDesign(mean_diff = seq(0, 1, by = .1), 
                       sample_size = c(10, 20, 30))

#--------------------------------------------------------------------

Generate <- function(condition, fixed_objects = NULL) {
  Attach(condition)
  dat1 <- rnorm(sample_size)
  dat2 <- rnorm(sample_size, mean=mean_diff)
  dat <- list(dat1=dat1, dat2=dat2)
  dat
}

Analyse <- function(condition, dat, fixed_objects = NULL) {
  ret <- c(p = t.test(dat$dat1, dat$dat2, 
                      var.equal=TRUE)$p.value)
  ret
}

Summarise <- function(condition, results, fixed_objects = NULL) {
  ret <- EDR(results, alpha = .05)
  ret
}

#--------------------------------------------------------------------

### Run the simulation (using all available cores)
res <- runSimulation(Design, replications=10000, verbose=FALSE, parallel=TRUE,
                     generate=Generate, analyse=Analyse, summarise=Summarise)
# summary of simulation object
summary(res)
$sessionInfo
─ Session info ───────────────────────────────────────────────────────────────
 setting  value
 version  R version 4.3.3 (2024-02-29)
 os       Ubuntu 20.04.6 LTS
 system   x86_64, linux-gnu
 ui       X11
 language (EN)
 collate  en_US.UTF-8
 ctype    en_US.UTF-8
 tz       Etc/UTC
 date     2025-04-17
 pandoc   2.9.1.1 @ /usr/bin/ (via rmarkdown)
 quarto   1.7.25 @ /usr/local/bin/quarto

─ Packages ───────────────────────────────────────────────────────────────────
 package      * version date (UTC) lib source
 audio          0.1-11  2023-08-18 [1] CRAN (R 4.3.1)
 beepr          2.0     2024-07-06 [1] CRAN (R 4.3.3)
 brio           1.1.5   2024-04-24 [1] CRAN (R 4.3.3)
 cli            3.6.4   2025-02-13 [1] CRAN (R 4.3.3)
 codetools      0.2-20  2024-03-31 [4] CRAN (R 4.3.3)
 digest         0.6.37  2024-08-19 [1] CRAN (R 4.3.3)
 dplyr          1.1.4   2023-11-17 [1] CRAN (R 4.3.1)
 evaluate       1.0.3   2025-01-10 [1] CRAN (R 4.3.3)
 fastmap        1.2.0   2024-05-15 [1] CRAN (R 4.3.3)
 future         1.40.0  2025-04-10 [1] CRAN (R 4.3.3)
 future.apply   1.11.3  2024-10-27 [1] CRAN (R 4.3.3)
 generics       0.1.3   2022-07-05 [1] CRAN (R 4.3.1)
 globals        0.17.0  2025-04-16 [1] CRAN (R 4.3.3)
 glue           1.8.0   2024-09-30 [1] CRAN (R 4.3.3)
 htmltools      0.5.8.1 2024-04-04 [1] CRAN (R 4.3.3)
 htmlwidgets    1.6.4   2023-12-06 [1] CRAN (R 4.3.2)
 jsonlite       2.0.0   2025-03-27 [1] CRAN (R 4.3.3)
 knitr          1.50    2025-03-16 [1] CRAN (R 4.3.3)
 lifecycle      1.0.4   2023-11-07 [1] CRAN (R 4.3.1)
 listenv        0.9.1   2024-01-29 [1] CRAN (R 4.3.2)
 magrittr       2.0.3   2022-03-30 [1] CRAN (R 4.3.1)
 parallelly     1.43.0  2025-03-24 [1] CRAN (R 4.3.3)
 pbapply        1.7-2   2023-06-27 [1] CRAN (R 4.3.1)
 pillar         1.10.2  2025-04-05 [1] CRAN (R 4.3.3)
 pkgconfig      2.0.3   2019-09-22 [1] CRAN (R 4.3.1)
 progressr      0.15.1  2024-11-22 [1] CRAN (R 4.3.3)
 R.methodsS3    1.8.2   2022-06-13 [1] CRAN (R 4.3.2)
 R.oo           1.27.0  2024-11-01 [1] CRAN (R 4.3.3)
 R.utils        2.13.0  2025-02-24 [1] CRAN (R 4.3.3)
 R6             2.6.1   2025-02-15 [1] CRAN (R 4.3.3)
 rlang          1.1.6   2025-04-11 [1] CRAN (R 4.3.3)
 rmarkdown      2.29    2024-11-04 [1] CRAN (R 4.3.3)
 sessioninfo    1.2.3   2025-02-05 [1] CRAN (R 4.3.3)
 SimDesign    * 2.19.3  2025-04-16 [1] local
 testthat       3.2.3   2025-01-13 [1] CRAN (R 4.3.3)
 tibble         3.2.1   2023-03-20 [1] CRAN (R 4.3.1)
 tidyselect     1.2.1   2024-03-11 [1] CRAN (R 4.3.3)
 vctrs          0.6.5   2023-12-01 [1] CRAN (R 4.3.1)
 xfun           0.52    2025-04-02 [1] CRAN (R 4.3.3)
 yaml           2.3.10  2024-07-26 [1] CRAN (R 4.3.3)

 [1] /home/phil/R/x86_64-pc-linux-gnu-library/4.3
 [2] /usr/local/lib/R/site-library
 [3] /usr/lib/R/site-library
 [4] /usr/lib/R/library
 * ── Packages attached to the search path.

──────────────────────────────────────────────────────────────────────────────

$save_info
named character(0)

$seeds
 [1] 1140350788  312928385  866248189 1909893419  554504146  884616499
 [7]  803234389 1158971242  934673902 1632225031 1867003471  651436069
[13] 1147691737   57500133 1587828514 2070518142 2119708588 1406053153
[19] 1263737763  960850250 1378461094 1215204756  127650324  100235820
[25]  997765463 2051398777 1881620140  303569271 1358398662  695822375
[31] 1773292330 1971672344  647971342

$ncores
[1] 47

$number_of_conditions
[1] 33

$date_completed
[1] Thu Apr 17 21:30:12 2025

$total_elapsed_time
[1] 15.00s

$Design.ID
 [1]  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
[26] 26 27 28 29 30 31 32 33
# print results
print(res)
# A tibble: 33 × 7
   mean_diff sample_size      p REPLICATIONS SIM_TIME       SEED COMPLETED      
       <dbl>       <dbl>  <dbl>        <dbl> <chr>         <int> <chr>          
 1       0            10 0.0495        10000 0.56s    1140350788 Thu Apr 17 21:…
 2       0.1          10 0.0541        10000 0.37s     312928385 Thu Apr 17 21:…
 3       0.2          10 0.0698        10000 0.34s     866248189 Thu Apr 17 21:…
 4       0.3          10 0.1002        10000 0.46s    1909893419 Thu Apr 17 21:…
 5       0.4          10 0.1365        10000 0.61s     554504146 Thu Apr 17 21:…
 6       0.5          10 0.1847        10000 0.41s     884616499 Thu Apr 17 21:…
 7       0.6          10 0.2445        10000 0.52s     803234389 Thu Apr 17 21:…
 8       0.7          10 0.3149        10000 0.45s    1158971242 Thu Apr 17 21:…
 9       0.8          10 0.3864        10000 0.57s     934673902 Thu Apr 17 21:…
10       0.9          10 0.4686        10000 0.51s    1632225031 Thu Apr 17 21:…
# ℹ 23 more rows

A power curve is created by placing the detection rates on a \(y\)-axis and including different factors on the x-axis and other aesthetics (shading, colours, etc). Here we use ggplot2 to construct suitable power-curves.

library(ggplot2)
ggplot(res, aes(mean_diff, p, colour=factor(sample_size))) + geom_point() + geom_line() +
    xlab('Mean Difference') + ylab('Detection Rate') + ggtitle('Empirical Power Curves') +
    scale_color_discrete('N')

Compare these results to analytic formulas, we can see that the two are essentially coinciding with good accuracy.

library(pwr)
pwr.t.test(d=0.5, n=30, sig.level=0.05, type="two.sample", alternative="two.sided")

     Two-sample t test power calculation 

              n = 30
              d = 0.5
      sig.level = 0.05
          power = 0.478
    alternative = two.sided

NOTE: n is number in *each* group
subset(res, mean_diff == .5 & sample_size == 30)
# A tibble: 1 × 7
  mean_diff sample_size      p REPLICATIONS SIM_TIME      SEED COMPLETED        
      <dbl>       <dbl>  <dbl>        <dbl> <chr>        <int> <chr>            
1       0.5          30 0.4827        10000 0.47s    303569271 Thu Apr 17 21:30…
pwr.t.test(d=0.5, n=10, sig.level=0.05, type="two.sample", alternative="two.sided")

     Two-sample t test power calculation 

              n = 10
              d = 0.5
      sig.level = 0.05
          power = 0.185
    alternative = two.sided

NOTE: n is number in *each* group
subset(res, mean_diff == .5 & sample_size == 10)
# A tibble: 1 × 7
  mean_diff sample_size      p REPLICATIONS SIM_TIME      SEED COMPLETED        
      <dbl>       <dbl>  <dbl>        <dbl> <chr>        <int> <chr>            
1       0.5          10 0.1847        10000 0.41s    884616499 Thu Apr 17 21:29…
Power-curve with Bootstrapped Confidence Intervals

In situations where analytic power estimation is not available, and therefore simulations are used as a suitable stand-in, it’s important to evaluate the precision of the observed power estimates from the simulation study. As such, one reasonable (and extremely general approach) is to apply bootstrapping methodology to the within replication; for simulated power analyses this involves bootstrapping the \(R\) replicated \(p\)-values to obtain suitable non-parametric confidence intervals.

The following implements the empirical (i.e., basic) bootstrap CI method for the simulation above, and instead plots the point-wise 95% CIs estimates for each condition in the Design input. Notice that the replications were reduced from 10000 to 1000 to demonstrate the potential uncertainty when using too few replications when estimating power via simulation.

### Run the simulation (using all available cores)
res <- runSimulation(Design, replications=1000, verbose=FALSE, parallel=TRUE,
                     generate=Generate, analyse=Analyse, summarise=Summarise,
                     boot_method = 'basic')
res
# A tibble: 33 × 9
   mean_diff sample_size     p BOOT_p_2.5 BOOT_p_97.5 REPLICATIONS SIM_TIME
       <dbl>       <dbl> <dbl>      <dbl>       <dbl>        <dbl> <chr>   
 1       0            10 0.052    0.038       0.065           1000 0.78s   
 2       0.1          10 0.064    0.048       0.079           1000 0.68s   
 3       0.2          10 0.075    0.058       0.091           1000 0.69s   
 4       0.3          10 0.09     0.072       0.10702         1000 0.66s   
 5       0.4          10 0.129    0.109       0.15            1000 0.73s   
 6       0.5          10 0.206    0.181       0.229           1000 0.69s   
 7       0.6          10 0.252    0.226       0.27802         1000 0.70s   
 8       0.7          10 0.299    0.27097     0.327           1000 0.69s   
 9       0.8          10 0.395    0.367       0.424           1000 0.70s   
10       0.9          10 0.489    0.458       0.52002         1000 0.66s   
# ℹ 23 more rows
# ℹ 2 more variables: SEED <int>, COMPLETED <chr>
library(ggplot2)
ggplot(res, aes(mean_diff, p, colour=factor(sample_size))) + geom_point() + geom_line() +
    geom_ribbon(aes(ymin=BOOT_p_2.5, ymax=BOOT_p_97.5, fill=factor(sample_size)), alpha=.3, linetype=0) + 
    xlab('Mean Difference') + ylab('Detection Rate') + ggtitle('Empirical Power Curves with 95% CIs') +
    scale_fill_discrete('N') + scale_colour_discrete('N')

Finally, when increasing the replications back to 10000 it become visually clear how precise the original point-estimates presented in the previous section were.

### Run the simulation (using all available cores)
res <- runSimulation(Design, replications=10000, verbose=FALSE, parallel=TRUE,
                     generate=Generate, analyse=Analyse, summarise=Summarise,
                     boot_method = 'basic')
res
# A tibble: 33 × 9
   mean_diff sample_size      p BOOT_p_2.5 BOOT_p_97.5 REPLICATIONS SIM_TIME
       <dbl>       <dbl>  <dbl>      <dbl>       <dbl>        <dbl> <chr>   
 1       0            10 0.0528   0.0484      0.057102        10000 6.43s   
 2       0.1          10 0.053    0.0486      0.0574          10000 6.35s   
 3       0.2          10 0.0737   0.068498    0.078902        10000 6.29s   
 4       0.3          10 0.0962   0.090497    0.10170         10000 6.36s   
 5       0.4          10 0.1328   0.1261      0.1394          10000 6.15s   
 6       0.5          10 0.1898   0.18250     0.1977          10000 6.07s   
 7       0.6          10 0.2539   0.24520     0.26240         10000 6.10s   
 8       0.7          10 0.3249   0.3159      0.33450         10000 6.36s   
 9       0.8          10 0.3959   0.38550     0.4059          10000 6.11s   
10       0.9          10 0.475    0.46530     0.4848          10000 6.45s   
# ℹ 23 more rows
# ℹ 2 more variables: SEED <int>, COMPLETED <chr>
library(ggplot2)
ggplot(res, aes(mean_diff, p, colour=factor(sample_size))) + geom_point() + geom_line() +
    geom_ribbon(aes(ymin=BOOT_p_2.5, ymax=BOOT_p_97.5, fill=factor(sample_size)), alpha=.3, linetype=0) + 
    xlab('Mean Difference') + ylab('Detection Rate') + ggtitle('Empirical Power Curves with 95% CIs') +
    scale_fill_discrete('N') + scale_colour_discrete('N')

Power-curve for Solving Sample Size via Interpolations

Power-curves can also be useful for obtaining estimates to satisfy particular power rates when sample size planning. Note that this is a rather inefficient approach to solving sample sizes, though it has been very popular (see ?SimDesign::SimSolve for a stochastic root-solving alternative).

In the following, a \(d=.5\) effect size is assumed in the population, where the value of \(N\) associated with a target power rate of \(.80\) is desired. To obtain an \(\hat{N}\) that is roughly associated with this target power a range of \(N\) inputs are first evaluated, the adjacent points closest to .80 are isolated, and finally the \(\hat{N}\) is solved using linear interpolations to obtain an approximate target sample size.

Design <- createDesign(mean_diff = .5, # equivalent to Cohen's "medium" d effect size
                       sample_size = seq(10, 100, by=5))

res <- runSimulation(Design, replications=10000, verbose=FALSE, parallel=TRUE,
                     generate=Generate, analyse=Analyse, summarise=Summarise)

# target N somewhere between 60 and 65
res
# A tibble: 19 × 7
   mean_diff sample_size      p REPLICATIONS SIM_TIME       SEED COMPLETED      
       <dbl>       <dbl>  <dbl>        <dbl> <chr>         <int> <chr>          
 1       0.5          10 0.1836        10000 0.85s     540368552 Thu Apr 17 21:…
 2       0.5          15 0.2715        10000 0.51s     895502048 Thu Apr 17 21:…
 3       0.5          20 0.3334        10000 0.49s    1200947712 Thu Apr 17 21:…
 4       0.5          25 0.4101        10000 0.47s    1390946546 Thu Apr 17 21:…
 5       0.5          30 0.4776        10000 0.36s     297095334 Thu Apr 17 21:…
 6       0.5          35 0.5336        10000 0.39s      26408929 Thu Apr 17 21:…
 7       0.5          40 0.6075        10000 0.59s     953293844 Thu Apr 17 21:…
 8       0.5          45 0.6491        10000 0.47s     896131004 Thu Apr 17 21:…
 9       0.5          50 0.6958        10000 0.42s    1166181780 Thu Apr 17 21:…
10       0.5          55 0.7486        10000 0.50s     205322161 Thu Apr 17 21:…
11       0.5          60 0.7749        10000 0.50s    1570516689 Thu Apr 17 21:…
12       0.5          65 0.8085        10000 0.36s    1818143070 Thu Apr 17 21:…
13       0.5          70 0.8371        10000 0.55s    1103983110 Thu Apr 17 21:…
14       0.5          75 0.8573        10000 0.40s     627018390 Thu Apr 17 21:…
15       0.5          80 0.8755        10000 0.42s    1596415590 Thu Apr 17 21:…
16       0.5          85 0.8951        10000 0.49s    1728962223 Thu Apr 17 21:…
17       0.5          90 0.9171        10000 0.42s    1920077889 Thu Apr 17 21:…
18       0.5          95 0.927         10000 0.52s    1388819422 Thu Apr 17 21:…
19       0.5         100 0.9442        10000 0.46s     828181090 Thu Apr 17 21:…
# solve using linear interpolated power
pmin <- min(which(res$p > .80))
pick <- res[c(pmin-1, pmin),]
pick
# A tibble: 2 × 7
  mean_diff sample_size      p REPLICATIONS SIM_TIME       SEED COMPLETED       
      <dbl>       <dbl>  <dbl>        <dbl> <chr>         <int> <chr>           
1       0.5          60 0.7749        10000 0.50s    1570516689 Thu Apr 17 21:3…
2       0.5          65 0.8085        10000 0.36s    1818143070 Thu Apr 17 21:3…

After the empirical power curve has been built, and it’s clear the target power is within the range specified when building the power curve, interpolations can be used to solve for \(\hat{N}\). In this case a linear interpolation is used between the closest points, though alternatives are of course possible (e.g., fitting higher-order polynomials, splines, or other non-linear models).

mod <- lm(p ~ sample_size, pick)
coef(mod)
(Intercept) sample_size 
    0.37170     0.00672 
fn <- function(N, power = .8)
    predict(mod, newdata = data.frame(sample_size=N)) - power
root <- uniroot(fn, interval = c(10, 100))
root  # sample size estimate from the root solver
$root
[1] 63.7

$f.root
        1 
-1.11e-16 

$iter
[1] 2

$init.it
[1] NA

$estim.prec
[1] 6.1e-05
# build power curve and include interpolation estimates
ggplot(res, aes(sample_size, p)) + geom_point() + geom_line() + 
    xlab('N') + ylab('Detection Rate') + 
    geom_abline(intercept = .80, slope=0, col='red', linetype='dashed') +
    geom_vline(xintercept=root$root, col='blue', linetype='dashed') + 
    annotate("text", x=80, y=.6, label = paste0('N estimate = ', round(root$root, 2))) + 
    ggtitle('Power curve with linear interpolations')

Finally, in this case one can compare the \(\hat{N}\) with the \(N\) associated with a power of .80 given the well-known non-central \(t\)-distribution using the pwr package. Had the associated non-central distribution been unavailable further Monte Carlo simulations around the estimate would be required (e.g., using runSimulation() given the integer values for sample_size given 63 and 64).

# true N from root-solving the deterministic power function
pwr.t.test(d=0.5, sig.level=0.05, power=.8, type="two.sample", alternative="two.sided")

     Two-sample t test power calculation 

              n = 63.8
              d = 0.5
      sig.level = 0.05
          power = 0.8
    alternative = two.sided

NOTE: n is number in *each* group

RetroSearch is an open source project built by @garambo | Open a GitHub Issue

Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo

HTML: 3.2 | Encoding: UTF-8 | Version: 0.7.4