What this is for
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.
A minimal example
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)
#>
#> Full dataset: 12,180 observations, 11 variables
#>
#> Non-required columns provided, pruning for efficiency
#>
#> Pruned
#>
#> Original dataset (eligible subjects): 9,203 observations, 9 variables
#>
#> Expanding Data...
#>
#> Pre-filter expansion: 310,080 observations
#>
#> Expanded dataset: 248,485 observations, 13 variables
#>
#> Expansion Successful
#>
#> Final analysis dataset: 248,485 observations, 13 variables
#>
#> Moving forward with ITT analysis
#>
#> Bootstrapping with 80% of 300 subjects (240 subjects, ~198,788 observations per resample) 20 times
#>
#> Estimating end-of-follow-up outcome at follow-up time 12
#>
#> Completed
end_of_fup(model)
#> $`1`
#> $`1`$estimates
#> Type Time A Proportion Trial-periods (Eligible)
#> <char> <num> <fctr> <num> <int>
#> 1: binary 12 0 0.01986475 2946
#> 2: binary 12 1 0.02725648 6257
#> Trial-periods (Analysed) Trial-periods (Censored)
#> <int> <int>
#> 1: 2366 580
#> 2: 4476 1781
#> Trial-periods (No measurement) % Censored Subjects SE 95% LCI
#> <int> <num> <int> <num> <num>
#> 1: 0 19.68771 266 0.003081317 0.01382548
#> 2: 0 28.46412 236 0.001560669 0.02419762
#> 95% UCI
#> <num>
#> 1: 0.02590402
#> 2: 0.03031533
#>
#> $`1`$comparison
#> Key: <A_x, A_y>
#> Time A_x A_y Difference Difference 95% LCI Difference 95% UCI
#> <num> <fctr> <fctr> <num> <num> <num>
#> 1: 12 0 1 0.007391728 -5.362035e-05 1.483708e-02
#> 2: 12 1 0 -0.007391728 -1.483708e-02 5.362035e-05
#> Difference SE Ratio Ratio 95% LCI Ratio 95% UCI log(Ratio) SE
#> <num> <num> <num> <num> <num>
#> 1: 0.003798717 1.3721028 0.9653172 1.950308 0.179413
#> 2: 0.003798717 0.7288084 0.5127395 1.035929 0.179413end_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.
Missing measurements and the time window
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)
#>
#> Full dataset: 12,180 observations, 11 variables
#>
#> Non-required columns provided, pruning for efficiency
#>
#> Pruned
#>
#> Original dataset (eligible subjects): 9,203 observations, 9 variables
#>
#> Expanding Data...
#>
#> Pre-filter expansion: 310,080 observations
#>
#> Expanded dataset: 248,485 observations, 13 variables
#>
#> Expansion Successful
#>
#> Final analysis dataset: 248,485 observations, 13 variables
#>
#> Moving forward with ITT analysis
#>
#> Bootstrapping with 80% of 300 subjects (240 subjects, ~198,788 observations per resample) 20 times
#>
#> Estimating end-of-follow-up outcome at follow-up time 12
#>
#> Completed
end_of_fup(windowed)[[1]]$estimates
#> Type Time A Proportion Trial-periods (Eligible)
#> <char> <num> <fctr> <num> <int>
#> 1: binary 12 0 0.07570353 2946
#> 2: binary 12 1 0.10247494 6257
#> Trial-periods (Analysed) Trial-periods (Censored)
#> <int> <int>
#> 1: 2523 423
#> 2: 4889 1368
#> Trial-periods (No measurement) % Censored Subjects SE 95% LCI
#> <int> <num> <int> <num> <num>
#> 1: 0 14.35845 274 0.011306853 0.05354250
#> 2: 0 21.86351 248 0.005941862 0.09082911
#> 95% UCI
#> <num>
#> 1: 0.09786455
#> 2: 0.11412078
end_of_fup(windowed)[[1]]$comparison
#> Key: <A_x, A_y>
#> Time A_x A_y Difference Difference 95% LCI Difference 95% UCI
#> <num> <fctr> <fctr> <num> <num> <num>
#> 1: 12 0 1 0.02677142 -0.00118542 0.05472825
#> 2: 12 1 0 -0.02677142 -0.05472825 0.00118542
#> Difference SE Ratio Ratio 95% LCI Ratio 95% UCI log(Ratio) SE
#> <num> <num> <num> <num> <num>
#> 1: 0.01426395 1.3536350 0.9693496 1.890265 0.1703723
#> 2: 0.01426395 0.7387516 0.5290263 1.031620 0.1703723comparison 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:
- If there is a measurement at exactly
k, use it. - Otherwise, use the measurement nearest to
kwithin[k - window, k + window]. Where two measurements are equally far either side ofk, the later one is taken, so that at leastkof follow-up has elapsed. - If there is no measurement anywhere in the window, the trial-period is censored - it contributes nothing to the average.
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.
Checking what contributed
diagnostics() reports where every trial-period went.
eof.nonunique counts trial-periods and
eof.unique counts distinct subjects:
diagnostics(windowed)$eof.nonunique
#> [[1]]
#> Key: <tx_init_bas>
#> tx_init_bas Eligible At k In window Excluded (outside window)
#> <fctr> <int> <int> <int> <int>
#> 1: 0 2946 2366 157 423
#> 2: 1 6257 4476 413 1368
#> Excluded (no measurement)
#> <int>
#> 1: 0
#> 2: 0The four categories are mutually exclusive, so the trial-period
counts partition Eligible:
-
At k - contributed, using a measurement at exactly
k. - In window - contributed, having fallen back to the window.
- Excluded (outside window) - measured at some point, but not within the window.
-
Excluded (no measurement) - never measured at any follow-up
time. Under
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.
Continuous outcomes
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))
#>
#> Full dataset: 12,180 observations, 12 variables
#>
#> Non-required columns provided, pruning for efficiency
#>
#> Pruned
#>
#> Original dataset (eligible subjects): 9,203 observations, 9 variables
#>
#> Expanding Data...
#>
#> Pre-filter expansion: 310,080 observations
#>
#> Expanded dataset: 248,485 observations, 13 variables
#>
#> Expansion Successful
#>
#> Final analysis dataset: 248,485 observations, 13 variables
#>
#> Moving forward with ITT analysis
#>
#> Bootstrapping with 80% of 300 subjects (240 subjects, ~198,788 observations per resample) 20 times
#>
#> Estimating end-of-follow-up outcome at follow-up time 12
#>
#> Completed
end_of_fup(continuous)[[1]]$estimates
#> Type Time A Mean Trial-periods (Eligible)
#> <char> <num> <fctr> <num> <int>
#> 1: continuous 12 0 21.68154 2946
#> 2: continuous 12 1 21.73905 6257
#> Trial-periods (Analysed) Trial-periods (Censored)
#> <int> <int>
#> 1: 2523 423
#> 2: 4889 1368
#> Trial-periods (No measurement) % Censored Subjects SE 95% LCI
#> <int> <num> <int> <num> <num>
#> 1: 0 14.35845 274 0.1169507 21.45233
#> 2: 0 21.86351 248 0.1023680 21.53841
#> 95% UCI
#> <num>
#> 1: 21.91076
#> 2: 21.93969
end_of_fup(continuous)[[1]]$comparison
#> Key: <A_x, A_y>
#> Time A_x A_y Difference Difference 95% LCI Difference 95% UCI
#> <num> <fctr> <fctr> <num> <num> <num>
#> 1: 12 0 1 0.05750665 -0.2283656 0.3433789
#> 2: 12 1 0 -0.05750665 -0.3433789 0.2283656
#> Difference SE
#> <num>
#> 1: 0.1458559
#> 2: 0.1458559Here 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.
diagnostics(continuous)$eof.summary
#> [[1]]
#> A N Mean SD
#> <fctr> <int> <num> <num>
#> 1: 0 2523 21.68154 5.082334
#> 2: 1 4889 21.73905 5.182840Per-protocol effects
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))
#>
#> Full dataset: 12,180 observations, 11 variables
#>
#> Non-required columns provided, pruning for efficiency
#>
#> Pruned
#>
#> Original dataset (eligible subjects): 9,203 observations, 9 variables
#>
#> Expanding Data...
#>
#> Pre-filter expansion: 310,080 observations
#>
#> Expanded dataset (pre-censoring): 248,485 observations, 15 variables
#>
#> Expanded dataset (post-censoring): 102,749 observations, 15 variables
#> entering outcome model (uncensored): 96,251
#> artificially censored (treatment switch): 6,498
#>
#> Expansion Successful
#>
#> Final analysis dataset: 102,749 observations, 15 variables
#>
#> Moving forward with censoring analysis
#>
#> Bootstrapping with 80% of 300 subjects (240 subjects, ~82,199 observations per resample) 20 times
#>
#> Estimating end-of-follow-up outcome at follow-up time 12
#>
#> Completed
end_of_fup(perprotocol)[[1]]$estimates
#> Type Time A Proportion Trial-periods (Eligible)
#> <char> <num> <fctr> <num> <int>
#> 1: binary 12 0 0.04506949 2946
#> 2: binary 12 1 0.08873317 6257
#> Trial-periods (Analysed) Trial-periods (Censored)
#> <int> <int>
#> 1: 502 2444
#> 2: 3154 3103
#> Trial-periods (No measurement) % Censored Subjects SE 95% LCI
#> <int> <num> <int> <num> <num>
#> 1: 0 82.95995 86 0.022835744 0.0003122527
#> 2: 0 49.59246 211 0.008721064 0.0716402018
#> 95% UCI
#> <num>
#> 1: 0.08982673
#> 2: 0.10582614
end_of_fup(perprotocol)[[1]]$comparison
#> Key: <A_x, A_y>
#> Time A_x A_y Difference Difference 95% LCI Difference 95% UCI
#> <num> <fctr> <fctr> <num> <num> <num>
#> 1: 12 0 1 0.04366368 -0.002914227 0.090241593
#> 2: 12 1 0 -0.04366368 -0.090241593 0.002914227
#> Difference SE Ratio Ratio 95% LCI Ratio 95% UCI log(Ratio) SE
#> <num> <num> <num> <num> <num>
#> 1: 0.02376468 1.9688080 0.6334967 6.118746 0.5785458
#> 2: 0.02376468 0.5079215 0.1634322 1.578540 0.5785458Subjects 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.
