Target trial emulation with swereg
This vignette describes the target trial emulation (TTE) workflow in swereg, from skeleton files on disk to ETT-level per-protocol effect estimates. It covers the epidemiological rationale and the R6 and subprocess mechanics together. Read it from the top down.
For the methodological mapping to reference papers (Hernán 2008/2016,
Danaei 2013, Caniglia 2023, Cashin 2025) see
vignette("tte-methodology"). For a one-page glossary of the
terms used in this vignette see
vignette("tte-nomenclature"). For how the skeleton files
are built in the first place see
vignette("skeleton-pipeline").
The TTE concept in one page
A randomized trial of an intervention identifies the causal effect directly when the randomization is valid. Treatment assignment is then independent of potential outcomes, so a comparison of observed outcomes across arms is an unbiased estimator of the effect.
Most clinically important questions cannot be randomized. Common reasons:
- The intervention is a drug that is already approved and widely used.
- Randomization would be unethical.
- The outcome is too rare to power a feasible trial.
Target trial emulation analyses observational data as if they came from a trial. First write down the protocol of the target trial: eligibility, treatment arms, assignment, outcome, follow-up and analysis plan. Then use observational data to build a dataset that follows that protocol as closely as possible. The emulation differs from the trial in specific, named ways, and each difference has a specific statistical correction.
The emulation failures
Hernán and Robins (2016) name four specific failure modes that observational analyses suffer which randomized trials do not:
Treatment assignment is not random. People who choose the intervention differ systematically from people who do not. The correction is baseline confounder adjustment, typically by inverse probability of treatment weighting (IPW).
Time zero is not well-defined. In a trial, time zero is the moment of randomization. In observational data there is no such moment, so you have to define one. swereg uses enrollment periods: each enrollment period of
period_widthweeks opens one trial, and it classifies the arm. Time zero is a landmark: the first week after the enrollment period closes. A person enters the trial only if they reach that week under observation and free of every enrollment outcome. Seevignette("tte-timing").Follow-up is informatively truncated. People drop out of observation (emigration, death, end of data) in ways that depend on their baseline or time-varying characteristics. The correction is inverse probability of censoring weighting (IPCW).
Treatment is not adherent. In a trial, adherence is enforced (or non-adherence is measured and modeled). In observational data, people switch treatments over time. swereg reads the weekly treatment assessments and censors per-protocol follow-up at the start of the first discordant week beyond the arm’s tolerance. IPCW-PP then corrects for the selection that censoring creates.
The sequence-of-nested-trials construction
The canonical observational TTE (Hernán et al. 2008; Danaei et al. 2013) builds one trial per eligible enrollment period. Within each period, a new-user design identifies people initiating the treatment (“intervention arm”) and people eligible but not initiating (“comparator arm”). Each person can appear in multiple sequential trials as long as they remain eligible and non-initiated.
This produces a long panel: one row per person per trial per follow-up week. The panel is large, and its structure is uniform. On that panel the statistical operations are standard: the comparator draw, IPW, and pooled logistic or Poisson regression.
The swereg TTE model
The swereg implementation maps this concept onto three R6 classes and two parallelized loops.
TTEDesign: column name schema
TTEDesign holds the names of the columns that define the
trial schema: person ID, treatment, outcome, confounders, time. swereg
constructs it once and reuses it across every enrollment. It holds the
schema of a trial and no trial data.
TTEEnrollment: one sequence of trials
TTEEnrollment wraps the data for one group of trials
that share an eligibility definition. Its data_level field
tracks the lifecycle:
-
"person_week": raw skeleton subset after applying age-range / isoyear / exclusion filters. One row per person per ISO week. This is the input to enrollment. -
"trial": the counting-process trial panel, with one row per person per trial per time period.$enroll()produces it, andTTEEnrollment$new(..., ratio = )calls$enroll()internally.tstart/tstopcolumns appear here. This is the form IPW and IPCW-PP estimation operates on.
Enrollment itself is the comparator draw in each enrollment period.
Within each period_width-week enrollment period, swereg
takes comparator_to_intervention_ratio times that period’s
count of initiators. Where the enrollment period holds fewer comparators
than that, swereg takes all of them. The draw is seeded incidence
density sampling. It is stratified by the period_width-week
enrollment period, and not by the week. It reads no other variable, and
it attaches no comparator to an initiator, so it forms no matched set.
swereg reads only the weeks of an enrollment period in which the person
is eligible and the treatment column holds TRUE or
FALSE. A person is an initiator when at least one of those
weeks holds TRUE. A person is a comparator when all of
those weeks hold FALSE. swereg drops the remaining weeks
first, so an NA week does not stop a comparator
classification. A candidate person-trial with no such week is ineligible
for that trial, and enters neither arm. See
vignette("tte-methods"), section 1.3 (Assignment
procedures), for the full statement of that rule. The draw needs less
computation than full cloning. vignette("tte-methodology")
describes the trade-offs.
TTEPlan: the ETT grid builder
TTEPlan holds the ETT grid: the
Cartesian product of enrollments × outcomes × follow-up
durations. One ETT is one final analysis-ready dataset. The plan
runs the two loops that produce those datasets.
# study is a RegistryStudy with built skeletons. The spec is
# /path/to/spec/spec_v001.yaml.
plan <- swereg::tteplan_from_spec_and_registrystudy(
study = study,
candidate_dir_spec = "/path/to/spec",
candidate_dir_tteplan = "/path/to/plans/my_study",
candidate_dir_results = "/path/to/results",
spec_version = "v001"
)
plan$ett[, .(ett_id, enrollment_id, outcome_var, follow_up, file_analysis)]
#> ett_id enrollment_id outcome_var follow_up file_analysis
#> 1: ETT00001 01 osd_i21_to_i24 52 my_study_analysis_ETT00001.qs2
#> 2: ETT00002 01 osd_i21_to_i24 156 my_study_analysis_ETT00002.qs2
#> 3: ETT00003 01 osd_i21_to_i24 260 my_study_analysis_ETT00003.qs2
#> 4: ETT00004 01 osd_i60_i61_i63 52 my_study_analysis_ETT00004.qs2
#> 5: ETT00005 01 osd_i60_i61_i63 156 my_study_analysis_ETT00005.qs2
#> ...Every row in plan$ett points at three files.
file_raw is post-enrollment and pre-imputation.
file_imp is post-imputation and post-IPW, and it is reused
across outcomes within one enrollment_id.
file_analysis is one per ETT. Loop 1 produces the first
two, and Loop 2 produces the third.
The spec YAML
The clinical and methodological decisions are in a YAML file.
tteplan_read_spec() parses it into a nested R list. The
example below shows the top-level sections.
The example uses a classic TTE setup. It is a new-user comparison of statin initiation against no statin initiation, for primary prevention of myocardial infarction and stroke. The population is adults aged 40 to 75 with no prior cardiovascular disease. This is the canonical example from Hernan et al. (2008) and Danaei et al. (2013).
study:
title: "Statins for primary prevention of cardiovascular events"
principal_investigator: "..."
description: "..."
implementation:
project_prefix: "statins_primary_prevention"
version: "v001"
inclusion_criteria:
isoyears: [2010, 2023]
criteria:
- name: "Prior LDL cholesterol measurement"
rationale: "Primary prevention decisions need a recorded lipid panel"
type: has_event
implementation:
source_variable: lab_ldl
window: lifetime_before_baseline
exclusion_criteria:
- name: "Prior myocardial infarction (ICD-10 I21-I24)"
rationale: "Primary prevention cohort: no prior CVD events"
implementation:
source_variable: osd_i21_to_i24
window: "lifetime_before_baseline"
computed: true
- name: "Prior stroke (ICD-10 I60-I61, I63)"
rationale: "Primary prevention cohort: no prior CVD events"
implementation:
source_variable: osd_i60_i61_i63
window: "lifetime_before_baseline"
computed: true
- name: "Prior statin use (ATC C10AA)"
rationale: "New-user design: no prior statin use"
implementation:
source_variable: rx_c10aa
window: "lifetime_before_baseline"
computed: true
confounders:
- name: "Age (continuous)"
implementation:
variable: rd_age_continuous
- name: "Sex"
implementation:
variable: ri_sex
- name: "Diabetes in past year (ICD-10 E10-E14)"
implementation:
source_variable: osd_e10_to_e14
window: 52
computed: true
- name: "Hypertension in past year (ICD-10 I10-I15)"
implementation:
source_variable: osd_hypertension
window: 52
computed: true
outcomes:
- name: "Myocardial infarction (I21-I24)"
implementation:
variable: osd_i21_to_i24
- name: "Ischemic stroke (I60, I61, I63)"
implementation:
variable: osd_i60_i61_i63
follow_up:
- { label: "1 year", weeks: 52 }
- { label: "3 years", weeks: 156 }
- { label: "5 years", weeks: 260 }
min_expected_events_per_arm_per_year: 5 # number >= 0
enrollments:
- id: "01"
name: "Statin initiation vs none, age 40-75"
family: "Main"
observed_var:
sentinel: row_presence
intervention_tolerance_weeks: 0
comparator_tolerance_weeks: 0
additional_inclusion:
- type: age_range
min: 40
max: 75
implementation:
variable: rd_age_continuous
treatment:
arms:
intervention: "Statin initiation"
comparator: "No statin"
implementation:
comparator_to_intervention_ratio: 5
variable: rd_statin_status
intervention_value: initiated
comparator_value: not_initiated
seed: 4vignette("tte-spec-schema", package = "swereg") lists
every legal key path, and tteplan_read_spec() refuses any
key that list does not name. family is an optional label
that groups enrollments on the Summary sheet of
tables.xlsx, and tte-spec-schema gives its
rules.
The observation contract
Every enrollment MUST declare observed_var. It states
how the data records that a person was under observation in a week.
tteplan_read_spec() stops on an enrollment that omits it.
There is no exemption for an older spec. A spec that cannot say who was
under observation carries the immortal-time defect silently. It looks
exactly like a spec that can.
The key takes one of two forms.
-
observed_var: {column: rd_observed}names a real logical person-week column. -
observed_var: {sentinel: row_presence}asserts that you already deleted every unobserved person-week. A row then exists if and only if the person was observed that week.
Use the sentinel when your skeleton already deletes every person-week
the person was not under observation. The production skeleton is one
example. It deletes every person-week up to and including first
immigration, every person-week on or after emigration, and every
person-week after death. It keeps the death week itself. A real
observed column there would hold TRUE on every
retained row. It could not represent an absent week. swereg does not
accept row presence as a silent proxy. The sentinel makes the assumption
explicit and testable.
tteplan_validate_spec() checks a named column against
the skeleton: the column MUST exist and it MUST be logical.
tteplan_read_spec() cannot run that check, because it reads
no data.
Two flat sibling keys carry the arm tolerances:
intervention_tolerance_weeks and
comparator_tolerance_weeks. Each MUST be a whole number of
at least 0. Each defaults to 0.
To migrate an existing spec, copy it to a new version and add the key to every enrollment. Never edit a released spec version. That version is the record of what produced a run.
Anatomy of an exclusion criterion
Each item under exclusion_criteria has two halves:
- A clinical half (
name,rationale) that medical collaborators can review without knowing anything about R. - An implementation half
(
implementation:) that swereg consumes.source_variablenames the skeleton column that evaluates the rule.windowsets the time during which an event in that column causes exclusion. Acomputedflag marks a rule swereg applies automatically at enrollment time, over a rolling window.
window can be:
- An integer number of weeks (e.g.
104for “in the past 104 weeks”). - The string
"lifetime_before_baseline"(at any time before the candidate time zero). - The string
"lifetime_before_and_after_baseline"(ever, past or future).
The distinction between “lifetime_before” and
“lifetime_before_and_after” matters for prevalent and incident outcomes.
A prior myocardial infarction excludes a person backward only, because
forward is the outcome. A condition that confounds treatment choice
whatever its timing excludes a person in both directions.
tteplan_read_spec() and tteplan_check_spec()
refuse "lifetime_before_and_after_baseline" on an outcome
variable, because the criterion would then select on the follow-up
outcome.
Computed confounders
Confounders with computed: true under
implementation are rolling-window indicators that swereg
builds automatically from the skeleton via
tteplan_apply_derived_confounders(). For example, “diabetes
in past year” is computed as “any TRUE in osd_e10_to_e14
within the last 52 weeks at the candidate time-zero”. Non-computed
confounders read a column directly (e.g. rd_age_continuous
is already on the skeleton).
The computed flag is the same mechanism swereg uses for
computed exclusion criteria: a rolling window over existing skeleton
columns.
Enrollments and the comparator draw
Each item under enrollments is one sequence of trials.
They share a global inclusion/exclusion spec but add their own
additional_inclusion (almost always an age range) and
additional_exclusion (if any). The
treatment.implementation block names the skeleton column
that classifies treatment, which is rd_statin_status here,
a string column holding values such as "initiated" and
"not_initiated". It also names the value that counts as the
intervention arm, the value that counts as the comparator, and the
comparator sampling ratio.
Loop 1: enrollment + IPW
plan$s1_generate_enrollments_and_ipw() runs one
iteration per enrollment_id, not per ETT, because several
ETTs with different outcomes can share the same enrollment. Each
iteration starts a worker subprocess that:
- Loads the skeleton files for the batches assigned to this enrollment.
- Applies the global inclusion filter (
isoyears) and global exclusion criteria viatteplan_apply_exclusions(). - Applies the enrollment’s own
additional_inclusion/additional_exclusion. - Computes any
computed: trueconfounder columns viatteplan_apply_derived_confounders(). - Creates the
TTEEnrollmentwith the comparator draw, and saves it asfile_raw. - Imputes each missing entry-window confounder value once, via
$s1_impute_confounders(). The plan’simpute_fndoes the work, and the default draws one hot-deck value. - Carries the last observed confounder value forward through follow-up
via
$s1b_fill_followup_confounders(). - Fits the baseline IPW model (stabilized logistic regression of
treatment on confounders) via
$s2_ipw(). - Truncates extreme weights at 1/99 percentiles via
$s3_truncate_weights(). - Saves the result as
file_imp({prefix}_imp_{enrollment_id}.qs2).
swereg checks that every skeleton the plan reads is
pipeline-consistent. study$assert_skeletons_consistent()
runs twice:
-
tteplan_from_spec_and_registrystudy()calls it when it builds the plan. -
plan$s1_generate_enrollments_and_ipw()calls it again on the files it is about to read, before step 1 of Loop 1.
The call stops with an error in two cases. The
identity_hash differs between batches, or the stored
identity differs from the current pipeline identity of the study.
check_skeletons = FALSE skips the call at either point. The
check prevents one failure. Without it, you can edit a code in
ICD10_CODES and rebuild skeletons for only some batches.
The analysis then runs on a mix of old and new skeletons with no
error.
Steps 5 to 9 are all TTEEnrollment calls:
enrollment <- TTEEnrollment$new(
data = person_week_data,
design = my_design,
ratio = 2
)
# Passing `ratio` makes $new() call the private $enroll(). That is
# where person-week becomes trial: `data_level` transitions
# "person_week" -> "trial", and `enrollment_stage` reads "enrolled"
# from that point on, before any numbered step has run.
enrollment$enrollment_stage # "enrolled"
# s1e hands the imputation the `.tte_entry__` snapshot names, not
# the plain confounder names.
enrollment$s1_impute_confounders(confounder_vars = entry_confounder_cols)
enrollment$s1b_fill_followup_confounders()
enrollment$s2_ipw()
enrollment$s3_truncate_weights()Loop 1 produces two files per enrollment_id: a file_raw
intermediate and a file_imp final. file_raw
holds the enrolled trial panel before the imputation and the fill, so
you can audit both. tteenrollment_fill_summary() subtracts
the aggregates of the two panels and reports the filled rows and
person-trials per confounder. Loop 1 stores that table on
enrollment$fill_summary.
Loop 2: per-ETT outcome weighting
plan$s2_generate_analysis_files_and_ipcw_pp() runs one
iteration per row of plan$ett (one per outcome × follow-up
combination). Each iteration:
- Loads the
file_impfor this row’senrollment_id. Imputed confounders and baseline IPW weights are already on it. - Prepares the outcome: joins the outcome column, censors at first event or end of follow-up window.
- Fits three IPCW-PP censoring models via
$s4_prepare_for_analysis(): loss, deviation among the not-lost, and deviation at time zero. The loss and deviation models are GAMs or GLMs on time-varying covariates. This includes per-protocol censoring at treatment switch, so the output is NOT an ITT dataset. - Combines weights:
analysis_weight_pp = ipw * ipcw_pp. - Writes
analysis_weight_pp_trunc, a copy ofanalysis_weight_pptruncated at the 1st and 99th percentiles.analysis_weight_ppstays untruncated. The columnsipcw_pp_time_zero,ipcw_pp_loss,ipcw_pp_deviationandipcw_ppstay on the data. - Saves the result as
file_analysis({prefix}_analysis_{ett_id}.qs2).
Loop 2 runs sequentially in the main process, and it is not parallelized. I/O usually dominates the cost of each ETT, and the outcome and IPCW-PP models are fast.
The analysis file: rates, IRRs, survival curves
After Loop 2, each file_analysis is a trial-level panel
with truncated combined weights ready for effect estimation. The
TTEEnrollment R6 class provides seven estimation methods:
$rates(), $irr(),
$survival_curve(), $risk_difference(),
$heterogeneity_test(),
$effect_modification_test() and
$irr_by_subgroup(). Each takes the weight column as its
first argument.
enrollment <- swereg::qs2_read(x_file_analysis)
# Weighted rates (per person-year) by arm
enrollment$rates(weight_col = "analysis_weight_pp_trunc")
# Weighted incidence rate ratio via quasipoisson
# (pooled logistic / IRR-as-HR approximation). The method adds the
# trial-id term itself: natural splines for 5 or more trial ids, a
# linear term for 2 to 4, and no term for 1.
enrollment$irr(weight_col = "analysis_weight_pp_trunc")
# Weighted discrete-time survival curve, one row per arm and time
enrollment$survival_curve(weight_col = "analysis_weight_pp_trunc")swereg uses the IRR approximation to the hazard ratio because the
alternative, survey::svycoxph(), is computationally
prohibitive at registry scale. For rare outcomes, which dominate TTE
studies, the quasi-Poisson IRR is a close approximation to a
flexible-baseline Cox model (Thompson 1977). Its time terms are
flex(tstart), the time since time zero, and
flex(enrollment_period_id), the trial.
vignette("tte-methods") section 1.8.9, “Time terms”,
defines flex().
Heterogeneity across trials
res <- enrollment$heterogeneity_test(weight_col = "analysis_weight_pp_trunc")
# res$p_value: Wald test p-value of the treatment x
# splines::ns(enrollment_period_id) interaction terms
# res$n_trials: the number of trials
# res$interaction_coefs: one row per interaction termThe test checks whether the treatment effect varies across the sequential trials. A significant result suggests calendar-time effect modification or changing selection on treatment initiation.
Effect modification by a baseline subgroup
Two functions answer whether a categorical baseline covariate
modifies the treatment effect. irr_by_subgroup()
gives the stratum-specific IRRs. effect_modification_test()
gives the formal interaction test:
enrollment$irr_by_subgroup(
weight_col = "analysis_weight_pp_trunc",
subgroup_var = "rd_sex"
)
#> level IRR IRR_lower IRR_upper
#> all 2.9 2.1 4.0
#> 0 4.0 2.6 6.2
#> 1 2.0 1.3 3.1
#> attr "em_pvalue" = 0.018
#> attr "ratio_of_irrs" = 0.50 (IRR level 1 / IRR level 0)The interaction term of the single combined model decides whether the
stratum IRRs differ. That term is the em_pvalue. Do not
decide it by comparing the per-stratum confidence intervals. Overlapping
CIs do not imply equal stratum IRRs, and non-overlapping CIs are not the
test either. The subgroup variable MUST be a confounder, so the marginal
weights stay valid within each stratum.
In the spec-driven pipeline, add a top-level subgroups:
block. The analysis then runs automatically per ETT for both estimands.
It appears in the “Effect modification” sheet of the exported
workbook:
Running the full pipeline
The typical project script has the shape:
# 1. Load the study built by the skeleton pipeline
study <- swereg::registrystudy_load(candidate_dir_meta)
# 2. Build the plan from the spec + study (s0)
plan <- swereg::tteplan_from_spec_and_registrystudy(
study = study,
candidate_dir_spec = "/path/to/spec",
candidate_dir_tteplan = "/path/to/plans/my_study",
candidate_dir_results = "/path/to/results",
spec_version = "v001"
)
# 3. Loop 1: enrollment + IPW (parallel across enrollment_ids)
plan$s1_generate_enrollments_and_ipw(n_workers = 4L)
# 4. Loop 2: per-ETT outcome weighting (sequential, fast)
plan$s2_generate_analysis_files_and_ipcw_pp()
# 5. Per-ETT analysis
for (i in seq_len(nrow(plan$ett))) {
x_ett_id <- plan$ett$ett_id[i]
x_file_analysis <- file.path(plan$dir_tteplan, plan$ett$file_analysis[i])
x_outcome <- plan$ett$outcome_var[i]
enrollment <- swereg::qs2_read(x_file_analysis)
irr_result <- enrollment$irr(weight_col = "analysis_weight_pp_trunc")
# save irr_result to results/, named by x_ett_id and x_outcome ...
}The x_ prefix on a loop-extracted variable is a swereg
convention, so write x_outcome and not
outcome. It keeps a loop scalar visually distinct from a
dataset column inside the loop body.
$irr() takes no outcome argument.
$s4_prepare_for_analysis() already wrote the
event column for the active outcome, and
file_analysis carries it.
Feasibility after s2: $s3_feasibility()
$s3_feasibility() runs after s2 and before s3. It
applies the minimum-events rule of vignette("tte-methods")
to the per-protocol estimand of each ETT, and it fits no outcome model.
The intention-to-treat estimand follows the per-protocol decision. Use
it to choose the comparisons and the horizons before you register the
study.
plan$s3_feasibility(n_workers = 4L)
plan$save()The method reads the analysis file, weight, follow-up and threshold
that $s3_analyze() reads, through the same code. Its
decision is therefore the decision s3 makes. It stores the decision in
plan$feasibility and writes feasibility.xlsx
to plan$dir_results.
The workbook holds five to seven sheets, in this order:
- Study Specification.
- Rare conditions, only when the spec carries
rare_condition_rule. - Minimum events (Y or -).
- Minimum events (#).
- Attrition.
- Protocol adherence, only when an enrollment has an adherence table.
- Table 1, unweighted.
The Protocol adherence sheet shows how much per-protocol follow-up
each arm keeps. For each comparison and arm, N (ITT) counts
the person-trials still observed and N (PP) those still on
protocol. The sheet shows weeks 13 and 26 and every multiple of 52, up
to the last follow-up week of the raw panel. Section 1.8.5 of
vignette("tte-methods") defines the counts.
$export_tables() writes the same sheet after the last
Attrition_<id> and Love_<id>
sheet.
The two Minimum events sheets hold one row per outcome and comparison
and one column per follow-up horizon, for the per-protocol estimand.
Minimum events (Y or -) shows Y when the estimate passes the
minimum-events rule and - when it fails the rule. A failed estimate is
withheld unless its comparison is exempt. Minimum events (#) shows the
largest cut at which the estimate still passes the rule. The cuts are 1,
2, 3, 4, 5, 10 and 20 expected events per arm per year. Read a different
cut from it without a rerun. $export_tables() writes the
same two sheets, after Rare conditions.
Neither output holds an event count, a rate or expected events after time zero, and neither holds an effect estimate. The Attrition sheet keeps the eligibility counts by arm, including people excluded for an outcome event before time zero. The decision still depends on the observed events, through the pooled rate.
tte_s0_flags() reads the two flags of a project’s
s0_init.R, --check and
--feasibility. It stops on any other argument, and when
both flags appear.
Running a stage under Slurm: tte_stage()
A Slurm batch job runs one stage per script, so each script must load
the plan itself. swereg::tte_stage() is that script as one
call. It loads the plan from dir_tteplan, runs the stage
method for stage, then runs the steps that follow that
method.
# s1.R, submitted as its own Slurm job
swereg::tte_stage(
"s1",
dir_tteplan = c(
"/mnt/share/plans/my_study",
"/local/plans/my_study"
),
n_workers = 6L
)The stage id is "s1", "s2",
"s3" or "feasibility", and
?tte_stage lists the steps each one runs after its method.
dir_tteplan takes candidate directories in priority order.
tte_stage() uses the first one that exists.
Every argument in ... MUST carry a name. The four stage
methods take their arguments in different orders, so an unnamed argument
binds the wrong formal and reports no error. tte_stage()
rejects an unknown name before it loads the plan, so a typo costs no
read of the network share.
The TARGET checklist
Cashin et al. (2025) published a 21-item checklist for transparent
reporting of target trial emulations.
plan$print_target_checklist() generates a pre-populated
version of that checklist. It maps each TARGET item to the swereg
configuration that implements it, read from the spec YAML. It marks the
items that need manual narrative reporting.
plan$print_target_checklist()Read the output, edit it into a manuscript supplement, and submit it with the paper. It does not show that the analysis is correct. It shows that the reporting is complete.
Reproducibility via provenance
Every plan carries references back to its skeletons. Every skeleton
carries a pipeline_hash() over the pipeline that produced
it. The hash folds in five inputs: the framework function, the trim
function, the phase order, the randvars sequence and the code registry
fingerprints. Every spec file is versioned in git. Together, these give
you a reproducibility chain from final estimates back to the exact code.
That code built the time grid, applied the phase-3 randvars, and
registered the code entries.
The phase order names three phases: framework,
codes and randvars. The trim is not one of
them. The trim is phase 1b, and it runs at one fixed point, on the fresh
base and before the code registry. It has no position in the order to
record. The hash pins it through a field of its own,
Skeleton$trim_fn_hash, which sits alongside the phase order
inside the same digest.
Before Loop 1 runs, the plan asserts that the skeletons are
consistent (study$assert_skeletons_consistent()). If that
passes, every batch was built with the same framework, randvars and
codes configuration. Commit the spec_vNNN.yaml file. Then
git log answers “what was different between run A and run
B”.
Summary
-
Three R6 classes:
TTEDesign(schema),TTEEnrollment(one sequence of trials),TTEPlan(the ETT grid builder). - Two loops: Loop 1 does enrollment + baseline IPW per enrollment_id in parallel worker subprocesses. Loop 2 does per-protocol censoring + IPCW-PP per ETT sequentially.
- Spec YAML captures every clinical and methodological decision in a file that medical collaborators can review and the swereg pipeline can execute.
-
Provenance:
pipeline_hashandassert_skeletons_consistentmake the reproducibility chain auditable. -
Weights: baseline IPW for confounder adjustment
(Loop 1), IPCW-PP for per-protocol censoring (Loop 2), combined as
analysis_weight_pp = ipw * ipcw_pp.ipcw_ppis the product of the factors of three censoring models: loss, deviation among the not-lost, and deviation at time zero.analysis_weight_ppis not truncated.analysis_weight_pp_truncis its copy truncated at the 1st and 99th percentiles. -
Estimation: quasi-Poisson IRR approximation to the
hazard ratio with flex(
tstart) + flex(enrollment_period_id) as time terms, and person-level clustered standard errors viasurvey.
For further reading:
-
vignette("skeleton-pipeline"): how swereg builds and incrementally rebuilds the skeleton files that aTTEPlanconsumes. -
vignette("tte-methodology"): mapping to reference papers. -
vignette("tte-nomenclature"): one-page glossary. -
?TTEPlan,?TTEEnrollment,?TTEDesign: full method reference.
