Pharmaverse workflows

Lesson 11 — Clinical Programming with R

Lesson 11 of 12 Intermediate to advanced ~70 min

Learning objectives

  • Navigate the pharmaverse package ecosystem
  • Assemble an end-to-end pipeline from the available packages
  • Use the therapeutic-area extensions
  • Contribute to and depend on community packages responsibly
  • Decide what to build in-house and what to adopt

What the pharmaverse is

A collaboration between pharmaceutical companies, CROs and technology vendors to build an open-source ecosystem for clinical reporting in R. Started in 2021, now covers most of the pipeline.

The important structural point: these are not one company’s packages released publicly. They are jointly developed, with contributors from Roche, GSK, Novartis, J&J, Merck, Pfizer, Atorus, Appsilon and others. That matters because it means no single organisation’s departure kills a package.

pharmaverse.org maintains the current catalogue.

The ecosystem

Data

Package Provides
pharmaversesdtm Example SDTM datasets for examples and tests
pharmaverseadam Example ADaM datasets
admiral.test Test data (superseded by pharmaversesdtm)
random.cdisc.data Randomly generated CDISC-shaped data
library(pharmaversesdtm)
data("dm"); data("ae"); data("lb"); data("vs"); data("ex")

These are synthetic data derived from the CDISC pilot study. Use them for examples, tests and training — never real patient data.

SDTM

Package Status
sdtm.oak EDC-agnostic SDTM mapping framework; newer, developing
sdtmchecks Data quality checks on SDTM

SDTM is the least mature part of the ecosystem, for the reasons discussed in lesson 3 — mapping logic is study-specific and does not generalise the way derivations do.

ADaM

Package Provides
admiral Core ADaM derivations
admiralonco Oncology: response, PFS, best overall response
admiralophtha Ophthalmology
admiralvaccine Vaccines: immunogenicity, reactogenicity
admiralpeds Paediatrics: growth standards
admiralmetabolic Metabolic and cardiovascular

The extensions follow the same conventions as admiral and depend on it, so learning one teaches you all of them.

library(admiralonco)

adrs <- adrs |>
  derive_param_confirmed_resp(
    dataset_adsl = adsl,
    filter_source = PARAMCD == "OVR",
    source_pd = pd_date,
    source_datasets = list(adsl = adsl),
    ref_confirm = 28,
    set_values_to = exprs(PARAMCD = "CBOR", PARAM = "Confirmed Best Overall Response")
  )

Metadata

Package Provides
metacore Read and hold a specification
metatools Apply a specification to data
xportr Transport-file conformance
datasetjson Dataset-JSON read and write

Tables, listings and figures

Package Provides
Tplyr Declarative clinical summaries
r2rtf Submission RTF
rtables Layout-based tables
tern Statistical outputs built on rtables
gtsummary Publication and report tables
tfrmt Metadata-driven table formatting
pharmaRTF RTF titles and footnotes (largely superseded by r2rtf)
rtflite Python — submission RTF, the r2rtf counterpart
chevron Standard TLF catalogue built on tern
visR Standardised clinical visualisations

Applications and infrastructure

Package Provides
teal Modular Shiny framework for clinical data exploration
teal.modules.clinical Ready-made review modules
riskmetric Package risk assessment
logrx Execution logging in a SAS-like log format
staged.dependencies Multi-repository dependency management

An end-to-end pipeline

#-------------------------------------------------------------------------------
# A complete pharmaverse pipeline
#-------------------------------------------------------------------------------

library(pharmaversesdtm)   # example SDTM
library(admiral)           # ADaM derivations
library(metacore)          # specification
library(metatools)         # apply specification
library(xportr)            # transport conformance
library(Tplyr)             # table numbers
library(r2rtf)             # RTF rendering
library(diffdf)            # QC comparison
library(logrx)             # execution log

# --- Specification ----------------------------------------------------------
meta <- spec_to_metacore("metadata/adam_spec.xlsx")

# --- SDTM -------------------------------------------------------------------
data("dm"); data("ex"); data("ae"); data("lb")
dm <- convert_blanks_to_na(dm)
ex <- convert_blanks_to_na(ex)
ae <- convert_blanks_to_na(ae)

# --- ADSL -------------------------------------------------------------------
adsl <- dm |>
  mutate(TRT01P = ARM, TRT01A = ACTARM) |>
  derive_vars_merged(
    dataset_add = ex, filter_add = EXDOSE > 0,
    new_vars = exprs(TRTSDT = convert_dtc_to_dt(EXSTDTC)),
    order = exprs(EXSTDTC), mode = "first",
    by_vars = exprs(STUDYID, USUBJID)
  ) |>
  mutate(SAFFL = if_else(!is.na(TRTSDT), "Y", "N"))

