# Load Packages
library(admiral)
library(admiralonco)
# pharmaverseadam contains example datasets generated from the CDISC pilot
# project SDTM ran through admiral templates
library(pharmaverseadam)
library(dplyr)
library(lubridate)
library(stringr)
library(purrr)
library(rlang)
library(metacore)
library(metatools)
library(xportr)ADER+
Exposure-Response Analysis Data
Introduction
Exposure-Response (ER) modeling is a critical tool in drug development for evaluating the relationships between drug exposure, safety, and efficacy. These analyses help characterize dose-response relationships, optimize dosing regimens, and support regulatory decision-making.
While CDISC released the Population Pharmacokinetic (PopPK) Implementation Guide in 2023 (see Basic Data Structure for ADaM Population PK), no equivalent standards currently exist specifically for ER data. However, ER datasets share many structural similarities with PopPK datasets, including numeric covariates, relative time variables, and pharmacokinetic exposure metrics. The examples below demonstrate how PopPK principles can be extended to create standardized ER analysis datasets.
The following four specialized datasets support different aspects of exposure-response analysis:
| Dataset | Full Name | Primary Purpose | Key Features |
|---|---|---|---|
| ADER | Exposure-Response Analysis Dataset | Integrated exposure-response relationships across safety and efficacy endpoints | Combines PK metrics (AUC, Cmax) with time-to-event outcomes (OS, PFS), tumor response, and baseline covariates |
| ADEE | Exposure-Efficacy Analysis Dataset | Exposure relationships with efficacy and tumor response endpoints | Focuses on efficacy outcomes such as tumor measurements, response rates, and progression metrics linked to drug exposure |
| ADES | Exposure-Safety Analysis Dataset | Exposure relationships with adverse events and safety endpoints | Links exposure metrics to adverse event occurrence, severity, and time-to-onset for safety signal detection |
| ADTRR | Exposure-Tumor Response Rate Analysis Dataset | Exposure relationships with categorical tumor response metrics | Analyzes exposure impact on response categories (CR, PR, SD, PD) and best overall response |
Each dataset builds upon standard ADaM datasets (ADSL, ADRS, ADTTE, ADAE, ADLB, ADVS) and incorporates pharmacokinetic parameters from ADPC/ADPP to create analysis-ready datasets for exposure-response modeling.
The top of this page includes common derivations. See the tabs below for specifics for each dataset.
First Load Packages
First we will load the packages required for our project. We will use {admiral} and {admiralonco} for the creation of analysis data. We will source these from {pharmaverseadam}. {admiral} requires {dplyr}, {lubridate} and {stringr}. Find other {admiral} functions and related variables by searching admiraldiscovery. We will use {metacore} and {metatools} to store and manipulate metadata from our specifications. We will use {xportr} to perform checks on the final data and export to a transport file.
Exposure Response data typically use ADaM data as source, so this example will depend on {pharmaverseadam} with data from existing {admiral} and {admiralonco} templates.
Load Specifications for Metacore
We have saved our specifications in an Excel file and will load them into {metacore} with the metacore::spec_to_metacore() function. We will subset for ADER first and load the other datasets below.
Load Source Datasets
We will load ADaM data from {pharmaverseadam}. The main sources will be ADRS, ADTTE, ADAE, and ADTR. We will use ADSL for baseline characteristics and we will derive additional baselines from vital signs ADVS and laboratory data ADLB. We will use exposure metrics from ADPP.
# ---- Load source datasets ----
# Load ADRS, ADTTE, ADSL, ADLB, ADVS, ADEX, ADPP, ADAE and ADTR
adrs <- pharmaverseadam::adrs_onco
adtte <- pharmaverseadam::adtte_onco
adsl <- pharmaverseadam::adsl %>%
filter(TRT01A != "Screen Failure")
adlb <- pharmaverseadam::adlb
advs <- pharmaverseadam::advs
adex <- pharmaverseadam::adex %>%
filter(PARCAT1 == "INDIVIDUAL")
adpp <- pharmaverseadam::adpp
adae <- pharmaverseadam::adae
adtr <- pharmaverseadam::adtr_oncoCommon Derivations
We will include common derivations for all exposure-response datasets first. These include covariates and exposure metrics.
Derive Covariates Using {metatools}
In this step we will create our numeric covariates using the metatools::create_var_from_codelist() function. Because there are 12 separate input/output variable pairs to map from controlled terminology codelists, we use purrr::reduce() to chain the calls programmatically rather than repeating them individually. Additional numeric identifiers (STUDYIDN, USUBJIDN, etc.) and study-level constants (ROUTE, FORM, REGION1) are derived with mutate().
Show codelist derivations
# ---- Derive Covariates ----
# Include numeric values for STUDYIDN, USUBJIDN, SEXN, RACEN etc.
covar <- purrr::reduce(
list(
c("STUDYID", "STUDYIDN"),
c("SEX", "SEXN"),
c("RACE", "RACEN"),
c("ETHNIC", "ETHNICN"),
c("ARMCD", "COHORT"),
c("ARMCD", "COHORTC"),
c("ARM", "ARMN"),
c("ACTARM", "ACTARMN"),
c("TRT01A", "TRT01AN"),
c("TRT01P", "TRT01PN"),
c("COUNTRY", "COUNTRYN"),
c("COUNTRY", "COUNTRYL")
),
~ create_var_from_codelist(.x, metacore,
input_var = !!rlang::sym(.y[1]),
out_var = !!rlang::sym(.y[2])
),
.init = adsl
) %>%
mutate(
STUDYIDN = as.numeric(word(USUBJID, 1, sep = fixed("-"))),
SITEIDN = as.numeric(word(USUBJID, 2, sep = fixed("-"))),
USUBJIDN = as.numeric(word(USUBJID, 3, sep = fixed("-"))),
SUBJIDN = as.numeric(SUBJID),
ROUTE = unique(adex$EXROUTE)[1],
FORM = unique(adex$EXDOSFRM)[1],
REGION1 = COUNTRY,
REGION1N = COUNTRYN
) %>%
create_var_from_codelist(metacore, input_var = FORM, out_var = FORMN) %>%
create_var_from_codelist(metacore, input_var = ROUTE, out_var = ROUTEN)Sample of Data
Derive Additional Baselines
Next we add additional baselines from vital signs and laboratory data. We will use admiral::derive_vars_merged() and admiral::derive_vars_transposed() to merge these datasets. Note that we use get_admiral_option("subject_keys") throughout to identify unique subjects instead of listing STUDYID and USUBJID.
Show baseline derivations
# ---- Derive additional baselines from ADVS and ADLB ----
labsbl <- adlb %>%
filter(ABLFL == "Y" & PARAMCD %in% c("CREAT", "ALT", "AST", "BILI")) %>%
mutate(PARAMCDB = paste0(PARAMCD, "BL")) %>%
select(!!!get_admiral_option("subject_keys"), PARAMCDB, AVAL)
covar_vslb <- covar %>%
derive_vars_merged(
dataset_add = advs,
filter_add = PARAMCD == "HEIGHT" & ABLFL == "Y",
by_vars = get_admiral_option("subject_keys"),
new_vars = exprs(HTBL = AVAL)
) %>%
derive_vars_merged(
dataset_add = advs,
filter_add = PARAMCD == "WEIGHT" & ABLFL == "Y",
by_vars = get_admiral_option("subject_keys"),
new_vars = exprs(WTBL = AVAL)
) %>%
derive_vars_transposed(
dataset_merge = labsbl,
by_vars = get_admiral_option("subject_keys"),
key_var = PARAMCDB,
value_var = AVAL
) %>%
mutate(
BMIBL = compute_bmi(height = HTBL, weight = WTBL),
BSABL = compute_bsa(
height = HTBL,
weight = WTBL,
method = "Mosteller"
),
CRCLBL = compute_egfr(
creat = CREATBL, creatu = "SI", age = AGE, weight = WTBL, sex = SEX,
method = "CRCL"
),
EGFRBL = compute_egfr(
creat = CREATBL, creatu = "SI", age = AGE, weight = WTBL, sex = SEX,
method = "CKD-EPI"
)
) %>%
rename(TBILBL = BILIBL)Sample of Data
Add Exposure Metrics from ADPP
An essential component of ER modeling is the exposure metrics calculated from the PK concentration data in ADPC, ADPPK, or ADPP. Here we will use AUCLST and CMAX from the ADPP dataset from {admiral} in {pharmaverseadam}. See the {aNCA} package for details about calculating AUC, CMAX, and other parameters. If ADPP contains parameters from multiple visits (e.g., Cycle 1 Day 1 and steady-state), add a filter condition to restrict to the steady-state visit — for example filter = PARAMCD %in% c("AUCLST", "CMAX") & AVISIT == "Cycle 1 Day 8" — to ensure one record per subject is transposed.
# ---- Add Exposure Metrics ----
# Add appropriate exposure metrics from ADPP. Here we use AUCLST and CMAX as examples
# this could be extended to include other parameters such as AUCINF, AUCALL, Tmax, Tlast etc.
# depending on the needs of the analysis.
# NOTE: If ADPP contains multiple visits (e.g., Day 1 and steady-state), add an AVISIT
# filter below to select only the steady-state visit, e.g. filter_add = AVISIT == "Cycle 1 Day 8",
# to ensure derive_vars_transposed() produces one record per subject.
covar_auc <- covar_vslb %>%
derive_vars_transposed(
dataset_merge = adpp,
filter = PARAMCD %in% c("AUCLST", "CMAX"),
by_vars = get_admiral_option("subject_keys"),
key_var = PARAMCD,
value_var = AVAL
) %>%
rename(AUCSS = AUCLST, CMAXSS = CMAX)Sample of Data
Conclusions
These Exposure-Response datasets demonstrate a practical approach to standardizing ER data using CDISC ADaM principles and the Pharmaverse ecosystem. The four datasets work together to support different modeling objectives: ADER for integrated exposure-response analysis, ADEE for efficacy endpoints, ADES for safety evaluation, and ADTRR for tumor response assessment. The framework demonstrated here can be adapted for study-specific ER endpoints. By standardizing the structure of exposure-response data and maintaining consistency with other ADaM formats, these datasets facilitate regulatory review, enhance traceability, and enable reproducible pharmacometric analyses. We encourage the community to adopt and refine these approaches as CDISC continues to develop formal ER data standards.
Code for Individual Datasets
Derivations specific to each dataset continue below. Select the appropriate tab to view.
Dataset-Specific Derivations
Derive Time to Event Variables from ADTTE
We will use parameters from ADTTE for Overall Survival, Progression Free Survival and Duration of Response. We will use admiral::derive_vars_transposed() to transpose the AVAL for each PARAMCD. We also transpose the censor variables CNSR here as indicator variables appending “IND” to the parameter name.
# ---- Create ADER base dataset
# For ADTTE censor variables add "IND" to PARAMCD
adttei <- adtte %>%
mutate(PARAMCD = paste0(PARAMCD, "IND"))
ader_tte <- adsl %>%
select(!!!get_admiral_option("subject_keys")) %>%
# Create OS and PFS variables from ADTTE
derive_vars_transposed(
dataset_merge = adtte,
by_vars = get_admiral_option("subject_keys"),
key_var = PARAMCD,
value_var = AVAL
) %>%
# Create OS and PFS censor variables
derive_vars_transposed(
dataset_merge = adttei,
by_vars = get_admiral_option("subject_keys"),
key_var = PARAMCD,
value_var = CNSR
)Sample of Data
We will use Best Overall Response by Investigator (BOR) from ADRS. We attach this to the data with admiral::derive_vars_merged(). Note that derive_vars_transposed() and derive_vars_merged() will be our primary ways of combining data from the different {admiral} and {admiralonco} datasets.
# ---- Add ADRS data ----
# Add response date to ADER for duration of response calculation
ader_bor <- ader_tte %>%
derive_vars_merged(
dataset_add = adrs,
filter_add = PARAMCD == "BOR" & SAFFL == "Y" & ANL01FL == "Y",
by_vars = get_admiral_option("subject_keys"),
new_vars = exprs(BOR = AVAL, BORC = AVALC)
)Add Analysis Sequence Number using admiral::derive_var_obs_number().
Sample of Data
Combine with Covariates and Exposure
We combine our covariates with the rest of the data
Check Data With metacore and metatools
We use {metacore} objects with {metatools} functions to perform a number of checks on the data. We will drop variables not in the specs and make sure all the variables from the specs are included.
ader <- ader_prefinal %>%
drop_unspec_vars(metacore) %>% # Drop unspecified variables from specs
check_variables(metacore) %>% # Check all variables specified are present and no more
check_ct_data(metacore) %>% # Checks all variables with CT only contain values within the CT
order_cols(metacore) %>% # Orders the columns according to the specs
sort_by_key(metacore) # Sorts the rows by the sort keysApply Labels and Formats with xportr
Using {xportr} we check variable type, assign variable length, add variable labels, add variable formats, and save a transport file with xportr::xportr_write().
dir <- tempdir() # Change to whichever directory you want to save the dataset in
ader_xpt <- ader %>%
xportr_type(metacore, domain = "ADER") %>% # Coerce variable type to match specs
xportr_length(metacore) %>% # Assigns SAS length from a variable level metadata
xportr_label(metacore) %>% # Assigns variable label from metacore specifications
xportr_format(metacore) %>% # Assigns variable format from metacore specifications
xportr_df_label(metacore) %>% # Assigns dataset label from metacore specifications
xportr_write(file.path(dir, "ader.xpt")) # Write xpt v5 transport fileLoad Specifications
Create Base ADEE Dataset
ADEE is a BDS dataset structured one record per subject per time-to-event parameter. It links pharmacokinetic exposure metrics from ADPP with efficacy endpoints (OS, PFS, TTP, TTNT) sourced from ADTTE, enabling exposure-efficacy modeling and analysis of progression and survival outcomes.
# ---- Create ADEE base dataset
# Get variable names from both datasets
adsl_vars <- names(adsl)
adtte_vars <- names(adtte)
# Find common variables
common_vars <- intersect(adsl_vars, adtte_vars)
# Remove key variables to get variables to drop
vars_to_drop <- setdiff(common_vars, c("STUDYID", "USUBJID"))
# Ensure PARAMN exists in ADTTE
if (!"PARAMN" %in% names(adtte)) {
adtte <- adtte %>%
mutate(
PARAMN = case_when(
PARAMCD == "PFS" ~ 1,
PARAMCD == "OS" ~ 2,
PARAMCD == "TTP" ~ 3,
PARAMCD == "TTNT" ~ 4,
TRUE ~ 99
)
)
}
# ---- Create ADEE Base
adee_base <- adtte %>%
# Filter to efficacy endpoints
filter(PARAMCD %in% c("OS", "PFS", "TTP", "TTNT")) %>%
# Add derived variables
mutate(
EVENT = 1 - CNSR,
AVALU = if_else(!is.na(AVAL), "DAYS", NA_character_),
) %>%
# Remove overlapping variables (use clean method)
select(-any_of(vars_to_drop))Sample of Data
Add Analysis Variables
# ---- Add Analysis variables
adee_aseq <- adee_base %>%
# Analysis flags
mutate(
ANL01FL = if_else(PARAMCD == "PFS", "Y", ""),
ANL02FL = if_else(PARAMCD == "OS", "Y", ""),
ANL03FL = if_else(PARAMCD == "TTP", "Y", ""),
ANL04FL = if_else(PARAMCD == "TTNT", "Y", "")
) %>%
# Parameter categories
mutate(
PARCAT1 = "EFFICACY",
PARCAT2 = "TIME TO EVENT"
) %>%
# Sequence number
derive_var_obs_number(
by_vars = get_admiral_option("subject_keys"),
order = exprs(PARAMCD),
new_var = ASEQ,
check_type = "error"
)Sample of Data
Combine with Covariates and Exposure
We combine our covariates with the rest of the data
Sample of Data
Check Data With metacore and metatools
We use {metacore} objects with {metatools} functions to perform a number of checks on the data. We will drop variables not in the specs and make sure all the variables from the specs are included.
adee <- adee_prefinal %>%
drop_unspec_vars(metacore) %>% # Drop unspecified variables from specs
check_variables(metacore) %>% # Check all variables specified are present and no more
check_ct_data(metacore) %>% # Checks all variables with CT only contain values within the CT
order_cols(metacore) %>% # Orders the columns according to the spec
sort_by_key(metacore) # Sorts the rows by the sort keysApply Labels and Formats with xportr
Using {xportr} we check variable type, assign variable length, add variable labels, add variable formats, and save a transport file with xportr::xportr_write().
dir <- tempdir() # Change to whichever directory you want to save the dataset in
adee_xpt <- adee %>%
xportr_type(metacore, domain = "ADEE") %>% # Coerce variable type to match specs
xportr_length(metacore) %>% # Assigns SAS length from a variable level metadata
xportr_label(metacore) %>% # Assigns variable label from metacore specifications
xportr_format(metacore) %>% # Assigns variable format from metacore specifications
xportr_df_label(metacore) %>% # Assigns dataset label from metacore specifications
xportr_write(file.path(dir, "adee.xpt")) # Write xpt v5 transport fileLoad Specifications
Create Base ADES Dataset
ADES contains two levels of records: subject-level flag parameters and event-level AE records. Here we create the five subject-level summary flags using admiral::derive_param_exist_flag(), which evaluates a condition against ADAE for each subject in ADSL and sets AVALC to "Y" or "N" accordingly. admiral::yn_to_numeric() is used within set_values_to to derive the numeric AVAL directly. Note that pharmaverseadam records severity in ASEV/ASEVN rather than AETOXGR/AETOXGRN.
# ---- Create ADES base dataset
# Derive subject-level summary parameters from ADAE using derive_param_exist_flag()
# NOTE: pharmaverseadam uses ASEV/ASEVN (severity) not AETOXGR/AETOXGRN
# Get all subjects from ADSL
adsl_sub <- adsl %>%
select(!!!get_admiral_option("subject_keys"))
# Derive binary subject-level AE flag parameters
subject_params <- derive_param_exist_flag(
dataset_ref = adsl_sub,
dataset_add = adae,
condition = TRTEMFL == "Y",
false_value = "N",
missing_value = "N",
set_values_to = exprs(
AVAL = yn_to_numeric(AVALC),
PARAMCD = "TEAE",
PARAM = "Any Treatment-Emergent Adverse Event",
PARAMN = 1
)
) %>%
derive_param_exist_flag(
dataset_ref = adsl_sub,
dataset_add = adae,
condition = TRTEMFL == "Y" & ASEVN == 3,
false_value = "N",
missing_value = "N",
set_values_to = exprs(
AVAL = yn_to_numeric(AVALC),
PARAMCD = "TEAESEV",
PARAM = "Any Severe Treatment-Emergent Adverse Event",
PARAMN = 2
)
) %>%
derive_param_exist_flag(
dataset_ref = adsl_sub,
dataset_add = adae,
condition = AESER == "Y",
false_value = "N",
missing_value = "N",
set_values_to = exprs(
AVAL = yn_to_numeric(AVALC),
PARAMCD = "SAE",
PARAM = "Any Serious Adverse Event",
PARAMN = 3
)
) %>%
derive_param_exist_flag(
dataset_ref = adsl_sub,
dataset_add = adae,
condition = AREL %in% c("POSSIBLE", "PROBABLE", "RELATED"),
false_value = "N",
missing_value = "N",
set_values_to = exprs(
AVAL = yn_to_numeric(AVALC),
PARAMCD = "TRAE",
PARAM = "Any Treatment-Related Adverse Event",
PARAMN = 4
)
) %>%
derive_param_exist_flag(
dataset_ref = adsl_sub,
dataset_add = adae,
condition = AEACN == "DRUG WITHDRAWN",
false_value = "N",
missing_value = "N",
set_values_to = exprs(
AVAL = yn_to_numeric(AVALC),
PARAMCD = "AEDCN",
PARAM = "AE Leading to Treatment Discontinuation",
PARAMN = 5
)
) %>%
arrange(USUBJID, PARAMCD)Sample of Data
Create Event Level Parameters
While subject-level parameters capture whether an event occurred at all, event-level records provide one row per individual adverse event and preserve granular information such as preferred term (AEDECOD), system organ class (AEBODSYS), severity (AESEV/AESEVN), seriousness (AESER), and relatedness (AEREL/AERELN). These records are restricted to treatment-emergent AEs (TRTEMFL == "Y") and combined with the subject-level parameters into a single ADES dataset.
# ---- Create event level parameters
# Get variable names for clean dropping
adsl_vars <- names(covar_auc)
adae_vars <- names(adae)
common_vars <- intersect(adsl_vars, adae_vars)
vars_to_drop <- setdiff(common_vars, c("STUDYID", "USUBJID"))
# Create event-level records from ADAE
# NOTE: Using actual pharmaverseadam variables (ASEV/ASEVN, AREL)
event_params <- adae %>%
filter(TRTEMFL == "Y") %>% # Treatment-emergent only
mutate(
PARAMCD = "AETERM",
PARAM = "Adverse Event Term",
PARAMN = 10,
AVAL = 1, # Event occurred
AVALC = "Y",
# Keep AE-specific variables (8-char names)
# Using actual pharmaverseadam ADAE variables
AEDECOD = AEDECOD, # Preferred term
AEBODSYS = AEBODSYS, # System organ class
AESEV = ASEV, # Severity (char): MILD, MODERATE, SEVERE
AESEVN = ASEVN, # Severity (num): 1, 2, 3
AESER = AESER, # Serious flag: Y/N
AEREL = AREL, # Relationship (char): NOT RELATED, POSSIBLE, etc.
# Create numeric relationship for analysis
AERELN = case_when(
AREL == "NOT RELATED" ~ 0,
AREL == "UNLIKELY RELATED" ~ 1,
AREL == "POSSIBLE" ~ 2,
AREL == "PROBABLE" ~ 3,
AREL == "RELATED" ~ 4,
TRUE ~ NA_real_
),
AESTDT = ASTDT, # AE start date (8 chars)
AEENDT = AENDT # AE end date (8 chars)
) %>%
select(-any_of(vars_to_drop))
# ---- Combine subject and event levels
# Ensure all AE-specific variables exist in subject_params (as NA)
# This prevents issues when binding with event_params
subject_params_complete <- subject_params %>%
derive_vars_merged(
dataset_add = adsl_sub,
by_vars = get_admiral_option("subject_keys"),
) %>%
mutate(
# Add event-level variables as NA for subject-level records
AESTDT = as.Date(NA),
AEENDT = as.Date(NA),
AEDECOD = NA_character_,
AEBODSYS = NA_character_,
AESEV = NA_character_,
AESEVN = NA_integer_,
AESER = NA_character_,
AEREL = NA_character_,
AERELN = NA_real_
)
event_params_complete <- event_params %>%
derive_vars_merged(
dataset_add = adsl_sub,
by_vars = get_admiral_option("subject_keys")
)
# Combine both levels
ades_base <- bind_rows(
subject_params_complete,
event_params_complete
) %>%
arrange(USUBJID, PARAMCD)Sample of Data
Add Analysis Variables
# ---- Add analysis variables
ades_flags <- ades_base %>%
# Analysis flags
mutate(
ANL01FL = if_else(PARAMCD == "TEAE", "Y", ""),
ANL02FL = if_else(PARAMCD == "TEAESEV", "Y", ""),
ANL03FL = if_else(PARAMCD == "SAE", "Y", ""),
ANL04FL = if_else(PARAMCD == "TRAE", "Y", ""),
ANL05FL = if_else(PARAMCD == "AETERM", "Y", "")
) %>%
# Parameter categories
mutate(
PARCAT1 = "SAFETY",
PARCAT2 = case_when(
PARAMN <= 5 ~ "SUBJECT-LEVEL",
PARAMN >= 10 ~ "EVENT-LEVEL",
TRUE ~ NA_character_
)
) %>%
# Analysis timepoint
mutate(
AVISIT = if_else(PARAMN <= 5, "OVERALL", "AT EVENT"),
AVISITN = if_else(PARAMN <= 5, 99, 0)
) %>%
# Sort and create sequence number
# Use coalesce to handle NA dates (puts them first in sort)
arrange(USUBJID, PARAMN, coalesce(AESTDT, as.Date("1900-01-01"))) %>%
group_by(!!!get_admiral_option("subject_keys")) %>%
mutate(ASEQ = row_number()) %>%
ungroup()Combine with Covariates and Exposure
We combine our covariates with the rest of the data
Check Data With metacore and metatools
## Check Data With metacore and metatools
ades <- ades_prefinal %>%
drop_unspec_vars(metacore) %>% # Drop unspecified variables from specs
check_variables(metacore) %>% # Check all variables specified are present and no more
check_ct_data(metacore) %>% # Checks all variables with CT only contain values within the CT
order_cols(metacore) %>% # Orders the columns according to the spec
sort_by_key(metacore) # Sorts the rows by the sort keysThe following variable(s) were dropped:
AESTDT
AEENDT
AESEV
AESEVN
DOMAIN
AETERM
AEBDSYCD
AELLT
AELLTCD
AEPTCD
AEHLT
AEHLTCD
AEHLGT
AEHLGTCD
AESOC
AESOCCD
AESTDTC
ASTDTM
ASTDTF
ASTTMF
AEENDTC
AENDTM
AENDTF
AENTMF
AESTDY
AEENDY
AOCCIFL
AESDTH
AESLIFE
AESHOSP
AESDISAB
AESCONG
AREL
AESCAN
AESOD
AEDTC
LDOSEDTM
DOSEON
DOSEU
ANL03FL
ANL04FL
ANL05FL
PARCAT1
PARCAT2
AVISIT
AVISITN
COUNTRY
RFSTDTC
RFENDTC
RFXSTDTC
RFXENDTC
RFPENDTC
SCRFDT
FRVDT
DTHDTC
DTHADY
DTHFL
LDDTHELD
LDDTHGR1
DTH30FL
DTHA30FL
DTHDOM
DTHB30FL
REGION1
DMDTC
DMDY
AGEU
AGEGR1
RACEGR1
ACTARM
ACTARMCD
TRTSDTM
TRTSTMF
TRTEDTM
TRTETMF
EOSSTT
EOSDT
RFICDTC
RANDDT
LSTALVDT
DTHDT
DTHDTF
DTHCAUS
DTHCGR1
BRTHDTC
COHORT
COHORTC
ACTARMN
COUNTRYN
COUNTRYL
ROUTE
FORM
REGION1N
FORMN
ROUTEN
No missing or extra variables
Apply Labels and Formats with xportr
Using {xportr} we check variable type, assign variable length, add variable labels, add variable formats, and save a transport file with xportr::xportr_write().
dir <- tempdir() # Change to whichever directory you want to save the dataset in
ades_xpt <- ades %>%
xportr_type(metacore, domain = "ADES") %>% # Coerce variable type to match specs
xportr_length(metacore) %>% # Assigns SAS length from a variable level metadata
xportr_label(metacore) %>% # Assigns variable label from metacore specifications
xportr_format(metacore) %>% # Assigns variable format from metacore specifications
xportr_df_label(metacore, domain = "ADES") %>% # Assigns dataset label from metacore specifications
xportr_write(file.path(dir, "ades.xpt")) # Write xpt v5 transport fileLoad Specifications
Create Base ADTRR Dataset
We will derive the tumor size parameter PARAMCD = "TSIZE" from ADTR. We will add nominal time and actual time using admiral::derive_var_nfrlt() and admiral::derive_vars_duration(). In traditional Exposure-Response datasets these might be TIME and NTIM variables.
# ---- Create ADTRR base dataset
# Get variable names for clean dropping
adsl_vars <- names(covar_auc)
adtr_vars <- names(adtr)
common_vars <- intersect(adsl_vars, adtr_vars)
vars_to_drop <- setdiff(common_vars, c("STUDYID", "USUBJID"))
tsize_final <- adtr %>%
filter(PARAMCD == "SDIAM") %>%
mutate(
PARAMCD = "TSIZE",
PARAM = "Target Lesions Sum of Diameters",
PARAMN = 1
) %>%
# Derive Nominal Relative Time from First Dose (NFRLT)
derive_var_nfrlt(
new_var = NFRLT,
new_var_unit = FRLTU,
out_unit = "DAYS",
visit_day = ADY
) %>%
# Derive Actual Relative Time from First Dose (AFRLT)
derive_vars_duration(
new_var = AFRLT,
start_date = TRTSDT,
end_date = ADT,
out_unit = "DAYS",
floor_in = FALSE,
add_one = FALSE
) %>%
select(-any_of(vars_to_drop))Sample of Data
Add BOR from ADRS
Best Overall Response (BOR) is sourced from ADRS and included as a second parameter in ADTRR. We filter to safety-evaluable subjects (SAFFL == "Y") and derive a numeric response code BORN ordered from most to least favourable: CR (4), PR (3), SD (2), PD (1), NE (0). Overlapping ADSL variables are dropped before combining with the tumour size records.
# ---- Add BOR from ADRS
adrs_vars <- names(adrs)
common_vars_adrs <- intersect(adsl_vars, adrs_vars)
vars_to_drop_adrs <- setdiff(common_vars_adrs, c("STUDYID", "USUBJID"))
bor <- adrs %>%
filter(PARAMCD == "BOR" & SAFFL == "Y" & ANL01FL == "Y") %>%
mutate(
PARAMN = 2,
# Create BORN from AVALC if AVAL doesn't exist
BORN = if ("AVAL" %in% names(.)) {
AVAL
} else {
case_when(
AVALC == "CR" ~ 4,
AVALC == "PR" ~ 3,
AVALC == "SD" ~ 2,
AVALC == "PD" ~ 1,
AVALC == "NE" ~ 0,
TRUE ~ NA_real_
)
}
) %>%
select(-any_of(vars_to_drop_adrs))Sample of Data
Derive Nadir
The nadir is the minimum post-baseline tumour size observed for each subject. It is derived from TSIZE records and included as a third ADTRR parameter (PARAMCD = "NADIR"). The percentage change from baseline at the nadir timepoint (NADPCHG) and the visit label at which nadir was reached (NADVST) are preserved to support waterfall plots and other tumour response visualisations.
# ---- Derive Nadir
# Calculate nadir from TSIZE records
# Keep BASE, CHG, PCHG from the nadir timepoint
nadir <- tsize_final %>%
filter(AVISITN > 0 & !is.na(AVAL)) %>%
group_by(!!!get_admiral_option("subject_keys")) %>%
filter(AVAL == min(AVAL, na.rm = TRUE)) %>%
slice(1) %>%
ungroup() %>%
mutate(
PARAMCD = "NADIR",
PARAM = "Nadir Tumor Size",
PARAMN = 3,
NADIR = AVAL,
NADPCHG = PCHG, # Keep PCHG at nadir
NADVST = AVISIT # Keep visit of nadir
)Sample of Data
Combine Parameters
The three parameter datasets — tumour size (TSIZE), best overall response (BOR), and nadir (NADIR) — are stacked with bind_rows() and sorted by subject and parameter order. Analysis flags, parameter categories, units, and the analysis sequence number are then added. We use derive_var_obs_number() to compute ASEQ consistently, ordered by PARAMN and AVISITN.
# ---- Combine parameters
adtrr_base <- bind_rows(
tsize_final,
bor,
nadir
) %>%
arrange(USUBJID, PARAMN, AVISITN)
# ---- Add analysis variables
# Ensure AVALU exists before mutating
if (!"AVALU" %in% names(adtrr_base)) {
adtrr_base <- adtrr_base %>%
mutate(AVALU = NA_character_)
}
adtrr_seq <- adtrr_base %>%
# Analysis flags
mutate(
# Baseline flag
ABLFL = case_when(
!is.na(ABLFL) ~ ABLFL,
!is.na(AVISITN) & AVISITN == 0 ~ "Y",
TRUE ~ ""
),
# Post-baseline flag
ANL01FL = if_else(!is.na(AVISITN) & AVISITN > 0, "Y", ""),
# Responders (CR or PR)
ANL02FL = if_else(!is.na(AVALC) & AVALC %in% c("CR", "PR"), "Y", ""),
# Has change from baseline
ANL03FL = if_else(!is.na(PCHG), "Y", "")
) %>%
# Parameter categories
mutate(
PARCAT1 = "TUMOR RESPONSE",
PARCAT2 = case_when(
PARAMCD == "TSIZE" ~ "MEASUREMENT",
PARAMCD == "BOR" ~ "OVERALL RESPONSE",
PARAMCD == "NADIR" ~ "BEST RESPONSE",
TRUE ~ NA_character_
)
) %>%
# Set AVALU (now guaranteed to exist)
mutate(
AVALU = case_when(
!is.na(AVALU) & AVALU != "" ~ AVALU, # Keep existing non-empty
PARAMCD == "TSIZE" ~ "mm",
PARAMCD == "NADIR" ~ "mm",
TRUE ~ NA_character_
)
) %>%
# Sequence number
derive_var_obs_number(
by_vars = get_admiral_option("subject_keys"),
order = exprs(PARAMN, AVISITN),
new_var = ASEQ,
check_type = "error"
) %>%
arrange(USUBJID, PARAMN, AVISITN)Sample of Data
Combine with Covariates and Exposure
Check Data With metacore and metatools
## Check Data With metacore and metatools
adtrr <- adtrr_prefinal %>%
drop_unspec_vars(metacore) %>% # Drop unspecified variables from specs
check_variables(metacore) %>% # Check all variables specified are present and no more
check_ct_data(metacore) %>% # Checks all variables with CT only contain values within the CT
order_cols(metacore) %>% # Orders the columns according to the spec
sort_by_key(metacore) # Sorts the rows by the sort keysThe following variable(s) were dropped:
DOMAIN
PDFL
ADTF
PARCAT1
PARCAT2
PARCAT3
CHGNAD
PCHGNAD
ANL04FL
TRSEQ
TRGRPID
TRLNKID
TRTESTCD
TRTEST
TRORRES
TRORRESU
TRSTRESC
TRSTRESN
TRSTRESU
TREVAL
TREVALID
TRACPTFL
VISITNUM
VISIT
TRDTC
TULOC
TULOCGR1
LSEXP
LSASS
RSTESTCD
RSTEST
RSORRES
RSSTRESC
RSEVAL
RSEVALID
RSACPTFL
RSDTC
RSSEQ
NADVST
COUNTRY
RFSTDTC
RFENDTC
RFXSTDTC
RFXENDTC
RFPENDTC
SCRFDT
FRVDT
DTHDTC
DTHADY
DTHFL
LDDTHELD
LDDTHGR1
DTH30FL
DTHA30FL
DTHDOM
DTHB30FL
REGION1
DMDTC
DMDY
AGEU
AGEGR1
RACEGR1
ACTARM
ACTARMCD
TRTSDTM
TRTSTMF
TRTEDTM
TRTETMF
EOSSTT
EOSDT
RFICDTC
RANDDT
LSTALVDT
DTHDT
DTHDTF
DTHCAUS
DTHCGR1
BRTHDTC
COHORT
COHORTC
ACTARMN
COUNTRYN
COUNTRYL
ROUTE
FORM
REGION1N
FORMN
ROUTEN
No missing or extra variables
Apply Labels and Formats with xportr
Using {xportr} we check variable type, assign variable length, add variable labels, add variable formats, and save a transport file with xportr::xportr_write().
dir <- tempdir() # Change to whichever directory you want to save the dataset in
adtrr_xpt <- adtrr %>%
xportr_type(metacore, domain = "ADTRR") %>% # Coerce variable type to match specs
xportr_length(metacore) %>% # Assigns SAS length from a variable level metadata
xportr_label(metacore) %>% # Assigns variable label from metacore specifications
xportr_format(metacore) %>% # Assigns variable format from metacore specifications
xportr_df_label(metacore, domain = "ADTRR") %>% # Assigns dataset label from metacore specifications
xportr_write(file.path(dir, "adtrr.xpt")) # Write xpt v5 transport file