Group Sequential Testing

Group sequential testing analyzes the data at several prespecified points and allows the study to stop at any of them, while keeping the overall false positive rate at the level you chose. It is the answer to the fact that fixed-horizon testing does not allow an unadjusted interim significance result to serve as an early efficacy declaration. Stopping for safety or operational faults is a different decision and may require an adjusted inferential analysis.

The mechanism is a budget. The 0.05 is spent across the planned looks rather than used once, so each look is judged against a stricter threshold than 0.05 and the total comes out right.

The budget, measured

import numpy as np

rng = np.random.default_rng(0)
N = 400_000

def looks(K, drift=0.0):
    """Cumulative z at K equally spaced looks. drift is the true effect,
       scaled so that drift=1 means the design's target effect at full data."""
    steps = rng.normal(size=(N, K)) / np.sqrt(K)
    B = np.cumsum(steps, axis=1) + drift * 2.8016 * np.arange(1, K + 1) / K
    return B / np.sqrt(np.arange(1, K + 1) / K)

POCOCK = {1: 1.960, 2: 2.178, 3: 2.289, 4: 2.361, 5: 2.413, 10: 2.555}
print(f"{'looks':>7}{'naive 1.96':>13}{'Pocock constant':>18}{'with that boundary':>21}")
for K, c in POCOCK.items():
    z = looks(K)
    naive = float((np.abs(z) >= 1.96).any(axis=1).mean())
    spent = float((np.abs(z) >= c).any(axis=1).mean())
    print(f"{K:>7}{naive:>13.4f}{c:>18.3f}{spent:>21.4f}")

print(f"\n{'design':>28}{'power':>9}{'average looks used':>21}{'of the full sample':>21}")
for name, bounds in (("fixed horizon, one test", [np.inf, np.inf, np.inf, 1.960]),
                     ("Pocock, 4 looks", [2.361] * 4),
                     ("O'Brien-Fleming, 4 looks", [4.048, 2.862, 2.337, 2.024])):
    z = looks(4, drift=1.0)
    crossed = np.abs(z) >= np.asarray(bounds)
    stopped = crossed.any(axis=1)
    first = np.where(stopped, crossed.argmax(axis=1) + 1, 4)
    print(f"{name:>28}{stopped.mean():>9.4f}{first.mean():>21.2f}{first.mean() / 4:>20.1%}")
#   looks   naive 1.96   Pocock constant   with that boundary
#       1       0.0503             1.960               0.0503
#       2       0.0828             2.178               0.0499
#       3       0.1075             2.289               0.0503
#       4       0.1254             2.361               0.0501
#       5       0.1410             2.413               0.0502
#      10       0.1927             2.555               0.0496
#
#                       design    power   average looks used   of the full sample
#      fixed horizon, one test   0.8003                 4.00              100.0%
#              Pocock, 4 looks   0.7192                 2.87               71.8%
#     O'Brien-Fleming, 4 looks   0.7917                 3.26               81.6%

The first table puts the problem and the fix side by side. Testing at 1.96 at every look gives 0.0828 with two looks, 0.1254 with four, and 0.1927 with ten. Replace 1.96 with the constant for that number of looks and every row returns to 0.05 — 0.0499, 0.0501, 0.0496.

Read the constants column rather than the result. The threshold rises with the number of looks you planned: 2.178 for two, 2.361 for four, 2.555 for ten. The design has to be declared in advance because the threshold depends on it. Deciding to add a fifth look after seeing the fourth is not a group sequential design; it is peeking with extra steps.

The general form of this is an alpha spending function, which says what fraction of the budget has been consumed by each fraction of the data. Two shapes dominate practice: Pocock uses a constant nominal boundary, not equal first-crossing alpha at each look; O'Brien–Fleming uses high early boundaries and much lower later ones. The Lan–DeMets formulation expresses either as a continuous function of information, which is what allows the looks to happen at times that were not exactly as planned.

What the saving actually costs

The second table is the part that is usually stated as a free lunch. At the effect the study was sized for, the fixed design reaches power 0.8003 using 100% of the data. Pocock uses 71.8% of it — and reaches power 0.7192. O'Brien–Fleming uses 81.6% and reaches 0.7917.

So at the same planned maximum, the expected saving is paid for in power: eight points for Pocock, one point for O'Brien–Fleming. The standard remedy is to raise the planned maximum a little — a few percent for O'Brien–Fleming, more for Pocock — so that power is preserved and the saving is still there in expectation. That inflation is the real price, and it means a group sequential design can end up using more data than the fixed design when the effect turns out to be small.

The honest summary is that these designs trade a worse worst case for a better average. Whether that is the right trade depends on what an extra week of traffic costs relative to the value of stopping a month early.

Efficacy, futility, and what to report

Boundaries come in two kinds and they behave differently. An efficacy boundary stops for a positive result and has to be paid for out of the alpha budget. A futility boundary stops when the effect is too small to be worth continuing; it costs power rather than alpha, and it is often the more valuable of the two in business settings, where most tested ideas do nothing.

One consequence is easy to miss when reporting. An estimate taken at the moment a boundary was crossed is biased away from zero, because crossing required an unusually large observed effect. Group sequential methods have adjusted estimators and confidence intervals for exactly this reason; quoting the raw effect from the stopping look overstates it.

Finally, the discipline these designs require is the same discipline fixed-horizon testing requires, applied to more decisions. The number of looks, their timing, the spending function, and the futility rule all belong in the plan written before the data. What group sequential testing removes is the need to choose between statistical validity and stopping early — not the need to decide in advance.

References

  • NumPy documentation, numpy.random.Generator.normal. numpy.org
  • SciPy documentation, scipy.stats.norm. docs.scipy.org
  • gsDesign technical manual, Spending functions. Source

Discover more from Insightful Data Lab

Subscribe to get the latest posts sent to your email.

Similar Posts

Questions, corrections, or additional insights?

This site uses Akismet to reduce spam. Learn how your comment data is processed.