Introduction: admiral for Statistical Programmers
If you are evaluating {admiral} as a statistical programmer, the most useful question is not "How do I run an R package?" It is: How do I translate an ADaM specification into transparent, reviewable, reproducible R derivations?
That is the level at which admiral becomes particularly interesting. The package provides a large collection of modular derivation functions, while the statistical programmer remains responsible for the study-specific interpretation of the protocol, SAP, ADaM specification, source data, population definitions, timing rules, baseline rules, and analysis intent.
The official documentation describes the core idea as building an ADaM dataset through a sequence of derivations. Functions add variables or records, and the result can be passed to the next derivation. This makes a complex analysis dataset readable as a chain of transformations rather than as one monolithic program. Official Get Started documentation.
1. The Mental Model: Specification → Derivation Graph → Dataset
A production ADaM program is best viewed as a derivation graph. The final dataset is the output, but the important object for programming and QC is the sequence of transformations that produces it.
The advantage of this model is that every major requirement can be mapped to a small number of derivation steps. When a reviewer asks "Where is the baseline definition implemented?", there should be a recognizable answer in the program.
2. Installation and Environment Control
For a controlled production environment, install a deliberate package version and record the complete software environment. The official package documentation currently provides the CRAN release as the standard installation route.
install.packages("admiral")
library(admiral)
library(dplyr)
library(lubridate)
library(stringr)
For a development branch, the official documentation also describes installation from the pharmaverse GitHub repository. A regulated or submission-oriented workflow should not silently float between development versions.
# Development installation example pak::pkg_install( "pharmaverse/admiral", dependencies = TRUE )
3. Start From an ADaM Template
For statistical programmers, the package templates are more useful than a generic "hello world" example. The official template catalog provides starter scripts for ADaMs such as ADSL, ADAE, ADLB, ADVS, ADEG, and others.
library(admiral) use_ad_template( adam_name = "ADSL", save_path = "./ad_adsl.R", package = "admiral" )
The template is not the study's final program. It is a structured starting point that exposes the expected programming workflow. The official documentation also provides a catalog for exploring the available templates before deciding how much of the starter program is applicable to a study.
A useful production workflow is:
4. Expression Semantics: The admiral Programming Language Within R
One of the most important skills in admiral is understanding the distinction between symbols, character strings, and expressions. Many arguments are deliberately designed to accept unquoted R expressions.
Single variables
new_var = TRTDURD start_date = TRTSDT end_date = TRTEDT
Multiple variables
by_vars = exprs( STUDYID, USUBJID, PARAMCD, BASETYPE )
Expressions
filter = PARAMCD == "SYSBP" order = exprs( ADT, ATPTN, VISITNUM )
This is more than syntax. It allows admiral functions to receive pieces of the program as structured expressions and use them to implement reusable derivation algorithms.
5. Advanced ADSL Example: Building the Subject-Level Foundation
ADSL is usually the most important dataset to understand first because downstream ADaMs frequently inherit treatment, population, timing, and subject-level variables from it. The official ADSL workflow includes treatment variables, treatment dates and duration, disposition, age, death, last-known-alive, population flags, and other study-specific variables.
5.1 Start with DM
library(admiral)
library(dplyr)
library(lubridate)
adsl <- dm %>%
select(
STUDYID,
USUBJID,
SUBJID,
SITEID,
RFSTDTC,
ARM,
ARMCD,
ACTARM,
ACTARMCD,
SEX,
RACE,
ETHNIC
)
The exact variables retained should be driven by downstream requirements. Avoid blindly carrying every source variable into ADSL merely because it is available. A controlled ADSL is easier to review when its variables have a defined purpose.
5.2 Derive analysis dates
Character ISO dates and datetimes commonly need to become R date/datetime objects. The admiral date functions also support controlled imputation and imputation flags.
adsl <- adsl %>%
derive_vars_dt(
dtc = RFSTDTC,
new_vars_prefix = "RFST"
)
For partial dates, the imputation strategy must be a study decision. The program should not choose an imputation merely because a function can perform one.
5.3 Derive treatment start from EX
A common production pattern is to identify the first qualifying exposure record and merge its treatment start information into ADSL.
ex_ext <- ex %>%
filter(!is.na(EXSTDTM)) %>%
arrange(STUDYID, USUBJID, EXSTDTM, EXSEQ)
adsl <- adsl %>%
derive_vars_merged(
dataset_add = ex_ext,
filter_add = !is.na(EXSTDTM),
new_vars = exprs(
TRTSDTM = EXSTDTM,
TRTSTMF = EXSTTMF
),
order = exprs(EXSTDTM, EXSEQ),
mode = "first",
by_vars = exprs(STUDYID, USUBJID)
)
This illustrates an important distinction. If the records to merge can be selected
using conditions and ordering within the additional dataset,
derive_vars_merged() is often the natural choice.
When record selection depends on variables from both datasets, a more powerful
joined derivation may be appropriate.
5.4 Derive treatment duration
adsl <- adsl %>%
derive_var_trtdurd(
new_var = TRTDURD,
start_date = TRTSDT,
end_date = TRTEDT
)
The important statistical-programming question is not the function call. It is how
TRTSDT and TRTEDT were
defined. If treatment end is based on last exposure, disposition, or a protocol
specific window, those rules must be implemented before duration is calculated.
5.5 Derive disposition variables
adsl <- adsl %>%
derive_vars_merged(
dataset_add = ds,
filter_add = DSDECOD == "RANDOMIZED",
new_vars = exprs(RANDDT = DSSTDTC),
order = exprs(DSSTDTC, DSSEQ),
mode = "first",
by_vars = exprs(STUDYID, USUBJID)
)
In a real study, the source date would generally be converted to the appropriate date type before the final ADSL variable is assigned. The key point is the selection rule: "first qualifying randomization record" is materially different from "any randomization record."
6. Cross-Dataset Joins: Choosing the Right Tool
There are three increasingly sophisticated situations to distinguish:
| Situation | Typical approach | Programming question |
|---|---|---|
| Simple subject-level merge | derive_vars_merged() | Can the additional record be selected using only its own variables? |
| Complex relationship between datasets | derive_vars_joined() | Does selection depend on values in both datasets? |
| Conditional derivation on a subset | restrict_derivation() | Should the derivation apply only to selected output records? |
The distinction matters because a join that appears to work on a small test dataset can produce duplicated records or incorrect assignments when the production data contain multiple source observations.
Example: period assignment concept
# Conceptual pattern:
# An event belongs to the ADSL period whose start/end dates
# contain the event date.
adae <- adae %>%
derive_vars_joined(
dataset_add = adsl,
by_vars = exprs(STUDYID, USUBJID),
new_vars = exprs(
APERIOD = APERIOD,
APHASE = APHASE
)
# Study-specific join conditions are supplied here.
)
The exact join condition should be written from the study's period specification. Do not treat a generic date-window example as a universal definition of period.
7. BDS Findings Example: ADVS From Source Measurements
The BDS structure is where admiral's modular approach becomes especially powerful. A typical Findings ADaM workflow includes dates and analysis days, parameters, analysis values, timing variables, treatment flags, reference-range indicators, baseline, change, shift, criteria flags, treatment, and final ordering.
The official ADVS/BDS guidance follows essentially this staged architecture.
7.1 Prepare the source domain
library(pharmaversesdtm) vs <- pharmaversesdtm::vs %>% convert_blanks_to_na() adsl_vars <- exprs( TRTSDT, TRTEDT, TRT01A, TRT01P ) advs <- derive_vars_merged( vs, dataset_add = admiral::admiral_adsl, new_vars = adsl_vars, by_vars = exprs(STUDYID, USUBJID) )
This is an important production pattern: bring only the ADSL variables required for the current derivation stage. Add the broader ADSL payload later when the specification requires it.
7.2 Derive analysis dates
advs <- advs %>%
derive_vars_dt(
dtc = VSDTC,
new_vars_prefix = "A"
)
The actual prefix and date variables should be aligned with the ADaM specification. For date/time variables, document the imputation rule rather than hiding it inside an opaque helper step.
7.3 Assign parameters
advs <- advs %>%
mutate(
PARAMCD = VSTESTCD,
PARAM = VSTEST,
AVAL = VSSTRESN,
AVALU = VSSTRESU
)
In production, parameter mapping is often more controlled than a direct one-to-one copy. For example, a specification may require harmonized parameter names, derived parameters, category variables, or controlled parameter numbers.
8. Baseline Is a Definition, Not a Calculation
A common beginner mistake is to think that BASE is the
baseline record. It is not. The baseline record is identified by a rule;
BASE is then propagated from that selected record.
For example, suppose the study defines baseline as the last non-missing value on or before treatment start. The program must first identify that observation.
advs <- advs %>%
mutate(
BASETYPE = "LAST"
) %>%
restrict_derivation(
derivation = derive_var_extreme_flag,
args = params(
by_vars = exprs(
STUDYID,
USUBJID,
BASETYPE,
PARAMCD
),
order = exprs(ADT, ATPTN, VISITNUM),
new_var = ABLFL,
mode = "last"
),
filter = !is.na(AVAL) &
ADT <= TRTSDT &
!is.na(BASETYPE)
)
Once ABLFL has identified the baseline observation,
derive_var_base() can propagate the baseline value.
advs <- advs %>%
derive_var_base(
by_vars = exprs(
STUDYID,
USUBJID,
PARAMCD,
BASETYPE
),
source_var = AVAL,
new_var = BASE
)
BASETYPE
before baseline flags, then derives BASE, and only after
that derives CHG/PCHG.
That sequence prevents downstream derivations from depending on an incompletely
defined baseline concept.
9. Change and Percent Change
Once AVAL and BASE exist,
change is straightforward:
The corresponding percent change is:
\[ PCHG = \frac{AVAL - BASE}{|BASE|}\times 100 \]The package provides dedicated functions for these calculations.
advs <- advs %>%
restrict_derivation(
derivation = derive_var_chg,
filter = AVISITN > 0
) %>%
restrict_derivation(
derivation = derive_var_pchg,
filter = AVISITN > 0
)
The restriction is study-specific. Some specifications populate change on post-baseline records only; others may have different requirements. The important programming principle is to make the population of the derived variable explicit.
10. Advanced Computed Parameters: MAP
Derived parameters demonstrate one of admiral's strongest BDS patterns: creating a new analysis record from existing parameter records.
For mean arterial pressure:
\[ MAP = \frac{SYSBP + 2(DBP)}{3} \]
A direct implementation with derive_param_computed()
can look like this:
advs <- advs %>%
derive_param_computed(
by_vars = exprs(
USUBJID,
AVISIT,
AVISITN
),
parameters = c("SYSBP", "DIABP"),
set_values_to = exprs(
AVAL = (AVAL.SYSBP + 2 * AVAL.DIABP) / 3,
PARAMCD = "MAP",
PARAM = "Mean Arterial Pressure (mmHg)",
AVALU = "mmHg"
)
)
The function works by requiring the specified parameters to be available for the group and then evaluating the expression against their analysis values. The documentation also describes support for constant parameters, which is useful when one component is measured once and another repeatedly.
10.1 A constant-parameter example
Suppose height is effectively constant for a subject while weight is measured at each visit. A BMI-like derivation conceptually follows:
\[ BMI = \frac{Weight}{(Height/100)^2} \]advs <- advs %>%
derive_vars_computed(
dataset_add = advs,
by_vars = exprs(USUBJID, AVISIT, AVISITN),
parameters = exprs(WEIGHT),
constant_parameters = exprs(HEIGHT),
constant_by_vars = exprs(USUBJID),
new_vars = exprs(
BMIBL = (AVAL.WEIGHT / (AVAL.HEIGHT / 100)^2)
)
)
The exact target variable and parameter structure must follow the study's ADaM specification. The important concept is the separation between repeatedly measured parameters and subject-level constant parameters.
11. Missingness Is Part of the Derivation Logic
A sophisticated ADaM program does not simply "let R handle missing values." Missingness is often part of the statistical definition.
For a computed parameter, ask:
- What happens if one required parameter is missing?
- Should the derived record be omitted?
- Should the record be retained with a missing
AVAL? - Is a missing constant parameter different from a missing repeated parameter?
- Should the missingness be represented in
AVALC?
For example, derive_param_computed() provides
keep_nas specifically to control whether observations
with missing contributing values are retained. That is a programming decision,
not merely a technical option.
12. Conditional Derivation With restrict_derivation()
Production datasets frequently contain multiple analysis populations in the same structure. You may need to calculate a variable for post-baseline observations but not baseline or pre-baseline observations.
adlb <- adlb %>%
restrict_derivation(
derivation = derive_var_chg,
filter = AVISITN > 0
)
The power of restrict_derivation() is that the
derivation function remains reusable while the subset rule is expressed separately.
The official documentation requires the supplied derivation to accept a dataset
as its first argument and return a dataset.
12.1 Passing parameters to a derivation
adlb <- adlb %>%
restrict_derivation(
derivation = derive_var_base,
args = params(
by_vars = exprs(USUBJID, PARAMCD),
source_var = AVAL,
new_var = BASE
),
filter = AVISITN >= 0
)
This is particularly useful when a reusable derivation has several arguments but the study needs to constrain where it executes.
13. OCCDS Example: Treatment-Emergent Adverse Events
OCCDS programming has a different grain from BDS. For ADAE, the record represents an event rather than a repeated measurement. The programming workflow therefore centers on event dates, treatment, duration, severity, causality, treatment emergence, occurrence flags, query variables, and subject-level additions.
13.1 Bring treatment timing into ADAE
adae <- ae %>%
derive_vars_merged(
dataset_add = adsl,
new_vars = exprs(
TRTSDTM,
TRTEDTM,
TRT01A,
TRT01P
),
by_vars = exprs(STUDYID, USUBJID)
)
13.2 Derive analysis event dates
adae <- adae %>%
derive_vars_dtm(
dtc = AESTDTC,
new_vars_prefix = "AST",
highest_imputation = "n"
) %>%
derive_vars_dtm(
dtc = AEENDTC,
new_vars_prefix = "AEN",
highest_imputation = "n"
)
The actual imputation strategy should be explicitly specified. Never assume that a partial adverse-event date should be imputed simply because a complete datetime is convenient for programming.
13.3 Treatment-emergent flag
adae <- adae %>%
derive_var_trtemfl(
new_var = TRTEMFL,
start_date = ASTDTM,
end_date = AENDTM,
trt_start_date = TRTSDTM,
trt_end_date = TRTEDTM
)
The current admiral implementation supports additional treatment-emergent scenarios, including treatment-end windows and worsening of an event's intensity. Those options should be used only when they match the study's predefined rule.
13.4 On-treatment flag
adae <- adae %>%
derive_var_ontrtfl(
new_var = ONTRTFL,
start_date = ASTDT,
end_date = AENDT,
ref_start_date = TRTSDT,
ref_end_date = TRTEDT
)
Treatment-emergent and on-treatment are not interchangeable concepts. A study can define them differently, so both should be treated as specification-driven flags.
14. Higher-Order Programming: Make Repeated Logic Explicit
Large studies often repeat the same derivation across several parameters. The temptation is to copy and edit code. A more maintainable approach is to isolate the reusable operation.
derive_post_baseline_chg <- function(dataset, visit_cutoff = 0) {
restrict_derivation(
dataset,
derivation = derive_var_chg,
filter = AVISITN > visit_cutoff
)
}
adlb <- derive_post_baseline_chg(adlb)
advs <- derive_post_baseline_chg(advs)
A reusable function should be introduced only when it genuinely reduces repeated logic without hiding important study-specific decisions. A function that obscures a critical baseline or population rule can make QC harder rather than easier.
15. Lookup-Driven Derivations
Clinical datasets frequently require controlled mappings: parameter labels, parameter numbers, treatment codes, category variables, or sponsor-specific classification variables.
param_lookup <- tibble::tribble(
~PARAMCD, ~PARAM, ~PARAMN,
"SYSBP", "Systolic Blood Pressure (mmHg)", 1,
"DIABP", "Diastolic Blood Pressure (mmHg)", 2,
"MAP", "Mean Arterial Pressure (mmHg)", 3
)
advs <- advs %>%
derive_vars_merged(
dataset_add = param_lookup,
by_vars = exprs(PARAMCD),
new_vars = exprs(
PARAM = PARAM,
PARAMN = PARAMN
)
)
A lookup table can be easier to audit than a long nested case_when().
It also gives QC programmers a compact artifact to compare with the specification.
16. ADSL Variables Should Not Be Added Blindly
Downstream ADaMs often need ADSL information, but "merge all of ADSL" is rarely the best programming pattern.
adsl_vars <- exprs(
TRT01P,
TRT01A,
TRTSDT,
TRTEDT,
SAFFL,
ITTFL
)
adlb <- adlb %>%
derive_vars_merged(
dataset_add = adsl,
new_vars = adsl_vars,
by_vars = exprs(STUDYID, USUBJID)
)
Selecting only required variables reduces the chance of accidental name collisions, unnecessary duplication, and undocumented dependencies.
17. Join Cardinality Is a QC Requirement
Suppose a programmer expects one ADSL row per subject but the source contains two qualifying exposure records. A simple merge may silently create an unexpected many-to-one relationship unless the selection logic is explicit.
Before using a merge, test the source key:
ex_ext %>% count(STUDYID, USUBJID) %>% filter(n > 1)
A non-empty result is not automatically an error. It tells you that the source contains multiple records per subject and therefore that a selection rule is needed.
18. Date and Datetime Imputation Requires a Specification
Date imputation is one of the easiest places to introduce a clinically meaningful error. The technical ability to impute a partial ISO date does not determine the correct imputation.
adsl <- adsl %>%
derive_vars_dt(
dtc = RFENDTC,
new_vars_prefix = "RFEN",
highest_imputation = "m",
date_imputation = "last",
flag_imputation = "auto"
)
For a partial value such as 2026-04, "first" and
"last" day-of-month imputation lead to different dates. The appropriate choice
depends on the derivation definition, not on programmer preference.
19. Traceability: Preserve the Why, Not Just the What
A strong ADaM program should make it possible to trace a variable backward:
For example, a TRTEMFL value should not merely be
"generated by an admiral function." The programmer should be able to explain the
treatment start/end dates, event start/end dates, any treatment window, and any
intensity-worsening rule that determines the flag.
20. Validation: What admiral Does Not Replace
Using a standardized package function does not automatically prove that the resulting ADaM is correct. The package implements an algorithm; the programmer must determine whether that algorithm is being applied to the right data and whether its assumptions match the study.
| QC layer | Question |
|---|---|
| Specification QC | Does the code implement the written ADaM specification? |
| Source QC | Are the expected SDTM records and variables being used? |
| Derivation QC | Does each calculation produce the expected result? |
| Structural QC | Are keys, record counts, variable types, and required variables correct? |
| Independent programming QC | Does an independent implementation agree with the production result? |
| Statistical QC | Do downstream summaries and analyses behave as expected? |
21. Targeted Unit Tests for Derivation Logic
Statistical programmers should test difficult derivations with small constructed datasets rather than relying exclusively on a full-study run.
Example: baseline edge cases
baseline_test <- tibble::tribble(
~USUBJID, ~PARAMCD, ~ADT, ~AVAL, ~TRTSDT,
"01", "ALT", as.Date("2026-01-01"), 10, as.Date("2026-01-05"),
"01", "ALT", as.Date("2026-01-04"), 12, as.Date("2026-01-05"),
"01", "ALT", as.Date("2026-01-07"), 15, as.Date("2026-01-05"),
"02", "ALT", as.Date("2026-01-04"), NA, as.Date("2026-01-05")
)
This small dataset can test whether the baseline algorithm chooses the last non-missing pre-treatment value, ignores post-treatment observations, and handles a subject with no eligible value.
Example: computed-parameter edge cases
# Test cases should include: # 1. Both inputs present # 2. SYSBP missing # 3. DIABP missing # 4. Both missing # 5. Duplicate parameter records # 6. Multiple visits # 7. Multiple subjects
The objective is not merely to test that the happy path works. It is to test the conditions under which the algorithm could make a clinically important mistake.
22. Independent QC: Reproduce the Rule, Not the Code
A weak QC strategy copies the production implementation in different syntax. A stronger strategy independently implements the specification.
For example, if the production program derives:
\[ CHG = AVAL - BASE \]the QC program can calculate the same quantity independently and compare the result. For a more complex baseline rule, the QC programmer should independently identify the expected baseline record rather than simply calling the same admiral function.
%>% to another pipe or renaming variables
does not make a copy-and-paste derivation independent.
23. Production Program Architecture
A large ADaM program becomes easier to maintain when its sections correspond to conceptual derivation stages.
# ============================================================
# 01. Setup
# ============================================================
library(admiral)
library(dplyr)
library(lubridate)
# ============================================================
# 02. Read source data
# ============================================================
dm <- read_source("DM")
ex <- read_source("EX")
ds <- read_source("DS")
# ============================================================
# 03. Prepare source data
# ============================================================
ex_ext <- ex %>%
filter(!is.na(EXSTDTM)) %>%
arrange(STUDYID, USUBJID, EXSTDTM, EXSEQ)
# ============================================================
# 04. Subject-level derivations
# ============================================================
adsl <- dm %>%
# treatment
# dates
# disposition
# populations
# other ADSL variables
...
# ============================================================
# 05. Analysis dataset derivations
# ============================================================
adlb <- lb %>%
# merge required ADSL variables
# derive dates
# assign parameters
# derive baseline
# derive CHG/PCHG
# derive analysis flags
...
# ============================================================
# 06. Finalization
# ============================================================
# labels
# ordering
# sequence variables where required
# remove temporary variables
# ============================================================
# 07. QC outputs
# ============================================================
# record counts
# key checks
# derivation checks
# comparison to independent QC
The exact architecture is sponsor-specific, but the principle is broadly useful: the program should be organized so that a reviewer can navigate directly to the derivation area relevant to a specification question.
24. Avoid the "Giant Pipeline" Anti-Pattern
Pipes are useful, but an enormous uninterrupted pipeline can become difficult to debug. Consider breaking major conceptual stages into named intermediate objects.
advs_source <- prepare_vs(vs, adsl) advs_parameterized <- advs_source %>% derive_parameters() advs_baseline <- advs_parameterized %>% derive_baseline() advs_analysis <- advs_baseline %>% derive_changes() %>% derive_flags() advs_final <- advs_analysis %>% finalize_advs()
This approach can make intermediate data available for targeted review and debugging. The functions shown above are illustrative project-level functions; they demonstrate an architecture rather than claiming to be admiral functions.
25. Temporary Variables Are Useful—If You Control Them
Intermediate variables often make a derivation clearer. Examples include previous/next dates, temporary parameter codes, helper flags, source-selection variables, and ordering variables.
adlb <- adlb %>%
mutate(
eligible_for_baseline = !is.na(AVAL) & ADT <= TRTSDT
) %>%
# use helper variable in the derivation
... %>%
select(-eligible_for_baseline)
Do not let temporary variables leak into the final dataset without a documented purpose. Conversely, do not remove an intermediate variable merely because it looks temporary if it is needed for traceability or a downstream derivation.
26. Parameter Grain and Keys
For BDS datasets, the key is rarely just USUBJID.
Depending on the dataset, the analysis grain may include subject, parameter,
visit, timepoint, category, baseline type, analysis date, or other variables.
Before using functions such as derive_param_computed(),
write down the intended grain. For example:
# Example conceptual BDS grain: # USUBJID + AVISIT + AVISITN + PARAMCD by_vars = exprs( USUBJID, AVISIT, AVISITN )
If two observations exist for the same subject/visit/parameter when the algorithm expects one, the derivation can become ambiguous. Grain checks should therefore be part of both development and QC.
27. Duplicate Detection Should Be Deliberate
advs %>%
count(
STUDYID,
USUBJID,
AVISIT,
AVISITN,
PARAMCD
) %>%
filter(n > 1)
The correct uniqueness variables depend on the ADaM specification. The point is to make the intended grain executable as a check rather than leaving it implicit.
28. Population Flags Are Analysis Definitions
Flags such as SAFFL, ITTFL,
and EFFFL are not merely administrative fields.
They define analysis populations and can change the denominator of an analysis.
adsl <- adsl %>%
mutate(
SAFFL = if_else(
!is.na(TRTSDT),
"Y",
NA_character_
)
)
This is only an illustration. The real safety population definition must come from the protocol/SAP and may require exposure thresholds, treatment receipt, or other conditions. Never substitute a convenient programming proxy for the actual population definition.
29. Why admiral Functions Are Valuable to a Senior Programmer
The value of admiral becomes clearer when comparing three approaches.
| Approach | Strength | Risk |
|---|---|---|
| Raw dplyr only | Maximum flexibility | Repeated low-level derivation logic and inconsistent implementations |
| Monolithic custom framework | Centralized sponsor conventions | Opaque abstractions and high maintenance cost |
| admiral + sponsor conventions | Reusable clinical derivations plus study-specific control | Requires learning the package's expression and derivation model |
The third approach is often attractive because common clinical programming operations can be expressed in a recognizable vocabulary while the programmer retains control of study-specific logic.
30. When Not to Use a Specialized admiral Function
A specialized wrapper is valuable when its semantics match the specification. It is not valuable merely because it exists.
For example, if a sponsor's baseline definition differs materially from the assumptions of a convenient wrapper, implement the specification explicitly, possibly using lower-level admiral functions such as merges, restrictions, and flags.
31. A Complete Advanced BDS Pattern
The following condensed pattern combines several concepts into one workflow. It is intentionally structured rather than presented as a single giant call.
adlb <- lb %>%
# ----------------------------------------------------------
# Source preparation
# ----------------------------------------------------------
convert_blanks_to_na() %>%
# ----------------------------------------------------------
# Add only required ADSL variables
# ----------------------------------------------------------
derive_vars_merged(
dataset_add = adsl,
new_vars = exprs(
TRTSDT,
TRTEDT,
TRT01P,
TRT01A
),
by_vars = exprs(STUDYID, USUBJID)
) %>%
# ----------------------------------------------------------
# Analysis date
# ----------------------------------------------------------
derive_vars_dt(
dtc = LBDTC,
new_vars_prefix = "A"
) %>%
# ----------------------------------------------------------
# Analysis value / parameter mapping
# ----------------------------------------------------------
mutate(
PARAMCD = LBTESTCD,
PARAM = LBTEST,
AVAL = LBSTRESN,
AVALU = LBSTRESU
) %>%
# ----------------------------------------------------------
# Baseline flag
# ----------------------------------------------------------
restrict_derivation(
derivation = derive_var_extreme_flag,
args = params(
by_vars = exprs(STUDYID, USUBJID, PARAMCD),
order = exprs(ADT, VISITNUM),
new_var = ABLFL,
mode = "last"
),
filter = !is.na(AVAL) & ADT <= TRTSDT
) %>%
# ----------------------------------------------------------
# Baseline value
# ----------------------------------------------------------
derive_var_base(
by_vars = exprs(STUDYID, USUBJID, PARAMCD),
source_var = AVAL,
new_var = BASE
) %>%
# ----------------------------------------------------------
# Change from baseline
# ----------------------------------------------------------
restrict_derivation(
derivation = derive_var_chg,
filter = AVISITN > 0
)
This pattern demonstrates the central philosophy: each clinically meaningful requirement has a visible programming step.
32. A Practical QC Matrix
| Requirement | Production check | Independent QC |
|---|---|---|
| Subject count | Count ADSL subjects | Compare with DM subject population |
| Treatment start | Inspect first qualifying EX record | Independently select first dose |
| Baseline | Check ABLFL and BASE | Independently identify baseline record |
| CHG | Check AVAL - BASE | Recalculate independently |
| Derived parameter | Check required component parameters | Recalculate formula independently |
| TRTEMFL | Check event/treatment dates | Independent event classification |
| Join | Check key/cardinality | Compare source selection and counts |
| Population flags | Tabulate flags | Compare to SAP-defined rules |
33. A Senior Programmer's Review Checklist
- Specification: Is every required variable mapped to a source and derivation?
- Grain: Is the intended record-level key explicitly understood?
- Dates: Are partial dates, imputation, and time zones handled according to the specification?
- Joins: Is every merge relationship intentional and cardinality-controlled?
- Baseline: Is the baseline record selection rule explicit?
- Missingness: Are missing component values handled deliberately?
- Population: Are analysis flags based on the actual SAP definition?
- Parameters: Are PARAMCD/PARAM/PARAMN and category variables controlled?
- Traceability: Can every important output variable be traced to its source and rule?
- Reusability: Is repeated mechanical logic abstracted without hiding study interpretation?
- Testing: Are edge cases tested with small constructed datasets?
- QC: Is the independent implementation genuinely independent?
34. Common Advanced Mistakes
Mistake 1: Treating a function as a specification
A function can implement an algorithm correctly while the algorithm is wrong for the study. The specification remains authoritative.
Mistake 2: Ignoring record grain
Many subtle errors are caused by duplicate records at a point where a function expects one observation per parameter/group.
Mistake 3: Using a simple merge for a temporal relationship
Subject-level equality joins are not enough when an event must be assigned to a period, phase, or treatment interval based on dates.
Mistake 4: Hiding baseline logic in a helper
Baseline is usually important enough to deserve an explicit, reviewable section.
Mistake 5: Over-abstracting the program
If a reviewer cannot determine why a record was selected, the abstraction has gone too far.
Mistake 6: Testing only the full study
Small deterministic datasets are much better for proving edge-case derivation logic.
35. How to Read the admiral Documentation Efficiently
For a statistical programmer, do not attempt to memorize hundreds of functions. Instead, learn the naming and architectural conventions:
| Pattern | What to look for |
|---|---|
derive_var_* | A focused variable derivation |
derive_vars_* | One or more variable derivations |
derive_param_* | Creation of a BDS parameter/record |
restrict_derivation() | Apply a derivation only to a subset |
derive_vars_merged() | Bring variables from another dataset |
derive_vars_joined() | More complex cross-dataset record selection |
exprs() | Pass multiple variables or expressions |
The official documentation's package index and user guides are much more useful once these patterns are familiar.
36. Suggested Learning Path for Experienced SAS Programmers
exprs(), derivation naming, and templates.37. Bottom Line
For a statistical programmer, learning admiral is less about memorizing package functions and more about learning a disciplined way to express ADaM specifications in R.
The strongest programs make the clinical reasoning visible:
- which records are eligible;
- which source wins when multiple records exist;
- how dates are derived and imputed;
- how baseline is selected;
- how analysis values are calculated;
- how derived parameters are constructed;
- how treatment-emergent and on-treatment concepts differ;
- how ADSL information enters downstream datasets;
- how the record grain is controlled; and
- how the final result is independently validated.
That is where admiral becomes more than an R package. It becomes a vocabulary for writing ADaM derivations that are modular, inspectable, reusable, and suitable for the realities of clinical statistical programming.