Adaptive Trial Design
Advanced Statistical Methods
Learning and Adjusting During Clinical Trials
Adaptive trial designs allow pre-planned modifications to ongoing trials based on accumulating data, improving efficiency and ethics. Group sequential methods and alpha spending functions control overall error rates.
- Oncology — Drop ineffective treatment arms early and allocate more patients to promising therapies
- Rare diseases — Use Bayesian adaptive allocation to maximize learning from limited patient pools
- Vaccine trials — Interim analyses enable early stopping for efficacy or futility
Adaptive designs make clinical trials smarter by learning as they go.
"The adaptive design is not about being flexible during the trial — it is about being flexible before the trial begins." — Mehta & Pocock, 2011
Why Adaptive Designs?
Traditional fixed designs require specifying every detail before enrollment begins. When the trial begins, investigators must complete the entire study regardless of what the data reveal. This rigidity leads to:
- Wasted resources on ineffective doses or hopeless populations
- Ethical concerns when patients are randomized to arms that are clearly inferior
- Prolonged timelines when sample size assumptions are wrong
- Missed opportunities to enrich the study population mid-stream
Adaptive designs address these problems by embedding decision-making rules into the protocol.
Group Sequential Designs
Key Components
At each interim analysis , we compute a test statistic and compare it to a critical boundary . The trial stops at the first stage where .
The information fraction at stage is:
where is the sample size at stage and is the total planned sample size.
Alpha Spending Functions
Pocock boundaries spend alpha equally across analyses:
O'Brien-Fleming boundaries spend very little alpha early and concentrate it at the end:
Conditional Power
where is the observed information at stage , is the total planned information, is the assumed true effect, and is the null hypothesis value.
Bayesian Adaptive Designs
Bayesian adaptive designs use posterior distributions to make real-time decisions. Instead of fixed boundaries, we update the probability that each treatment arm is best.
Posterior Probability of Superiority
For two arms with binary outcomes:
Using Beta conjugate priors , the posterior is .
Thompson Sampling (Response-Adaptive Randomization)
Dose-Finding Designs
The Continual Reassessment Method (CRM)
After observing toxicity outcome at dose , the model parameters are re-estimated, and the next patient is assigned to the dose closest to the target toxicity level (typically 0.30 for Phase I).
BOIN Design
Sample Size Re-estimation
The conditional sample size at stage is:
where is the variance estimated from interim data and was the design assumption.
Operational Bias
Operational bias occurs when knowledge of interim results influences trial conduct:
- Investigator bias: Unconsciously enrolling different patients or managing side effects differently
- Patient selection bias: Choosing patients perceived as more likely to respond
- Endpoint adjudication bias: Subtle differences in how outcomes are classified
Regulatory Considerations
The FDA's 2019 guidance on adaptive designs categorizes modifications into:
| Category | Example | Regulatory Risk |
|---|---|---|
| Design refinement | Sample size re-estimation | Low |
| Sample size reassessment | Internal pilot study | Low–Moderate |
| Population enrichment | Enrichment for responders | Moderate |
| Treatment arm selection | Dropping inferior arms | Moderate–High |
| Endpoint switching | Changing primary endpoint | High |
Type I Error Control
The key challenge in adaptive designs is controlling the familywise error rate when multiple interim looks are conducted. For looks at significance level :
This is conservative. More efficient approaches include:
- Conditional error function (Müller & Schäfer, 2001): Preserves the conditional Type I error at each adaptation point
- Repeated significance testing with adjusted boundaries
- Promising zone designs that only increase sample size in the favorable region
Python Implementation
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
# --- Pocock and O'Brien-Fleming boundaries ---
def pocock_boundary(alpha, K):
"""Compute Pocock critical boundaries for K looks."""
from scipy.optimize import brentq
def objective(c):
spent = 0
for k in range(1, K + 1):
t_k = k / K
alpha_spent = alpha * np.log(1 + (np.e - 1) * t_k)
increment = alpha_spent - spent
spent = alpha_spent
# Simplified: equal increments
return 2 * K * (1 - stats.norm.cdf(c)) - alpha
c = brentq(objective, 0.001, 5.0)
return c
def obrien_fleming_boundary(alpha, K):
"""Compute O'Brien-Fleming critical boundaries."""
boundaries = []
z_alpha = stats.norm.ppf(1 - alpha / 2)
for k in range(1, K + 1):
t_k = k / K
c_k = z_alpha / np.sqrt(t_k)
boundaries.append(c_k)
return boundaries
# --- Simulate group sequential trial ---
np.random.seed(42)
n_per_stage = 50
K = 4 # Number of interim analyses + final
true_effect = 0.3 # True difference in means
results = []
cumulative_n = 0
for stage in range(1, K + 1):
# Generate data for this stage
control = np.random.normal(0, 1, n_per_stage)
treatment = np.random.normal(true_effect, 1, n_per_stage)
cumulative_n += n_per_stage
# Two-sample t-test
t_stat, p_val = stats.ttest_ind(treatment, control)
z_stat = t_stat # Large sample approximation
# O'Brien-Fleming boundary
boundaries = obrien_fleming_boundary(0.05, K)
boundary = boundaries[stage - 1]
results.append({
'stage': stage,
'n': cumulative_n,
'z_stat': z_stat,
'boundary': boundary,
'significant': abs(z_stat) >= boundary
})
print(f"Stage {stage}: n={cumulative_n}, Z={z_stat:.3f}, "
f"Boundary=±{boundary:.3f}, Stop={'YES' if abs(z_stat) >= boundary else 'NO'}")
# --- Visualize boundaries and test statistics ---
stages = [r['stage'] for r in results]
z_stats = [r['z_stat'] for r in results]
bounds = [r['boundary'] for r in results]
plt.figure(figsize=(10, 6))
plt.plot(stages, z_stats, 'bo-', label='Test statistic', markersize=8)
plt.plot(stages, bounds, 'r--', label='OBF boundary', linewidth=2)
plt.plot(stages, [-b for b in bounds], 'r--', linewidth=2)
plt.axhline(y=0, color='gray', linestyle='-', linewidth=0.5)
plt.xlabel('Interim Analysis Stage')
plt.ylabel('Test Statistic (Z)')
plt.title('Group Sequential Trial with O\'Brien-Fleming Boundaries')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('group_sequential.png', dpi=150)
plt.show()
# --- Bayesian dose-finding simulation ---
def simulate_crm(true_toxicities, target_toxicity=0.30, n_patients=30):
"""Simulate CRM dose-finding."""
n_doses = len(true_toxicities)
dose_levels = np.arange(1, n_doses + 1)
alpha0, beta0 = 1, 1 # Prior for each dose
n_assigned = np.zeros(n_doses)
n_toxic = np.zeros(n_doses)
allocation = []
for i in range(n_patients):
# Posterior means
posterior_mean = (alpha0 + n_toxic) / (alpha0 + beta0 + n_assigned)
# Find dose closest to target
diffs = np.abs(posterior_mean - target_toxicity)
selected_dose = np.argmin(diffs)
# Observe toxicity
tox = np.random.binomial(1, true_toxicities[selected_dose])
n_assigned[selected_dose] += 1
n_toxic[selected_dose] += tox
allocation.append(selected_dose + 1)
return allocation, n_toxic, n_assigned
true_tox = [0.05, 0.15, 0.30, 0.50, 0.70]
allocation, tox, assigned = simulate_crm(true_tox)
print("\n--- CRM Dose-Finding Results ---")
for d in range(len(true_tox)):
print(f"Dose {d+1}: Assigned={int(assigned[d])}, "
f"Toxicities={int(tox[d])}, "
f"Observed Tox Rate={tox[d]/max(assigned[d],1):.2f}, "
f"True Tox Rate={true_tox[d]:.2f}")
# Allocation plot
plt.figure(figsize=(8, 4))
plt.bar(range(1, len(true_tox)+1), assigned, color='steelblue', alpha=0.7)
plt.axvline(x=np.argmax(assigned) + 1, color='red', linestyle='--',
label=f'Most allocated: Dose {np.argmax(assigned)+1}')
plt.xlabel('Dose Level')
plt.ylabel('Number of Patients')
plt.title('CRM Dose Allocation (n=30 patients)')
plt.legend()
plt.tight_layout()
plt.savefig('crm_allocation.png', dpi=150)
plt.show()