To solve this, use an alpha-spending O'Brien–Fleming (Lan-DeMets) approximation to compute, at each interim, the cumulative alpha spent and corresponding two-sided critical z. Compute a z-statistic for difference in proportions (two-sample z-test, pooled variance). Stop for efficacy if |z| > z_crit. For futility, use a simple conditional-power rule: project final z if current effect persists to planned final sample; if conditional power < threshold (e.g., 0.2), stop for futility.python
import math
from scipy.stats import norm
def obrien_fleming_alpha_spent(info_frac, alpha=0.05):
"""
Lan-DeMets O'Brien-Fleming approximate spending:
alpha(t) = 2 * (1 - Phi( Z_{1-alpha/2} / sqrt(t) ))
where t in (0,1] is information fraction.
"""
if info_frac <= 0:
return 0.0
z_half = norm.ppf(1 - alpha / 2.0)
spent = 2.0 * (1.0 - norm.cdf(z_half / math.sqrt(info_frac)))
return min(max(spent, 0.0), alpha)
def two_sample_z(success_a, n_a, success_b, n_b):
# pooled proportion
p_pool = (success_a + success_b) / (n_a + n_b)
se = math.sqrt(p_pool * (1 - p_pool) * (1.0 / n_a + 1.0 / n_b))
if se == 0:
return 0.0
p_diff = (success_b / n_b) - (success_a / n_a)
return p_diff / se # positive means treatment > control
def sequential_decision(cum_success_ctrl, cum_n_ctrl, cum_success_trt, cum_n_trt,
planned_total_ctrl, planned_total_trt,
interim_index, total_interims,
alpha=0.05, futility_cp_thresh=0.2):
"""
Returns: 'stop-for-efficacy', 'stop-for-futility', or 'continue'
Assumptions & limitations (in-code comment):
- Uses two-sided O'Brien-Fleming alpha spending approximation.
- Assumes information fraction ~ (current total N) / (planned final total N).
- Uses two-sample pooled z-test (large-sample approx).
- Futility rule: conditional power under current observed effect projecting to final sample.
This is a pragmatic approximate rule (not exact group-sequential beta spending).
- Planned totals must be > current totals. If equal, treat as final analysis.
- Does not adjust for covariates, unequal allocation beyond planned, or multiplicity beyond interims.
"""
# compute information fraction (based on total subjects)
current_info = (cum_n_ctrl + cum_n_trt)
planned_info = (planned_total_ctrl + planned_total_trt)
info_frac = min(current_info / max(planned_info, 1), 1.0)
# alpha spent so far at this info fraction
alpha_spent = obrien_fleming_alpha_spent(info_frac, alpha=alpha)
# two-sided critical z at this interim
z_crit = norm.ppf(1 - alpha_spent / 2.0)
z = two_sample_z(cum_success_ctrl, cum_n_ctrl, cum_success_trt, cum_n_trt)
# Efficacy stopping (two-sided)
if abs(z) >= z_crit:
return 'stop-for-efficacy'
# Futility: compute conditional power projecting current observed effect to planned final sample
# approximate final standard error assuming planned sample sizes
p_ctrl = cum_success_ctrl / cum_n_ctrl if cum_n_ctrl > 0 else 0.0
p_trt = cum_success_trt / cum_n_trt if cum_n_trt > 0 else 0.0
observed_diff = p_trt - p_ctrl
# final SE under pooled variance estimated from current pooled p
pooled_p = (cum_success_ctrl + cum_success_trt) / max(current_info, 1)
se_final = math.sqrt(pooled_p * (1 - pooled_p) * (1.0 / planned_total_ctrl + 1.0 / planned_total_trt))
if se_final == 0:
return 'continue'
# expected final z if observed_diff persists
expected_final_z = observed_diff / se_final
# conditional power approx: probability that final |Z| > z_final_crit given current estimate
# For simplicity use final alpha ~ overall alpha at info_frac=1
alpha_final = obrien_fleming_alpha_spent(1.0, alpha=alpha)
z_final_crit = norm.ppf(1 - alpha_final / 2.0)
# approximate as one-sided normal tail considering effect direction
# use two-sided threshold converted to one-sided by symmetry
cp = 1.0 - norm.cdf(z_final_crit - abs(expected_final_z)) # approx
if cp < futility_cp_thresh:
return 'stop-for-futility'
return 'continue'
Key points:- Time complexity O(1) per interim. Space O(1).- Edge cases: very small sample sizes violate normal approx; pooled variance zero when proportions 0/1.- Alternatives: use exact sequential tests (e.g., Pocock boundaries, fully specified alpha/beta spending, Bayesian predictive probability) or simulate operating characteristics to calibrate futility threshold.