30 Days of Pharmaverse
  • Week 1: SDTM Fundamentals
  • Week 2: Production SDTM
  • Week 3: ADaM Deep Dive
  • Week 4: Tables, Listings and Figures
  1. Day 23: ADCM and ADRS - Concomitant Meds and Oncology Response
  • Day 22: Demography Table with gtsummary + gt
  • Day 23: ADCM and ADRS - Concomitant Meds and Oncology Response
  • Day 24: ARD-First Reporting with cards and cardx
  • Day 25: gtsummary and tfrmt - ARD-Backed Production Tables
  • Day 26: flextable and officer - Word and RTF Clinical Tables
  • Day 27: rtables, tern, and r2rtf - Structured Clinical Tables
  • Day 28: Tplyr - Declarative Clinical Table Programming
  • Day 29: ggsurvfit + gtsummary - Survival Plots and Clinical Figures
  • Day 30: Capstone - Full Clinical Reporting Workflow

On this page

  • 1 Overview
  • 2 Setup
  • 3 ADCM - Concomitant Medications
    • 3.1 Step 1: Check CM Structure
    • 3.2 Step 2: Merge ADSL into CM
    • 3.3 Step 3: Derive Start and End Dates
    • 3.4 Step 4: Derive Period Flags
    • 3.5 Step 5: Derive Analysis Sequence
    • 3.6 Step 6: ADCM Validation
  • 4 ADRS - Oncology Tumor Response
    • 4.1 Step 7: Check RS Oncology Structure
    • 4.2 Step 8: Merge ADSL into RS
    • 4.3 Step 9: Derive AVAL from Response
    • 4.4 Step 10: Filter to Overall Visit Response
    • 4.5 Step 11: Derive Best Overall Response
    • 4.6 Step 12: Derive Responder Flag
    • 4.7 Step 13: Combine ADRS Parameters
    • 4.8 Step 14: ADRS Validation
  • 5 Summary
  • 6 Export
  • 7 Key Takeaways
  • 8 Next Steps
  • 9 Resources

Day 23: ADCM and ADRS - Concomitant Meds and Oncology Response

Week 4, Day 23: OCCDS period flags and RECIST 1.1 with admiralonco

Back to Roadmap

1 Overview

Today covers two supporting ADaM datasets:

  • ADCM - Concomitant Medications (OCCDS) with pre/concomitant/prior period flags
  • ADRS - Oncology Response (OCCDS) with RECIST 1.1 Best Overall Response using admiralonco

Note: pharmaversesdtm exports rs_onco (not rs) for oncology response data.


2 Setup

library(admiral)
library(admiralonco)
library(pharmaversesdtm)
library(pharmaverseadam)
library(dplyr)
library(lubridate)
library(xportr)

# Load SDTM source data
cm     <- pharmaversesdtm::cm
rs_onco <- pharmaversesdtm::rs_onco   # oncology-specific RS domain

# Load ADSL for merging
adsl <- pharmaverseadam::adsl

cat("Loaded CM:", nrow(cm), "records\n")
Loaded CM: 7510 records
cat("Loaded RS (oncology):", nrow(rs_onco), "records\n")
Loaded RS (oncology): 5808 records
cat("Loaded ADSL:", nrow(adsl), "subjects\n")
Loaded ADSL: 306 subjects

3 ADCM - Concomitant Medications

3.1 Step 1: Check CM Structure

cm %>%
  dplyr::select(STUDYID, USUBJID, CMTRT, CMDECOD, CMSTDTC, CMENDTC) %>%
  head(5)
# A tibble: 5 × 6
  STUDYID      USUBJID     CMTRT   CMDECOD              CMSTDTC CMENDTC
  <chr>        <chr>       <chr>   <chr>                <chr>   <chr>  