adsl <- adsl |>
  drop_unspec_vars(select_dataset(meta, "ADSL")) |>
  check_variables(select_dataset(meta, "ADSL")) |>
  order_cols(select_dataset(meta, "ADSL"))

# --- ADAE -------------------------------------------------------------------
adae <- ae |>
  derive_vars_merged(dataset_add = adsl,
                     new_vars = exprs(TRTSDT, TRTEDT, TRT01A, SAFFL),
                     by_vars = exprs(STUDYID, USUBJID)) |>
  derive_vars_dt(dtc = AESTDTC, new_vars_prefix = "AST",
                 highest_imputation = "M", min_dates = exprs(TRTSDT)) |>
  derive_var_trtemfl(trt_start_date = TRTSDT, trt_end_date = TRTEDT,
                     end_window = 30)

# --- Transport --------------------------------------------------------------
adsl |>
  xportr_type(select_dataset(meta, "ADSL")) |>
  xportr_length(select_dataset(meta, "ADSL")) |>
  xportr_label(select_dataset(meta, "ADSL")) |>
  xportr_write("data/submission/adsl.xpt", strict_checks = TRUE)

# --- Table ------------------------------------------------------------------
t <- tplyr_table(adae, TRT01A, where = TRTEMFL == "Y") |>
  set_pop_data(adsl) |> set_pop_treat_var(TRT01A) |>
  add_layer(group_count(vars(AEBODSYS, AEDECOD)) |> set_distinct_by(USUBJID))

build(t) |>
  rtf_title("Table 14.3.1", "Treatment-Emergent Adverse Events") |>
  rtf_body() |>
  rtf_encode() |>
  write_rtf("output/tables/t_14_3_1.rtf")

Every step uses a package designed to work with the others: admiral outputs feed metatools, which feeds xportr; Tplyr outputs feed r2rtf.

logrx

Produces a SAS-style execution log, which matters when a validation process expects one.

library(logrx)

axecute(
  "programs/adam/ad_adsl.R",
  log_name = "ad_adsl.log",
  log_path = "output/logs",
  remove_log_object = FALSE,
  to_report = c("messages", "output", "result")
)

The log contains:

--------------------------------------------------------------------------------
                                  logrx Metadata
--------------------------------------------------------------------------------
File Name: ad_adsl.R
File Path: /study/abc101/programs/adam
File HashSum: 8f3a2b1c...
Log Name: ad_adsl.log
Log Path: /study/abc101/output/logs
Start time: 2026-07-28 14:32:10 UTC
End time: 2026-07-28 14:32:47 UTC
Run time: 37 seconds

--------------------------------------------------------------------------------
                                User and File Information
--------------------------------------------------------------------------------
User: rgaduputi
Hostname: validation-server-01
R Version: R version 4.4.1 (2024-06-14)
Platform: x86_64-pc-linux-gnu

--------------------------------------------------------------------------------
                                Masked Functions
--------------------------------------------------------------------------------
function `filter` from {dplyr} by {stats}

--------------------------------------------------------------------------------
                          Used Package and Functions
--------------------------------------------------------------------------------
{admiral} v1.1.1: derive_vars_merged, derive_var_trtdurd
{dplyr}   v1.1.4: mutate, if_else, filter
{xportr}  v0.4.1: xportr_type, xportr_length, xportr_write

--------------------------------------------------------------------------------
                                Program Source Code
--------------------------------------------------------------------------------
[full source of the program]

--------------------------------------------------------------------------------
                                Errors and Warnings
--------------------------------------------------------------------------------
[any conditions raised]

The file hash and the masked functions section are the two most useful parts. The hash proves which version of the program ran; masked functions reveals the dplyr::filter / stats::filter class of bug that produces silently wrong results.

teal

A modular Shiny framework for clinical data exploration, from Roche.

library(teal)
library(teal.modules.clinical)

app <- init(
  data = teal_data(
    ADSL = adsl,
    ADAE = adae,
    ADLB = adlb
  ),
  modules = modules(
    tm_data_table("Data listing"),
    tm_t_summary("Demographics",
                 dataname = "ADSL",
                 arm_var = choices_selected(c("ARM", "ARMCD"), "ARM"),
                 summarize_vars = choices_selected(c("AGE", "SEX", "RACE"),
                                                   c("AGE", "SEX"))),
    tm_t_events("Adverse events",
                dataname = "ADAE",
                arm_var = choices_selected("ARM", "ARM"),
                llt = choices_selected("AEDECOD", "AEDECOD"),
                hlt = choices_selected("AEBODSYS", "AEBODSYS")),
    tm_g_km("Kaplan-Meier",
            dataname = "ADTTE",
            arm_var = choices_selected("ARM", "ARM"),
            paramcd = choices_selected("OS", "OS"))
  ),
  filter = teal_slices(
    teal_slice("ADSL", "SAFFL", selected = "Y")
  ),
  header = "Study ABC-101 Data Review"
)

