vignettes/clonecensorweighting.Rmd
clonecensorweighting.RmdSuppose we want to know whether surgery improves survival in lung cancer patients. Comparing patients who had surgery against those who did not, using a naive analysis, can be badly biased. One reason is immortal time bias: to appear in the “surgery” group, a patient has to survive long enough to actually receive surgery. That guaranteed survival time gets unfairly credited to the surgery group.
Clone-censor-weighting (CCW) is a technique used in target trial emulation to address this. The idea has three parts, which also give the method its name:
This vignette walks through the pipeline end to end, using a lung cancer dataset that ships with the package.
The package includes lungcancer, a dataset of 200 lung
cancer patients.
data(lungcancer)
head(lungcancer)
#> id surgery timetosurgery death sex charlson perf stage emergency fup_obs
#> 1 P5958 0 NA 0 2 1 2 2 1 365.24
#> 2 P3075 0 NA 0 2 1 0 2 0 365.24
#> 3 P1410 0 NA 0 1 1 2 1 0 365.24
#> 4 P4957 0 NA 0 2 0 1 1 0 365.24
#> 5 P7340 0 NA 1 1 0 2 2 1 123.00
#> 6 P1917 0 NA 0 2 0 0 1 0 365.24
#> age deprivation
#> 1 79.57290 0.42
#> 2 82.91855 0.07
#> 3 84.03011 0.22
#> 4 78.34634 0.12
#> 5 83.63860 0.40
#> 6 82.26147 0.03The columns we rely on are:
id: patient identifiersurgery: whether the patient received surgery
(1) or not (0)timetosurgery: time from baseline to surgery
(NA if never operated on)death: whether the patient died (1) or not
(0) — the outcome eventfup_obs: observed follow-up timeThe remaining columns (sex, charlson,
perf, stage, emergency,
age, deprivation) describe patient
characteristics that can be used as covariates when estimating censoring
weights.
We compare two strategies, and give them names we will reuse throughout:
arms <- c("Control", "Surgery")The grace period is central to this analysis. It is the window during which a patient is still considered compatible with the “surgery” strategy. A patient who has surgery within the grace period is consistent with the treated strategy; one who does not is consistent with the control strategy. Here we use a grace period of 90 days.
grace_period <- 90clone_arms() copies the full dataset once per strategy
and returns a list with one data frame per arm.
clones <- clone_arms(lungcancer, arms)
names(clones)
#> [1] "Control" "Surgery"At this point the two data frames are identical copies of the original data. The strategy-specific logic is applied in the next steps.
Two helper functions describe how each arm’s emulated outcome, follow-up, and censoring should be derived. They do not touch the data yet; they return the rules (as expressions) that will be applied in Step 3.
create_policy_A() produces the emulated outcome and
follow-up under the grace period policy.
policy <- create_policy_A(
arms = arms,
treatment = "surgery",
time_to_treatment = "timetosurgery",
grace_period = grace_period,
outcome = "death",
followup = "fup_obs"
)create_censoring_logics_A() produces the censoring
indicator and the time at which that indicator can first be
determined.
censoring <- create_censoring_logics_A(
arms = arms,
treatment = "surgery",
time_to_treatment = "timetosurgery",
grace_period = grace_period,
followup = "fup_obs"
)Each of these returns a nested list, one entry per arm. The policy
rules create the variables .outcome and .fup;
the censoring rules create .censoring and
.fup_uncensored.
apply_logics() evaluates the rules against each arm’s
data. We combine the policy and censoring rules for each arm into a
single set of new variables.
logics <- list(
Control = c(policy$Control, censoring$Control),
Surgery = c(policy$Surgery, censoring$Surgery)
)
emulated <- apply_logics(clones, logics)
lapply(emulated, names)
#> $Control
#> [1] "id" "surgery" "timetosurgery" "death"
#> [5] "sex" "charlson" "perf" "stage"
#> [9] "emergency" "fup_obs" "age" "deprivation"
#> [13] ".outcome" ".fup" ".censoring" ".fup_uncensored"
#>
#> $Surgery
#> [1] "id" "surgery" "timetosurgery" "death"
#> [5] "sex" "charlson" "perf" "stage"
#> [9] "emergency" "fup_obs" "age" "deprivation"
#> [13] ".outcome" ".fup" ".censoring" ".fup_uncensored"Each arm’s data frame now carries the emulated variables
(.outcome, .fup, .censoring,
.fup_uncensored) alongside the original columns.
create_final_data() turns each patient into a sequence
of time intervals (a counting-process, or “long”, format). Internally it
finds every event time, splits each patient’s follow-up at those times,
and combines the outcome and censoring information into one table per
arm.
final <- create_final_data(
clones = emulated,
clone_followup = ".fup",
clone_outcome = ".outcome",
clone_censoring = ".censoring",
col_ids = "id"
)
head(final$Surgery)
#> id surgery timetosurgery death sex charlson perf stage emergency fup_obs
#> 1 P5958 0 NA 0 2 1 2 2 1 365.24
#> 2 P5958 0 NA 0 2 1 2 2 1 365.24
#> 3 P5958 0 NA 0 2 1 2 2 1 365.24
#> 4 P5958 0 NA 0 2 1 2 2 1 365.24
#> 5 P5958 0 NA 0 2 1 2 2 1 365.24
#> 6 P5958 0 NA 0 2 1 2 2 1 365.24
#> age deprivation .fup_uncensored ID Tstart .fup .outcome .censoring Tstop
#> 1 79.5729 0.42 90 1 0 1 0 0 1
#> 2 79.5729 0.42 90 1 1 7 0 0 7
#> 3 79.5729 0.42 90 1 7 8 0 0 8
#> 4 79.5729 0.42 90 1 8 10 0 0 10
#> 5 79.5729 0.42 90 1 10 16 0 0 16
#> 6 79.5729 0.42 90 1 16 18 0 0 18
#> ID_t
#> 1 0
#> 2 1
#> 3 2
#> 4 3
#> 5 4
#> 6 5Each row is now one patient-interval, bounded by Tstart
and Tstop. Because a single patient contributes several
intervals, each arm has many more rows than the original 200
patients:
lapply(final, dim)
#> $Control
#> [1] 8383 20
#>
#> $Surgery
#> [1] 11532 20This long-form data is the input for the final steps of a CCW analysis: estimating inverse-probability-of-censoring weights over time and fitting a weighted survival model to compare the two strategies.
The example above uses the bundled lungcancer data. To
run the same workflow on your own data, it needs, at a minimum:
0 /
1
NA for the untreated)0 /
1
The column names are up to you; you pass them to the arguments of the policy and censoring helpers, as we did above. Any additional columns are carried along and can serve as covariates.
read_trial_data() is a small convenience wrapper for
reading such data from a CSV file into a tibble. It does not impose any
particular column structure; it simply reads the file:
my_data <- read_trial_data("path/to/your-data.csv")In this vignette we:
clone_arms(),create_policy_A() and
create_censoring_logics_A(),apply_logics(),create_final_data().The remaining steps of a full CCW analysis — estimating censoring weights and fitting a weighted outcome model — build directly on this long-form dataset.