From 1139456d0a5a90bdf0c9e31ef8fa3f0f80237f5e Mon Sep 17 00:00:00 2001 From: Hyewon Date: Tue, 14 Jul 2026 04:03:21 +0900 Subject: [PATCH] Add overview vignette --- vignettes/clonecensorweighting_hw.Rmd | 465 ++++++++++++++++++++++++++ 1 file changed, 465 insertions(+) create mode 100644 vignettes/clonecensorweighting_hw.Rmd diff --git a/vignettes/clonecensorweighting_hw.Rmd b/vignettes/clonecensorweighting_hw.Rmd new file mode 100644 index 0000000..c6688c7 --- /dev/null +++ b/vignettes/clonecensorweighting_hw.Rmd @@ -0,0 +1,465 @@ +--- +title: "An Overview of the clonecensorweighting Package for R" +author: | + | First Author$^{*}$ Second Author$^{\dagger}$ + | + | $^{*}$Affiliation One $^{\dagger}$Affiliation Two +date: "`r Sys.Date()`" +output: + rmarkdown::html_document: + toc: true + toc_depth: 2 + number_sections: true +vignette: > + %\VignetteIndexEntry{An Overview of the clonecensorweighting Package for R} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + prompt = TRUE, # show the ">" console prompt, as in the pcalg vignette + comment = NA, # no "#>" prefix on output, again matching pcalg + collapse = FALSE +) +set.seed(1) +``` + +```{r setup, include = FALSE} +library(clonecensorweighting) +``` + + + +# Introduction + +`clonecensorweighting` implements the *clone-censor-weight* (CCW) procedure for +emulating a target trial from observational time-to-event data. + +Suppose we want to know whether surgery improves survival in lung cancer +patients. Naively comparing those who had surgery against those who did not can +be badly misleading. A central culprit is *immortal time bias*: to be counted in +the "surgery" group, a patient must first survive long enough to receive +surgery, and that guaranteed survival time is then credited to the surgery group +as though it were a benefit of the operation. + +CCW is the standard target-trial-emulation remedy for this bias, and its name +traces the three steps it takes: + +- **Clone** -- each patient is duplicated into every treatment strategy being + compared, so that at the start of follow-up every clone is compatible with the + strategy it is assigned to. +- **Censor** -- a clone is censored the moment the patient's observed history + stops being consistent with the strategy that clone represents. +- **Weight** -- inverse-probability-of-censoring weights undo the selection bias + introduced by that artificial censoring. + +The package exposes these steps as a small set of composable functions: they +clone a subject-level dataset across strategies, apply the artificial censoring +each strategy implies, expand the result into the person-time format that an +inverse-probability-of-censoring weighted (IPCW) analysis requires, and fit the +weighted per-protocol effect. + +The rest of this document is organised as follows. Section 2 reviews target +trial emulation. Section 3 works through the method on a real dataset, +introducing notation where it is needed and presenting each of the three ideas +-- cloning, artificial censoring, and IPC weighting -- with its rationale and +immediately in code, and then estimating the weighted per-protocol effect. +Readers who instead want a step-by-step account of *building* the analysis +dataset may prefer the companion "Getting started" vignette. + +We assume familiarity with the basic ideas of target trial emulation and +inverse-probability weighting. The target-trial framework we emulate is set out +by Hernán and Robins [2016] and Hernán, Wang and Leaf [2022]; the use of +marginal structural models and IPC weighting to adjust for time-varying +selection goes back to Hernán, Brumback and Robins [2000]. Maringe et al. [2020] +discuss trial emulation specifically as a remedy for immortal-time bias, and +Gaber et al. [2024] give a practical account of implementing clone-censor-weight +with stabilized IPCW, which the implementation here follows. + +# Target trial emulation + +## The problem it solves + +A randomized controlled trial (RCT) can directly compare "following strategy A +to the end" versus "following strategy B to the end." The difficulty with +observational data is that no such assignment exists. People are not split into +A/B at baseline; over time they begin, switch, or stop treatments according to +their evolving health. + +The hardest case is when the strategy is defined by something that happens in the +future, not at baseline. Consider the strategy "receive surgery within 6 months +of diagnosis." At diagnosis (baseline) we cannot know whether a person will go on +to have surgery within 6 months. If we split people at baseline into "surgery" +versus "no surgery," anyone who dies before surgery is automatically classified +into the no-surgery arm, producing *immortal time bias*. + +Hernán and Robins [2016] proposed a simple remedy: specify the RCT you would +ideally run (the *target trial*), then emulate that trial using observational +data. A target trial is specified through seven components. Table 1 maps those +components onto this vignette's running example (surgery within 6 months of +diagnosis). + +Table: Target trial components mapped onto the running example. + +| Target trial component | Definition in this example | +|---|---| +| 1. Eligibility | Patients alive at diagnosis and not yet operated on | +| 2. Treatment strategies | (A) surgery within 6 months / (B) no surgery within 6 months | +| 3. Assignment | No assignment in observational data $\rightarrow$ replaced by cloning | +| 4. Follow-up | From baseline (diagnosis) to death or end of observation | +| 5. Outcome | Overall survival | +| 6. Causal contrast | Per-protocol effect (effect of following the strategy to the end) | +| 7. Analysis | Artificial censoring + IPCW, then weighted survival analysis | + +## Data + +The `clonecensorweighting` package is concerned with the causal analysis of +time-to-event outcomes. The bundled `lungcancer` dataset contains 200 simulated +lung cancer patients followed for up to one year after diagnosis; 106 received +surgery within six months and 48 died during follow-up. + +```{r data} +data(lungcancer) +head(lungcancer) +``` + +The variables used below are the patient identifier `id`, the observed treatment +`surgery` (`1`/`0`), the time to surgery `timetosurgery` (`NA` if never +operated on), the outcome `death` (`1`/`0`), the observed follow-up `fup_obs`, +and the covariates `age` and `sex`. + +## Reading your own data + +The example uses the bundled `lungcancer` data, but the same workflow runs on any +subject-level dataset that provides, at a minimum: + +- an identifier column for each patient; +- a binary treatment column, coded `0` / `1`; +- a time-to-treatment column (numeric, `NA` for the untreated); +- a binary outcome column, coded `0` / `1`; +- a numeric follow-up-time column. + +The column *names* are up to you: you pass them to the relevant function +arguments (the treatment, outcome and follow-up names to the policy and +censoring helpers; the identifier to `create_final_data()`; any covariates to +`estimate_censoring()`), as we do in Section 3. 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 imposes no particular column structure; it simply +reads the file: + +```{r read-trial-data, eval = FALSE, prompt = FALSE} +my_data <- read_trial_data("path/to/your-data.csv") +``` + +The companion "Getting started" vignette walks through preparing and checking an +input dataset in more detail. + +# Clone-censor-weighting + +We now walk through the method on the `lungcancer` data. Each of the three ideas +that give the method its name -- clone, censor, weight -- is presented with its +rationale and then applied immediately in code, checking the result at each turn. +A final step estimates the effect from the clone-censor-weighted data. We compare +two strategies with a six-month grace period ($\approx 182.62$ days). + +```{r setup-arms} +arms <- c("Control", "Surgery") +grace_period <- 182.62 +``` + +## Clone + +Each eligible individual is duplicated once per strategy being compared. With two +strategies, one person becomes two clones. At baseline nobody has yet violated +any strategy, so at the moment of duplication both clones share an identical +history. This makes the baseline covariates *exactly* balanced across arms, +removing the source of immortal time bias. + +`clone_arms()` carries this out, returning one data frame per arm. At this point +the two are identical copies of the original data; the strategy-specific logic +comes next. + +```{r clone} +clones <- clone_arms(lungcancer, arms) +names(clones) +sapply(clones, nrow) +``` + +## Censor + +Each clone is artificially censored the first moment it deviates from the +strategy it represents. + +- The "surgery within 6 months" clone: if 6 months pass without surgery, the + clone has now violated its strategy and is censored at the 6-month mark. If + surgery occurs in time, the clone remains in the risk set. +- The "no surgery within 6 months" clone: if surgery occurs within 6 months, the + clone is censored at the time of surgery. + +In other words, each clone stays under follow-up only while it remains consistent +with its strategy. + +The following is a tiny illustrative example. Five patients, where `surg_time` is +the surgery time (months; `NA` = no surgery during observation), along with +`death_time` and `event` ($1$ = death). + +```{r toy-data} +patients <- data.frame( + id = c("A", "B", "C", "D", "E"), + surg_time = c(2, NA, 4, 7, NA), # surgery time (months), NA = none + death_time = c(10, 5, 12, 9, 8), + event = c(1, 1, 1, 1, 1) +) +patients +``` + +With the strategy boundary set at 6 months, we trace by hand when each clone is +censored. + +```{r toy-clone-censor} +horizon <- 6 # boundary for the "surgery within 6 months" strategy + +## --- Surgery-arm clone: surgery must occur within 6 months --- +## * surgery within 6mo -> adherent, no censoring (until death / end of obs) +## * no surgery in 6mo -> censored at horizon +surg_clone <- within(patients, { + followed <- !is.na(surg_time) & surg_time <= horizon + fu_time <- ifelse(followed, death_time, pmin(horizon, death_time)) + status <- ifelse(followed, event, 0) # censored -> status = 0 + arm <- "surgery" +}) + +## --- Control-arm clone: surgery must NOT occur within 6 months --- +## * no surgery in 6mo -> adherent +## * surgery within 6mo -> censored at time of surgery +ctrl_clone <- within(patients, { + violated <- !is.na(surg_time) & surg_time <= horizon + fu_time <- ifelse(violated, surg_time, death_time) + status <- ifelse(violated, 0, event) + arm <- "control" +}) + +cols <- c("id", "arm", "fu_time", "status") +rbind(surg_clone[cols], ctrl_clone[cols]) +``` + +In the output, patient B (no surgery, dies at month 5) dies before the 6-month +boundary, so in neither arm has the strategy been violated. Patient A (surgery at +month 2) is adherent in the surgery arm but is censored at month 2 in the control +arm. This table is the core intuition of CCW. + +The package derives exactly this censoring from the grace-period rules, on the +real data. `create_policy_A()` and `create_censoring_logics_A()` return the rules +for the emulated outcome, follow-up, and censoring; `apply_logics()` evaluates +them against the clones. + +```{r censor-apply} +policies <- create_policy_A( + arms, + treatment = "surgery", + time_to_treatment = "timetosurgery", + grace_period = grace_period, + outcome = "death", + followup = "fup_obs", + clone_outcome = "outcome", + clone_followup = "fup" +) +clones_policy <- apply_logics(clones, policies) + +censoring_logics <- create_censoring_logics_A( + arms, + treatment = "surgery", + time_to_treatment = "timetosurgery", + grace_period = grace_period, + followup = "fup_obs", + clone_censoring = "censoring", + clone_uncensored_followup = "fup_uncensored" +) +clones_censored <- apply_logics(clones_policy, censoring_logics) + +setdiff(names(clones_censored$Surgery), names(lungcancer)) +``` + +Finally, `create_final_data()` expands each clone into a counting-process +("long") table -- one row per patient-time interval, bounded by `Tstart` and +`Tstop` -- the format the weighting step requires. Because each patient +contributes several intervals, each arm has many more rows than the original 200 +patients. + +```{r censor-final} +clones_final <- create_final_data( + clones_censored, + clone_followup = "fup", + clone_outcome = "outcome", + clone_censoring = "censoring", + col_ids = "id" +) +head(clones_final$Surgery) +sapply(clones_final, nrow) +``` + +## Weight + +The catch is that this artificial censoring is *not random*. Whether a clone +deviates (and is censored) is related to its evolving health. If, say, sicker +patients are more or less likely to have surgery, the censoring introduces +selection bias, which *inverse-probability-of-censoring weighting* (IPCW) +corrects. + +Some notation first. Index individuals by $i$ and discrete follow-up times by +$k = 1, 2, \dots$; let $L_0$ be the baseline covariates and +$\bar L_k = (L_0, \dots, L_k)$ the covariate history through time $k$. For a clone +in a given arm, let $C_k = 1$ mean it is artificially censored at time $k$, and +$\bar C_{k-1} = 0$ that it was still uncensored up to time $k-1$. The probability +of being censored in an interval is modelled by pooled logistic regression on the +person-time data, + +$$ +\operatorname{logit}\Pr\!\big(C_k = 1 \mid \bar C_{k-1}=0,\; \bar L_k,\; \text{arm}\big) +\;=\; \alpha_0 + \alpha_1 t_k + \beta^\top L_k, +$$ + +with $t_k$ the interval start time. IPCW then multiplies each clone by the inverse +probability of "remaining uncensored so far," reallocating the contribution of +censored clones to similar clones that remain. The **unstabilized** weight is + +$$ +W_i(k) \;=\; +\frac{1}{\displaystyle\prod_{j=1}^{k} + \Pr\!\big(C_j = 0 \,\mid\, \bar C_{j-1}=0,\; \bar L_j,\; \text{arm}\big)}, +$$ + +and the **stabilized** weight replaces the constant numerator by a baseline-only +model -- the same regression, but with $L_0$ in place of $\bar L_j$: + +$$ +SW_i(k) \;=\; +\prod_{j=1}^{k} +\frac{\Pr\!\big(C_j = 0 \,\mid\, \bar C_{j-1}=0,\; \bar L_0,\; \text{arm}\big)} + {\Pr\!\big(C_j = 0 \,\mid\, \bar C_{j-1}=0,\; \bar L_j,\; \text{arm}\big)}. +$$ + +The *denominator* conditions on the full time-varying history $\bar L_j$ and +models the actual censoring mechanism; the *numerator* conditions on baseline +covariates $\bar L_0$ only, reducing the variance of the weights without changing +the causal estimand. Taking the cumulative product across time gives $W_i(k)$ or +$SW_i(k)$. + +In the package, `estimate_censoring()` fits these probabilities and adds the +uncensoring probability `P_uncens` to each row; `weight_cases()` forms the weight +in the column `weight_Cox`. Here `age` and `sex` are fixed at baseline, so the +numerator and denominator models coincide and stabilization would make no +difference; we therefore use the unstabilized `pooled_logit` weight. + +```{r weight} +clones_estimated <- estimate_censoring( + clones_final, + predictors = c("age", "sex"), + method = "pooled_logit" +) +clones_weighted <- weight_cases(clones_estimated) +``` + +It is good practice to inspect the weight distribution: extremely large weights +inflate variance and can signal near-violations of positivity. + +```{r weight-diagnostics, fig.width = 6, fig.height = 4, fig.cap = "Distribution of IPC weights."} +weights_all <- unlist( + lapply(clones_weighted, function(x) x[["weight_Cox"]]), + use.names = FALSE +) +summary(weights_all) +hist(weights_all, breaks = 40, col = "grey80", border = "white", + main = NULL, xlab = "IPC weight") +``` + +## Estimating the effect + +With clone-censor-weighted data in hand, `emul_estimate()` fits the weighted Cox +model + +$$ +\lambda\!\big(t \mid \text{arm}, L\big) +\;=\; \lambda_0(t)\,\exp\!\big\{\gamma\,\mathbb{1}(\text{arm}=\text{treated}) + \delta^\top L\big\}, +$$ + +weighting each person-time record by its IPC weight. The per-protocol hazard +ratio is $\mathrm{HR} = e^{\gamma}$ -- the causal contrast specified in Section 2. + +```{r cox} +cox_fit <- emul_estimate( + clones_weighted, + method = "Cox", + weights = "weight_Cox", + predictors = c("age", "sex") +) +summary(cox_fit)$conf.int +``` + +Because the weights make the effective sample differ from the raw data, the +model-based standard error is unreliable. `emul_estimate_bootstrap()` resamples +patients (the clustering unit) $B$ times, refits the model to each resample, and +takes the percentile interval of the resulting hazard ratios, + +$$ +\big(\widehat{\mathrm{HR}}^{*}_{(\alpha/2)},\ \widehat{\mathrm{HR}}^{*}_{(1-\alpha/2)}\big), +$$ + +at confidence level $1-\alpha$. The number of resamples $B$ is kept small here +for speed. + +```{r bootstrap} +boot <- emul_estimate_bootstrap( + clones_weighted, + method = "Cox", + weights = "weight_Cox", + predictors = c("age", "sex"), + n_bootstrap = 200, + seed = 1 +) +c(HR_lower = unname(boot$ci_lower), HR_upper = unname(boot$ci_upper)) +``` + +For a visual summary, `method = "KM"` returns weighted Kaplan-Meier curves for +the two strategies. + +```{r km, fig.width = 6, fig.height = 4, fig.cap = "Weighted survival curves by strategy."} +km_fit <- emul_estimate(clones_weighted, method = "KM", weights = "weight_Cox") +plot(km_fit, col = c("#1b9e77", "#d95f02"), lwd = 2, + xlab = "Days since time zero", ylab = "Survival probability") +legend("bottomleft", legend = arms, col = c("#1b9e77", "#d95f02"), + lwd = 2, bty = "n") +``` + +# References + +Gaber, C. E., Ghazarian, A. A., Strassle, P. D., Ribeiro, T. B., Salas, M., & +Maringe, C. (2024). De-mystifying the clone-censor-weight method for causal +research using observational data: a primer for cancer researchers. +*Cancer Medicine*, 13(23), e70461. + +Hernán, M. A., Brumback, B., & Robins, J. M. (2000). Marginal structural models +to estimate the causal effect of zidovudine on the survival of HIV-positive men. +*Epidemiology*, 11(5), 561-570. + +Hernán, M. A., & Robins, J. M. (2016). Using big data to emulate a target trial +when a randomized trial is not available. *American Journal of Epidemiology*, +183(8), 758-764. + +Hernán, M. A., Wang, W., & Leaf, D. E. (2022). Target trial emulation: a +framework for causal inference from observational data. *JAMA*, 328(24), +2446-2447. + +Maringe, C., Benitez Majano, S., Exarchakou, A., et al. (2020). Reflection on +modern methods: trial emulation in the presence of immortal-time bias. +*International Journal of Epidemiology*, 49(5), 1719-1729.