The rest of the SEQTaRget package estimates survival
outcomes: a binary event that may occur at any point during follow-up,
summarised through risks, survival curves or a hazard ratio.
An end-of-follow-up outcome is different. It is measured once, at a single follow-up time chosen by the user, e.g., a biomarker at 12 months, disease status at two years, a questionnaire score at the end of the trial. There is no time-to-event to model, and the quantity of interest is simply the average outcome in each treatment arm.
The SEQopts() option end_of_fup = TRUE
switches to that estimand. For each trial-period the outcome is read at
the requested follow-up time and averaged within each baseline treatment
arm, weighted by the period-trial-specific weight at the time the
measurement was taken. For a binary outcome this is the weighted
proportion in each arm; for a continuous outcome, the weighted mean.
Because there is no outcome model, end_of_fup cannot be
combined with km.curves, hazard,
compevent, or the dose-response method.
end_of_fup.time is the follow-up time k at
which the outcome is evaluated, counted in follow-up periods since trial
enrollment (not calendar time).
options <- SEQopts(end_of_fup = TRUE,
# evaluate the outcome 12 follow-up periods after enrollment
end_of_fup.time = 12,
# "binary" reports a proportion, "continuous" a mean
end_of_fup.type = "binary",
bootstrap = TRUE,
# fixes the bootstrap resamples, so the intervals below are
# reproducible; without it each run draws a fresh seed
seed = 1636,
bootstrap.nboot = 20)
model <- SEQuential(SEQdata, id.col = "ID",
time.col = "time",
eligible.col = "eligible",
treatment.col = "tx_init",
outcome.col = "outcome",
time_varying.cols = c("N", "L", "P"),
fixed.cols = "sex",
method = "ITT",
options = options)
end_of_fup(model)end_of_fup() returns, per subgroup, two tables:
estimates gives the weighted proportion (or mean) in each
arm with its bootstrap confidence interval, and an account of how much
of the arm it rests on. Trial-periods (Eligible) is every
trial-period that reached the follow-up time, and the next three
partition it: (Analysed) contribute to the estimate,
(Censored) were measured at some point but not within the
window, and (No measurement) were never measured at all -
so the three sum back to the eligible total. % Censored
gives the censored share of that total. Subjects counts the
distinct people behind the analysed trial-periods; one subject
contributes several trial-periods and can be analysed in some and
censored in others. comparison gives the pairwise
between-arm contrast: the difference in proportions here, or the
difference in means for a continuous outcome, with its standard error
and confidence interval. For a binary outcome the ratio of proportions
is reported alongside it, with an interval computed on the log scale and
a log(Ratio) SE for inverse-variance pooling. Contrasts are
paired by bootstrap iteration, so their intervals account for the
correlation between arms.
Outcomes measured at particular visits are rarely available for
everyone at exactly time k. Encode “not measured at this
time” as NA in the outcome column - end_of_fup
is the one mode that accepts missing outcomes, precisely because
missingness is meaningful here. Every other column must still be
complete.
end_of_fup.window sets the half-width of a window used
when a trial-period has no measurement at exactly k:
options <- SEQopts(end_of_fup = TRUE,
end_of_fup.time = 12,
# accept a measurement anywhere in [9, 15] when there is none at 12
end_of_fup.window = 3,
bootstrap = TRUE,
seed = 1636,
bootstrap.nboot = 20)
windowed <- SEQuential(SEQdata, id.col = "ID",
time.col = "time",
eligible.col = "eligible",
treatment.col = "tx_init",
outcome.col = "outcome",
time_varying.cols = c("N", "L", "P"),
fixed.cols = "sex",
method = "ITT",
options = options)
end_of_fup(windowed)[[1]]$estimates
end_of_fup(windowed)[[1]]$comparisoncomparison is where the treatment effect lives:
Difference is the difference in proportions between the two
arms, and Ratio their ratio. Both directions of each arm
pair are reported, so the row you want is the one whose A_x
is your reference arm.
The selection rule, applied to each trial-period independently, is:
k, use it.k
within [k - window, k + window]. Where two measurements are
equally far either side of k, the later one is
taken, so that at least k of follow-up has elapsed.The weight used is always the weight at the time the chosen
measurement was taken, not the weight at k.
A window is not free. Widening it recovers trial-periods that would
otherwise be dropped, but the measurements it recovers are taken further
from the time you actually care about, and the trial-periods it recovers
are not a random subset - a trial-period with no measurement at
k is often one whose follow-up ended early. Treat the
window as a trade-off between precision and how literally the estimate
answers “the outcome at time k”, and check how much of the
estimate rests on it using the accounting table below.
diagnostics() reports where every trial-period went.
eof.nonunique counts trial-periods and
eof.unique counts distinct subjects:
The four categories are mutually exclusive, so the trial-period
counts partition Eligible:
k.method = "censoring" this also picks up
trial-periods artificially censored before any measurement was
taken.At k plus In window is exactly the number
of trial-periods behind the estimate, so the two tables always
reconcile. The subject counts in eof.unique need
not sum to Eligible, because one subject can fall
into different categories for different trials.
If In window is large relative to At k, or
Excluded dominates, the estimate is resting on much less -
or much more indirect - data than the arm totals alone suggest.
Set end_of_fup.type = "continuous" for an outcome that
is not 0/1. The estimate becomes a weighted mean, reported in a
Mean column rather than Proportion, and its
confidence interval is not clamped to [0, 1]. The
between-arm contrast is the difference in means; no ratio is reported,
since a continuous outcome need not be bounded away from zero.
data <- data.table::copy(SEQdata)
set.seed(42)
data[, biomarker := 10 + 2 * as.numeric(as.character(tx_init)) + N + rnorm(.N)]
continuous <- SEQuential(data, id.col = "ID",
time.col = "time",
eligible.col = "eligible",
treatment.col = "tx_init",
outcome.col = "biomarker",
time_varying.cols = c("N", "L", "P"),
fixed.cols = "sex",
method = "ITT",
options = SEQopts(end_of_fup = TRUE,
end_of_fup.time = 12,
end_of_fup.type = "continuous",
end_of_fup.window = 3,
bootstrap = TRUE,
seed = 1636,
bootstrap.nboot = 20))
end_of_fup(continuous)[[1]]$estimates
end_of_fup(continuous)[[1]]$comparisonHere Difference is the difference in means, and there is
no Ratio column.
Note that the usual outcome diagnostic tables count
outcome == 1 rows, which has no meaning for a continuous
outcome, so they are reported as NA. In their place
diagnostics() reports the N, mean and SD of the raw
analysed measurements per arm in eof.summary; the follow-up
and end-of-follow-up tables remain available.
end_of_fup composes with weighting in the usual way, so
a per-protocol end-of-follow-up effect is the censoring method with
weighted = TRUE:
perprotocol <- SEQuential(SEQdata, id.col = "ID",
time.col = "time",
eligible.col = "eligible",
treatment.col = "tx_init",
outcome.col = "outcome",
time_varying.cols = c("N", "L", "P"),
fixed.cols = "sex",
method = "censoring",
options = SEQopts(weighted = TRUE,
numerator = "sex",
denominator = "N + L + P + sex",
end_of_fup = TRUE,
end_of_fup.time = 12,
end_of_fup.window = 3,
bootstrap = TRUE,
seed = 1636,
bootstrap.nboot = 20))
end_of_fup(perprotocol)[[1]]$estimates
end_of_fup(perprotocol)[[1]]$comparisonSubjects who deviate from their assigned strategy are artificially
censored at the point of deviation, and their outcome is missing from
then on. A trial-period that deviates before k therefore
has no measurement to contribute and is excluded rather than carried
forward - it appears under Excluded (no measurement) or
Excluded (outside window) in the accounting table,
depending on what it had measured earlier.