1 CDISCPILOT01 01-701-1015 ASPIRIN ACETYLSALICYLIC ACID 2003    <NA>   
2 CDISCPILOT01 01-701-1015 ASPIRIN ACETYLSALICYLIC ACID 2003    <NA>   
3 CDISCPILOT01 01-701-1015 ASPIRIN ACETYLSALICYLIC ACID 2003    <NA>   
4 CDISCPILOT01 01-701-1015 ASPIRIN ACETYLSALICYLIC ACID 2003    <NA>   
5 CDISCPILOT01 01-701-1015 ASPIRIN ACETYLSALICYLIC ACID 2003    <NA>   

Key CM Variables:

  • CMTRT: Verbatim medication name
  • CMDECOD: Standardised medication name (WHODrug)
  • CMSTDTC / CMENDTC: Start / end dates (ISO 8601)

3.2 Step 2: Merge ADSL into CM

adcm <- cm %>%
  admiral::derive_vars_merged(
    dataset_add = adsl,
    new_vars = exprs(TRTSDT, TRTEDT, TRT01A),
    by_vars = exprs(STUDYID, USUBJID)
  )

cat("ADCM after ADSL merge:", nrow(adcm), "records\n")
ADCM after ADSL merge: 7510 records

3.3 Step 3: Derive Start and End Dates

adcm <- adcm %>%
  admiral::derive_vars_dt(
    new_vars_prefix = "AST",
    dtc = CMSTDTC,
    highest_imputation = "M"
  ) %>%
  admiral::derive_vars_dt(
    new_vars_prefix = "AEN",
    dtc = CMENDTC,
    highest_imputation = "M"
  )

cat("Dates derived\n")
Dates derived
adcm %>%
  dplyr::select(USUBJID, CMTRT, ASTDT, AENDT) %>%
  head(5)
# A tibble: 5 × 4
  USUBJID     CMTRT   ASTDT      AENDT 
  <chr>       <chr>   <date>     <date>
1 01-701-1015 ASPIRIN 2003-01-01 NA    
2 01-701-1015 ASPIRIN 2003-01-01 NA    
3 01-701-1015 ASPIRIN 2003-01-01 NA    
4 01-701-1015 ASPIRIN 2003-01-01 NA    
5 01-701-1015 ASPIRIN 2003-01-01 NA    

3.4 Step 4: Derive Period Flags

adcm <- adcm %>%
  dplyr::mutate(
    # Pre-treatment: started before TRTSDT and ongoing at TRTSDT
    PREFL = dplyr::case_when(
      !is.na(ASTDT) & ASTDT < TRTSDT &
        (is.na(AENDT) | AENDT >= TRTSDT) ~ "Y",
      TRUE ~ NA_character_
    ),
    # Concomitant: overlaps with treatment period
    CONFL = dplyr::case_when(
      !is.na(ASTDT) &
        ASTDT <= TRTEDT &
        (is.na(AENDT) | AENDT >= TRTSDT) ~ "Y",
      TRUE ~ NA_character_
    ),
    # Prior: ended strictly before treatment start
    PRVFL = dplyr::case_when(
      !is.na(AENDT) & AENDT < TRTSDT ~ "Y",
      TRUE ~ NA_character_
    )
  )

cat("Period flags derived\n")
Period flags derived
adcm %>%
  dplyr::summarise(
    N_PREFL = sum(PREFL == "Y", na.rm = TRUE),
    N_CONFL = sum(CONFL == "Y", na.rm = TRUE),
    N_PRVFL = sum(PRVFL == "Y", na.rm = TRUE)
  )
# A tibble: 1 × 3
  N_PREFL N_CONFL N_PRVFL
    <int>   <int>   <int>
1    6155    7337      72

Period Flag Logic:

Flag Meaning
PREFL Started before TRTSDT, ongoing at TRTSDT
CONFL Overlapping with treatment window
PRVFL Ended strictly before TRTSDT

3.5 Step 5: Derive Analysis Sequence

adcm <- adcm %>%
  dplyr::arrange(USUBJID, ASTDT, CMSEQ) %>%
  dplyr::group_by(USUBJID) %>%
  dplyr::mutate(ASEQ = dplyr::row_number()) %>%
  dplyr::ungroup()

cat("ADCM records:", nrow(adcm), "\n")
ADCM records: 7510 

