--- title: "Using tters with TrialEmulation" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Using tters with TrialEmulation} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} # The examples need a built Rust engine and (for the backend workflow) the # TrialEmulation package, so they are shown but not evaluated when the vignette # is built. Copy them into an R session with `tters` (and `TrialEmulation`) # installed to run them. knitr::opts_chunk$set(collapse = TRUE, comment = "#>", eval = FALSE) ``` `tters` reproduces the sequential trial-emulation *data expansion* of [`TrialEmulation`](https://cran.r-project.org/package=TrialEmulation) **bit-for-bit**, in a memory-safe Rust + Polars engine. Rust owns the deterministic data transformation; R keeps statistical estimation. That split is exposed two ways: standalone expansion functions, and a drop-in `te_datastore` backend for a `TrialEmulation` pipeline. ## Install ```{r} install.packages("tters", repos = c("https://oldschoolcool2.r-universe.dev", "https://cloud.r-project.org")) ``` No Rust toolchain is needed to install the binary build from r-universe. ## Standalone: expand a cohort in memory The `*_df` functions take a `data.frame` and return one, with no intermediate files. Column dtypes are preserved exactly (including 64-bit `bit64::integer64` ids). ```{r} library(tters) # long person-time: one row per (id, period) cohort <- data.frame( id = c(1L, 1L, 1L, 2L, 2L), period = c(0L, 1L, 2L, 0L, 1L), treatment = c(1L, 1L, 0L, 0L, 1L), eligible = c(1L, 0L, 0L, 1L, 0L), outcome = c(0L, 0L, 1L, 0L, 0L) ) expanded <- expand_trial_df(cohort, estimand = "ITT") head(expanded) ``` A Parquet-in / Parquet-out path (`expand_trial()`), pre-computed-weight (`expand_trial_weighted()`) and in-Rust weight-fitting (`fit_trial_weights()`, `expand_trial_weighted_fitted()`) variants exist too — see their help pages and the package reference. ## As a TrialEmulation backend Set up a `trial_sequence()` exactly as you would for `TrialEmulation::expand_trials()`, then run the expansion in Rust by pointing the output at `save_to_tters()` and calling `expand_trials_tters()`. Everything downstream — sampling, `fit_msm()` — is unchanged and bit-identical. ```{r} library(TrialEmulation) library(tters) data("data_censored") trial <- trial_sequence("ITT") |> set_data(data = data_censored) |> set_censor_weight_model( censor_event = "censored", numerator = ~x2, denominator = ~ x2 + x1, pool_models = "numerator", model_fitter = stats_glm_logit(save_path = tempfile()) ) |> calculate_weights() |> # weight MODELS fit in R set_outcome_model(adjustment_terms = ~x2) |> set_expansion_options(output = save_to_tters(), chunk_size = 0) trial <- expand_trials_tters(trial) # the EXPANSION runs in Rust trial <- load_expanded_data(trial, seed = 1234, p_control = 0.5) trial <- fit_msm(trial) # estimation stays in R ``` The produced frame is byte-equivalent to `TrialEmulation::expand_trials()` (structural columns bit-exact, `weight` to within machine precision), so `load_expanded_data()`, `sample_controls()`, and `fit_msm()` behave identically. If `TrialEmulation` or the Rust build is unavailable — or for the not-yet- supported "as treated" estimand — `expand_trials_tters()` falls back to `TrialEmulation::expand_trials()` with a message.