shinyApp(app$ui, app$server)

teal’s distinguishing feature is that every module shows the R code that produced its output, so a reviewer can reproduce it outside the app. That addresses the usual objection to interactive review tools in a regulated setting.

See the R Shiny course for the underlying Shiny concepts.

Adopting community packages

The decision

Consider Question
Coverage Does it do what you need, or 70% of it?
Maturity Version, release history, breaking changes
Maintenance Recent commits? Issues closed? Multiple maintainers?
Testing Coverage, meaningful tests, CI
Documentation Complete help pages, vignettes, a specification
Dependencies How many, and how stable are they?
Alternatives Could you write and test the 50 lines yourself?
Exit If it is abandoned, what is the cost of leaving?

The last question is underrated. A package doing something you could write in a day is low-risk regardless of its maintenance status. A package that is architecturally central — admiral in an ADaM pipeline — needs to be a deliberate bet.

Pin versions

renv::snapshot()
"admiral": {
  "Package": "admiral",
  "Version": "1.1.1",
  "Source": "Repository",
  "Repository": "CRAN"
}

A study locks its versions at the start and does not move them without a documented decision. admiral is actively developed, and a minor release can change a default.

Contributing back

The pharmaverse packages accept contributions. If you fix a bug or add a derivation your organisation needs, upstreaming it means you stop maintaining a fork.

1. Open an issue describing the problem
2. Discuss the approach before writing code
3. Fork, branch, implement with tests
4. Follow the package's contribution guide (admiral has a detailed one)
5. Open a pull request; expect review

admiral’s programming strategy document is worth reading even if you never contribute — it explains the conventions behind the function naming and argument design, which makes the package much easier to use.

Build or adopt?

Adopt Build in-house
Standard CDISC derivations (admiral) Company-specific standards
Transport conformance (xportr) Proprietary output formats
RTF rendering (r2rtf) Internal system integrations
Table summaries (Tplyr, rtables) Therapeutic-area logic not yet covered
Metadata handling (metacore) Your metadata repository’s interface

The mature pattern in most organisations that have made the move: a company standards package that depends on pharmaverse packages, adds company-specific derivations and conventions, and is validated once. Study repositories then depend on the standards package.

Study repository (thin, study-specific)
        ↓ depends on
Company standards package (validated once, versioned)
        ↓ depends on
pharmaverse packages (admiral, xportr, r2rtf, ...)
        ↓ depends on
tidyverse, base R

Each layer is thinner than the one below it, which is the right shape.

The pharmaverse is no longer R-only

rtflite is a pharmaverse package written in Python, and pharmaverse.org now maintains Python material alongside the R ecosystem. That matters for two reasons:

  • A team standardising on Python for TLF production is no longer working outside the ecosystem
  • The conventions — component-based table construction, ADaM as the input, RTF as the deliverable — are shared, so knowledge transfers between the two

The derivation layer (admiral, metacore, xportr) remains R-only, so the common pattern is R for ADaM and either language for TLFs. See the Python TLF lesson for the equivalent workflow.

Staying current

  • pharmaverse.org — the catalogue and blog
  • The pharmaverse Slack workspace — where the development discussion happens
  • Each package’s NEWS.md — read it before upgrading
  • PHUSE and PharmaSUG conference proceedings
  • The R Consortium Submissions Working Group — pilot submission materials

Common mistakes

Mistake Consequence Fix
Not pinning versions A minor release changes results mid-study renv.lock
Upgrading mid-study Re-validation required Lock at study start
Using a package without assessment Cannot justify it in an audit Document the assessment
Forking instead of contributing Perpetual maintenance burden Upstream the change
Adopting everything Large dependency surface Adopt what you need
Building what exists Wasted effort, unvalidated Check the catalogue first
Ignoring NEWS.md Surprised by a behaviour change Read it before upgrading

Exercise 11.1 — Assess a package for study use

Your team wants to use Tplyr for TLF programming on a submission study. Write the risk assessment.

Show solution

Package risk assessment — Tplyr

