ADaM Derivations in R with admiral: a Mini ADSL, End to End

clinical
cdisc
biostatistics
Derive an ADSL-style analysis dataset in R with the pharmaverse admiral package: treatment variables, analysis flags, age groups and time-to-event variables — every step traceable.
Author

Rverse Analytics

Published

August 8, 2026

If your statistical programming lives in the CDISC world, the question is no longer can R do ADaM — the pharmaverse exists precisely for this, and its flagship package admiral is developed openly by Roche, GSK and friends. The real question is what a derivation actually looks like. Here is a miniature but honest ADSL flow: SDTM-shaped inputs in, analysis-ready dataset out, every variable traceable.

SDTM-shaped inputs

In production these come from your SDTM datasets (DM, EX, DS). For a self-contained example we synthesise tiny versions with the same structure:

library(admiral)
library(dplyr)
library(lubridate)

dm <- tribble(
  ~STUDYID, ~USUBJID, ~AGE, ~SEX, ~ARM,       ~COUNTRY,
  "XYZ-1",  "01-101", 64,   "F",  "Drug A",   "TUR",
  "XYZ-1",  "01-102", 71,   "M",  "Drug A",   "TUR",
  "XYZ-1",  "01-103", 58,   "F",  "Placebo",  "DEU",
  "XYZ-1",  "01-104", 80,   "M",  "Placebo",  "DEU",
  "XYZ-1",  "01-105", 47,   "F",  "Drug A",   "GBR"
)

ex <- tribble(
  ~STUDYID, ~USUBJID, ~EXSTDTC,     ~EXENDTC,
  "XYZ-1",  "01-101", "2026-01-10", "2026-06-30",
  "XYZ-1",  "01-102", "2026-01-14", "2026-03-02",
  "XYZ-1",  "01-103", "2026-01-11", "2026-06-28",
  "XYZ-1",  "01-104", "2026-01-20", "2026-05-15"
  # 01-105 randomised but never dosed - watch what happens to her flags
)

Derive treatment dates and flags with admiral

derive_vars_merged() is admiral’s workhorse: merge a summarised source onto the target with explicit semantics, no silent many-to-many surprises.

adsl <- dm |>
  derive_vars_merged(
    dataset_add = ex,
    by_vars     = exprs(STUDYID, USUBJID),
    order       = exprs(EXSTDTC),
    mode        = "first",
    new_vars    = exprs(TRTSDTC = EXSTDTC)
  ) |>
  derive_vars_merged(
    dataset_add = ex,
    by_vars     = exprs(STUDYID, USUBJID),
    order       = exprs(EXENDTC),
    mode        = "last",
    new_vars    = exprs(TRTEDTC = EXENDTC)
  ) |>
  derive_vars_dt(new_vars_prefix = "TRTS", dtc = TRTSDTC) |>
  derive_vars_dt(new_vars_prefix = "TRTE", dtc = TRTEDTC) |>
  mutate(
    SAFFL  = if_else(!is.na(TRTSDT), "Y", "N"),   # safety population: treated
    ITTFL  = "Y",                                  # ITT: all randomised
    TRT01P = ARM,
    TRT01A = if_else(SAFFL == "Y", ARM, NA_character_)
  )

adsl |> select(USUBJID, TRT01P, TRTSDT, TRTEDT, SAFFL, ITTFL)
# A tibble: 5 × 6
  USUBJID TRT01P  TRTSDT     TRTEDT     SAFFL ITTFL
  <chr>   <chr>   <date>     <date>     <chr> <chr>
1 01-101  Drug A  2026-01-10 2026-06-30 Y     Y    
2 01-102  Drug A  2026-01-14 2026-03-02 Y     Y    
3 01-103  Placebo 2026-01-11 2026-06-28 Y     Y    
4 01-104  Placebo 2026-01-20 2026-05-15 Y     Y    
5 01-105  Drug A  NA         NA         N     Y    

Subject 01-105 shows why flags matter: randomised, so ITTFL = "Y" — but never dosed, so SAFFL = "N" and she has no actual treatment. An intention-to-treat table includes her; a safety table must not. The estimand language of ICH E9(R1) starts exactly here.

Age groups and treatment duration

adsl <- adsl |>
  mutate(
    AGEGR1 = case_when(AGE < 65 ~ "<65", AGE < 75 ~ "65-<75", TRUE ~ ">=75"),
    TRTDURD = as.integer(TRTEDT - TRTSDT) + 1
  )

adsl |> select(USUBJID, AGE, AGEGR1, TRTDURD)
# A tibble: 5 × 4
  USUBJID   AGE AGEGR1 TRTDURD
  <chr>   <dbl> <chr>    <dbl>
1 01-101     64 <65        172
2 01-102     71 65-<75      48
3 01-103     58 <65        169
4 01-104     80 >=75       116
5 01-105     47 <65         NA

Why this beats a hand-rolled script

  • Traceability — each admiral call names its source, keys and selection rule; the derivation is the documentation your define.xml points to.
  • Convention over improvisation — variable names (TRT01P, SAFFL, AGEGR1) follow ADaM, so downstream TLF code and reviewers both know what they are looking at.
  • Testable — derivations are functions, so they can carry unit tests; your QC programmer reruns them against the spec instead of eyeballing a DATA step.

In production you would add DTHFL/DTHDT from DS, last-known-alive dates, and time-to-event variables via derive_vars_duration() — the grammar stays identical.


Need ADaM-style datasets or their QC done in R? That’s part of our CRO services — or see the SAS to R migration guide if the whole pipeline is moving.