Event-Driven Trials¶
Exact, simulation-free design calculations for two-arm comparative trials
whose primary analysis counts events, such as vaccine-efficacy or
prophylaxis trials with a success criterion on efficacy = 1 - rate ratio.
Given D total events and treatment-to-control person-time ratio rho,
treatment-arm events are Binomial(D, theta) with
theta = rho (1 - E) / (rho (1 - E) + 1). Conditioning on D removes the
unknown baseline rate, so the efficacy interval, power and group-sequential
boundaries are exact binomial calculations.
from trade_study import (
design_group_sequential,
events_required,
operating_characteristics,
)
events_required(0.7, margin=0.3) # 78 events for 90% power
design = design_group_sequential([39, 78], margin=0.3, spending="obf")
operating_characteristics(design, 0.7).power
Assumptions the caller establishes. Proportional event rates over
follow-up, a known person-time ratio, and independent events across
participants. Clustering (for example by village or household), outcome
misclassification and time-varying effects inflate variance or bias the
estimate. Assess them by simulation with run_grid and
power_curve.
Discreteness. Exact power is saw-toothed in the number of events.
events_required returns the smallest target beyond which power never falls
below the goal. Group-sequential boundaries spend at most the Lan-DeMets
alpha available at each look, so the alpha actually spent (alpha_spent) can
be below nominal; unspent alpha carries forward to later looks. Futility is
non-binding: boundaries ignore it, so type I error is controlled whether or
not it is applied.
Accrual. expected_events_by_period integrates expected events under
staggered uniform enrolment, exponential dropout and a cosine seasonal
hazard. periods_to_events reads off when an event target is expected.
All functions require SciPy (trade-study[stats]).
trade_study.efficacy_share(efficacy, rho=1.0)
¶
Expected treatment-arm share of events.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
efficacy
|
float
|
|
required |
rho
|
float
|
Treatment-to-control person-time ratio (positive). |
1.0
|
Returns:
| Type | Description |
|---|---|
float
|
|
Examples:
Source code in src/trade_study/trials.py
trade_study.efficacy_from_share(theta, rho=1.0)
¶
Invert :func:efficacy_share.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
theta
|
float
|
Treatment-arm share of events in [0, 1). |
required |
rho
|
float
|
Treatment-to-control person-time ratio. |
1.0
|
Returns:
| Type | Description |
|---|---|
float
|
The implied efficacy. |
Examples:
Source code in src/trade_study/trials.py
trade_study.EfficacyInterval(treatment_events, total_events, estimate, lower, upper, margin, confidence, success)
dataclass
¶
Exact conditional efficacy estimate and two-sided interval.
Attributes:
| Name | Type | Description |
|---|---|---|
treatment_events |
int
|
Events in the treatment arm. |
total_events |
int
|
Events in both arms. |
estimate |
float
|
Conditional maximum-likelihood efficacy ( |
lower |
float
|
Lower bound ( |
upper |
float
|
Upper bound. |
margin |
float
|
Success requires |
confidence |
float
|
Two-sided confidence level. |
success |
bool
|
Whether |
trade_study.efficacy_interval(treatment_events, total_events, *, rho=1.0, margin=0.0, confidence=0.95)
¶
Estimate efficacy with a Clopper-Pearson interval on the event share.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
treatment_events
|
int
|
Events in the treatment arm. |
required |
total_events
|
int
|
Events in both arms. |
required |
rho
|
float
|
Treatment-to-control person-time ratio. |
1.0
|
margin
|
float
|
Null efficacy margin for the success criterion. |
0.0
|
confidence
|
float
|
Two-sided confidence level in (0, 1). |
0.95
|
Returns:
| Type | Description |
|---|---|
EfficacyInterval
|
Estimate, exact bounds and success indicator. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If counts or levels are invalid. |
Examples:
>>> result = efficacy_interval(5, 40, margin=0.3)
>>> round(result.estimate, 3), result.success
(0.857, True)
Source code in src/trade_study/trials.py
trade_study.critical_count(total_events, *, rho=1.0, margin=0.0, confidence=0.95)
¶
Largest treatment-arm event count at which the success rule holds.
The rule is the exact conditional-binomial efficacy interval of
efficacy_interval: success means the lower bound exceeds margin.
Success is monotone in the count (the lower bound falls as the treatment
count rises), so the boundary is a single integer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
total_events
|
int
|
Total events |
required |
rho
|
float
|
Treatment-to-control person-time ratio. |
1.0
|
margin
|
float
|
Null efficacy margin. |
0.0
|
confidence
|
float
|
Two-sided confidence level. |
0.95
|
Returns:
| Type | Description |
|---|---|
int
|
The largest count |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
Source code in src/trade_study/trials.py
trade_study.conditional_power(total_events, efficacy, *, rho=1.0, margin=0.0, confidence=0.95)
¶
Exact power of the fixed-sample success rule given D events.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
total_events
|
int
|
Total events |
required |
efficacy
|
float
|
True efficacy. |
required |
rho
|
float
|
Treatment-to-control person-time ratio. |
1.0
|
margin
|
float
|
Null efficacy margin. |
0.0
|
confidence
|
float
|
Two-sided confidence level. |
0.95
|
Returns:
| Type | Description |
|---|---|
float
|
Probability that the lower bound exceeds |
Examples:
Source code in src/trade_study/trials.py
trade_study.events_required(efficacy, *, power=0.9, rho=1.0, margin=0.0, confidence=0.95, max_events=2000)
¶
Smallest event target whose exact power, and every larger one, meets power.
Exact power is saw-toothed in D; requiring every larger target up to
max_events to qualify avoids selecting an isolated peak.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
efficacy
|
float
|
Assumed true efficacy. |
required |
power
|
float
|
Target power in [0, 1]. |
0.9
|
rho
|
float
|
Treatment-to-control person-time ratio. |
1.0
|
margin
|
float
|
Null efficacy margin. |
0.0
|
confidence
|
float
|
Two-sided confidence level. |
0.95
|
max_events
|
int
|
Search limit. |
2000
|
Returns:
| Type | Description |
|---|---|
int
|
The event target. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If no target up to |
Examples:
Source code in src/trade_study/trials.py
trade_study.unconditional_power(expected_control_events, efficacy, *, rho=1.0, margin=0.0, confidence=0.95)
¶
Power of a fixed-duration trial with a Poisson event total.
The total is Poisson(mu_c (1 + rho (1 - E))) and the analysis
conditions on its realized value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expected_control_events
|
float
|
Expected control-arm events. |
required |
efficacy
|
float
|
True efficacy. |
required |
rho
|
float
|
Treatment-to-control person-time ratio. |
1.0
|
margin
|
float
|
Null efficacy margin. |
0.0
|
confidence
|
float
|
Two-sided confidence level. |
0.95
|
Returns:
| Type | Description |
|---|---|
float
|
Probability of meeting the success criterion. |
Examples:
Source code in src/trade_study/trials.py
trade_study.alpha_spending(fraction, alpha, kind='obf')
¶
Lan-DeMets cumulative one-sided alpha spent by an information fraction.
"obf": 2 (1 - Phi(z_{1 - alpha/2} / sqrt(t))).
"pocock": alpha log(1 + (e - 1) t).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fraction
|
float
|
Information fraction in (0, 1]. |
required |
alpha
|
float
|
Total one-sided alpha. |
required |
kind
|
Literal['obf', 'pocock']
|
Spending-function family. |
'obf'
|
Returns:
| Type | Description |
|---|---|
float
|
Cumulative alpha spent. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> round(alpha_spending(1.0, 0.025), 6)
0.025
>>> alpha_spending(0.5, 0.025) < alpha_spending(0.5, 0.025, "pocock")
True
Source code in src/trade_study/trials.py
trade_study.GroupSequentialDesign(looks, critical, alpha_spent, margin, alpha, rho, spending)
dataclass
¶
Exact efficacy boundaries for an event-driven trial.
Attributes:
| Name | Type | Description |
|---|---|---|
looks |
tuple[int, ...]
|
Cumulative event totals at each analysis. |
critical |
tuple[int, ...]
|
Largest cumulative treatment-arm count that stops for
efficacy at each look ( |
alpha_spent |
tuple[float, ...]
|
Cumulative one-sided alpha actually spent at each look. |
margin |
float
|
Null efficacy margin. |
alpha |
float
|
Nominal total one-sided alpha. |
rho |
float
|
Treatment-to-control person-time ratio. |
spending |
str
|
Spending-function family. |
trade_study.design_group_sequential(looks, *, margin=0.0, alpha=0.025, rho=1.0, spending='obf')
¶
Compute exact efficacy boundaries under Lan-DeMets alpha spending.
At each look the critical count is the largest whose exact null probability of first crossing does not exceed the alpha newly available. Discreteness means the alpha actually spent can be below the nominal amount; each look's budget is the spending function minus the alpha actually spent so far, so unspent alpha carries forward to later looks.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
looks
|
list[int]
|
Strictly increasing positive cumulative event totals; the last is the final analysis. |
required |
margin
|
float
|
Null efficacy margin ( |
0.0
|
alpha
|
float
|
One-sided type I error. |
0.025
|
rho
|
float
|
Treatment-to-control person-time ratio. |
1.0
|
spending
|
Literal['obf', 'pocock']
|
|
'obf'
|
Returns:
| Type | Description |
|---|---|
GroupSequentialDesign
|
The design. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
Source code in src/trade_study/trials.py
trade_study.OperatingCharacteristics(efficacy, efficacy_stop, futility_stop, power, expected_events, futility_threshold)
dataclass
¶
Exact stopping behaviour of a group-sequential design.
Attributes:
| Name | Type | Description |
|---|---|---|
efficacy |
float
|
Assumed true efficacy. |
efficacy_stop |
tuple[float, ...]
|
Probability of stopping for efficacy at each look. |
futility_stop |
tuple[float, ...]
|
Probability of stopping for futility at each look (always zero at the final look). |
power |
float
|
Total probability of an efficacy conclusion. |
expected_events |
float
|
Expected event total at stopping. |
futility_threshold |
float | None
|
Interim futility rule (stop when the efficacy
point estimate is at most this), or |
trade_study.operating_characteristics(design, efficacy, *, futility_threshold=0.0)
¶
Exact stopping probabilities, power and expected events.
The futility rule is non-binding: boundaries ignore it, so type I error is controlled whether or not it is followed, and applying it here only lowers power.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
design
|
GroupSequentialDesign
|
Efficacy boundaries. |
required |
efficacy
|
float
|
Assumed true efficacy. |
required |
futility_threshold
|
float | None
|
Stop at an interim look when the point estimate
is at most this; |
0.0
|
Returns:
| Type | Description |
|---|---|
OperatingCharacteristics
|
Operating characteristics. |
Examples:
>>> design = design_group_sequential([80], margin=0.3)
>>> oc = operating_characteristics(design, 0.3, futility_threshold=None)
>>> oc.power <= 0.025
True
Source code in src/trade_study/trials.py
trade_study.max_events_for_power(fractions, efficacy, *, power=0.9, margin=0.0, alpha=0.025, rho=1.0, spending='obf', futility_threshold=0.0, max_events=2000)
¶
Smallest maximum event total whose group-sequential power meets power.
Looks are at round(fraction * total); the scan starts at the fixed
design's event requirement minus 20%. Like fixed designs, discrete power
can be non-monotone in the total, so the returned design is the first
qualifying one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fractions
|
list[float]
|
Information fractions of each look, ending at 1. |
required |
efficacy
|
float
|
Assumed true efficacy. |
required |
power
|
float
|
Target power. |
0.9
|
margin
|
float
|
Null efficacy margin. |
0.0
|
alpha
|
float
|
One-sided type I error. |
0.025
|
rho
|
float
|
Treatment-to-control person-time ratio. |
1.0
|
spending
|
Literal['obf', 'pocock']
|
|
'obf'
|
futility_threshold
|
float | None
|
Interim futility rule used for planning power. |
0.0
|
max_events
|
int
|
Search limit. |
2000
|
Returns:
| Type | Description |
|---|---|
GroupSequentialDesign
|
The first qualifying design. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If no design up to |
Examples:
>>> design = max_events_for_power([0.5, 1.0], 0.7, margin=0.3)
>>> design.looks[-1] >= events_required(0.7, margin=0.3) - 10
True
Source code in src/trade_study/trials.py
trade_study.AccrualPlan(enrolled, ramp_periods=0.0, dropout_per_period=0.0, seasonal_amplitude=0.0, peak_period=0.0, season_length=12.0)
dataclass
¶
Enrolment, dropout and seasonality assumptions in calendar time.
Attributes:
| Name | Type | Description |
|---|---|---|
enrolled |
int
|
Total participants (both arms, 1:1). |
ramp_periods |
float
|
Periods to complete uniform enrolment (0 = all at start). |
dropout_per_period |
float
|
Loss-to-follow-up hazard per period. |
seasonal_amplitude |
float
|
Relative amplitude of a cosine seasonal hazard, in [0, 1). |
peak_period |
float
|
Period of peak hazard. |
season_length |
float
|
Periods per seasonal cycle. |
trade_study.expected_events_by_period(plan, *, control_rate, efficacy, periods, substeps=8)
¶
Expected cumulative events (both arms) at the end of each period.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
plan
|
AccrualPlan
|
Enrolment and seasonality assumptions. |
required |
control_rate
|
float
|
Control-arm event rate per participant per period. |
required |
efficacy
|
float
|
True efficacy. |
required |
periods
|
int
|
Horizon. |
required |
substeps
|
int
|
Midpoint-rule substeps per period. |
8
|
Returns:
| Type | Description |
|---|---|
NDArray[float64]
|
Cumulative expected events at periods |
Examples:
>>> plan = AccrualPlan(enrolled=1000)
>>> events = expected_events_by_period(
... plan, control_rate=0.01, efficacy=0.0, periods=2
... )
>>> [round(float(x), 6) for x in events]
[10.0, 20.0]
Source code in src/trade_study/trials.py
trade_study.periods_to_events(cumulative, target)
¶
First period at whose end expected cumulative events reach target.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cumulative
|
NDArray[float64]
|
Output of :func: |
required |
target
|
float
|
Event target. |
required |
Returns:
| Type | Description |
|---|---|
float
|
Period index (1-based), or |
Examples: