๐Ÿ“– Free Preview — Pharmaverse in Practice · by Bhanoji Duppada ← All Books

Pharmaverse in Practice

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.


Pharmaverse Mastery: R Packages for Clinical Trials

By Bhanoji Duppada

The Complete Guide to admiral, sdtmoak, xportr, metacore, and the Pharmaverse Ecosystem


About This Book

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.

Table of Contents

  1. Pharmaverse Architecture and Philosophy
  2. admiral Deep Dive --- ADaM Derivations
  3. admiralonco --- Oncology Extensions
  4. sdtmoak --- SDTM Mapping from Raw Data
  5. metacore and metatools --- Metadata Management
  6. xportr --- XPT Transport Files and Labels
  7. renv --- Reproducibility for Regulated Environments
  8. riskmetric --- Package Risk Scoring
  9. End-to-End Case Study: Complete CDISC Pipeline
  10. Advanced Topics: Custom Extensions, CI/CD, Enterprise Deployment

Published: 2026 | Pages: 200+ | Packages Covered: 12+

ยฉ 2026 Bhanoji Duppada. All rights reserved.


Chapter 1: The Pharmaverse Architecture --- Why It Exists and How It Works

The Problem Pharmaverse Solves

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.


The Three Layers

Pharmaverse packages organize into three functional layers:

Layer 1: Data I/O (Reading and Writing)

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)

Layer 2: Data Transformation (The Core Pipeline)

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

Layer 3: Output Generation (Tables, Listings, Figures)

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

How Packages Talk to Each Other

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.


Package Selection Guide

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.


What's Next

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.


Chapter 2: admiral Deep Dive --- ADaM Dataset Creation in R

What admiral Does

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.


admiral Function Naming Conventions

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

Building ADSL with admiral

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


The derive_vars_merged() Pattern

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


What's Next

Chapter 3 covers admiralonco --- admiral's oncology extension for RECIST endpoints, tumor measurements, and response analysis.


Chapter 3: admiralonco --- Oncology-Specific ADaM Derivations

Why Oncology Needs Its Own Package

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.

Key admiralonco Functions

derive_param_bor --- Best Overall Response

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.

derive_param_confirmed_resp --- Confirmed Response

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"
    )
  )

derive_param_clinbene --- Clinical Benefit Rate

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"
    )
  )

ADRS Dataset Structure

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.

ADTTE for Oncology Endpoints

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.

Key Takeaways

  1. admiralonco handles RECIST 1.1 confirmation logic that takes 100+ lines in SAS
  2. BOR, confirmed response, and clinical benefit rate are one-function-call derivations
  3. ADRS follows BDS structure with oncology-specific PARAMCDs
  4. PFS derivation correctly handles the progression/death/censoring hierarchy
  5. If you work in oncology, admiralonco is the highest-value pharmaverse package after admiral itself
  6. Always validate against your study's specific RECIST protocol --- some studies modify standard RECIST rules

โ€” End of Preview โ€”

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.