axiom

Case study · all five pillars

HYPER-3: the trial that has to stop an arm

Adults with elevated blood pressure are randomized to standard care or to one of three daily doses of an investigational agent. Seated systolic pressure is measured weekly for 24 weeks. Everything below is computed with axiom against a synthetic world whose truth we know — which is the only way to check that a method catches what it is supposed to catch.

The protocol

400 patients, four arms, three age bands

Randomization is stratified by age band in 2:1:1:1 blocks, so the shared control arm is twice the size of each dose arm. Enrollment is staggered over 16 calendar weeks, which is what makes an interim review a real event: at any given review some patients have months of follow-up and some have weeks.

ARMS

Four

standard of care, 10 mg, 20 mg and 40 mg daily.

80 patients in each dose arm; 160 on standard care

STRATA

Three age bands

25-35, 36-50 and 51+, in shares 25% / 40% / 35%.

clearance is slowest in the oldest band — which is where the trouble is

REVIEWS

6 safety looks

At calendar weeks 12, 14, 16, 18, 20, 22, monitoring 12 contrasts — every dose arm, pooled and inside each age band.

12 × 6 chances to stop something early

The truth the trial is trying to find

Benefit in the young cancels harm in the old

The agent lowers blood pressure through a saturating dose–response. It also has a superlinear pressor effect that is negligible below 20 mg and concentrated in the oldest band, where clearance is slowest. Below is what that does to the net benefit — and why looking only at the arm average is not a conservative choice.

Change in systolic pressure versus control at week 12

Below zero is a fall in blood pressure, which is the point of the drug. Above zero is harm. This is the intention-to-treat contrast — the quantity randomization actually licenses, averaged over how much medication people take.

25–35 36–50 51+ pooled over the trial population

hyper3.intent_to_treat_contrast(dose, stratum, week=12) — the data-generating process itself, integrated over the adherence distribution. In a real trial this curve is exactly what you do not have.

Why the average is the dangerous statistic here

Follow the grey line to 40 mg. Pooled over the whole trial population the contrast is -0.64 mmHg — which is to say the top dose looks indistinguishable from doing nothing. Unremarkable, not alarming. A monitoring rule watching that number has nothing to fire on.

Now split it. At the same dose the youngest band is at -5.90 — a substantial benefit — while the oldest is at +5.85, a substantial rise in blood pressure. The average is near zero because the two are cancelling. Averaging is not the conservative choice here; it is the choice that destroys the only signal that matters.

Worth noting what the dose is doing to the pooled curve even so: at 20 mg the pooled contrast is -4.30 mmHg, and going up to 40 mg moves it to -0.64. The higher dose is worse on average — but “worse than the middle dose” is not the same alarm as “harming a quarter of the patients”, and only one of those triggers a stopping rule.

The rule, written before the data

A boundary the statistic has to cross

The protocol commits in advance: stop an arm when there is at least a 95% posterior probability that it is worse than control by more than 2 mmHg. axiom turns that sentence into a Z threshold at every scheduled review, accounting for how much information has accumulated by then.

the rule, for one contrast
information = information_fractions(enrolled_by_look, n_total)
schedule    = LookSchedule(labels=..., information=information)

rule = StoppingRule(
    name="harm|age_51_plus|dose_40",
    looks=schedule,
    boundaries=(harm_boundary(
        0.95,            # posterior probability that fires the rule
        information,
        margin=2.0,        # mmHg worse than control
        se_at_full_information=se,
    ),),
)

path = monitor(rule, observed_z, effects=effects, ses=ses)

Two things are worth noticing. The boundary is derived from the protocol sentence rather than chosen from a table — change the margin or the probability and the thresholds move accordingly. And the same StoppingRule object is used both to check the realized data and to compute the rule’s operating characteristics before the trial opens, so the thing being evaluated is the thing that will actually run.

