MASS
package, called birthwt
. You can load this data using the data()
function.bwt
) for mothers who smoke and don’t smoke (smoke
).
Welch Two Sample t-test
data: bwt by smoke
t = 2.7299, df = 170.1, p-value = 0.007003
alternative hypothesis: true difference in means between group 0 and group 1 is not equal to 0
95 percent confidence interval:
78.57486 488.97860
sample estimates:
mean in group 0 mean in group 1
3055.696 2771.919
group_sample_sizes <- aggregate(bwt ~ smoke, data = birthwt, FUN = length)
group_means <- aggregate(bwt ~ smoke, data = birthwt, FUN = mean)
group_sds <- aggregate(bwt ~ smoke, data = birthwt, FUN = sd)
power.t.test(
# Use the average group sample size
n = mean(group_sample_sizes$bwt),
# d = TRUE difference in means / so we use our post-hoc observation
delta = diff(group_means$bwt),
# Pool the SD's
sd = sqrt(mean(group_sds$bwt ^ 2)),
# Alpha level of test
sig.level = 0.05,
# Details about the test
type = "two.sample",
alternative = "two.sided"
)
Two-sample t test power calculation
n = 94.5
delta = 283.7767
sd = 707.6758
sig.level = 0.05
power = 0.7829694
alternative = two.sided
NOTE: n is number in *each* group
power.t.test()
this time.power.t.test(,
delta = 0.1,
sd = 0.25,
sig.level = 0.05,
power = 0.80,
# Details about the test
type = "two.sample",
alternative = "two.sided"
)
Two-sample t test power calculation
n = 99.08057
delta = 0.1
sd = 0.25
sig.level = 0.05
power = 0.8
alternative = two.sided
NOTE: n is number in *each* group
birthwt
example, let’s pretend we didn’t run our study yet, but we know that the pooled sd should be around 700. Let’s calculate the sample size needed to achieve 80% power at multiple effect sizes.effect_sizes <- seq(10, 500, 10)
sample_size_calc <- lapply(
effect_sizes,
\(d) power.t.test(delta = d, sd = 700, sig.level = 0.05, power = 0.80)
)
str(sample_size_calc, 1)
List of 50
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
$ :List of 8
..- attr(*, "class")= chr "power.htest"
power.t.test()
returns an S3 object of class power.htest
, and the documentation tells us how to get the quantities we want.lapply()
, instead of having to do a nested one, just iterate over the number of rows in the grid.plot(
NULL, NULL,
xlim = range(effect_sizes),
ylim = range(log10(power_variables$sample_size)),
xlab = "Effect size to detect at alpha = 0.05, power = 0.8",
ylab = "Log10 sample size needed",
type = "l"
)
variance_levels <- unique(power_variables$sd)
colors <- colorRampPalette(c("lightblue", "darkblue"))(length(variance_levels))
for (i in 1:length(variance_levels)) {
sub <- subset(power_variables, sd == variance_levels[[i]])
lines(x = sub$delta, y = log10(sub$sample_size), col = colors[[i]])
}
ggplot2
, this is way easier in ggplot2
.ggplot2
.ggplot(power_variables) +
aes(x = delta, y = sd, z = log10(sample_size)) +
geom_contour_filled() +
coord_cartesian(expand = FALSE) +
labs(
x = "Effect size to detect at alpha = 0.05, power = 0.8",
y = "Assumed pooled SD",
fill = "Log10 sample sized need"
) +
theme_minimal() +
theme(legend.position = "bottom")
N_sims
number of datasets from a model described by the \(t\)-test: that is, we draw n1
observations from a normal distribution wiht mean mu1
and sd sd1
, and likewise for group 2.simulate_one_dataset <- function(n1, n2, mu1, mu2, sd1, sd2) {
group1_y <- rnorm(n1, mu1, sd1)
group2_y <- rnorm(n2, mu2, sd2)
out <- list(group1 = group1_y, group2 = group2_y)
return(out)
}
# replicate() is just a fancy way to use lapply() when the function
# you want to repeat doesn't depend on any iteration-specific argument
simulated_data <- replicate(
N_sims,
simulate_one_dataset(n1, n2, mu1, mu2, sd1, sd2),
# Set simplify = FALSE or you get a weird array thats hard to understand
simplify = FALSE
)
# We can use this function to get the exact CI (Clopper-Pearson) for us
# Just ignore the p-value, it doesn't make sense!!
binom.test(sum(rejected_h0), N_sims)
Exact binomial test
data: sum(rejected_h0) and N_sims
number of successes = 8227, number of trials = 10000, p-value < 2.2e-16
alternative hypothesis: true probability of success is not equal to 0.5
95 percent confidence interval:
0.8150708 0.8301408
sample estimates:
probability of success
0.8227
lapply()
this function over multiple values of n
(this is easy if you assume n1
and n2
are equal).simulate_t_test_power <- function(N_sim, n1, n2, mu1, mu2, sd1, sd2, alpha = 0.05) {
simulated_data <-
replicate(
N_sims,
simulate_one_dataset(n1, n2, mu1, mu2, sd1, sd2),
simplify = FALSE
)
simulated_t_tests <- lapply(
simulated_data,
\(d) ...
)
simulation_p_values <- sapply(simulated_t_tests, ...)
rejected_h0 <- ...
return(rejected_h0)
}
n_per_group <- seq(...)
t_test_n_simulations <- sapply(
n_per_group,
\(n) simulate_t_test_power(
1000,
n, n,
mu1, mu2, sd1, sd2
)
)
simulate_t_test_power <- function(N_sim, n1, n2, mu1, mu2, sd1, sd2, alpha = 0.05) {
simulated_data <-
replicate(
N_sims,
simulate_one_dataset(n1, n2, mu1, mu2, sd1, sd2),
simplify = FALSE
)
simulated_t_tests <- lapply(
simulated_data,
\(d) t.test(d$group1, d$group2, conf.level = alpha)
)
simulation_p_values <- sapply(simulated_t_tests, \(x) x$p.value)
rejected_h0 <- simulation_p_values <= alpha
return(rejected_h0)
}
# You can do less of these
# and make N_sim lower
# if this takes too long
n_per_group <- seq(25, 250, 25)
set.seed(102)
t_test_n_simulations <- sapply(
n_per_group,
\(n) simulate_t_test_power(
1000,
n, n,
mu1, mu2, sd1, sd2
)
)
# sapply() gives us a matrix that we can easily apply() on to get summaries
# or use colMeans() cause its super fast and easy
res <- colMeans(t_test_n_simulations)
res
[1] 0.3197 0.5670 0.7407 0.8495 0.9164 0.9576 0.9769 0.9880 0.9942 0.9976
n_per_group
we get to our target.# Interpolate between those two numbers, but don't repeat them
# cause we already did those sims!
n_per_group2 <- seq(n_per_group[3], n_per_group[4], 1)
n_per_group2_trim <- n_per_group2[2:(length(n_per_group2) - 1)]
set.seed(103)
t_test_n_simulations2 <- sapply(
n_per_group2_trim,
\(n) simulate_t_test_power(
1000,
n, n,
mu1, mu2, sd1, sd2
)
)
# sapply() gives us a matrix that we can easily apply() on to get summaries
# or use colMeans() cause its super fast and easy
res2 <- colMeans(t_test_n_simulations2)
res2
[1] 0.7448 0.7449 0.7681 0.7615 0.7647 0.7690 0.7764 0.7819 0.7814 0.7889
[11] 0.7952 0.8072 0.8007 0.8061 0.8180 0.8135 0.8149 0.8316 0.8313 0.8361
[21] 0.8410 0.8420 0.8463 0.8508
all_n <- c(n_per_group, n_per_group2_trim)
all_res <- c(res, res2)
sort_order <- order(all_n)
plot(
all_n[sort_order], all_res[sort_order],
type = "l",
xlab = "Sample size", ylab = "Power"
)
# Find the smallest sample size with a power above 0.8
best_samp_size <- min(all_n[all_res > 0.8])
bss_power <- all_res[all_n == best_samp_size]
abline(h = bss_power, col = "red", lty = 2)
abline(v = best_samp_size, col = "red", lty = 2)
\[ y_i \sim \text{Bernoulli}(p_i) \\ \text{logit}(p_i) = \beta_0 + \beta_1 x_i \\ \]
generate_logistic_model_data <- function(n, beta_0, beta_1, exposure_prevalence) {
x <- rbinom(n, size = 1, prob = exposure_prevalence)
logit_p <- beta_0 + beta_1 * x
p <- plogis(logit_p)
y <- rbinom(n, size = 1, prob = p)
out <- list(x = x, y = y)
return(out)
}
set.seed(100)
test_logistic_data <- generate_logistic_model_data(100, -2.2, 1.5, 0.5)
test_logistic_fit <- glm(
test_logistic_data$y ~ test_logistic_data$x,
family = "binomial"
)
summary(test_logistic_fit)
Call:
glm(formula = test_logistic_data$y ~ test_logistic_data$x, family = "binomial")
Coefficients:
Estimate Std. Error z value Pr(>|z|)
(Intercept) -2.7515 0.5955 -4.621 3.83e-06 ***
test_logistic_data$x 2.3461 0.6618 3.545 0.000392 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
(Dispersion parameter for binomial family taken to be 1)
Null deviance: 107.855 on 99 degrees of freedom
Residual deviance: 89.998 on 98 degrees of freedom
AIC: 93.998
Number of Fisher Scoring iterations: 5
n
values now?logistic_power_simulation <- function(
N_sims, n, beta_0, beta_1, exposure_prevalence, alpha
) {
simulated_datasets <- replicate(
N_sims,
generate_logistic_model_data(n, beta_0, beta_1, exposure_prevalence),
simplify = FALSE
)
simulated_p_values <- sapply(
simulated_datasets,
get_exposure_slope_p_value
)
rejections <- simulated_p_values <= alpha
return(rejections)
}
n_values <- seq(25, 250, 25)
set.seed(110)
logistic_sim <- sapply(
n_values,
\(n) logistic_power_simulation(
1000,
n,
-2.2, 1.5, 0.4, 0.05
)
)
logistic_sim_power <- colMeans(logistic_sim)
plot(
n_values, logistic_sim_power,
xlab = "Sample size", ylab = "Power",
type = "l"
)
n_exposed
and n_unexposed
, or assume the sample size of the two groups has to be equal like we did for the t-test example.n_exposed / n_total = 0.4
, but there’s lots of ways you can do the same thing.