3.6 Step 6: ADCM Validation

cat("\n=== ADCM Validation ===\n\n")

=== ADCM Validation ===
check1 <- adcm %>% dplyr::filter(is.na(ASTDT))
cat("Check 1 - Missing ASTDT:", nrow(check1), "\n")
Check 1 - Missing ASTDT: 21 
check2 <- adcm %>% dplyr::filter(!is.na(PREFL) & PREFL != "Y")
cat("Check 2 - Invalid PREFL values:", nrow(check2), "\n")
Check 2 - Invalid PREFL values: 0 
check3 <- adcm %>%
  dplyr::count(USUBJID, ASEQ) %>%
  dplyr::filter(n > 1)
cat("Check 3 - Duplicate ASEQ per subject:", nrow(check3), "\n")
Check 3 - Duplicate ASEQ per subject: 0 
cat("\n✓ ADCM validation complete\n")

✓ ADCM validation complete

4 ADRS - Oncology Tumor Response

4.1 Step 7: Check RS Oncology Structure

# pharmaversesdtm exports rs_onco (not rs)
names(rs_onco)
 [1] "STUDYID"  "DOMAIN"   "USUBJID"  "RSSEQ"    "RSLNKGRP" "RSTESTCD"
 [7] "RSTEST"   "RSCAT"    "RSORRES"  "RSSTRESC" "RSSTAT"   "RSREASND"
[13] "RSEVAL"   "RSEVALID" "RSACPTFL" "VISITNUM" "VISIT"    "RSDTC"   
[19] "RSDY"    
rs_onco %>%
  dplyr::select(USUBJID, RSTESTCD, RSSTRESC, VISIT, RSDTC) %>%
  head(5)
# A tibble: 5 × 5
  USUBJID     RSTESTCD RSSTRESC      VISIT  RSDTC     
  <chr>       <chr>    <chr>         <chr>  <chr>     
1 01-701-1015 OVRLRESP PD            WEEK 6 2014-02-12
2 01-701-1015 NTRGRESP PD            WEEK 6 2014-02-12
3 01-701-1015 TRGRESP  PR            WEEK 6 2014-02-12
4 01-701-1015 OVRLRESP SD            WEEK 6 2014-02-12
5 01-701-1015 NTRGRESP NON-CR/NON-PD WEEK 6 2014-02-12

4.2 Step 8: Merge ADSL into RS

adrs <- rs_onco %>%
  admiral::derive_vars_merged(
    dataset_add = adsl,
    new_vars = exprs(RANDDT, TRTSDT, TRT01A),
    by_vars = exprs(STUDYID, USUBJID)
  ) %>%
  admiral::derive_vars_dt(
    new_vars_prefix = "A",
    dtc = RSDTC
  )

cat("ADRS base created:", nrow(adrs), "records\n")
ADRS base created: 5808 records
adrs %>%
  dplyr::select(USUBJID, RSTESTCD, RSSTRESC, ADT, TRTSDT) %>%
  head(5)
# A tibble: 5 × 5
  USUBJID     RSTESTCD RSSTRESC      ADT        TRTSDT    
  <chr>       <chr>    <chr>         <date>     <date>    
1 01-701-1015 OVRLRESP PD            2014-02-12 2014-01-02
2 01-701-1015 NTRGRESP PD            2014-02-12 2014-01-02
3 01-701-1015 TRGRESP  PR            2014-02-12 2014-01-02
4 01-701-1015 OVRLRESP SD            2014-02-12 2014-01-02
5 01-701-1015 NTRGRESP NON-CR/NON-PD 2014-02-12 2014-01-02

4.3 Step 9: Derive AVAL from Response

# admiralonco::aval_resp() maps RECIST 1.1 labels to numeric values
adrs <- adrs %>%
  dplyr::mutate(
    PARAMCD = RSTESTCD,
    PARAM   = RSTEST,
    AVALC   = RSSTRESC,
    AVAL    = admiralonco::aval_resp(RSSTRESC)
  )

cat("AVAL derived\n")
AVAL derived
adrs %>%
  dplyr::count(AVALC, AVAL) %>%
  dplyr::arrange(AVAL)
