November 20, 2025

Usage inspired by the typical workflow in trial design and conduct:
design <- getDesignGroupSequential()getSampleSizeMeans(), getPowerMeans()getSimulationMeans()data <- getDataset()getAnalysisResults(design, data)Objective: To evaluate the efficacy of a new antihypertensive therapy
(compared to a placebo) in patients with hypertension.
Endpoints and Assumptions:
Assumptions:
Expected Values:
Using rpact we will calculate the required sample size to achieve a power of 80% for detecting a 12 mmHg difference in systolic blood pressure change from baseline in the treatment group relative to the placebo group, assuming a standard deviation of 15 mmHg.
We will see a nice feature when using rpact output in RMarkdown or Quarto docs: you directly get a nicely formatted result output.
# Load the rpact package
library(rpact)
# Define the design parameters for sample size calculation
design <- getDesignGroupSequential(
kMax = 1, # Only one analysis (classic fixed design)
alpha = 0.025, # Significance level
beta = 0.20, # 80% power
sided = 1 # One-sided test
)
# Estimate sample size
sampleSizeResult <- design |>
getSampleSizeMeans(
groups = 2, # Two groups: Treatment vs. Placebo
alternative = 12, # Expected effect size
stDev = 15 # Common standard deviation
)Sample size calculation for a continuous endpoint
Fixed sample analysis, one-sided significance level 2.5%, power 80%. The results were calculated for a two-sample t-test, H0: mu(1) - mu(2) = 0, H1: effect = 12, standard deviation = 15.
| Stage | Fixed |
|---|---|
| Stage level (one-sided) | 0.0250 |
| Efficacy boundary (z-value scale) | 1.960 |
| Efficacy boundary (t) | 8.438 |
| Number of subjects | 51.0 |
Legend:
Using fetch() we can extract specific quantities from the result object.
Now let’s go the other way around - calculate the power for given sample size, and vary other parameters. This helps us to:
Using the rpact package, we can calculate the power of the study under the defined parameters. Here’s how:
# Define parameters based on initial assumptions
calculatedSampleSize <- ceiling(sampleSizeResult$numberOfSubjects)
powerResult <- design |>
getPowerMeans(
groups = 2, # Two groups: Treatment and Placebo
alternative = 12, # Expected effect size
stDev = 15, # Common standard deviation
maxNumberOfSubjects = calculatedSampleSize
)Power calculation for a continuous endpoint
Fixed sample analysis, one-sided significance level 2.5%. The results were calculated for a two-sample t-test, H0: mu(1) - mu(2) = 0, power directed towards larger values, H1: effect = 12, standard deviation = 15, number of subjects = 52.
| Stage | Fixed |
|---|---|
| Stage level (one-sided) | 0.0250 |
| Efficacy boundary (z-value scale) | 1.960 |
| Efficacy boundary (t) | 8.356 |
| Power | 0.8075 |
| Number of subjects | 52.0 |
Legend:
Here’s R code to analyze the scenarios described above:
# Define scenarios for adjustments
scenarios <- list(
list(alternative = 10, stDev = 15), # 1: Reduced effect
list(alternative = 10, stDev = 14), # 2: Reduced effect + sd
list(alternative = 11, stDev = 15), # 3: Reduced effect
list(alternative = 11, stDev = 14), # 4: Reduced effect + sd
list(alternative = 12, stDev = 15), # 5: Base scenario
list(alternative = 13, stDev = 16), # 6: Incr. sd + effect
list(alternative = 13, stDev = 17), # 7: Incr. sd + effect
list(alternative = 12, stDev = 16), # 8: Increased sd
list(alternative = 12, stDev = 17) # 9: Increased sd
)
# Run calculations for each scenario
results <- scenarios |>
lapply(function(scenario) {
getPowerMeans(
design = design,
groups = 2,
alternative = scenario$alternative,
stDev = scenario$stDev,
maxNumberOfSubjects = 52
)
})
# Fetch only the power from the result objects
x <- sapply(results, function(result) {
result |> fetch("Overall reject")
})| alternative | stDev | power | |
|---|---|---|---|
| Scenario 1 | 10 | 15 | 0.6544534 |
| Scenario 2 | 10 | 14 | 0.7142007 |
| Scenario 3 | 11 | 15 | 0.7366421 |
| Scenario 4 | 11 | 14 | 0.7933714 |
| Scenario 5 | 12 | 15 | 0.8074858 |
| Scenario 6 | 13 | 16 | 0.8193392 |
| Scenario 7 | 13 | 17 | 0.7715373 |
| Scenario 8 | 12 | 16 | 0.7555122 |
| Scenario 9 | 12 | 17 | 0.7040188 |
Interpreting the results from the different scenarios:
By exploring these adjustments, we gain insight into whether a larger sample or refined target parameters might be needed for a more reliable assessment of the new treatments’s efficacy.
allocationRatioPlanned argument in rpact enables us to specify these ratios.Here are the new scenarios we will examine:
Using the rpact package, we can set up and calculate these scenarios with the allocationRatioPlanned parameter:
# Define the initial design
design <- getDesignGroupSequential(
kMax = 1,
alpha = 0.025,
beta = 0.2,
sided = 1
)
# Define scenarios for different allocation ratios
scenarios <- list(
list(allocationRatio = 1), # 1:1 allocation
list(allocationRatio = 2), # 2:1 allocation
list(allocationRatio = 3) # 3:1 allocation
)
# Run calculations for each scenario with specified allocation ratios
results <- scenarios |>
lapply(function(scenario) {
getPowerMeans(
design = design,
groups = 2,
alternative = 12,
stDev = 15,
maxNumberOfSubjects = 50,
allocationRatioPlanned = scenario$allocationRatio
)
})
# Fetch only the power from the result objects
x <- sapply(results, function(result) {
result |> fetch("Overall reject")
})| allocationRatio | power | |
|---|---|---|
| Scenario 1 | 1 | 0.7914502 |
| Scenario 2 | 2 | 0.7431284 |
| Scenario 3 | 3 | 0.6701351 |
Let’s assume that a 2:1 allocation shall be used going forward.
Given the slight reduction in power observed with the 2:1 allocation ratio (Power = 74.3%), we can implement a group-sequential design with an interim analysis instead of simply increasing the sample size.
First we start again with a fixed sample size design:
nFixed
58
We need numberOfSubjectsFixed for the subsequent power analysis.
rpact to define a group-sequential design with two planned analyses.n subjects, with a final analysis if the study proceeds to the full sample.n shall be determined by exploring different information rates


















