The powergrid package is made to facilitate the exploration of statistical power of a study. In a typical use case (also shown in this vignette) you want to explore how sample size, the effect size of interest and the assumed variability of the data influence the power of your planned analysis.
This package does not dictate any of the above ingredients for you. It just allows you to evaluate a function at a grid of parameters and visualize the interrelation in plots that are fine-tuned for analyses of power and sample size.
You may, however use powergrid for very different purposes. An example close to a power analysis would be an analysis of the precision you may expect of a study design. As a more remote application, you can think of a coverage study for different methods to calculate confidence intervals. Or an exploration for planning recruitment of study participants under various scenarios.
This vignette covers the following functions of
powergrid
PowerGrid
for evaluating the power across a grid, with
and without iterationsExample
for inspecting the relation between power and
sample size in a specific scenarioPowerplot
Graphical exploration of power under
different scenariosGridPlot
Graphical exploration of power under even more
scenariosAddExample
Add an example to an existing PowerPlot or
GridPlotFindTarget
For a range of parameters, searching along
one other parameter for a setting yielding a target power (or other
value)In the next section, I start by showing a technical example that should illustrate the essence of PowerGrid and FindTarget. Then, in the third section, I show 4 actual use cases exploiting the flexibility of powergrid.
Before turning to a real-world example, I show an example illustrating general working of the core functions of powergrid: PowerGrid and FindTarget. Note that you will hardly ever explicitly call FindTarget.
PowerGrid is in essence similar to tapply
(or
dplyr::group_by1
+ dplyr::summarise
).
FindTarget is in essence a combination of apply
+
match
+ min
+ which.min
.
## I aim to create an array with the product of three numbers
## A function taking the product of three numbers:
Prod = function(x, y, z){
x * y * z
}
## A grid of numbers I want to calculate the products of. Note that the names of
## the list equal the argument names in Prod()
par = list(x = 1:10,
y = 1:3,
z = c(1, 10))
products = PowerGrid(par, Prod)
print(products)
#> , , z = 1
#>
#> y
#> x 1 2 3
#> 1 1 2 3
#> 2 2 4 6
#> 3 3 6 9
#> 4 4 8 12
#> 5 5 10 15
#> 6 6 12 18
#> 7 7 14 21
#> 8 8 16 24
#> 9 9 18 27
#> 10 10 20 30
#>
#> , , z = 10
#>
#> y
#> x 1 2 3
#> 1 10 20 30
#> 2 20 40 60
#> 3 30 60 90
#> 4 40 80 120
#> 5 50 100 150
#> 6 60 120 180
#> 7 70 140 210
#> 8 80 160 240
#> 9 90 180 270
#> 10 100 200 300
#>
#> Array of class `power_array` created using
#> PowerGrid.
#> Resulting dimensions:
#> x, y, z.
## Now, I can ask: for each combination of y and z, what is the lowest x where
## the product is at least 20?
FindTarget(products,
par_to_search = 'x',
find_lowest = TRUE,
target_at_least = TRUE,
target_value = 20)
#> z
#> y 1 10
#> 1 NA 2
#> 2 10 1
#> 3 7 1
## Note: when both y and z are 1, there is no x so that the product becomes 20.
If you understand the R chunk above, you understand the essence of powergrid.
In typical use cases, you will use Example(), PowerPlot(), and GridPlot() and AddExample(), which rely on FindTarget internally.
I describe two basic use cases here, and two slightly more fancy ones. The first two concern the relation between power, sample size, and two further parameters. In the first situation, a function for calculating the power is available. In the second situation, the power needs to be found through (re-)sampling. The third example concerns a situation where the objective is not a minimal power, but a CI-width that must be at most a certain value. In the fourth example, we do not search the lowest required sample size, but the highest permissible standard deviation to achieve a target power.
Assume we aim to collect data in a two-armed RCT and plan to perform a simple t-test. In this situation, the situation concerning power can be summarized in the following ingredients:
I use the function PowerGrid
to evaluate the situation
sketched above. This is done as follows
## A function that returns the power as a function of three input parameters
PowFun <- function(n, delta, sd){
ptt = power.t.test(n = n/2,
delta = delta,
sd = sd,
sig.level = 0.03) # the typical 3% alpha threshold
return(ptt$power)
}
## A list of values of input parameters to study
pars = list( # a normal list
n = seq(from = 10, to = 60, by = 5), # sample size
delta = seq(from = 0.5, to = 1.7, by = 0.1), # effect size
sd = seq(.5, 1, .1) # variability
)
## Apply PowFun to all crossings of the parameters in pars
power = PowerGrid(pars = pars, fun = PowFun)
summary(power)
#> Object of class: power_array
#>
#> Range of values: [0.07, 1]
#> Evaluated at:
#> n 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60
#> delta 0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4,
#> delta 1.5, 1.6, 1.7
#> sd 0.5, 0.6, 0.7, 0.8, 0.9, 1
PowerPlot(power,
slicer = list(sd = .7))
In the code above, note that the names of the elements in the list
pars
match the names of the function arguments in PowFun.
This is a requirement for PowerGrid to work.
Now, say, you want to be pretty sure (say, power = 90%) to detect an effect size as small as 1.1, and the best guess for SD = .7. We can calculate this example:
Example(power,
example = list(delta = 1.1, sd = .7),
target_value = .9) # power = 90%
#> ================================================
#> To achieve the target value of at most 0.9 assuming
#> delta = 1.1
#> sd = 0.7,
#> the minimal required n = 25
#> ------------------------------------------------
#> Description: Method "step" was used to find the
#> lowest n in the searched grid that yields a
#> target_value (typically power) of at least 0.9.
#> ================================================
Some things to note in the figure code:
sd = .9
. The slice has the form
of the figure: delta by n.delta = .8
and
show how the relation between power and n depends on the standard
deviation. Or slice out a plain where n = 50
, and see how
power behaves as a function of delta and sd.example = list(delta = c(1, 1.2)))
or by using the higher
level plotting function AddExample
. The latter allows you
more flexibility, like setting different colors or line types.The wording “target_value” in both the function argument and the
printed result may be a bit confusing at first. ‘Why not “power”?’, you
may ask. The reason is, that there is nothing that keeps you from
optimizing other things with the functionality of
powergrid
. Indeed, instead of finding a target power, you
may be looking for a target precision.
The PowerPlot already gives some insight into how the relation between sample size and power depends on a third parameter, effect size. Now, GridPlot offers a figure to explore one extra parameter.
The figure created by PowerPlot
can only show the
interplay of two variables and power. GridPlot often offers a more
insightful picture, in particular when, as in this example, we have more
than 2 dimensions in our pars
argument.
The code below shows how to plot the interplay between n, delta and sd when the goals is the achieve 90% power.
GridPlot(power,
target_value = .9, # you need to choose one target level of power
example = list(delta = 1, sd = .7)) # defined by two parameters now.
#> Warning: At some combinations of `x_par` and `l_par`, no `y_par` was found that
#> yielded the required target value, which may result in lines ending abruptly.
#> In most common use cases, you may want to increasing the range of n.
Note that there are many options in this plot. See the help file of
GridPlot
for more info. Importantly, any dimension of the
the power_array
in argument x may be mapped to the x- or
y-axis, or to the different lines.
Assume we have about the same situation as above, but we do not have a simple solution to calculate the power: we only have a limited pilot data set that looks as follows:
Since we do not really understand the distribution (it looks pretty right-skewed), we plan to perform a Mann-Whitney U-test. We do not want to simply simulate, but draw from our pilot sample to mimic the variability and distributional form. We do have an idea about effect size (somewhere in the range of .5 and 2). The following code my be our approach to the exploration of power:
sse_pars = list(
n = seq(10, 100, 20),
delta = seq(.5, 2, .2)) # only effect size
PowFun = function(n, delta, pilot_data){
arm_1 = sample(pilot_data, n, replace = TRUE)
arm_2 = sample(pilot_data, n, replace = TRUE) + delta
significant = wilcox.test(arm_1, arm_2)$p.value < .03 # the typical 3% alpha threshold
return(significant) # each call of this function gives significant either TRUE
# or FALSE
}
power = PowerGrid(pars = sse_pars,
fun = PowFun,
more_args = list(pilot_data = pilot_scores), # pass the pilot
# data on to the
# fun argument
n_iter = 99) # we need to iterate over simulated experiemtns
# to get a power. I would take a higher value
# than 99; this is to keep the example quick.
summary(power)
#> Object of class: power_array
#> Containing summary over 99 iterations,
#> summarized by function `summary_function` (for
#> function definition, see attribute
#> `summary_function`).
#> Range of values: [0.14, 1]
#> Evaluated at:
#> n 10, 30, 50, 70, 90
#> delta 0.5, 0.7, 0.9, 1.1, 1.3, 1.5, 1.7, 1.9
PowerPlot(power)
A couple of notes:
mean
to yield the power.summarize = FALSE
. In this
situation, the resulting array will have one additional dimension,
“iter”.Example
automatically summarize
these taking the mean. You can, however, choose a different
summary_function
.power_array
may
be represented by the x-axis, y-axis.The powergrid package allows to do more than finding a power of at least some value. Instead, we can target, for example, a 95% confidence interval that should have at most a certain width. Say, we have the following situation, where we plan to compare two groups with a t-test and we want the CI not to be wider than .7 points on our outcome scale. Maybe, 7 is considered a clinically important difference according to the literature and when our estimates are not further than .7 points apart, we can’t really conclude anything. We can use the powergrid functionality to find the sample size that gives us an expected CI of at most that value:
CIFun = function(n, delta, sd){
x1 = rnorm(n, mean = 0, sd = sd)
x2 = rnorm(n, mean = delta, sd = sd)
abs(diff(t.test(x1, x2)$conf.int)) # return the CI-width
}
pars = list( # a normal list
n = seq(from = 10, to = 60, by = 5), # sample size
delta = seq(from = 0.5, to = 1.7, by = 0.1), # effect size
sd = seq(.5, 1, .1) # variability
)
set.seed(1)
CI_array = PowerGrid(pars, CIFun, n_iter = 20)
summary(CI_array)
#> Object of class: power_array
#> Containing summary over 20 iterations,
#> summarized by function `summary_function` (for
#> function definition, see attribute
#> `summary_function`).
#> Range of values: [0.35, 2.01]
#> Evaluated at:
#> n 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60
#> delta 0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4,
#> delta 1.5, 1.6, 1.7
#> sd 0.5, 0.6, 0.7, 0.8, 0.9, 1
## This object now contains, for each parameter combination, the CI-width
## averaged over 20 iterations.
Example(CI_array,
example = list(delta = .7, sd = .8),
target_value = .7,
target_at_least = FALSE,
find_lowest = TRUE)
#> ================================================
#> To achieve the target value of at most 0.7 assuming
#> delta = 0.7
#> sd = 0.8,
#> the minimal required n = 45
#> ------------------------------------------------
#> Description: Method "step" was used to find the
#> lowest n in the searched grid that yields a
#> target_value (typically power) of at most 0.7.
#> ================================================
## Show results
PowerPlot(CI_array, slicer = list(delta = .7),
target_levels = c(.6, .7, .8), # this defines the lines
title = "CI-width as a funtion of sd and n,\nassuming delta = .7",
shades_of_grey = FALSE) # Grey scale is optimized for situation where
# the array contains power.
AddExample(CI_array,
slicer = list(delta = .7),
example = list(sd = .8),
target_value = .7,
target_at_least = FALSE)
A couple of things to observe: * We set “target_at_least” to FALSE, since we are looking for a CI of at most .7 * The lines in the figure now connect situations with the same CI-width (not power, as in the earlier examples) * We changed the title accordingly.
We have the same situation as in the first use case, where we plan a t-test and aim for a power of 90%. In the current situation, however, we have a very limited flexibility in the number of participants that we can recruit. We do have some way of decreasing the variability by improving our measurements, however. We want to study how small our SD must be for our study to have desirable power.
find_lowest = FALSE
.sse_pars = list(
n = c(30, 40),
delta = seq(from = 0.4, to = 1.2, by = 0.01), ## effect size
sd = seq(.3, .9, .01)) ## Standard deviation
PowFun <- function(n, delta, sd){
ptt = power.t.test(n = n/2, delta = delta, sd = sd,
sig.level = 0.05)
return(ptt$power)
}
power_array = PowerGrid(pars = sse_pars, fun = PowFun, n_iter = NA)
Example(power_array,
example = list(n = 30, delta = .8),
find_lowest = FALSE,
target_value = .9)
#> ================================================
#> To achieve the target value of at most 0.9 assuming
#> n = 30
#> delta = 0.8,
#> maximal permissible sd = 0.65
#> ------------------------------------------------
#> Description: Method "step" was used to find the
#> highest sd in the searched grid that yields a
#> target_value (typically power) of at least 0.9.
#> ================================================
We see that, by setting find_lowest = FALSE
Example()
shows us the “maximal permissible” value that sd may take in order to
achieve the set target.
Below, inspect the situation in graphic form. Note that the argument
par_to_search
is set to “sd”, putting that parameter on the
y-axis.
PowerPlot(power_array,
slicer = list(n = 30),
par_to_search = 'sd',
example = list(delta = .8),
find_lowest = FALSE,
target_value = .9)