Operating characteristics of this rule, computed exactly rather than simulated. Drift is the true standardized harm; 0 means the arm is genuinely no worse than control.
true driftP(stop at some review) expected reviews takenexpected information used
+0.0 — no harm 1.0% 5.96 0.995
-1.0 6.0% 5.78 0.972
-2.0 24.3% 5.18 0.897
-3.0 58.2% 3.98 0.748
-4.0 — severe 87.4% 2.60 0.575

The rule's power curve

How often the rule stops an arm, against how much harm is genuinely there. The table states five points; this is the shape between them, which is what says whether a rule is trigger-happy or asleep.

axiom.design.operating_characteristics(rule, drift) at each drift — computed exactly, not simulated

Under no harm at all the rule fires 1.0% of the time — a low false-stop rate is what buys the right to look six times. Under severe harm it fires 87% of the time and, on average, after 2.6 reviews rather than all 6.

The trial runs

Pooled it is fine. Split by age it is not.

All four panels below monitor the same 40 mg arm against the same protocol rule, on the same vertical scale. The only difference is who is counted.

Safety monitoring of the 40 mg arm, four ways

Shaded region is harm. Faint dotted continuations are reviews the arm never reached, because it had already been stopped.

harm boundary pooled 25–35 36–50 51+

axiom.design.monitor(rule, z, effects, ses) per contrast · effects from axiom.identify.ols with ANCOVA covariates at each look · seed 20260821

The crossing, in the protocol’s own words

0.988

P(40 mg is worse than control by more than 2 mmHg | data) — against a rule that fires at 0.95

decision stop_harm at review 1 of 6 — calendar week 12, the first scheduled look stopped
effect +6.58 mmHg against control (se 2.04, n = 33) — blood pressure higher on treatment Z -3.221
threshold the boundary at this information fraction (0.375) -2.527
pooled arm the same arm, all 400 patients: never below Z = +0.45 at any of the six reviews never fires

Stopping early costs you something, and axiom records what

The decision writes a ledger line: “harm|age_51_plus|dose_40: harm boundary crossed at look 1 (week_12), Z = -3.221 against -2.527, at information fraction 0.375”

The assumption it carries is stopped_estimate_bias — an estimate reported at the look that stopped the study is biased away from the null; its naive interval does not have the nominal coverage It is challenged by: a median-unbiased or stage-wise-ordered estimate computed for the same path

This matters more than it sounds. An arm stopped at a boundary crossing has a biased effect estimate by construction: you stopped precisely because the reading was extreme, so the reading overstates the effect. The right thing for a library to do is not to hide that, and not to silently correct it either, but to record it as an assumption attached to the number so that whoever writes the report knows a stage-wise-ordered estimate is owed.

Every monitored contrast

Twelve contrasts, six reviews each. Exactly one fired.

The lowest Z each contrast ever reached

Sorted worst first. Eleven of the twelve never leave the middle of the range; the one in red is the 40 mg arm inside the oldest band, and it is the reason the trial has a stratum-level rule at all.

axiom.design.monitor(rule, z, effects, ses) per contrast · hover a row for what it did

Lowest Z reached by each contrast over the reviews it took. Lower is worse; the boundary sits near −2.5 at the first look and tightens after.
stratumarmreviews takenlowest Z decisionstopped at
all units10 mg 6+1.91 completed—
all units20 mg 6+1.55 completed—
all units40 mg 6+0.45 completed—
25-3510 mg 6+1.31 completed—
25-3520 mg 6+1.12 completed—
25-3540 mg 6+1.86 completed—
36-5010 mg 6+1.29 completed—
36-5020 mg 6+1.04 completed—
36-5040 mg 6+1.08 completed—
51+10 mg 6+0.33 completed—
51+20 mg 6-0.13 completed—
51+40 mg 1-3.22 stop_harm week 12

What it took

Every pillar showed up

The stopping decision is one line of output. Getting to a trial where that line means something used the whole toolkit.

The six notebooks

This page is a summary. The full case study lives in the repository at nbs/case-studies/hypertension/ — six executed notebooks over one synthetic world, from the causal graph through sizing, the dose–response, the sequential design, and the review that stops the arm. Its design notes are in docs/notes/0004-case-study-hypertension.md.