Here’s how to set up the design comparison in R:
First we define the possible information rates at which to conduct the interim analysis.
# Run calculations for each scenario
results <- scenarios |>
lapply(function(scenario) {
getDesignGroupSequential(
informationRates = c(scenario$informationRate, 1),
alpha = 0.025,
sided = 1,
typeOfDesign = "OF"
) |>
getPowerMeans(
groups = 2,
alternative = 12,
stDev = 15,
maxNumberOfSubjects = numberOfSubjectsFixed, # 58
allocationRatioPlanned = 2
)
})| informationRate | expectedNumberOfSubjects | earlyStop | |
|---|---|---|---|
| Scenario 1 | 0.5 | 51.86201 | 0.2116549 |
| Scenario 2 | 0.6 | 49.88180 | 0.3499223 |
| Scenario 3 | 0.7 | 49.56749 | 0.4846273 |
| Scenario 4 | 0.8 | 50.99013 | 0.6042989 |
| Scenario 5 | 0.9 | 53.90422 | 0.7061693 |
In this example we decide to use information rate 0.7 because the expected number of subjects is lowest and the probability for an early stopping is nearly 50%.
design <- getDesignGroupSequential(
informationRates = c(0.7, 1),
alpha = 0.025, # Overall significance level
beta = 0.2, # Power 80%
sided = 1, # One-sided test
typeOfDesign = "OF" # O'Brien & Fleming design
)
sampleSizeResult <- getSampleSizeMeans(
design = design,
groups = 2,
alternative = 12, # Target effect
stDev = 15, # Common standard deviation
allocationRatioPlanned = 2 # 2:1 allocation
)
# Print the summary of the results
sampleSizeResult |>
summary()Sample size calculation for a continuous endpoint
Sequential analysis with a maximum of 2 looks (group sequential design), one-sided overall significance level 2.5%, power 80%. The results were calculated for a two-sample t-test, H0: mu(1) - mu(2) = 0, H1: effect = 12, standard deviation = 15, planned allocation ratio = 2.
| Stage | 1 | 2 |
|---|---|---|
| Planned information rate | 70% | 100% |
| Cumulative alpha spent | 0.0082 | 0.0250 |
| Stage levels (one-sided) | 0.0082 | 0.0223 |
| Efficacy boundary (z-value scale) | 2.400 | 2.008 |
| Efficacy boundary (t) | 12.508 | 8.566 |
| Cumulative power | 0.4861 | 0.8000 |
| Number of subjects | 40.7 | 58.2 |
| Expected number of subjects under H1 | 49.7 | |
| Exit probability for efficacy (under H0) | 0.0082 | |
| Exit probability for efficacy (under H1) | 0.4861 |
Legend:
To confirm that the design meets the target power while allowing for early stopping, we calculate the power of the study under the adjusted parameters:
powerResult <- getPowerMeans(
design = design,
groups = 2,
alternative = 12, # Expected effect size
stDev = 15, # Common standard deviation
# Sample size per stage from previous calculation
maxNumberOfSubjects = ceiling(sampleSizeResult$numberOfSubjects)[2, 1],
allocationRatioPlanned = 2 # Allocation ratio 2:1
)
# Print the summary of the results
powerResult |>
summary()Power calculation for a continuous endpoint
Sequential analysis with a maximum of 2 looks (group sequential design), one-sided overall significance level 2.5%. The results were calculated for a two-sample t-test, H0: mu(1) - mu(2) = 0, power directed towards larger values, H1: effect = 12, standard deviation = 15, maximum number of subjects = 59, planned allocation ratio = 2.
| Stage | 1 | 2 |
|---|---|---|
| Planned information rate | 70% | 100% |
| Cumulative alpha spent | 0.0082 | 0.0250 |
| Stage levels (one-sided) | 0.0082 | 0.0223 |
| Efficacy boundary (z-value scale) | 2.400 | 2.008 |
| Efficacy boundary (t) | 12.416 | 8.506 |
| Cumulative power | 0.4930 | 0.8057 |
| Number of subjects | 41.3 | 59.0 |
| Expected number of subjects under H1 | 50.3 | |
| Exit probability for efficacy (under H0) | 0.0082 | |
| Exit probability for efficacy (under H1) | 0.4930 |
Legend:
# Run calculations for each scenario
results <- scenarios |>
lapply(function(scenario) {
getDesignGroupSequential(
informationRates = c(0.7, 1),
futilityBounds = scenario$futilityBounds,
alpha = 0.025,
sided = 1,
typeOfDesign = "OF"
) |>
getSampleSizeMeans(
groups = 2,
alternative = 12,
stDev = 15,
allocationRatioPlanned = 2
)
})# Display results for each scenario
x <- sapply(results, function(result) {
result |> fetch("Futility bounds (treatment effect scale)")
})
#| echo: true
scenarios |>
bind_rows() |>
mutate("Futility bounds (treatment effect scale)" = x) |>
as.data.frame() |>
set_rownames(paste("Scenario", 1:length(x))) |>
kable()| futilityBounds | Futility bounds (treatment effect scale) | |
|---|---|---|
| Scenario 1 | -1.0 | -5.050045 |
| Scenario 2 | -0.5 | -2.512664 |
| Scenario 3 | 0.0 | 0 |
| Scenario 4 | 0.5 | 2.508815 |
| Scenario 5 | 1.0 | 4.97699 |
| Scenario 6 | 1.5 | 7.102175 |
We will first search the futility bound on the Z scale that corresponds to a treatment effect of 2 mmHg. We do this with stats::uniroot():
One Dimensional Root (Zero) Finding
Description
The function uniroot searches the interval from lower to upper for a root (i.e., zero) of the function f with respect to its first argument.
soughtBoundaryTreatmentEffectScale <- 2
futilityBound <- uniroot(
function(x) {
soughtBoundaryTreatmentEffectScale -
getDesignGroupSequential(
informationRates = c(0.7, 1),
futilityBounds = x,
alpha = 0.025,
sided = 1,
typeOfDesign = "OF"
) |>
getSampleSizeMeans(
groups = 2,
alternative = 12,
stDev = 15,
allocationRatioPlanned = 2
) |>
fetch("Futility bounds (treatment effect scale)") |>
as.numeric()
},
lower = 0,
upper = 2
)$root
futilityBound[1] 0.3985836
Alternatively, we can also calculate this in an approximate closed form here via:
\[ u_{1}^{0} = \delta_{0} \sqrt{\frac{n_{11}n_{21}}{n_{11} + n_{21}}} / \sigma \]
Sample size calculation for a continuous endpoint
Sequential analysis with a maximum of 2 looks (group sequential design), one-sided overall significance level 2.5%, power 80%. The results were calculated for a two-sample t-test, H0: mu(1) - mu(2) = 0, H1: effect = 12, standard deviation = 15, planned allocation ratio = 2.
| Stage | 1 | 2 |
|---|---|---|
| Planned information rate | 70% | 100% |
| Cumulative alpha spent | 0.0082 | 0.0250 |
| Stage levels (one-sided) | 0.0082 | 0.0223 |
| Efficacy boundary (z-value scale) | 2.400 | 2.008 |
| Futility boundary (z-value scale) | 0.401 | |
| Efficacy boundary (t) | 12.496 | 8.558 |
| Futility boundary (t) | 2.013 | |
| Cumulative power | 0.4869 | 0.8000 |
| Number of subjects | 40.8 | 58.3 |
| Expected number of subjects under H1 | 49.4 | |
| Overall exit probability (under H0) | 0.6641 | |
| Overall exit probability (under H1) | 0.5116 | |
| Exit probability for efficacy (under H0) | 0.0082 | |
| Exit probability for efficacy (under H1) | 0.4869 | |
| Exit probability for futility (under H0) | 0.6559 | |
| Exit probability for futility (under H1) | 0.0246 |
Legend:

In this example, we could of course skip simulations, because for a continuous outcome with known standard deviation we can calculate everything in closed form.
However, to illustrate the simulation capabilities of rpact, we perform a simulation study to confirm the operating characteristics of our group-sequential design with futility stopping.
This is important for non-normal endpoints and adaptive designs where closed form solutions are not available.
Now we just call getSimulationMeans() with the design and parameters defined above:
simResults <- getDesignGroupSequential(
informationRates = c(0.7, 1),
futilityBounds = futilityBound,
alpha = 0.025,
sided = 1,
typeOfDesign = "OF"
) |>
getSimulationMeans(
groups = 2,
alternative = c(0, 12), # H0 and H1
stDev = 15,
allocationRatioPlanned = 2,
plannedSubjects = c(41, 59),
maxNumberOfIterations = 1000,
seed = 12345
)Simulation of a continuous endpoint
Sequential analysis with a maximum of 2 looks (group sequential design), one-sided overall significance level 2.5%. The results were simulated for a two-sample t-test (normal approximation), H0: mu(1) - mu(2) = 0, power directed towards larger values, H1: effect as specified, standard deviation = 15, planned cumulative sample size = c(41, 59), planned allocation ratio = 2, simulation runs = 1000, seed = 12345.
| Stage | 1 | 2 |
|---|---|---|
| Planned information rate | 70% | 100% |
| Cumulative alpha spent | 0.0082 | 0.0250 |
| Stage levels (one-sided) | 0.0082 | 0.0223 |
| Efficacy boundary (z-value scale) | 2.400 | 2.008 |
| Futility boundary (z-value scale) | 0.399 | |
| Cumulative power, alt. = 0 | 0.0040 | 0.0210 |
| Cumulative power, alt. = 12 | 0.5060 | 0.8120 |
| Stage-wise number of subjects, alt. = 0 | 41.0 | 18.0 |
| Stage-wise number of subjects, alt. = 12 | 41.0 | 18.0 |
| Expected number of subjects under H1, alt. = 0 | 47.4 | |
| Expected number of subjects under H1, alt. = 12 | 49.5 | |
| Conditional power (achieved), alt. = 0 | 0.1604 | |
| Conditional power (achieved), alt. = 12 | 0.5304 | |
| Exit probability for efficacy, alt. = 0 | 0.0040 | |
| Exit probability for efficacy, alt. = 12 | 0.5060 | |
| Exit probability for futility, alt. = 0 | 0.6390 | |
| Exit probability for futility, alt. = 12 | 0.0230 |
Legend:
Interpretation:
Cumulative power, alt. = 0 is 2.1% at the final analysis.Cumulative power, alt. = 12 is 81.2% at the final analysis.Exit probability for efficacy, alt. = 12 is 50.6%Download the example dataset: trial_data_stage1.csv
# A tibble: 42 × 4
subjectId group bloodPressureBaseline bloodPressure
<int> <fct> <dbl> <dbl>
1 1 1 150. 138.
2 2 1 144. 160.
3 3 1 151. 128.
4 4 1 157. 147.
5 5 1 153. 136.
6 6 1 155. 146.
7 7 1 154. 120.
8 8 1 150. 173.
9 9 1 149. 147.
10 10 1 145. 113.
# ℹ 32 more rows
# A tibble: 42 × 5
subjectId group bloodPressureBaseline bloodPressure bloodPressureDiff
<int> <fct> <dbl> <dbl> <dbl>
1 1 1 150. 138. -11.6
2 2 1 144. 160. 15.8
3 3 1 151. 128. -23.2
4 4 1 157. 147. -9.72
5 5 1 153. 136. -17.5
6 6 1 155. 146. -8.76
7 7 1 154. 120. -34.4
8 8 1 150. 173. 22.9
9 9 1 149. 147. -2.66
10 10 1 145. 113. -31.7
# ℹ 32 more rows
rpact requires the summary statistics, therefore let’s calculate them:
dataSummary <- trialData |>
group_by(group) |>
summarise(
n = n(),
meanBaseline = mean(bloodPressureBaseline),
stDevBaseline = sd(bloodPressureBaseline),
meanTherapy = mean(bloodPressure),
stDevTherapy = sd(bloodPressure),
mean = mean(bloodPressureDiff),
stDev = sd(bloodPressureDiff)
)
dataSummary |>
kable()| group | n | meanBaseline | stDevBaseline | meanTherapy | stDevTherapy | mean | stDev |
|---|---|---|---|---|---|---|---|
| 1 | 28 | 149.2407 | 5.677756 | 139.0127 | 14.33155 | -10.2280732 | 14.07891 |
| 2 | 14 | 148.1788 | 3.743160 | 147.7018 | 17.22372 | -0.4770206 | 17.14525 |
Create an rpact dataset from summary statistics of the trial data:
Analysis results for a continuous endpoint
Sequential analysis with 2 looks (group sequential design), one-sided overall significance level 2.5%. The results were calculated using a two-sample t-test, equal variances option. H0: mu(1) - mu(2) = 0 against H1: mu(1) - mu(2) < 0. The conditional power calculation with planned sample size is based on planned allocation ratio = 2, overall effect = -9.751, and assumed standard deviation = 15.
| Stage | 1 | 2 |
|---|---|---|
| Planned information rate | 70% | 100% |
| Cumulative alpha spent | 0.0082 | 0.0250 |
| Stage levels (one-sided) | 0.0082 | 0.0223 |
| Efficacy boundary (z-value scale) | 2.400 | 2.008 |
| Cumulative effect size | -9.751 | |
| Cumulative (pooled) standard deviation | 15.144 | |
| Overall test statistic | -1.967 | |
| Overall p-value | 0.0281 | |
| Test action | continue | |
| Conditional rejection probability | 0.2271 | |
| Planned sample size | 18 | |
| Conditional power | 0.7094 | |
| 95% repeated confidence interval | [-22.171; 2.669] | |
| Repeated p-value | 0.0636 |
Note that we must set directionUpper = FALSE because we have a higher blood pressure reduction in the treatment group, i.e., the blood pressure reduction difference between control and treatment is negative.
Download the example dataset: trial_data.csv
# A tibble: 60 × 5
subjectId stage group bloodPressureBaseline bloodPressure
<int> <int> <fct> <dbl> <dbl>
1 1 1 1 150. 138.
2 2 1 1 144. 160.
3 3 1 1 151. 128.
4 4 1 1 157. 147.
5 5 1 1 153. 136.
6 6 1 1 155. 146.
7 7 1 1 154. 120.
8 8 1 1 150. 173.
9 9 1 1 149. 147.
10 10 1 1 145. 113.
# ℹ 50 more rows
Alternatively we can use the emmeans package together with a model definition to load the raw data into an rpact dataset.
library(emmeans)
trialData$group <- relevel(trialData$group, ref = "2")
trialData$bloodPressureDiff <-
trialData$bloodPressure - trialData$bloodPressureBaseline
dataset <- getDataset(
lm(bloodPressureDiff ~ group, data = trialData, subset = (stage == 1)) |>
emmeans("group"),
lm(bloodPressureDiff ~ group, data = trialData, subset = (stage == 2)) |>
emmeans("group")
)Note: Here we are not interested in the model output but we need it to compute the estimated marginal means (EMMs; least-squares means) for the factor group in the model. This type of raw data import is particularly useful when the raw data contains covariates that need to be adjusted for.
Dataset of means
The dataset contains the sample sizes, means, and standard deviations of one treatment and one control group. The total number of looks is two; stage-wise and cumulative data are included.
| Stage | 1 | 1 | 2 | 2 |
|---|---|---|---|---|
| Group | 1 | 2 | 1 | 2 |
| Stage-wise sample size | 28 | 14 | 12 | 6 |
| Cumulative sample size | 28 | 14 | 40 | 20 |
| Stage-wise mean | -10.228 | -0.477 | -13.268 | 0.313 |
| Cumulative mean | -10.228 | -0.477 | -11.140 | -0.240 |
| Stage-wise standard deviation | 15.144 | 15.144 | 15.275 | 15.275 |
| Cumulative standard deviation | 15.144 | 15.144 | 15.052 | 14.780 |
Note that the standard deviation looks different depending of the method you use to import the raw data:
For the analysis with rpact this makes no difference.
Analysis results for a continuous endpoint
Sequential analysis with 2 looks (group sequential design), one-sided overall significance level 2.5%. The results were calculated using a two-sample t-test, equal variances option. H0: mu(1) - mu(2) = 0 against H1: mu(1) - mu(2) < 0.
| Stage | 1 | 2 |
|---|---|---|
| Planned information rate | 70% | 100% |
| Cumulative alpha spent | 0.0082 | 0.0250 |
| Stage levels (one-sided) | 0.0082 | 0.0223 |
| Efficacy boundary (z-value scale) | 2.400 | 2.008 |
| Cumulative effect size | -9.751 | -10.900 |
| Cumulative (pooled) standard deviation | 15.144 | 14.964 |
| Overall test statistic | -1.967 | -2.660 |
| Overall p-value | 0.0281 | 0.0050 |
| Test action | continue | reject |
| Conditional rejection probability | 0.2271 | |
| 95% repeated confidence interval | [-22.171; 2.669] | [-19.311; -2.489] |
| Repeated p-value | 0.0636 | 0.0054 |
| Final p-value | 0.0107 | |
| Final confidence interval | [-18.228; -1.525] | |
| Median unbiased estimate | -10.004 |
Questions and Answers
