Introduction to bbssr: Blinded Sample Size Re-estimation for Binary Endpoints

Gosuke Homma

2025-06-18

Introduction

The bbssr package provides comprehensive tools for implementing blinded sample size re-estimation (BSSR) in two-arm clinical trials with binary endpoints. This vignette introduces the key concepts and demonstrates how to use the main functions in the package.

What is Blinded Sample Size Re-estimation?

Traditional clinical trials use fixed sample sizes determined during the planning phase. However, these calculations often rely on assumptions about treatment effects and response rates that may prove incorrect. BSSR addresses this limitation by allowing adaptive sample size adjustments during the trial while maintaining:

Key Advantages of BSSR

  1. Maintains Blinding: Only pooled response rates are used, preserving treatment allocation concealment
  2. Exact Statistical Control: Uses exact tests rather than asymptotic approximations
  3. Flexible Designs: Supports different adaptation rules (restricted/unrestricted/weighted)
  4. Regulatory Compliance: Based on established statistical methodology accepted by regulatory agencies

Getting Started

library(bbssr)
library(dplyr)
library(ggplot2)

Basic Usage

Power Calculation for Traditional Design

Let’s start with a basic power calculation for a traditional fixed-sample design:

# Calculate power for a traditional design
power_result <- BinaryPower(
  p1 = 0.5,          # Response rate in treatment group
  p2 = 0.2,          # Response rate in control group
  N1 = 40,           # Sample size in treatment group
  N2 = 40,           # Sample size in control group
  alpha = 0.025,     # One-sided significance level
  Test = 'Fisher'    # Fisher exact test
)

print(paste("Power:", round(power_result, 3)))
#> [1] "Power: 0.749"

Sample Size Calculation

Now let’s calculate the required sample size for a desired power:

# Calculate required sample size
sample_size_result <- BinarySampleSize(
  p1 = 0.5,           # Expected response rate in treatment group
  p2 = 0.2,           # Expected response rate in control group
  r = 1,              # Allocation ratio (1:1)
  alpha = 0.025,      # One-sided significance level
  tar.power = 0.8,    # Target power
  Test = 'Boschloo'   # Boschloo exact test (most powerful)
)

print(sample_size_result)
#>    p1  p2 r alpha tar.power     Test     Power N1 N2  N
#> 1 0.5 0.2 1 0.025       0.8 Boschloo 0.8096508 40 40 80

Blinded Sample Size Re-estimation (BSSR)

Basic BSSR Example

Here’s how to perform BSSR analysis:

# Basic BSSR calculation
bssr_result <- BinaryPowerBSSR(
  asmd.p1 = 0.45,      # Assumed response rate in treatment group
  asmd.p2 = 0.09,      # Assumed response rate in control group
  p = seq(0.1, 0.9, by = 0.1), # Range of pooled response rates
  Delta.A = 0.36,      # Assumed treatment effect (risk difference)
  Delta.T = 0.36,      # True treatment effect
  N1 = 24,             # Initial sample size in treatment group
  N2 = 24,             # Initial sample size in control group
  omega = 0.5,         # Fraction of data for interim analysis
  r = 1,               # Allocation ratio
  alpha = 0.025,       # Significance level
  tar.power = 0.8,     # Target power
  Test = 'Z-pool',     # Statistical test
  restricted = FALSE,  # Unrestricted design
  weighted = FALSE     # Non-weighted approach
)

# Display first few rows
head(bssr_result)
#>     p1   p2   p power.BSSR power.TRAD
#> 1 0.38 0.02 0.2  0.8728849  0.9310364
#> 2 0.48 0.12 0.3  0.7714587  0.7880436
#> 3 0.58 0.22 0.4  0.7816618  0.7168506
#> 4 0.68 0.32 0.5  0.7851850  0.6677197
#> 5 0.78 0.42 0.6  0.7816618  0.7168506
#> 6 0.88 0.52 0.7  0.7714587  0.7880436

Comparing BSSR Design Rules

One of the key features of bbssr is the ability to compare different BSSR design rules. Here’s the comprehensive comparison as requested:

