Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/check-reproducible.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ jobs:
R-CMD-check-renv:
runs-on: ubuntu-latest

env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
R_KEEP_PKG_SOURCE: yes
RENV_CONFIG_REPOS_OVERRIDE: https://cloud.r-project.org

steps:
- uses: actions/checkout@v5

Expand All @@ -19,7 +24,10 @@ jobs:
- uses: r-lib/actions/setup-r@v2
with:
r-version: "4.4.2"
use-public-rspm: true
# renv is loaded from .Rprofile before setup-renv can install it.
# Use CRAN directly so that bootstrap does not depend on a
# platform-specific Posit Package Manager endpoint.
use-public-rspm: false

- uses: r-lib/actions/setup-renv@v2

Expand Down
24 changes: 13 additions & 11 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,34 +1,36 @@
Package: clonecensorweighting
Title: Infrastructure for Clone-Censor-Weighting Analyses
Title: Clone-Censor-Weight Methods for Target Trial Emulation
Version: 0.0.0.9000
Authors@R: c(
person("Sang Ho", "Park", email = "shstat1729@gmail.com", role = c("aut", "cre")),
person("Youngrok", "Lee", role = "aut")
)
Author: Sang Ho Park [aut, cre],
Youngrok Lee [aut]
Maintainer: Sang Ho Park <shstat1729@gmail.com>
Description: Provides a starter package for clone-censor-weighting workflows
used in target trial emulation. The package currently includes data
ingestion helpers, a minimal data cloning routine, and utilities for
constructing survival responses.
Description: Implements clone-censor-weight analyses for emulating target
trials with grace-period treatment strategies using observational
time-to-event data. Provides tools to clone subjects across strategies,
apply strategy-specific artificial censoring, estimate inverse probability
of censoring weights using pooled logistic or Cox models, fit weighted
outcome models, and obtain subject-level bootstrap confidence intervals by
repeating the complete analysis. The methods are described by Maringe et
al. (2020) <doi:10.1093/ije/dyaa057> and Gaber et al. (2024)
<doi:10.1002/cam4.70461>.
License: MIT + file LICENSE
URL: https://github.com/CausalInferenceLab/clonecensorweighting
BugReports: https://github.com/CausalInferenceLab/clonecensorweighting/issues
Encoding: UTF-8
Depends:
R (>= 4.4.0)
Imports:
dplyr,
purrr,
readr,
survival,
tibble,
tidyr,
rlang,
glue
Suggests:
knitr,
rmarkdown,
testthat (>= 3.0.0)
testthat (>= 3.2.0)
Config/testthat/edition: 3
Roxygen: list(markdown = TRUE)
RoxygenNote: 8.0.0
Expand Down
1 change: 0 additions & 1 deletion NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

export(apply_logics)
export(clone_arms)
export(clone_censor_weighting)
export(create_censoring_logics_A)
export(create_final_data)
export(create_policy_A)
Expand Down
18 changes: 16 additions & 2 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# clonecensorweighting 0.0.0.9000

## Correctness

* Fixed Cox censoring probabilities to evaluate the cumulative baseline hazard
as a step function at every interval start rather than assigning zero when an
interval start did not exactly match a censoring-event time.
* Added a configurable natural cubic spline for interval start time in
pooled-logistic censoring models (`time_spline_df`; `NULL` retains the
linear-time default).
* Changed `emul_estimate_bootstrap()` to resample the original subjects and
repeat cloning, artificial censoring, person-time expansion, censoring-model
estimation, weighting, and outcome-model estimation in every replicate.
* Removed the development scaffold `clone_censor_weighting()` from the public
API.

## Refactoring