# A tibble: 9 × 3
  AVALC          AVAL     n
  <chr>         <dbl> <int>
1 CR                1   563
2 PR                2   939
3 SD                3   592
4 NON-CR/NON-PD     4   767
5 PD                5  2588
6 NE                6   242
7 CHECK            NA     3
8 EQUIVOCAL        NA    81
9 UNEQUIVOCAL      NA    33

RECIST 1.1 Response Coding:

AVALC AVAL
CR 1
PR 2
SD 3
PD 4
NE 5

4.4 Step 10: Filter to Overall Visit Response

# Filter to overall response records and rename parameter
adrs_ovr <- adrs %>%
  dplyr::filter(RSTESTCD == "OVRLRESP") %>%
  dplyr::mutate(
    PARAMCD = "OVR",
    PARAM   = "Overall Response by Visit"
  )

cat("OVR records:", nrow(adrs_ovr), "\n")
OVR records: 1899 
adrs_ovr %>%
  dplyr::count(AVALC, AVAL)
# A tibble: 5 × 3
  AVALC  AVAL     n
  <chr> <dbl> <int>
1 CHECK    NA     3
2 CR        1   172
3 PD        5  1144
4 PR        2   359
5 SD        3   221

4.5 Step 11: Derive Best Overall Response

# BOR = best (lowest AVAL) response across all visits per subject
adrs_bor <- adrs_ovr %>%
  dplyr::group_by(STUDYID, USUBJID) %>%
  dplyr::slice_min(AVAL, n = 1, with_ties = FALSE) %>%
  dplyr::ungroup() %>%
  dplyr::mutate(
    PARAMCD = "BOR",
    PARAM   = "Best Overall Response"
  )

cat("BOR records:", nrow(adrs_bor), "\n")
BOR records: 205 
adrs_bor %>%
  dplyr::count(AVALC, AVAL)
# A tibble: 4 × 3
  AVALC  AVAL     n
  <chr> <dbl> <int>
1 CR        1    36
2 PD        5    50
3 PR        2    92
4 SD        3    27

4.6 Step 12: Derive Responder Flag

# Responder = CR (AVAL=1) or PR (AVAL=2)
adrs_rsp <- adrs_bor %>%
  dplyr::mutate(
    PARAMCD = "RSP",
    PARAM   = "Responder (CR or PR)",
    AVALC   = dplyr::if_else(AVAL <= 2, "Y", "N"),
    AVAL    = dplyr::if_else(AVALC == "Y", 1, 0)
  )

cat("RSP records:", nrow(adrs_rsp), "\n")
RSP records: 205 
adrs_rsp %>%
  dplyr::count(AVALC, AVAL)
# A tibble: 2 × 3
  AVALC  AVAL     n
  <chr> <dbl> <int>
1 N         0    77
2 Y         1   128

4.7 Step 13: Combine ADRS Parameters

adrs_final <- dplyr::bind_rows(adrs_ovr, adrs_bor, adrs_rsp) %>%
  dplyr::arrange(USUBJID, PARAMCD, ADT) %>%
  dplyr::group_by(USUBJID) %>%
  dplyr::mutate(ASEQ = dplyr::row_number()) %>%
  dplyr::ungroup()

cat("ADRS final:", nrow(adrs_final), "records\n")
ADRS final: 2309 records
adrs_final %>%
  dplyr::count(PARAMCD, PARAM)
# A tibble: 3 × 3
  PARAMCD PARAM                         n
  <chr>   <chr>                     <int>
1 BOR     Best Overall Response       205
2 OVR     Overall Response by Visit  1899
3 RSP     Responder (CR or PR)        205

4.8 Step 14: ADRS Validation

cat("\n=== ADRS Validation ===\n\n")

=== ADRS Validation ===
# One BOR per subject
check1 <- adrs_final %>%
  dplyr::filter(PARAMCD == "BOR") %>%
  dplyr::count(USUBJID) %>%
  dplyr::filter(n > 1)