# Calculate power of the BSSR with binary endpoint
power.BSSR <- tibble(
  Rule = factor(
    c('Restricted', 'Unrestricted', 'Weighted'), 
    levels = c('Restricted', 'Unrestricted', 'Weighted')
  ),
  restricted = c(TRUE, FALSE, FALSE),
  weighted = c(FALSE, FALSE, TRUE)
) %>% 
  group_by_all() %>% 
  reframe(r = c(1, 2), N1 = c(24, 36), N2 = c(24, 18)) %>% 
  group_by_all() %>% 
  reframe(
    BinaryPowerBSSR(
      asmd.p1 = 0.45, asmd.p2 = 0.09, p = seq(0, 1, by = 0.01),
      Delta.A = 0.36, Delta.T = 0.36, N1, N2, omega = 0.5, r, 
      alpha = 0.025, tar.power = 0.8, Test = 'Z-pool', 
      restricted, weighted
    )
  ) %>% 
  mutate(
    Rule = factor(Rule, levels = c('Restricted', 'Unrestricted', 'Weighted')),
    r_label = paste0('Allocation ratio = ', r, ':1')
  ) %>% 
  rename(Theta = p, Power = power.BSSR)

# Create the improved figure
power.BSSR %>% 
  ggplot(aes(x = Theta, y = Power)) +
  geom_line(aes(color = Rule), linewidth = 1.0) +
  theme_bw() +
  facet_grid(. ~ r_label) + 
  geom_hline(yintercept = 0.8, color = 'gray', linetype = 'longdash', linewidth = 1.0) +
  scale_x_continuous(
    limits = c(0.1, 0.9),
    breaks = seq(0.2, 0.8, by = 0.2),  # Fewer x-axis breaks
    labels = c("0.2", "0.4", "0.6", "0.8")
  ) +
  scale_y_continuous(
    limits = c(0.7, 1.0),
    breaks = seq(0.7, 1.0, by = 0.1),
    labels = c("0.7", "0.8", "0.9", "1.0")
  ) +
  scale_color_manual(
    values = c("Restricted" = "#E31A1C", "Unrestricted" = "#1F78B4", "Weighted" = "#33A02C")
  ) +
  labs(
    x = expression(theta),
    y = "Power",
    title = "Power Comparison: BSSR Design Rules",
    subtitle = "Dashed line indicates target power = 0.8"
  ) +
  theme(
    text = element_text(size = 11),
    plot.title = element_text(size = 13, hjust = 0.5, margin = margin(b = 5)),
    plot.subtitle = element_text(size = 10, hjust = 0.5, margin = margin(b = 10)),
    panel.spacing = unit(0.8, 'lines'),
    legend.position = 'bottom',
    legend.key.width = unit(1.2, 'cm'),
    legend.text = element_text(size = 10),
    legend.title = element_blank(),
    legend.margin = margin(t = 8, r = 0, b = 0, l = 0),
    axis.title.x = element_text(size = 11, margin = margin(t = 8)),
    axis.title.y = element_text(size = 11, margin = margin(r = 8)),
    axis.text = element_text(size = 9),
    strip.text = element_text(size = 10),
    plot.margin = margin(t = 10, r = 10, b = 5, l = 5)
  )

Understanding the Results

Design Rule Comparison

The plot above shows three different BSSR approaches:

Key Observations

  1. All designs maintain target power: The horizontal dashed line shows the target power of 0.8
  2. Weighted design shows robust performance: More consistent power across different pooled response rates
  3. Allocation ratio matters: Different allocation ratios (r=1 vs r=2) show different patterns

Statistical Tests Available

The package supports five different exact statistical tests:

Test Code Description Characteristics
Pearson chi-squared 'Chisq' One-sided exact test Good for large samples
Fisher exact 'Fisher' Classical conditional test Conservative, widely accepted
Fisher mid-p 'Fisher-midP' Less conservative Fisher Better power than Fisher exact
Z-pooled 'Z-pool' Unconditional exact test Good compromise
Boschloo 'Boschloo' Most powerful unconditional Highest power, computationally intensive

Test Comparison Example

# Compare different statistical tests
tests <- c('Chisq', 'Fisher', 'Fisher-midP', 'Z-pool', 'Boschloo')
test_results <- sapply(tests, function(test) {
  BinaryPower(p1 = 0.5, p2 = 0.2, N1 = 30, N2 = 30, alpha = 0.025, Test = test)
})

test_comparison_df <- data.frame(
  Test = tests,
  Power = round(test_results, 3)
)

print(test_comparison_df)
#>                    Test Power
#> Chisq             Chisq 0.692
#> Fisher           Fisher 0.596
#> Fisher-midP Fisher-midP 0.679
#> Z-pool           Z-pool 0.678
#> Boschloo       Boschloo 0.671

Practical Considerations

When to Use BSSR

BSSR is particularly valuable when: - Initial assumptions about response rates are uncertain - Regulatory efficiency is important - Maintaining study blinding is critical - Exact statistical control is required

Design Choice Guidelines

Next Steps

This vignette provided a basic introduction to bbssr. For more advanced topics, see: