Build a landmark-analysis dataset
landmark.RdConstruct a landmark-analysis dataset that corrects for immortal time bias.
Given right-censored survival data and a covariate that may be measured after baseline, landmark() restricts the cohort to subjects still at risk at a chosen landmark time, fixes the covariate at its value as known at that time, and returns an analysis-ready dataset together with a structured conversion report.
Arguments
- df
(data.frame)
Survival data with one row per subject. All original columns are retained in the returned dataset.- formula
(formula)
A two-sided formulasurvival::Surv(time, status) ~ var. The left side is a right-censoredsurvival::Surv()call. The right side must be a single bare column name ofdf, the covariate requiring landmark adjustment, with no transformation, interaction, or.(dot). Event-status coding is delegated tosurvival::Surv(), sostatusmust be coercible by it (0/1, logical, or a two-level factor), not an arbitrary character vector.- landmark
(Scalar numeric: finite,
>= 0)
The landmark time, in the same units as the survival time andvar_time.- var_time
(Scalar character: name of a numeric column of
df)
Name of the column giving the measurement time of the covariate.- var_baseline
(Scalar: type-compatible with the covariate)
The baseline level of the covariate, passed in its native type (numeric for a numeric column, character for a factor or character column, logical for a logical column). A covariate measured after the landmark is reset to this value. For a factor covariate it must be a declared level, observed or not. For a non-factor covariate it need not appear in the covariate column, because the landmark reset itself introduces baseline values. Matching to the covariate is by exact equality.- horizon
(Scalar numeric: default
NULL; finite,> 0)
Optional prediction window. When supplied, follow-up is administratively censored atlandmark + horizon.NULLkeeps the full post-landmark follow-up.
Value
A list of class landmark with elements:
data: the landmark cohort. All original columns ofdfare retained, with the covariate reset in place to its value as known at the landmark. Two derived columns are appended:lm_time, the numeric time measured from the landmark, andlm_event, the integer 0/1 event indicator. Analysis is run withsurvival::Surv(lm_time, lm_event). The original survival time and status columns are kept unchanged.counts: a named list of integer counts describing the conversion. The first three record cohort size.n_original: rows indf.n_landmark: rows in the landmark cohort.n_excluded: rows excluded, equal ton_original - n_landmark.
Eight counts give the exclusion reasons. Together with
n_landmarkthey partitionn_original, so every row is counted exactly once.n_missing_status: excluded for a missing survival status.n_missing_time: excluded for a missing survival time.n_event_before_landmark: excluded for an event strictly before the landmark.n_censor_before_landmark: excluded for a censoring strictly before the landmark.n_event_at_landmark: excluded for an event exactly at the landmark.n_censor_at_landmark: excluded for a censoring exactly at the landmark.n_missing_var_time: excluded because a non-baseline covariate has a missing measurement time.n_missing_var: excluded for a missing covariate value not repaired by the reset.
Three counts summarize the covariate reset within the cohort.
n_baseline: cohort rows at the baseline level after the reset.n_converted: cohort rows whose covariate was changed to the baseline level by the reset.n_converted_from_missing: cohort rows counted inn_convertedwhose pre-reset covariate value was missing, and which have no row inconversion_by_level.
n_convertedminusn_converted_from_missingequals the sum ofn_resetacrossconversion_by_level.Two counts summarize horizon truncation, and are
0whenhorizonisNULL.n_truncated_at_horizon: cohort rows whose follow-up was capped atlandmark + horizon.n_events_censored_at_horizon: cohort events reclassified as censored because they occur afterlandmark + horizon.
conversion_by_level: a data.frame with one row per non-baseline covariate level present in the cohort, with columnslevel,n(cohort subjects at that pre-reset level), andn_reset(how many were reset to baseline).params: a list recordinglandmark,horizon,var,var_time,var_baseline, andformula.
The print method renders a human-readable conversion report.
Details
Immortal time bias arises when a survival model includes a covariate that was measured after the start of follow-up. A treatment indicator is a common example. A subject must survive long enough to receive treatment, so treated subjects have, by construction, longer survival times under a naive analysis. The interval between baseline and the covariate measurement is "immortal" because the subject cannot have the event during it and still enter the treated group.
The landmark method removes this bias. A landmark time is chosen, the cohort is restricted to subjects still at risk at the landmark, and the covariate is fixed at the value known at the landmark. That fixed value is the measured value when the measurement time is at or before the landmark, and the baseline value otherwise. Analysis then proceeds with survival time measured from the landmark. The result is interpreted conditionally. A typical statement reads "Among subjects still at risk at the landmark time, ...".
The landmark population is defined by the strict conditioning event \(\text{time} > \text{landmark}\). A subject whose survival time equals the landmark exactly is excluded. Such a subject contributes no post-landmark follow-up; its restarted time would be zero, and an event at time zero would force the conditional survival function below 1 at the origin, where it must equal 1 by construction. A censoring exactly at the landmark carries no post-landmark information and is likewise excluded. These subjects are reported in their own count categories so that a pile-up of ties at the landmark is visible to the analyst.
A row is also excluded when its recorded data cannot place the subject in the analysis, and each reason has its own category in counts.
A row with a missing survival time or a missing survival status is excluded, because the subject's at-risk state at the landmark cannot be determined.
A row with a missing covariate value is excluded, unless the covariate was measured after the landmark, in which case the landmark reset supplies the baseline value and the row is kept.
A row whose covariate has a non-baseline value but a missing measurement time is excluded, because the covariate cannot be placed relative to the landmark and its landmark-time value is unknown.
A covariate already equal to the baseline value is unaffected by the missing-measurement-time exclusion, because its landmark-time value is baseline whether or not the measurement time is known.
An optional prediction horizon administratively censors follow-up at landmark + horizon.
Events after that point become censored and follow-up time is capped.
This is the standard fixed-width prediction-window construction.
An alternative to landmark analysis is a time-dependent Cox model. It is more involved but is preferable when there is no agreed landmark time, when the covariate is genuinely longitudinal with several measurements per subject, or when a landmark would exclude an unacceptable share of the cohort.
The covariate is treated as a discrete grouping.
It is matched to var_baseline by exact equality, never by numeric proximity, because a tolerance would merge genuinely distinct levels.
For a numeric covariate this is exact floating-point equality, so discrete codes that are exactly representable should be used; a covariate with arithmetic-derived values is better stored as a factor or character column.
References
Anderson JR, Cain KC, Gelber RD (1983). “Analysis of survival by tumor response.” Journal of Clinical Oncology, 1(11), 710–719. ISSN 0732-183X, 1527-7755, doi:10.1200/JCO.1983.1.11.710 .
Dafni U (2011). “Landmark Analysis at the 25-Year Landmark Point.” Circulation: Cardiovascular Quality and Outcomes, 4(3), 363–371. doi:10.1161/circoutcomes.110.957951 .
Van Houwelingen H, Putter H (2011). Dynamic Prediction in Clinical Survival Analysis, 0 edition. CRC Press. ISBN 9780429094330, doi:10.1201/b11311 .
Examples
#----------------------------------------------------------------------------
# landmark() example
#----------------------------------------------------------------------------
library(survival)
library(bkstat)
# One row per subject. The covariate `x` is a treatment level that may be
# measured after baseline; `x_time` records when it was measured.
df <- data.frame(
time = c(5, 6, 1, 7, 8, 2, 9, 4, 2, 6, 7, 5),
status = c(1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1),
x = c(0, 2, 1, 1, 2, 0, 1, 0, 1, 2, 1, 0),
x_time = c(1, 5, 1, 2, 6, 1, 4, 1, 1, 7, 3, 1),
sex = c(1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0)
)
# Build the landmark dataset at time 3, with baseline covariate level 0.
res <- landmark(
df = df,
formula = Surv(time, status) ~ x,
landmark = 3,
var_time = "x_time",
var_baseline = 0
)
res
#> Landmark analysis dataset
#>
#> Landmark time: 3
#> Prediction horizon: none
#> Rows: 12 original, 9 landmark (75%), 3 excluded (25%)
#>
#> Exclusions
#> missing survival status 0
#> missing survival time 0
#> event before landmark 3
#> censored before landmark 0
#> missing covariate time 0
#> missing covariate value 0
#>
#> Covariate adjustment
#> reset to baseline (x = 0): 4 of 9 landmark rows
#> at baseline after reset: 7 of 9 landmark rows
#> x = 1: 1/3 (33.3%)
#> x = 2: 3/3 (100%)
res$data
#> time status x x_time sex lm_time lm_event
#> 1 5 1 0 1 1 2 1
#> 2 6 0 0 5 0 3 0
#> 4 7 1 1 2 1 4 1
#> 5 8 0 0 6 1 5 0
#> 7 9 1 0 4 0 6 1
#> 8 4 0 0 1 1 1 0
#> 10 6 1 0 7 1 3 1
#> 11 7 0 1 3 1 4 0
#> 12 5 1 0 1 0 2 1
# Fit the survival model on the landmark cohort.
coxph(Surv(lm_time, lm_event) ~ x + sex, data = res$data)
#> Call:
#> coxph(formula = Surv(lm_time, lm_event) ~ x + sex, data = res$data)
#>
#> coef exp(coef) se(coef) z p
#> x -0.7509 0.4719 1.2449 -0.603 0.546
#> sex 0.6931 2.0000 1.2247 0.566 0.571
#>
#> Likelihood ratio test=0.51 on 2 df, p=0.7766
#> n= 9, number of events= 5
#----------------------------------------------------------------------------
# With a prediction horizon: follow-up is censored at landmark + horizon.
#----------------------------------------------------------------------------
res_h <- landmark(
df = df,
formula = Surv(time, status) ~ x,
landmark = 3,
var_time = "x_time",
var_baseline = 0,
horizon = 4
)
res_h
#> Landmark analysis dataset
#>
#> Landmark time: 3
#> Prediction horizon: 4
#> Rows: 12 original, 9 landmark (75%), 3 excluded (25%)
#>
#> Exclusions
#> missing survival status 0
#> missing survival time 0
#> event before landmark 3
#> censored before landmark 0
#> missing covariate time 0
#> missing covariate value 0
#>
#> Covariate adjustment
#> reset to baseline (x = 0): 4 of 9 landmark rows
#> at baseline after reset: 7 of 9 landmark rows
#> x = 1: 1/3 (33.3%)
#> x = 2: 3/3 (100%)
#>
#> Horizon (window = 4)
#> rows truncated at horizon 2
#> events censored by horizon 1