Skip to contents

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:

  1. 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).

  2. 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_width weeks 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. See vignette("tte-timing").

  3. 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).

  4. 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, and TTEEnrollment$new(..., ratio = ) calls $enroll() internally. tstart / tstop columns 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: 4

vignette("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_variable names the skeleton column that evaluates the rule. window sets the time during which an event in that column causes exclusion. A computed flag marks a rule swereg applies automatically at enrollment time, over a rolling window.

window can be:

  • An integer number of weeks (e.g. 104 for “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:

  1. Loads the skeleton files for the batches assigned to this enrollment.
  2. Applies the global inclusion filter (isoyears) and global exclusion criteria via tteplan_apply_exclusions().
  3. Applies the enrollment’s own additional_inclusion / additional_exclusion.
  4. Computes any computed: true confounder columns via tteplan_apply_derived_confounders().
  5. Creates the TTEEnrollment with the comparator draw, and saves it as file_raw.
  6. Imputes each missing entry-window confounder value once, via $s1_impute_confounders(). The plan’s impute_fn does the work, and the default draws one hot-deck value.
  7. Carries the last observed confounder value forward through follow-up via $s1b_fill_followup_confounders().
  8. Fits the baseline IPW model (stabilized logistic regression of treatment on confounders) via $s2_ipw().
  9. Truncates extreme weights at 1/99 percentiles via $s3_truncate_weights().
  10. 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:

  1. tteplan_from_spec_and_registrystudy() calls it when it builds the plan.
  2. 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:

  1. Loads the file_imp for this row’s enrollment_id. Imputed confounders and baseline IPW weights are already on it.
  2. Prepares the outcome: joins the outcome column, censors at first event or end of follow-up window.
  3. 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.
  4. Combines weights: analysis_weight_pp = ipw * ipcw_pp.
  5. Writes analysis_weight_pp_trunc, a copy of analysis_weight_pp truncated at the 1st and 99th percentiles. analysis_weight_pp stays untruncated. The columns ipcw_pp_time_zero, ipcw_pp_loss, ipcw_pp_deviation and ipcw_pp stay on the data.
  6. 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 term

The 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:

subgroups:
  - name: "Sex"
    implementation:
      variable: rd_sex      # must also be a confounder

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:

  1. Study Specification.
  2. Rare conditions, only when the spec carries rare_condition_rule.
  3. Minimum events (Y or -).
  4. Minimum events (#).
  5. Attrition.
  6. Protocol adherence, only when an enrollment has an adherence table.
  7. 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_hash and assert_skeletons_consistent make 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_pp is the product of the factors of three censoring models: loss, deviation among the not-lost, and deviation at time zero. analysis_weight_pp is not truncated. analysis_weight_pp_trunc is 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 via survey.

For further reading: