Introduction
If Part 1 introduced the philosophy of admiral and the
basic mechanics of creating ADaM datasets in R, Part 2 is where the programming
becomes substantially more like real oncology production work.
The difficult part of an oncology ADaM program is rarely the final
mutate(). The difficult part is translating a clinical
endpoint definition into an auditable sequence of record selection, reference-date
logic, analysis flags, event precedence, confirmation rules, and parameter metadata.
That is exactly where admiralonco becomes useful.
1. Where admiralonco Fits in the ADaM Architecture
A useful mental model is to treat admiralonco as an
oncology-specific layer on top of the general-purpose derivation machinery in
admiral.
| Layer | Typical responsibility | Examples |
|---|---|---|
| SDTM | Collected clinical observations | RS, TU, TR, CE, DS |
| ADSL | Subject-level analysis variables | TRTSDT, RANDDT, SAFFL, ITTFL |
| ADTR | Longitudinal tumor measurements | Target-lesion sum, nadir, CHG, PCHG |
| ADRS | Response assessments and derived response endpoints | OVR, BOR, CBOR, confirmed response |
| ADTTE | Time-to-event endpoints | OS, PFS, DoR |
2. Start With the Template, Not With a Blank Script
A production programmer should generally begin by examining the package template
and then adapting it to the study specification. The admiralonco documentation
provides ADaM templates that can be generated through
admiral::use_ad_template().
library(admiral) library(admiralonco) use_ad_template( adam_name = "ADRS", save_path = "./programs/ad_adrs.R", package = "admiralonco" )
The exact template name should be checked against the version of admiralonco being used.
This is more than a convenience. Templates encode a recommended programming workflow and make the resulting program easier for another programmer to audit. They also reduce the risk of silently reinventing package patterns that already exist.
3. Establish a Reproducible Programming Environment
For production work, package versions should be controlled. A derivation that works today because of an unpinned development version is not necessarily a reproducible submission program.
library(admiral) library(admiralonco) library(dplyr) library(lubridate) packageVersion("admiral") packageVersion("admiralonco") get_admiral_option("subject_keys")
One particularly useful admiral convention is to use the package's subject-key configuration rather than repeatedly hard-coding subject identifiers. This becomes important when code is reused across studies or organizations with different subject-key conventions.
4. Build a Study-Specific Derivation Contract
Before writing derivation code, translate the SAP and ADaM specifications into a small programming contract. For every parameter, explicitly record:
- source domains or ADaM datasets;
- source records eligible for the parameter;
- reference date;
- analysis population;
- assessment window;
- required analysis flags;
- event versus censoring precedence;
- confirmation rules;
- cutoff-date behavior;
- parameter code, label, and analysis value conventions.
5. Advanced ADRS Programming
ADRS is where oncology programming becomes most clinically specific. A typical workflow starts with response assessments and builds increasingly derived parameters: progression, response, clinical benefit, best overall response, confirmed response, death, last assessment, and measurable disease status.
5.1 Normalize the Response Input
library(dplyr) library(admiral) library(admiralonco) rs_adsl <- rs %>% convert_blanks_to_na() %>% derive_vars_merged( dataset_add = adsl, new_vars = exprs(TRTSDT, RANDDT), by_vars = get_admiral_option("subject_keys") )
The important design choice is to merge only variables required for the derivation. Avoid blindly joining all of ADSL onto every record; doing so makes lineage harder to understand and can introduce accidental variable collisions.
5.2 Analysis Flags Are Part of the Clinical Definition
An oncology endpoint is often not simply "take the maximum response." It is "take the maximum response among records satisfying a defined analysis flag, reference-date rule, and assessment eligibility condition."
# Example pattern: derive a study-specific analysis flag. adrs <- adrs %>% mutate( ANL01FL = if_else( !is.na(ADT) & ADT >= TRTSDT & ADT <= as.Date(cutoffdt), "Y", NA_character_ ) )
5.3 Deriving Progressive Disease as an Event
The modern admiralonco workflow favors reusable event machinery rather than hard-coding a separate custom algorithm for every endpoint. In recent versions, oncology event definitions are designed to work with admiral's extreme-event framework.
# Illustrative event-object pattern. pd_event <- event( description = "Progressive disease", dataset_name = "adrs", filter = PARAMCD == "PD" & ANL01FL == "Y", set_values_to = exprs(EVNTDESC = "Progressive disease") )
The exact helper and argument structure should be taken from the version-specific admiralonco/admiral vignette. The architectural point is more important: define events declaratively and then reuse them in multiple endpoint derivations.
5.4 Best Overall Response
Best overall response is a classic example of why a programmer should separate clinical rules from data manipulation. The response hierarchy, analysis period, confirmation requirements, and special handling of CR/PR/SD/PD/NE must come from the protocol and SAP.
# Illustrative structure; use the version-specific function signature # supplied by the admiralonco template for production code. adrs_bor <- adrs %>% derive_param_bor( by_vars = exprs(USUBJID), set_values_to = exprs( PARAMCD = "BOR", PARAM = "Best Overall Response" ) )
The key is not memorizing a function call. The key is understanding what records the function is allowed to see. If a pre-processing step accidentally retains an ineligible PD record, the final BOR can be wrong even though the function itself runs without error.
6. Confirmed Response: Where Advanced QC Matters
Confirmation is one of the most error-prone parts of oncology ADaM programming. A confirmed response is a longitudinal rule, not a simple lookup of the patient's best observed response.
Consider the following simplified sequence:
| Visit | OVR | Potential interpretation |
|---|---|---|
| Week 8 | PR | First response |
| Week 12 | PR | Response confirmed |
| Week 16 | PD | Later progression |
The programming question is not merely whether PR exists. It is whether a subsequent eligible assessment confirms it under the study's specified timing and response rules.
7. Non-Standard Response Criteria
One of the strongest reasons to use admiralonco is that oncology trials do not all use the same response framework. The package documentation includes extensions and examples for criteria such as iRECIST, IMWG, GCIG, PCWG3 and Lugano 2014.
For non-standard endpoints, resist the temptation to overload a standard RECIST function with undocumented assumptions. Instead, use the flexible event/record derivation patterns provided by admiral and build the disease-specific rule in small, testable components.
7.1 Example: Separating Clinical Rules From Dataset Construction
# Step 1: create an eligibility flag. eligible <- rs %>% filter(ANL01FL == "Y") %>% mutate( response_eligible = case_when( !is.na(AVALC) ~ TRUE, TRUE ~ FALSE ) ) # Step 2: isolate the event-driving records. response_events <- eligible %>% filter(response_eligible) %>% select(USUBJID, ADT, AVALC, PARAMCD, ANL01FL)
This decomposition makes the logic inspectable. A reviewer can ask "Which records were eligible?" independently of "How was the endpoint selected?"
8. ADTR: The Measurement Layer Behind Response
ADTR is useful when the study requires analysis of tumor measurements themselves. The key distinction is between measurement-level variables and response-category variables.
| Concept | Typical ADTR representation | Why it matters |
|---|---|---|
| Absolute tumor burden | AVAL | Supports longitudinal measurement displays |
| Baseline | BASE | Reference for CHG/PCHG |
| Change | CHG | Absolute change from baseline |
| Percent change | PCHG | Waterfall/spider displays |
| Nadir | Derived minimum | Supports progression logic |
8.1 Percent Change Must Be Traceable
mutate( CHG = AVAL - BASE, PCHG = if_else( is.na(BASE) | BASE == 0, NA_real_, 100 * (AVAL - BASE) / BASE ) )
9. Nadir Logic: A Frequent Source of Subtle Errors
The nadir is a measurement concept, while progression is a clinical endpoint rule. Those concepts must not be conflated.
nadir <- adtr %>% filter(ANL01FL == "Y") %>% group_by(USUBJID) %>% summarise( NADIR = min(AVAL, na.rm = TRUE), .groups = "drop" )
In a real study, the record set used for the nadir must match the prespecified analysis rules. For example, post-progression measurements, unscheduled scans, assessments after a new anticancer therapy, or records outside a cutoff may need special treatment.
10. ADTTE: Turning Clinical Events Into Time-to-Event Endpoints
ADTTE is conceptually different from ADRS. Instead of asking "what response did the patient achieve?", ADTTE asks "when did the endpoint occur, and if it did not occur, when and why was the patient censored?"
10.1 Overall Survival
# Illustrative OS event construction. os_event <- event( description = "Death", dataset_name = "adsl", filter = !is.na(DTHDT), date = DTHDT )
The exact event helper syntax depends on the admiral version and template in use. The important point is to define the clinical event separately from the downstream ADTTE construction.
10.2 Progression-Free Survival
PFS commonly combines progression and death. A robust implementation should make that precedence explicit rather than relying on whichever dataset happens to be sorted first.
# Conceptual event precedence: # 1. PD event # 2. Death event # 3. If neither occurs, apply the SAP-defined censoring rule. pfs_sources <- list( pd_event, os_event )
11. Duration of Response
Duration of response introduces an additional dependency: the patient must first be a responder according to the prespecified response definition. The response start date and progression/death end date must therefore be derived consistently.
| Component | Question |
|---|---|
| Eligibility | Who qualifies as a responder? |
| Start | What date is the response considered to begin? |
| End event | PD, death, or another prespecified event? |
| Censoring | What happens when no end event is observed? |
| Cutoff | How are observations after the data cutoff treated? |
12. New Anti-Cancer Therapy Is Not Just Another Date
Many oncology endpoints have special rules concerning the start of subsequent anticancer therapy. The date can affect whether an event is eligible, whether an observation is censored, and whether an assessment contributes to the endpoint.
# Example pattern for deriving an eligible assessment period. adrs <- adrs %>% mutate( after_new_therapy = case_when( is.na(NEWTRTSDT) ~ FALSE, ADT >= NEWTRTSDT ~ TRUE, TRUE ~ FALSE ) )
This is intentionally shown as a pattern rather than a universal implementation. The SAP determines whether the event or censoring date is before or at the new therapy date and whether the new therapy itself defines an analysis boundary.
13. Cutoff Dates and Data Snapshots
A clinical analysis is a snapshot. The program must therefore distinguish between "the latest record in the database" and "the latest record available at the analysis cutoff."
# Always make the cutoff explicit. analysis_cutoff <- as.Date("2026-06-30") eligible <- source %>% filter( is.na(ADT) | ADT <= analysis_cutoff )
14. Reusable Functions Beat Copy-and-Paste
Advanced admiral programming is most maintainable when study-specific rules are encapsulated in small functions or configuration objects.
derive_pct_change <- function(data) { data %>% mutate( CHG = AVAL - BASE, PCHG = 100 * CHG / BASE ) } derive_pct_change(adtr)
For production work, consider adding explicit input checks, documented assumptions, and controlled handling of missing or zero baselines.
15. Parameter Metadata Should Be Programmatic
Avoid scattered assignments such as repeatedly typing PARAMCD and PARAM in unrelated sections. Centralizing parameter metadata makes multi-parameter ADRS and ADTTE programs easier to review.
param_meta <- tibble::tribble( ~PARAMCD, ~PARAM, "BOR", "Best Overall Response", "CBOR", "Best Overall Clinical Benefit", "PD", "Progressive Disease" )
16. Advanced Data-Lineage Pattern
A strong production program should make it possible to trace a final parameter back to the source assessment. One practical pattern is to retain source identifiers until the endpoint is fully derived.
adrs_work <- rs %>% select( STUDYID, USUBJID, RSSEQ, RSDTC, RSTESTCD, RSORRES, RSSTRESC, ANL01FL ) adrs_final <- adrs_work %>% # derive endpoint... mutate( PARAMCD = "OVR", PARAM = "Overall Response" )
Keeping RSSEQ or another source identifier during development makes targeted QC much easier. It can then be retained or removed according to the final ADaM specification.
17. Validation: Test the Algorithm, Not Just the Dataset
A common mistake is to validate only variable-level outputs. Oncology programming needs scenario-based validation because the difficult bugs occur at boundaries.
Assessment exactly on, one day before, and one day after the analysis boundary.
Response followed by response, NE, PD, or missing assessment.
Deepest measurement before and after progression.
PD and death occurring on the same date.
Assessments immediately before and after new anticancer therapy.
Missing baseline, missing assessment, and incomplete dates.
18. Manual Validation Example
Suppose a patient has the following simplified target-lesion measurements:
| Day | Sum of Diameters | Change from Baseline | Percent Change |
|---|---|---|---|
| 0 | 100 | 0 | 0% |
| 28 | 80 | −20 | −20% |
| 56 | 65 | −35 | −35% |
| 84 | 70 | −30 | −30% |
| 112 | 90 | −10 | −10% |
The nadir is 65. The final observation is still below baseline, but it represents regrowth from the nadir. A reviewer should therefore verify the progression logic using the actual tumor-burden scale and the complete response assessment rather than interpreting the −10% value in isolation.
19. Programmatic Assertions
Assertions can catch structural errors before a dataset reaches downstream TLG programs.
stopifnot( all(duplicated( adsl %>% select(USUBJID) ) == FALSE) ) stopifnot( all(adrs$ADT <= analysis_cutoff | is.na(adrs$ADT)) )
In a regulated environment, assertions should complement—not replace—formal independent QC.
20. Independent QC Should Be Algorithmically Different
If the production program uses the same function calls and the same derivation strategy as QC, both programs can reproduce the same mistake. A stronger QC program uses a simpler, independently structured calculation.
# Production: complex event derivation. # QC: independently calculate the expected subject-level result. qc_bor <- response_records %>% group_by(USUBJID) %>% summarise( expected_bor = case_when( any(AVALC == "CR") ~ "CR", any(AVALC == "PR") ~ "PR", any(AVALC == "SD") ~ "SD", TRUE ~ "NE" ), .groups = "drop" )
The example is deliberately simplified and is not a replacement for the complete response algorithm. Its purpose is to demonstrate the independence principle.
21. Handling Post-Progression Measurements
Post-progression measurements are a classic source of accidental endpoint contamination. Whether they are retained depends on the endpoint and the SAP. For some derivations they are excluded from the event-driving record set; for others they remain useful for descriptive displays.
analysis_records <- adtr %>% filter( ANL01FL == "Y", is.na(PD_DATE) | ADT <= PD_DATE )
22. Immuno-Oncology and iRECIST
Immuno-oncology trials can require response frameworks that distinguish conventional progression from immune-confirmed progression. This is precisely where a modular event architecture becomes valuable.
Do not rename standard RECIST variables and assume that the resulting dataset is iRECIST-compliant. The response state machine itself changes, and the timing and confirmation logic must be implemented explicitly.
23. Disease-Specific Extensions
The current admiralonco documentation includes examples beyond basic RECIST, including IMWG, GCIG, PCWG3 and Lugano 2014 workflows. These examples illustrate an important engineering principle: disease-specific endpoints should be layered on top of general ADaM derivation infrastructure rather than implemented as unreviewable monolithic scripts.
24. A Production ADRS Program Skeleton
# ============================================================ # ADRS - production-oriented skeleton # ============================================================ library(admiral) library(admiralonco) library(dplyr) library(lubridate) # 1. Read source data adsl <- read_adsl() rs <- read_rs() tu <- read_tu() # 2. Normalize / validate input rs <- rs %>% convert_blanks_to_na() # 3. Merge only required ADSL variables adrs <- rs %>% derive_vars_merged( dataset_add = adsl, new_vars = exprs(TRTSDT, RANDDT), by_vars = get_admiral_option("subject_keys") ) # 4. Apply analysis eligibility adrs <- adrs %>% filter(ADT <= analysis_cutoff) # 5. Derive oncology endpoints using version-specific functions adrs <- derive_pd(adrs) adrs <- derive_response(adrs) adrs <- derive_bor(adrs) adrs <- derive_confirmed_response(adrs) # 6. Add ADSL variables adrs <- add_adsl_variables(adrs, adsl) # 7. Derive ASEQ last adrs <- adrs %>% group_by(USUBJID) %>% mutate(ASEQ = row_number()) %>% ungroup() # 8. QC and export validate_adrs(adrs) write_adam(adrs)
The functions read_adsl(), read_rs(),
derive_pd(), and similar calls above are intentionally
illustrative study-level wrappers. In production, the corresponding package
functions and project utilities should be taken from the validated study template.
25. A Production ADTTE Program Skeleton
# ============================================================ # ADTTE - production-oriented skeleton # ============================================================ # 1. Define source events independently events <- list( pd_event, death_event ) # 2. Define censoring sources censoring <- list( last_assessment_censor, cutoff_censor ) # 3. Build each endpoint from the same event architecture adtte_pfs <- derive_pfs( events = events, censoring = censoring, start_date = TRTSDT ) adtte_os <- derive_os( events = list(death_event), censoring = censoring, start_date = TRTSDT )
26. Common Advanced Mistakes
| Mistake | Why It Is Dangerous | Better Approach |
|---|---|---|
| Hard-coding response hierarchy | Clinical rules can change by protocol | Use specification-driven logic |
| Using all RS records | Ineligible assessments can alter BOR/PD | Create explicit eligible record sets |
| Ignoring new therapy dates | Can change endpoint event/censoring status | Make the boundary explicit |
| Using baseline instead of nadir for PD | Can materially misclassify progression | Separate measurement and event logic |
| QC with identical code | Shared defects remain invisible | Use an independent algorithm |
| Deriving ASEQ too early | Later appends can create duplicates | Assign sequence after final assembly |
| Blind joins to ADSL | Variable collisions and lineage problems | Merge only required variables |
| Uncontrolled package versions | Function behavior can change | Lock versions and record session information |
27. Package-Version and Reproducibility QC
sessionInfo() installed.packages()[c( "admiral", "admiralonco", "dplyr" ), "Version"]
For submission-oriented programming, preserve the package versions used to generate the final datasets and figures. If the project uses a lockfile, validate that the locked environment reproduces the same results.
28. Practical Figure and Dataset QC
ADRS and ADTR should not be validated only by examining the dataset. Downstream visualizations are excellent secondary checks.
- Compare the number of subjects with measurable baseline disease.
- Compare the number of responders against the independent efficacy table.
- Inspect subjects with unusual response sequences.
- Plot longitudinal tumor measurements for a sample of subjects.
- Review all subjects with discordant BOR or confirmed-response results.
- Reconcile PFS event counts with the event listings.
29. An Advanced Subject-Level QC Extract
qc_subject <- adrs %>% group_by(USUBJID) %>% summarise( n_assessments = n(), first_assessment = min(ADT, na.rm = TRUE), last_assessment = max(ADT, na.rm = TRUE), n_pr = sum(AVALC == "PR", na.rm = TRUE), n_cr = sum(AVALC == "CR", na.rm = TRUE), n_pd = sum(AVALC == "PD", na.rm = TRUE), .groups = "drop" )
This kind of compact extract is extremely useful when reviewing edge cases with clinical statisticians and medical reviewers.
30. How to Think Like an Oncology ADaM Programmer
The most important shift from intermediate to advanced programming is to stop thinking of an endpoint as a single transformation. Treat it as a chain:
31. Practical Checklist for a Production Oncology ADaM Program
32. Summary
Advanced admiralonco programming is less about knowing a
large collection of function names and more about designing an auditable derivation
architecture. The package provides oncology-specific building blocks, while
admiral provides the general ADaM derivation machinery.
The most robust implementations separate source-record eligibility, temporal rules, clinical endpoint logic, parameter metadata, and QC. This makes difficult oncology endpoints easier to review and substantially reduces the risk that a small change in one derivation silently changes several downstream analyses.
References
Pharmaverse. admiralonco: Oncology extension package for ADaM in R Asset Library admiral. Package documentation and vignettes.
Pharmaverse. admiral: ADaM in R Asset Library. Package documentation and derivation framework.
Clinical response criteria and endpoint definitions should be interpreted according to the protocol, statistical analysis plan, and applicable disease-specific guidelines for the study.