cat("Check 1 - Multiple BOR per subject:", nrow(check1), "\n")
Check 1 - Multiple BOR per subject: 0 
# Valid AVAL range for OVR (1-5)
check2 <- adrs_final %>%
  dplyr::filter(PARAMCD == "OVR", !AVAL %in% 1:5)
cat("Check 2 - Invalid OVR AVAL:", nrow(check2), "\n")
Check 2 - Invalid OVR AVAL: 3 
# RSP only 0 or 1
check3 <- adrs_final %>%
  dplyr::filter(PARAMCD == "RSP", !AVAL %in% c(0, 1))
cat("Check 3 - Invalid RSP AVAL:", nrow(check3), "\n")
Check 3 - Invalid RSP AVAL: 0 
cat("\n✓ ADRS validation complete\n")

✓ ADRS validation complete

5 Summary

cat("\n=== Day 23 Summary ===\n\n")

=== Day 23 Summary ===
cat("ADCM:\n")
ADCM:
cat("  Records:", nrow(adcm), "\n")
  Records: 7510 
cat("  Concomitant (CONFL=Y):", sum(adcm$CONFL == "Y", na.rm = TRUE), "\n")
  Concomitant (CONFL=Y): 7337 
cat("  Prior (PRVFL=Y):", sum(adcm$PRVFL == "Y", na.rm = TRUE), "\n\n")
  Prior (PRVFL=Y): 72 
cat("ADRS:\n")
ADRS:
adrs_final %>%
  dplyr::count(PARAMCD, PARAM)
# A tibble: 3 × 3
  PARAMCD PARAM                         n
  <chr>   <chr>                     <int>
1 BOR     Best Overall Response       205
2 OVR     Overall Response by Visit  1899
3 RSP     Responder (CR or PR)        205

6 Export

attr(adcm$ASTDT, "label") <- "Analysis Start Date"
attr(adcm$AENDT, "label") <- "Analysis End Date"
attr(adcm$PREFL, "label") <- "Pre-treatment Flag"
attr(adcm$CONFL, "label") <- "Concomitant Flag"
attr(adcm$PRVFL, "label") <- "Prior Flag"
attr(adcm$ASEQ,  "label") <- "Analysis Sequence Number"

attr(adrs_final$PARAMCD, "label") <- "Parameter Code"
attr(adrs_final$PARAM,   "label") <- "Parameter"
attr(adrs_final$AVAL,    "label") <- "Analysis Value"
attr(adrs_final$AVALC,   "label") <- "Analysis Value (C)"
attr(adrs_final$ADT,     "label") <- "Analysis Date"
attr(adrs_final$ASEQ,    "label") <- "Analysis Sequence Number"

xportr_write(adcm,       path = "adcm.xpt", domain = "ADCM")
xportr_write(adrs_final, path = "adrs.xpt", domain = "ADRS")

cat("\n✓ adcm.xpt exported\n")

✓ adcm.xpt exported
cat("✓ adrs.xpt exported\n")
✓ adrs.xpt exported

7 Key Takeaways

  1. pharmaversesdtm exports rs_onco for oncology response - not rs
  2. ADCM period flags describe timing relative to TRTSDT/TRTEDT
  3. admiralonco::aval_resp() converts RECIST labels to numeric AVAL
  4. BOR = slice_min(AVAL) per subject across OVR records
  5. RSP = 1 if BOR is CR or PR (AVAL ≤ 2), else 0

8 Next Steps

Day 24: ARD-first reporting with cards and cardx Day 25: gtsummary + tfrmt ARD-backed tables Day 26: flextable + officer Word/RTF output


9 Resources

  • admiral ADCM vignette: https://pharmaverse.github.io/admiral/articles/occds.html
  • admiralonco ADRS vignette: https://pharmaverse.github.io/admiralonco/articles/adrs.html
  • pharmaversesdtm datasets: https://pharmaverse.github.io/pharmaversesdtm/

End of Day 23


 

30 Days of Pharmaverse  ·  Disclaimer  ·  Indraneel Chakraborty  ·  © 2026