Power Analysis in R: Sample Size with the pwr Package
How power analysis works in R
Run a study with too few subjects and you may collect six months of data that tells you nothing. Run it with too many and you burned through budget and participant time you did not need to. Power analysis is the math that lands the sample size in the right range before you ever see a data point. The R package pwr on CRAN covers every common test you’ll plan for, using a single mental model: pick three of the four inputs, and the function solves for the fourth.
Power analysis answers two practical questions before you collect data: how many observations do you need, and what is the chance that a given study will detect an effect of a particular size? Almost every power calculation reduces to four numbers that hang together:
sig.level(alpha): the false-positive rate you tolerate, conventionally 0.05.power(1 - beta): the probability of detecting a real effect, conventionally 0.80.n: sample size.- effect size: the standardised magnitude of the signal you expect.
You set three of them, and the fourth falls out. The pwr package follows this mental model exactly: every pwr.* function takes three of the inputs above and solves for the fourth, and one of those three must be the effect size. This guide is also useful if you are reporting achieved power for an existing study, but treat retrospective power with caution (see Common mistakes below).
Key takeaways
- Power analysis answers the planning question: how big a sample do you need to detect a real effect at acceptable cost?
- Every
pwr.*function takes three of{n, effect size, sig.level, power}and solves for the fourth. - Cohen’s conventional effect sizes (small/medium/large) are the default when you have no pilot data.
pwrreturns real-valued sample sizes, so wrap them inceiling()before reporting.- Post-hoc power is misleading: use
pwrfor prospective planning, not retrospective justification.
How do you install and load pwr?
install.packages("pwr")
library(pwr)
pwr has no heavy dependencies beyond base R and stats, and it ships with a short, well-written vignette on CRAN. If you want to derive effect sizes from real data, also install effectsize from the same family of CRAN packages.
Cohen’s conventional effect sizes
You cannot run a power calculation without an effect size. When you have no better source, fall back on the conventional values from Cohen (1988):
| Test family | Effect size | Small | Medium | Large |
|---|---|---|---|---|
| Tests for proportions | Cohen’s h | 0.2 | 0.5 | 0.8 |
| Tests for means (t-tests) | Cohen’s d | 0.2 | 0.5 | 0.8 |
| Chi-squared tests | Cohen’s w | 0.1 | 0.3 | 0.5 |
| Correlation | Cohen’s r | 0.1 | 0.3 | 0.5 |
| One-way ANOVA | Cohen’s f | 0.1 | 0.25 | 0.4 |
| General linear model | Cohen’s f-squared | 0.02 | 0.15 | 0.35 |
Pull any of these with cohen.ES():
cohen.ES(test = "t", size = "medium")
# effect.size
# 0.5
cohen.ES(test = "anov", size = "medium")
# effect.size
# 0.25
You can also pass the alias as a string directly to a pwr function. The function resolves the string against Cohen’s table internally before doing the calculation, which makes the conventional effect size visible in your script rather than buried as a 0.5 literal inside a call:
pwr.r.test(r = "medium", power = 0.8)$n
# [1] 84.07364
These conventions are defaults, not facts about the world. If you have a pilot study or a published estimate from a comparable design, use that. See the hypothesis testing tutorial for the test-selection logic that drives the choice.
Deriving an effect size from raw data
For a two-sample comparison of means, the formula is d = (mean(x) - mean(y)) / pooled_sd, where pooled_sd is the square root of the average of the two sample variances. The effectsize package gives you a one-liner that also reports a confidence interval:
library(effectsize)
cohens_d(mpg ~ am, data = mtcars)
# Cohen's d | 95% CI
# -------------------------
# -1.48 | [-2.52, -0.43]
# Estimated using pooled SD.
A pilot d with a wide CI is still a better input than a borrowed 0.5.
How many subjects do you need for a t-test?
The most common planning question. You want to detect a medium effect (d = 0.5) with 80% power at the 0.05 level using a two-sided two-sample t-test:
out <- pwr.t.test(d = 0.5, sig.level = 0.05, power = 0.80,
type = "two.sample", alternative = "two.sided")
out$n
# [1] 63.76561
ceiling(out$n)
# [1] 64
The reported n is the per-group sample size, so the study needs 64 subjects in each arm, 128 in total. Wrap ceiling() around the result because pwr returns real-valued numbers. Pair this with running the actual t-test in R once you have collected the data.
For a one-sample test, switch type = "one.sample". For a paired design, switch type = "paired"; the n returned is then the number of pairs, not the number of measurements.
Achieved power for a paired t-test
If you already have data and want to know what your study was capable of detecting, pass n instead of power:
pwr.t.test(n = 30, d = 0.4, sig.level = 0.05,
type = "paired", alternative = "two.sided")$power
# [1] 0.5678
That is roughly 57% power for a paired design with 30 pairs and an observed d of 0.4. Achieved power is a useful sanity check, but be careful about reading too much into it (see Common mistakes).
Sample size for proportion tests
For a one-sample proportion test, pwr takes Cohen’s h, an arcsine-transformed effect size. The ES.h() helper converts two raw proportions to h.
A canonical example, taken straight from the pwr vignette: you suspect a coin lands heads 75% of the time and want to test it against a fair coin. You want 80% power at alpha 0.05:
pwr.p.test(h = ES.h(p1 = 0.75, p2 = 0.50),
sig.level = 0.05, power = 0.80,
alternative = "greater")$n
# [1] 22.55126
ceiling(pwr.p.test(h = ES.h(p1 = 0.75, p2 = 0.50),
sig.level = 0.05, power = 0.80,
alternative = "greater")$n)
# [1] 23
23 flips will give you 80% power to detect that bias, one-sided at alpha 0.05. Drop the alternative argument to get the more conservative two-sided version, which asks for more flips.
ES.h is not the raw proportion difference. The arcsine transform stretches the scale near 0 and 1, so a 5-point gap between 5% and 10% yields a larger h than a 5-point gap between 50% and 55%. If you prefer to pass raw proportions, base R has power.prop.test():
power.prop.test(p1 = 0.75, p2 = 0.50, sig.level = 0.05,
power = 0.80, alternative = "greater")$n
# [1] 22.17482
For a two-sample proportion test with equal n, use pwr.2p.test. For unequal n, use pwr.2p2n.test and supply both sample sizes; it reports achieved power given the actual group sizes.
How do you size a one-way ANOVA study?
For a balanced one-way ANOVA, supply the number of groups k and Cohen’s f:
pwr.anova.test(k = 4, f = 0.25, sig.level = 0.05, power = 0.80)$n
# [1] 45.0
n is per group, so the study needs 45 subjects in each of the 4 groups, 180 in total. This function assumes a balanced design with equal cell sizes. For factorial or unbalanced designs, use simulation-based tools such as Superpower or simr instead. Pair this with the ANOVA in R tutorial when you are ready to run the analysis.
Sample size for a correlation test
pwr.r.test solves for any of n, r, power, or sig.level:
pwr.r.test(r = 0.3, sig.level = 0.05, power = 0.80,
alternative = "two.sided")$n
# [1] 84.07364
ceiling(pwr.r.test(r = 0.3, sig.level = 0.05, power = 0.80,
alternative = "two.sided")$n)
# [1] 85
The function uses an arctanh approximation. That is accurate for moderate n and small r; for very small n combined with large r, the estimate can be off by a few percent. If you need exact values there, simulate.
How do you run power analysis for chi-squared tests?
pwr.chisq.test takes Cohen’s w and the degrees of freedom. For an r x c association table, df = (r - 1) * (c - 1). For goodness of fit with k categories, df = k - 1:
pwr.chisq.test(w = 0.3, df = (3 - 1) * (3 - 1),
sig.level = 0.05, power = 0.80)$N
# [1] 162.0
N is the total sample size, not per cell.
Sample size for multiple regression
pwr.f2.test handles any general linear F test, including multiple regression. The effect size is f2 = R^2 / (1 - R^2). Suppose you want to detect an R-squared of 0.20 with 6 predictors in the model:
# f2 = R^2 / (1 - R^2) = 0.20 / 0.80 = 0.25
pwr.f2.test(u = 6, f2 = 0.25, sig.level = 0.05, power = 0.80)$v
# [1] 68.4
u is the numerator df, equal to the number of predictors you are testing. v is the denominator df, and total N is v + u + 1. Pass v = NULL to solve for total N directly. Pair this with regression models in R when you are ready to fit the model.
How do you handle unequal group sizes?
For a two-sample t-test with different group sizes, use pwr.t2n.test and pass n1 and n2 directly. This is the right function for retrospective power checks on existing studies with unbalanced arms. With 50 per group and a medium effect, you get roughly 70% power:
pwr.t2n.test(d = 0.5, n1 = 50, n2 = 50, sig.level = 0.05,
alternative = "two.sided")$power
# [1] 0.6968
That lines up with the 64-per-group figure you needed for 80% power in the two-sample t-test earlier. The smaller sample drags the achieved power below 0.80.
How do you plot a power curve?
Every pwr.* function is vectorised, so passing a vector for n returns a vector of powers. The result has a plot() method that draws a power-versus-n curve for you:
p.out <- pwr.t.test(d = 0.5, sig.level = 0.05, power = 0.80,
type = "two.sample", alternative = "two.sided")
plot(p.out)
If ggplot2 is installed, you get a ggplot. Otherwise base R graphics. You can also build the curve yourself with a base R plot:
n_seq <- seq(20, 200, by = 10)
res <- pwr.t.test(n = n_seq, d = 0.5, sig.level = 0.05,
type = "two.sample", alternative = "two.sided")
plot(n_seq, res$power, type = "b",
xlab = "Sample size per group",
ylab = "Power",
main = "Power curve for d = 0.5, alpha = 0.05")
abline(h = 0.80, lty = 2)
The dashed line at 0.80 marks the conventional power target. Pick the n where your curve first crosses it.
Common mistakes
- No effect size, no answer. Every pwr call needs three of the four inputs, and one of them must be the effect size. The function cannot invent a meaningful effect size from n and power alone.
- Per-group vs. total n. For
pwr.t.test(type = "two.sample")andpwr.2p.test,nis per group. Forpwr.chisq.test,Nis total. State this in any writeup. - Two-sided vs. one-sided. The pwr default is
alternative = "two.sided". One-sided tests need a pre-registered directional hypothesis. Picking one-sided after peeking at the data inflates Type I error. - Post-hoc power is misleading. Computing power from the observed effect size of the same study you just ran tells you nothing you did not already learn from the p-value. The reason is that the observed effect size is biased, and the resulting power estimate inherits that bias. Use pwr for prospective planning, not retrospective justification. See Hoenig and Heisey (2001) for the full argument.
- Balanced-only ANOVA.
pwr.anova.testis for balanced one-way designs. For factorial or unbalanced cases, reach forSuperpowerorsimr::powerSim(). - Closed-form pwr has limits. Mixed models, non-standard tests, and clustered designs need simulation. When in doubt, simulate with
simr::powerSim()against a fitted model. fandf2are different effect sizes.pwr.anova.testtakesf(Cohen’s f).pwr.f2.testtakesf2(Cohen’s f-squared). They are related:f2 = f^2 / (1 - f^2)for the case where f is the standard deviation of group means scaled by the within-group standard deviation. Do not swap them.
A quick sanity check before you commit to a sample size: solve for the same number two different ways and confirm they agree. The normal approximation for a two-sample t-test should land within rounding of pwr.t.test() for moderate effect sizes:
# Sanity check: normal approximation vs pwr (per group)
n_norm <- 2 * (qnorm(0.975) + qnorm(0.80))^2 / 0.5^2
n_norm
# [1] 62.78975
n_pwr <- pwr.t.test(d = 0.5, sig.level = 0.05, power = 0.80,
type = "two.sample")$n
n_pwr
# [1] 63.76561
The two estimates differ by about one subject per group. That gap is the t-distribution’s heavier tails showing up in the exact calculation, which is exactly why pwr exists. Treat the closed-form sanity check as a gut check, not a substitute.
Which pwr function should you use?
| Function | Test | Effect size | Solves for |
|---|---|---|---|
pwr.t.test | t-test (one-sample, two-sample, paired) | d | n, power, sig.level |
pwr.t2n.test | two-sample t-test, unequal n | d | power |
pwr.p.test | one-sample proportion | h | n, power, sig.level |
pwr.2p.test | two-sample proportion, equal n | h | n, power, sig.level |
pwr.2p2n.test | two-sample proportion, unequal n | h | power |
pwr.anova.test | one-way balanced ANOVA | f | n, power, sig.level |
pwr.r.test | correlation | r | n, power, sig.level |
pwr.chisq.test | chi-squared (goodness of fit, association) | w | N, power, sig.level |
pwr.f2.test | general linear F test (multiple regression) | f2 | v, power, sig.level |
For anything outside that list, drop down to simulation. simr::powerSim() works against a fitted mixed model, and Superpower covers factorial ANOVA designs that pwr.anova.test cannot handle.
Frequently asked questions
What is the difference between alpha, beta, and power?
Alpha is the false-positive rate, conventionally 0.05. Beta is the false-negative rate, conventionally 0.20. Power is 1 minus beta, conventionally 0.80. They are three of the four inputs to any pwr call.
What effect size should you use if you have no pilot data?
Fall back on Cohen’s conventional values. A medium d is 0.5, a medium f is 0.25, a medium r is 0.3, a medium h is 0.5. These are defaults, not facts about the world; replace them as soon as you have a better estimate from a pilot or a comparable published study.
Should you use a one-sided or two-sided test?
Default to two-sided unless you have a pre-registered directional hypothesis. Picking one-sided after peeking at the data inflates Type I error and undermines the power calculation.
Is post-hoc power analysis useful?
Not really. Computing power from the observed effect size of the same study you just ran tells you nothing you did not already learn from the p-value. Use pwr for prospective planning, not retrospective justification. See Hoenig and Heisey (2001) for the argument.
Can pwr handle mixed models or clustered designs?
No. pwr covers a fixed set of closed-form tests. For mixed models, factorial ANOVA, and clustered designs, use simr::powerSim() or Superpower to simulate power from a fitted model rather than rely on a closed-form approximation.
Conclusion
Power analysis in R is a three-minute job once you have an effect size in hand. The pwr package covers every common test - t-tests, ANOVA, correlation, chi-squared, regression, proportions - and the same ceiling() + report rule applies across all of them. For anything that does not fit the closed-form list, drop down to simr or Superpower and run a simulation. Either way, the math has to happen before the data collection starts, not after.