Assessed by: R Gaduputi Date: 2026-07-28 For: Study ABC-101, TLF programming


1. Purpose and criticality

Tplyr computes summary statistics for clinical tables — counts, percentages, descriptive statistics, shift tables — from ADaM datasets.

Criticality: HIGH. Output feeds tables in the Clinical Study Report and the submission package. An error in Tplyr produces wrong numbers in a regulatory deliverable.


2. Automated screening

library(riskmetric)
pkg_assess(pkg_ref("Tplyr")) |> pkg_score()
Metric Value
Test coverage 94%
Has vignettes Yes (12)
Has website Yes (pkgdown)
Has news Yes
Has source control Yes (GitHub, atorus-research/Tplyr)
Bugs closed (last 90 days) High proportion
Downloads (last month) Substantial and stable
R CMD check Passing on all CRAN platforms
Licence MIT
Overall risk score Low

3. Manual assessment

Provenance. Developed by Atorus Research, a CRO with a clinical programming focus. Part of the pharmaverse. Multiple maintainers; not dependent on one individual.

Maturity. Version 1.2.x. First CRAN release 2021. Stable public interface; NEWS.md documents deprecations with a transition period.

Testing. 94% coverage, with tests that assert on computed values rather than just checking that functions run. CI runs on push and on a schedule across several R versions.

Documentation. Complete help pages. Twelve vignettes covering layers, denominators, sorting, risk difference and metadata. A published statement of intended use.

Dependencies. Depends on dplyr, tidyr, rlang, purrr, stringr, magrittr, tibble, forcats, lifecycle. All tidyverse or r-lib, all themselves widely used and maintained. No unusual or single-maintainer dependencies.

Community. Active GitHub issues with maintainer responses typically within days. Used in production by several sponsors, discussed at PHUSE and PharmaSUG.

Exit cost. Moderate. Tplyr code is declarative and would need rewriting in dplyr if abandoned — perhaps two to three days per study for a typical TLF set. Not trivial, but not architectural in the way admiral is.


4. Verification approach

Given HIGH criticality, package quality alone is insufficient. Controls:

  1. Independent double programming of every table, per the study QC plan. The QC programmer uses base dplyr, not Tplyr — so a Tplyr defect cannot appear in both implementations.
  2. Unit tests for the study-specific wrapper functions built on Tplyr.
  3. Version pinned in renv.lock at 1.2.1, frozen for the study duration.
  4. Metadata traceabilityget_meta_subjects() used during QC to trace questioned cells back to the contributing subjects.
  5. Manual verification of the first table of each type against a hand calculation, documented.

Control 1 is the substantive one. Everything else is supporting.


5. Residual risks

Risk Likelihood Impact Mitigation
Defect in Tplyr produces wrong numbers Low High Independent QC in dplyr
Interface change on upgrade N/A Version pinned for study duration
Package abandoned mid-study Very low Low Version pinned; source archived
Misuse (wrong denominator, wrong distinct) Medium High Code review; QC comparison

The misuse risk is higher than the defect risk, and is the one worth spending review effort on. set_pop_data() omitted, or set_distinct_by() forgotten, produces plausible-looking wrong numbers. Add a checklist item to the code review.


6. Recommendation

Approved for use on ABC-101 for TLF programming, subject to:

  • Version pinned at 1.2.1 in renv.lock
  • Source tarball archived with the study documentation
  • Independent QC programming in dplyr for every table
  • Code review checklist to include set_pop_data() and set_distinct_by()
  • No version change during the study without a change request

Approved by: [QA representative] Date: ___________


Archiving the source

download.file(
  "https://cran.r-project.org/src/contrib/Archive/Tplyr/Tplyr_1.2.1.tar.gz",
  "docs/package_archive/Tplyr_1.2.1.tar.gz"
)
tools::md5sum("docs/package_archive/Tplyr_1.2.1.tar.gz")
Archiving the tarball and its checksum means the exact source can be produced in an audit, independent of CRAN’s archive policy.

Recap

  • The pharmaverse is a multi-company collaboration, not one vendor’s release
  • admiral for ADaM, metacore/metatools/xportr for metadata and transport, Tplyr/r2rtf for TLFs
  • Therapeutic-area extensions follow admiral’s conventions
  • logrx produces a SAS-style log with a file hash and masked-function detection
  • teal shows the R code behind every output, which addresses the review objection
  • Pin versions at study start; read NEWS.md before any upgrade
  • Company standards package in the middle: thin study repos, validated standards, pharmaverse below
  • Misuse is usually a bigger risk than package defects — review for it

Next: SAS-to-R migration.

Back to top