* Split the final censoring-probability training data workflow into focused
Expand All @@ -20,8 +34,8 @@
inverse probability of censoring weights.
* Added `emul_estimate()` as the public interface for Cox, logistic, and
Kaplan-Meier analyses of the emulated trial.
* Added `emul_estimate_bootstrap()` as the public interface for bootstrap
confidence intervals for two-arm Cox and logistic analyses.
* Added `emul_estimate_bootstrap()` as the public interface for full-workflow
bootstrap confidence intervals for two-arm Cox and logistic analyses.
* Kept formula construction, cumulative uncensoring, baseline predictor
extraction, predictor/weight normalization, arm binding, and coefficient
extraction as internal helpers.
Expand Down
56 changes: 41 additions & 15 deletions R/censoring_estimation.R
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
#' @param id Column name for the subject identifier.
#' @param time_start Column name for interval start time.
#' @param time_stop Column name for interval stop time.
#' @param time_spline_df Degrees of freedom for the natural cubic spline of
#' `time_start` in pooled-logistic censoring models. Set to an integer of at
#' least `2` to use a spline. The default `NULL` uses a linear time term.
#' Ignored when `method = "Cox"`.
#' @param eps Small probability floor to avoid division by zero.
#'
#' @returns A named list of clone data frames with censoring probability
Expand Down Expand Up @@ -51,6 +55,7 @@ estimate_censoring <- function(
id = "id",
time_start = "Tstart",
time_stop = "Tstop",
time_spline_df = NULL,
eps = 1e-6
) {
method <- match.arg(method)
Expand All @@ -67,6 +72,9 @@ estimate_censoring <- function(
}
.assert_clone_columns(clones, required_columns)
.assert_probability_floor(eps)
if (method != "Cox") {
.assert_spline_df(time_spline_df, "time_spline_df")
}

arms <- names(clones)
res <- vector("list", length = length(arms))
Expand All @@ -88,7 +96,8 @@ estimate_censoring <- function(
pooled_formula <- make_censoring_formula(
backtick_name(censoring),
predictors,
time_var = time_start
time_var = time_start,
time_spline_df = time_spline_df
)

for (arm in arms) {
Expand All @@ -112,16 +121,17 @@ estimate_censoring <- function(
survival::basehaz(ms_cens, centered = FALSE)
)
names(base_hazard) <- c("hazard", "t")
hazard_index <- findInterval(dat[[time_start]], base_hazard$t)
baseline_hazard <- numeric(nrow(dat))
has_hazard <- hazard_index > 0L
baseline_hazard[has_hazard] <-
base_hazard$hazard[hazard_index[has_hazard]]

res[[arm]] <-
dat |>
dplyr::mutate(lin_pred = .env[["lin_pred"]]) |>
dplyr::left_join(
base_hazard,
by = stats::setNames("t", time_start)
) |>
dplyr::mutate(
hazard = dplyr::coalesce(.data$hazard, 0),
lin_pred = .env[["lin_pred"]],
hazard = .env[["baseline_hazard"]],
P_uncens = exp(-.data$hazard * exp(.data$lin_pred))
)
} else {
Expand Down Expand Up @@ -161,7 +171,8 @@ estimate_censoring <- function(
numerator_formula <- make_censoring_formula(
backtick_name(censoring),
numerator_data$predictors,
time_var = time_start
time_var = time_start,
time_spline_df = time_spline_df
)
fit_num <- stats::glm(
numerator_formula,
Expand Down Expand Up @@ -200,15 +211,30 @@ estimate_censoring <- function(
make_censoring_formula <- function(
response,
predictors = NULL,
time_var = NULL
time_var = NULL,
time_spline_df = NULL
) {
terms <- unique(c(
normalize_predictors(time_var),
normalize_predictors(predictors)
))
terms <- backtick_name(terms)
time_var <- normalize_predictors(time_var)
predictors <- backtick_name(normalize_predictors(predictors))

formula <- stats::reformulate(terms, response = response)
time_term <- backtick_name(time_var)
if (length(time_var) > 0L && !is.null(time_spline_df)) {
.assert_spline_df(time_spline_df, "time_spline_df")
time_term <- paste0(
"splines::ns(",
backtick_name(time_var),
", df = ",
as.integer(time_spline_df),
")"
)
}
terms <- unique(c(time_term, predictors))

if (length(terms) == 0L) {
formula <- stats::as.formula(paste(response, "~ 1"))
} else {
formula <- stats::reformulate(terms, response = response)
}
environment(formula) <- parent.frame()
formula
}
Expand Down
111 changes: 0 additions & 111 deletions R/clone_censor_weighting.R

This file was deleted.

12 changes: 7 additions & 5 deletions R/data.R
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#' Simulated lung cancer patients data by Maringe et al. (2020)
#'
#' The dataset is from supplmentary files of [Maringe et al. (2020)](https://doi.org/10.1093/ije/dyaa057).
#' The dataset is from the supplementary files of Maringe et al. (2020),
#' \doi{10.1093/ije/dyaa057}.
#' The dataset is a set of 200 simulated lung cancer patients.
#' These patients are followed up for a year following their cancer diagnosis:
#' 106 of them received surgery within six months of their diagnosis and
Expand All @@ -22,15 +23,16 @@
#' \item{charlson}{Charlson's comorbidity index}
#' \item{emergency}{route to diagnosis}
#' }
#' @source <https://doi.org/10.1093/ije/dyaa057>
#' @source \doi{10.1093/ije/dyaa057}
#' @examples
#' data(lungcancer)
"lungcancer"


#' 13 types of patient records in Maringe et al. (2020)
#'
#' The dataset is from Figure 2 in [Maringe et al. (2020)](https://doi.org/10.1093/ije/dyaa057).
#' The dataset is from Figure 2 in Maringe et al. (2020),
#' \doi{10.1093/ije/dyaa057}.
#' The dataset illustrates all possible censoring mechanisms with 13 types of
#' patients records that could be seen in the cancer registry data, when
#' allowing at most one treatment (surgery in this example) for each patient
Expand All @@ -46,7 +48,7 @@
#' \item{death}{observed event at the latest follow-up, 1: dead, 0: alive}
#' \item{followup}{observed follow-up time (time to death or time to latest followup with 1 year (365 days) at maximum)}
#' }
#' @source <https://doi.org/10.1093/ije/dyaa057>
#' @source \doi{10.1093/ije/dyaa057}
#' @examples
#' data(patients13)
"patients13"
"patients13"
Loading