Pharmaverse workflows
Lesson 11 — Clinical Programming with R
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:
- Independent double programming of every table, per the study QC plan. The QC programmer uses base
dplyr, notTplyr— so aTplyrdefect cannot appear in both implementations. - Unit tests for the study-specific wrapper functions built on
Tplyr. - Version pinned in
renv.lockat 1.2.1, frozen for the study duration. - Metadata traceability —
get_meta_subjects()used during QC to trace questioned cells back to the contributing subjects. - 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
dplyrfor every table - Code review checklist to include
set_pop_data()andset_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")Recap
- The pharmaverse is a multi-company collaboration, not one vendor’s release
admiralfor ADaM,metacore/metatools/xportrfor metadata and transport,Tplyr/r2rtffor TLFs- Therapeutic-area extensions follow
admiral’s conventions logrxproduces a SAS-style log with a file hash and masked-function detectiontealshows the R code behind every output, which addresses the review objection- Pin versions at study start; read
NEWS.mdbefore 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.