The “Two-arm randomized trials” vignette uses
method = "logrank" and a single constant hazard for each
treatment group. That is convenient when the proportional-hazards
assumption is reasonable and the underlying event rate is
well-approximated by an exponential distribution. In practice, neither
assumption always holds. This vignette shows how to set up a Goldilocks
design with:
method = "bayes-surv") using a posterior probability
threshold on the cumulative-failure-probability scale.We use a small example so that the simulation can be run while
reading the vignette; the live survival_adapt() chunk takes
around 10–20 seconds and is cached on first knit.
Survival in many settings – post-surgical mortality, transplant outcomes, oncology with early treatment toxicity – shows a higher hazard early and a lower steady-state hazard later. A single exponential rate cannot capture both regimes; using the average rate over-states early survival and under-states late survival. A piecewise-exponential model with a single internal cut-point is often a good compromise, fitting one hazard for the early interval and another for everything afterwards.
The package handles piecewise hazards through two related arguments:
cutpoints: the positive interior times at which the
hazard changes. With \(J - 1\)
cutpoints there are \(J\) intervals and
therefore \(J\) hazard rates for each
treatment group. For example, cutpoints = 6 produces two
intervals, \([0, 6)\) and \([6, \infty)\). The default
cutpoints = NULL supplies no change times and produces a
single, constant hazard interval.hazard_treatment and hazard_control:
vectors of length \(J\) giving the
constant hazard for each interval.The helper prop_to_haz() maps cumulative event
probabilities at given times into the corresponding piecewise
hazards.
Suppose the primary endpoint is overall survival at 24 months. Based on prior data, we expect the control arm to have:
The treatment is hypothesised to attenuate the early-period hazard, leaving the late-period hazard unchanged. Concretely, we target:
We set the cut-point at 6 months and translate these proportions into piecewise hazards:
cutpoints <- 6 # one internal cut at 6 months -> two intervals
end_of_study <- 24
hc <- prop_to_haz(probs = c(0.30, 0.50), cutpoints = cutpoints, endtime = end_of_study)
ht <- prop_to_haz(probs = c(0.18, 0.40), cutpoints = cutpoints, endtime = end_of_study)
round(rbind(control = hc, treatment = ht), 4)
#> [,1] [,2]
#> control 0.0594 0.0187
#> treatment 0.0331 0.0174The first column is the hazard during \([0,
6)\) months and the second column is the hazard from 6 months
onward. Both arms have a higher hazard in the early window. We can
sanity-check that the implied survival probabilities at 24 months match
what we specified by running the cumulative incidence computation back
through ppwe():
ppwe(hazard = matrix(hc, nrow = 1),
cutpoints = cutpoints,
end_of_study = end_of_study)
#> [1] 0.5
ppwe(hazard = matrix(ht, nrow = 1),
cutpoints = cutpoints,
end_of_study = end_of_study)
#> [1] 0.4These should be 0.50 and 0.40 respectively (modulo rounding).
With method = "bayes-surv",
survival_adapt() puts independent \(\operatorname{Gamma}(\alpha, \beta)\)
priors on each piecewise hazard rate (one per interval, per treatment
group) and combines them with the observed exposure time and event
counts to obtain a closed-form Gamma posterior on each \(\lambda_j\). Posterior draws of \(\lambda_j\) are pushed through the
piecewise-exponential cumulative incidence function to obtain posterior
draws of the cumulative-failure probability \(p\) at end_of_study for the
treatment and control groups.
The decision rule is one-sided. The treatment effect is defined as
\[\Delta = p_{\text{treatment}} - p_{\text{control}},\]
i.e. the difference in failure (not survival) probabilities at
end_of_study. On the survival scale this is equivalent to
\(S_{\text{control}} -
S_{\text{treatment}}\), with a negative \(\Delta\) corresponding to the treatment
having higher survival. With alternative = "less" and a
margin h0 (default 0), the trial declares
success at the final analysis when
\(\Pr(\Delta < h_0 \mid \text{data}) \;>\; \texttt{prob\_ha}.\)
Because a beneficial treatment has a lower failure
probability, alternative = "less" is the appropriate choice
here. (method = "bayes-surv" does not allow
alternative = "two.sided" – it raises an error.)
The same posterior is also used at each interim look to compute
predictive probabilities of success. Imputed completions are drawn from
the posterior predictive distribution of the piecewise-exponential model
for subjects still under follow-up, and the analysis is repeated on each
imputed dataset. The fraction of imputations that would declare success
after enrollment continues to the maximum sample size is compared with
Fn for the futility rule. Separately, the fraction that
would declare success after completing follow-up for the subjects
currently enrolled is compared with Sn for the
expected-success rule.
We use an independent weakly informative \(\operatorname{Gamma}(0.1, 0.1)\) prior on every hazard component:
Before running the adaptive design, it is helpful to inspect the
subject-level data generated by sim_comp_data(). The
following example uses the same hazards, follow-up horizon, and monthly
time unit as the design below:
set.seed(7195)
example_trial_data <- sim_comp_data(
hazard_treatment = ht,
hazard_control = hc,
cutpoints = cutpoints,
N_total = 12,
lambda = 5,
lambda_time = NULL,
end_of_study = end_of_study,
block = 4,
rand_ratio = c(1, 1),
prop_loss = 0.05
)
knitr::kable(head(example_trial_data), digits = 2)| time | treatment | event | enrollment | id | loss_to_fu |
|---|---|---|---|---|---|
| 24.00 | 1 | 0 | 0.00 | 1 | FALSE |
| 3.61 | 0 | 1 | 0.34 | 2 | FALSE |
| 1.90 | 0 | 1 | 0.35 | 3 | FALSE |
| 4.58 | 1 | 0 | 0.63 | 4 | TRUE |
| 19.77 | 0 | 1 | 0.72 | 5 | FALSE |
| 3.10 | 0 | 1 | 0.80 | 6 | FALSE |
Each row represents one simulated subject:
time is follow-up time from enrollment/randomization to
the event or censoring, measured in months here.treatment is the randomized arm: 1 for
treatment and 0 for control.event is 1 when the event was observed and
0 when follow-up was right-censored.enrollment is trial-calendar time from first patient in
to that subject’s enrollment, also measured in months here.id is the subject’s simulated identifier.loss_to_fu indicates whether the subject was censored
because of simulated loss to follow-up.The time and enrollment columns therefore
use the same unit but different clocks: time is
subject-relative follow-up, whereas enrollment is
trial-calendar time.
We will run one trial under the alternative hypothesis to illustrate
the mechanics. We choose interim_look = 60 (the minimum
required is max(block) = 4) and a constant accrual rate of
5 enrollments per month. With lambda_time = NULL, the first
patient is placed at time zero and subsequent inter-arrival gaps are
generated exactly from an exponential distribution with rate 5.
Piecewise accrual is specified using positive internal knots only; for
example, lambda = c(2, 5) and lambda_time = 6
means 2 enrollments per month before month 6 and 5 thereafter. These
calendar-time enrollment knots are distinct from the subject-follow-up
hazard cutpoints used above.
set.seed(7194)
out <- survival_adapt(
hazard_treatment = ht,
hazard_control = hc,
cutpoints = cutpoints,
N_total = 100,
lambda = 5, # enrollments per month
lambda_time = NULL, # no internal enrollment-rate knots
interim_look = 60,
end_of_study = end_of_study,
prior = prior,
block = 4,
rand_ratio = c(1, 1),
prop_loss = 0.05,
alternative = "less",
h0 = 0,
Fn = 0.05,
Sn = 0.95,
prob_ha = 0.975,
N_impute = 50,
N_mcmc = 2000,
method = "bayes-surv")
out
#> prob_threshold margin alternative N_treatment N_control N_enrolled N_max
#> 1 0.975 0 less 50 50 100 100
#> post_prob_ha est_final ppp_success stop_futility stop_expected_success
#> 1 0.969 -0.1761562 0.12 0 0The output reports the posterior probability of the alternative at
the final (or stopped) analysis (post_prob_ha), the
posterior mean treatment effect on the cumulative-failure scale
(est_final), the predictive probability of success
(ppp_success), and indicators for whether the trial stopped
early for futility or expected success.
Two practical considerations are worth flagging:
Empty intervals at interim looks. Early interim
looks may have no subjects with follow-up reaching the later piecewise
intervals. The empty_interval argument controls how these
intervals are handled. The default,
empty_interval = "propagate", preserves historical package
behavior by propagating exposure time and event counts from the nearest
non-empty interval within the same treatment group, and emits a
warning when it does so. This supplies a placeholder Gamma posterior for
an interval with no information; it is a fallback when an interim look
has not yet generated follow-up in later intervals, not a substantive
estimate of those intervals’ hazards. Use
empty_interval = "prior" to leave such intervals at zero
exposure and zero events, making their posteriors prior-driven, or
empty_interval = "error" to stop the analysis whenever an
empty interval is encountered. By the final analysis, all intervals will
typically be populated.
Number of cut-points. Each additional cut-point adds two hazard parameters to estimate in a two-arm design (one per treatment group). With limited interim data this can make individual interval posteriors diffuse. In our experience, one or two well-motivated cut-points (e.g., tied to a clinical milestone) is usually sufficient; finer partitions tend to add variance without commensurate bias reduction.
If you suspect a piecewise structure but are unsure where the
cut-point should sit, a useful sensitivity check is to fix the
data-generating hazards (the truth) and vary the analysis
cut-points – that is, what survival_adapt() is told to
model. The simulator generates trial data from
hazard_treatment and hazard_control evaluated
against the supplied cutpoints, so to compare
analysis-model choices on a common ground you would run separate
sim_trials() simulations under the same data-generating
truth, varying the analytic cut-points each time. If the operating
characteristics are similar across choices, the design is robust to the
cut-point specification. If they differ markedly, the cut-point becomes
a design decision worth justifying in the protocol.
A simpler – but very different – comparison is the equivalent design that is both simulated and analyzed under a single constant hazard for each treatment group matched to the overall 24-month proportions:
hc_flat <- prop_to_haz(0.50, endtime = end_of_study) # control, single hazard
ht_flat <- prop_to_haz(0.40, endtime = end_of_study) # treatment, single hazard
out_flat <- survival_adapt(
hazard_treatment = ht_flat,
hazard_control = hc_flat,
cutpoints = NULL,
N_total = 100,
lambda = 5,
lambda_time = NULL,
interim_look = 60,
end_of_study = end_of_study,
prior = prior,
block = 4,
rand_ratio = c(1, 1),
prop_loss = 0.05,
alternative = "less",
h0 = 0,
Fn = 0.05,
Sn = 0.95,
prob_ha = 0.975,
N_impute = 50,
N_mcmc = 2000,
method = "bayes-surv")Note that this changes both the simulated data-generating process and the analysis model, so any difference in operating characteristics conflates the two effects. It is most useful when the question is “how would the trial behave if the world really were a single exponential?” rather than “how robust is my analysis cut-point?”.
?survival_adapt documents all arguments, including the
requirement that each interim_look in a two-arm design be
at least the block size.?prop_to_haz and ?ppwe document the
conversion between event proportions and piecewise hazards.