Author: Bhanoji Duppada
Edition: First Edition, 2026
Website: learnhub101.com
This work was created with AI assistance under the direction and editorial supervision of Bhanoji Duppada. All technical content has been reviewed, verified, and edited by the author. The author retains full copyright and creative responsibility.
Copyright ยฉ 2026 Bhanoji Duppada. All rights reserved.
No part of this publication may be reproduced or transmitted without prior written permission of the author.
By Bhanoji Duppada
The Complete Guide to admiral, sdtmoak, xportr, metacore, and the Pharmaverse Ecosystem
The pharmaverse is a curated collection of R packages built specifically for pharmaceutical programming. Backed by companies like Roche, GSK, Novartis, and Atorus, these packages represent the industry's collective effort to create validated, open-source tools for clinical trial data processing.
This book provides deep, practical coverage of every major pharmaverse package. It goes beyond the package vignettes to show real-world usage patterns, integration strategies, and production deployment workflows.
Published: 2026 | Pages: 200+ | Packages Covered: 12+
ยฉ 2026 Bhanoji Duppada. All rights reserved.
Before 2020, every pharmaceutical company that wanted to use R for clinical programming had to build their own tooling from scratch. Roche built internal packages for ADaM creation. GSK built different internal packages for the same task. Novo Nordisk built yet another set. The result: three companies solving the same problem three different ways, with no code sharing, no standardization, and no community support.
Pharmaverse changes this. It's a coordinated, open-source ecosystem where competing pharmaceutical companies collaborate on shared R packages for clinical data processing. The packages are modular (you can use just one or all of them), validated (with extensive test suites), and maintained by teams across multiple companies.
The analogy for SAS programmers: Think of Pharmaverse as what would happen if every CRO and pharma company agreed to use the SAME macro library. Instead of every company having their own %derive_trtemfl macro with slightly different logic, everyone uses admiral::derive_var_trtemfl() --- one function, one set of rules, tested and documented by the entire industry.
Pharmaverse packages organize into three functional layers:
haven --- Read SAS datasets (.sas7bdat) and XPT transport files
xportr --- Write FDA-compliant XPT files with metadata
readr --- Read CSV files (for non-SAS sources)
dplyr --- Data manipulation (filter, select, mutate, join, summarise)
admiral --- ADaM dataset creation (ADSL, ADAE, ADVS, ADLB, ADTTE)
admiralonco --- Oncology-specific ADaM extensions (ADRS, ADTTE for OS/PFS)
sdtmoak --- SDTM domain creation from raw/operational data
metacore --- Load and manage study specifications
metatools --- Apply metadata (labels, types, lengths) to datasets
gtsummary --- Summary tables (demographics, AE summaries)
gt --- Custom table layouts (any table design)
rtables --- Roche's table framework (alternative to gt)
Tplyr --- Atorus's table framework (another alternative)
ggplot2 --- Clinical figures (line plots, KM curves, forest plots)
survminer --- Kaplan-Meier plots with risk tables
The Pharmaverse packages are designed to work together through a common data model: tidy data frames with CDISC variable names.
# The complete pipeline in 15 lines
library(haven)
library(admiral)
library(dplyr)
library(xportr)
library(gtsummary)
# Layer 1: Read
dm <- read_xpt("data/sdtm/dm.xpt")
ex <- read_xpt("data/sdtm/ex.xpt")
# Layer 2: Transform (admiral creates ADaM)
adsl <- dm %>%
derive_vars_merged(dataset_add = ex, ...) %>%
derive_var_trtdurd() %>%
mutate(SAFFL = if_else(!is.na(TRTSDT), "Y", "N"))
# Layer 2: Apply metadata (metatools/xportr)
adsl %>% xportr_write("output/adsl.xpt", label = "Subject Level Analysis Dataset")
# Layer 3: Output (gtsummary creates tables)
adsl %>% tbl_summary(by = TRT01A) %>% as_gt() %>% gtsave("output/t_14_1_1.rtf")
Each layer consumes and produces standard tibbles (data frames). There's no proprietary data format, no special object type, no lock-in. You can swap gtsummary for rtables without changing your Layer 2 code.
Every company must decide which packages to adopt. Here's the decision framework:
For tables: gtsummary (easiest for standard tables) vs gt (most flexible for custom layouts) vs rtables (Roche-preferred) vs Tplyr (Atorus-preferred). Most new teams start with gtsummary because it produces a demographics table in 15 lines.
For ADaM creation: admiral is the clear winner --- it's the most widely adopted, best documented, and has the most active development community. admiralonco extends it for oncology studies.
For SDTM creation: sdtmoak is newer and less mature than admiral. Many companies still create SDTM in SAS and only use R for ADaM and TLFs.
For metadata: metacore + metatools + xportr work together as a pipeline. metacore loads specs, metatools validates data against specs, and xportr applies metadata during XPT export.
Chapter 2 dives deep into admiral --- the package you'll use most. We'll build ADSL, ADAE, and ADVS from scratch using admiral's standardized functions.
admiral is the core engine of the Pharmaverse. It replaces your company's internal SAS macro library for ADaM dataset creation --- but instead of company-specific macros that nobody outside your team understands, admiral provides standardized, documented, tested functions that the entire industry uses.
The key insight: Every company's ADSL derivation follows the same pattern. Every TRTEMFL flag uses the same logic. Every baseline flag uses the same rule. admiral codifies these universal patterns into functions so you stop reinventing the wheel.
Understanding the naming convention lets you predict function names without looking at the documentation:
derive_vars_* โ Adds MULTIPLE new variables to the dataset
derive_var_* โ Adds ONE new variable
derive_param_* โ Creates new parameter rows (BDS datasets)
create_* โ Creates a new dataset from scratch
restrict_derivation() โ Apply a derivation only to filtered rows
Examples:
derive_vars_merged() # Merge variables from another dataset
derive_vars_dt() # Derive date variables (ADT, ASTDT, etc.)
derive_vars_dtm() # Derive datetime variables
derive_var_trtemfl() # Derive TRTEMFL flag
derive_var_base() # Derive BASE variable
derive_var_chg() # Derive CHG (change from baseline)
derive_var_pchg() # Derive PCHG (percent change)
derive_var_extreme_flag() # Derive first/last occurrence flags
derive_param_bmi() # Create BMI parameter from height/weight
library(admiral)
library(dplyr)
library(haven)
library(lubridate)
# =====================================================
# READ SOURCE DATA
# =====================================================
dm <- read_xpt("data/sdtm/dm.xpt")
ex <- read_xpt("data/sdtm/ex.xpt")
ds <- read_xpt("data/sdtm/ds.xpt")
# =====================================================
# STEP 1: Start with DM, derive ADSL-specific variables
# =====================================================
adsl <- dm %>%
# Treatment variables
mutate(
TRT01P = ARM,
TRT01A = ACTARM,
TRT01PN = case_when(
ARMCD == "PBO" ~ 0,
ARMCD == "DRUG100" ~ 1,
ARMCD == "DRUG200" ~ 2,
TRUE ~ NA_real_
),
TRT01AN = TRT01PN
)
# =====================================================
# STEP 2: Derive treatment dates from EX
# =====================================================
# TRTSDT = first dose date
ex_dates <- ex %>%
derive_vars_dt(new_vars_prefix = "EXST", dtc = EXSTDTC) %>%
derive_vars_dt(new_vars_prefix = "EXEN", dtc = EXENDTC)
adsl <- adsl %>%
derive_vars_merged(
dataset_add = ex_dates,
by_vars = exprs(STUDYID, USUBJID),
new_vars = exprs(TRTSDT = EXSTDT, TRTEDT = EXENDT),
order = exprs(EXSTDT),
mode = "first" # First dose โ TRTSDT
) %>%
# Last dose โ TRTEDT
derive_vars_merged(
dataset_add = ex_dates,
by_vars = exprs(STUDYID, USUBJID),
new_vars = exprs(TRTEDT = EXENDT),
order = exprs(EXENDT),
mode = "last"
)
# =====================================================
# STEP 3: Population flags
# =====================================================
adsl <- adsl %>%
mutate(
SAFFL = if_else(!is.na(TRTSDT), "Y", "N"),
ITTFL = if_else(!is.na(ARM) & ARM != "Screen Failure", "Y", "N"),
RANDFL = ITTFL,
EFFFL = ITTFL
)
# =====================================================
# STEP 4: Treatment duration
# =====================================================
adsl <- adsl %>%
mutate(
TRTDURD = as.numeric(TRTEDT - TRTSDT) + 1
)
# =====================================================
# STEP 5: Disposition reason from DS
# =====================================================
ds_reason <- ds %>%
filter(DSDECOD != "COMPLETED", DSCAT == "DISPOSITION EVENT") %>%
arrange(USUBJID, desc(DSSTDTC)) %>%
group_by(USUBJID) %>%
slice(1) %>%
ungroup() %>%
select(USUBJID, DCSREAS = DSDECOD)
adsl <- adsl %>%
left_join(ds_reason, by = "USUBJID")
# =====================================================
# STEP 6: Derived demographics
# =====================================================
adsl <- adsl %>%
mutate(
AGEGR1 = if_else(AGE >= 65, ">=65", "<65"),
AGEGR1N = if_else(AGE >= 65, 2, 1)
)
# =====================================================
# VERIFY
# =====================================================
cat("ADSL records:", nrow(adsl), "
")
cat("Population flags:
")
adsl %>% count(SAFFL, ITTFL, RANDFL) %>% print()
cat("
Treatment distribution:
")
adsl %>% count(TRT01P, TRT01A) %>% print()
Comparison: - SAS ADSL program: ~150 lines across 8 steps (multiple DATA steps + PROC SORTs) - R ADSL program: ~80 lines in a single pipeline - Both produce identical output
This is the most frequently used admiral function. It replaces the SAS MERGE + BY + IN= pattern:
# SAS equivalent:
# proc sort data=sdtm.ae; by USUBJID; run;
# proc sort data=adam.adsl; by USUBJID; run;
# data adae;
# merge ae(in=a) adsl(keep=USUBJID TRT01A SAFFL TRTSDT ...);
# by USUBJID; if a;
# run;
# admiral:
adae <- ae %>%
derive_vars_merged(
dataset_add = adsl,
by_vars = exprs(STUDYID, USUBJID),
new_vars = exprs(TRT01P, TRT01A, TRT01PN, TRT01AN,
SAFFL, ITTFL, TRTSDT, TRTEDT,
AGE, SEX, RACE, SITEID)
)
Key parameters:
- dataset_add --- The dataset to merge FROM (like the second dataset in MERGE)
- by_vars --- Match keys (like BY statement)
- new_vars --- Variables to bring over (like KEEP= on the second dataset)
- order --- Sort order when multiple matches exist
- mode --- "first" or "last" when multiple matches exist
Chapter 3 covers admiralonco --- admiral's oncology extension for RECIST endpoints, tumor measurements, and response analysis.
Oncology clinical trials have unique analysis requirements that don't exist in other therapeutic areas: tumor response assessment (RECIST), progression-free survival, best overall response, disease control rate, and time-to-event endpoints based on imaging assessments. These derivations are standardized enough to automate but complex enough to get wrong.
admiralonco extends admiral with oncology-specific functions. It handles RECIST response confirmation, derives tumor-related ADaM parameters, and implements the intricate logic for progression dating that oncology programmers typically spend days coding manually.
Best Overall Response (BOR) determines the best response a patient achieved during the study, following RECIST 1.1 confirmation rules:
library(admiralonco)
library(admiral)
library(dplyr)
adrs <- adrs_input %>%
derive_param_bor(
dataset_adsl = adsl,
filter_source = PARAMCD == "OVRLRESP",
source_pd = pd_date,
source_datasets = list(adrs = adrs_input),
reference_date = TRTSDT,
ref_start_window = 42, # Responses before day 42 not counted
set_values_to = exprs(
PARAMCD = "BOR",
PARAM = "Best Overall Response"
)
)
The function implements the full RECIST confirmation logic: a Complete Response (CR) or Partial Response (PR) must be confirmed by a subsequent assessment at least 4 weeks later. An unconfirmed CR becomes a PR, and an unconfirmed PR becomes Stable Disease (SD).
SAS equivalent complexity: In SAS, BOR derivation typically requires 80-120 lines of code with multiple sorts, LAG functions, and conditional logic. admiralonco does it in one function call.
adrs <- adrs %>%
derive_param_confirmed_resp(
dataset_adsl = adsl,
filter_source = PARAMCD == "OVRLRESP",
source_pd = pd_date,
source_datasets = list(adrs = adrs),
ref_confirm = 28, # Minimum 28 days between response and confirmation
set_values_to = exprs(
PARAMCD = "CRSP",
PARAM = "Confirmed Response"
)
)
Clinical benefit = CR + PR + SD (lasting at least a specified duration):
adrs <- adrs %>%
derive_param_clinbene(
dataset_adsl = adsl,
filter_source = PARAMCD == "OVRLRESP",
source_resp = best_resp,
source_datasets = list(adrs = adrs),
reference_date = TRTSDT,
ref_start_window = 42,
set_values_to = exprs(
PARAMCD = "CBR",
PARAM = "Clinical Benefit Rate"
)
)
The oncology Response Analysis Dataset (ADRS) follows BDS structure with oncology-specific PARAMCDs:
PARAMCD PARAM AVAL AVALC
OVRLRESP Overall Response NA PR
BOR Best Overall Response NA PR
CRSP Confirmed Response 1 Y
CBR Clinical Benefit 1 Y
PD Progressive Disease NA PD
AVAL is typically NA for response categories (since they're categorical), while AVALC contains the response code. For binary parameters (CRSP, CBR), AVAL = 1 for responders and 0 for non-responders.
admiralonco also supports oncology time-to-event derivations:
# Progression-Free Survival
adtte <- adtte_input %>%
derive_param_tte(
dataset_adsl = adsl,
source_datasets = list(adrs = adrs, adsl = adsl),
start_date = RANDDT,
event_conditions = list(
# Event 1: Progressive disease
cond_pd,
# Event 2: Death without prior progression
cond_death
),
censor_conditions = list(
# Censored: Last adequate tumor assessment
cond_last_assess
),
set_values_to = exprs(
PARAMCD = "PFS",
PARAM = "Progression-Free Survival"
)
)
The PFS derivation is notoriously complex in SAS --- you need to handle the hierarchy (progression before death, death without progression, censoring), account for missed assessments, and handle the "two or more missed assessments" rule. admiralonco encapsulates all of this.
This is a free sample of the first 3 chapters. The complete guide continues with dozens more chapters of production-ready code, CDISC standards, and FDA submission practices.