6  ADaM ADAE Derivation: A Comparison of Dplyr and Admiral

Code
library(dplyr)
library(lubridate)
library(stringr)
library(admiral) # For Admiral package functions
library(admiraldev) # For Admiral development utilities, if needed

We explores the derivation of an ADaM (Analysis Data Model) Adverse Events (ADAE) dataset, a crucial component in clinical trial analysis.

We will compare two prominent R approaches:

dplyr: A general-purpose data manipulation package, offering high flexibility.

Admiral: A specialized package designed specifically for CDISC ADaM dataset creation, promoting standardization and reproducibility.

The goal is to demonstrate how common ADAE variables are derived from simulated SDTM (Study Data Tabulation Model) data, highlighting the strengths and differences of each method.

  1. Simulate SDTM Source Data To derive ADAE, we need source data from SDTM domains. For this example, we’ll simulate minimal SDTM.AE (Adverse Events), SDTM.DM (Demographics), and SDTM.EX (Exposure) datasets
#| label: simulate_sdtm_data
#| code-fold: false

#| label: simulate_sdtm_data
#| code-fold: false

# Simulate SDTM.DM (Demographics)
sdtm_dm <- tibble::tribble(
  ~STUDYID, ~USUBJID, ~RFSTDTC, ~RFXSTDTC, ~RFXENDTC, ~ARM, ~ACTARM,
  "STUDY001", "SUBJ001", "2023-01-01", "2023-01-01", "2023-03-30", "Treatment A", "Treatment A",
  "STUDY001", "SUBJ002", "2023-01-05", "2023-01-05", "2023-04-15", "Placebo", "Placebo",
  "STUDY001", "SUBJ003", "2023-01-10", "2023-01-10", "2023-05-01", "Treatment B", "Treatment B",
  "STUDY001", "SUBJ004", "2023-01-15", "2023-01-15", "2023-06-20", "Treatment A", "Treatment A",
  "STUDY001", "SUBJ005", "2023-01-20", "2023-01-20", "2023-07-05", "Placebo", "Placebo"
) %>%
  mutate(
    RFSTDTC = ymd(RFSTDTC),
    RFXSTDTC = ymd(RFXSTDTC),
    RFXENDTC = ymd(RFXENDTC)
  )

# Simulate SDTM.AE (Adverse Events)
sdtm_ae <- tibble::tribble(
  ~STUDYID, ~USUBJID, ~AESEQ, ~AETERM, ~AEDECOD, ~AESTDTC, ~AEENDTC, ~AEONGO, ~AESER, ~AEREL, ~AESEV, ~AETOXGR,
  "STUDY001", "SUBJ001", 1, "Headache", "Headache", "2023-01-15", "2023-01-17", "N", "N", "POSSIBLY RELATED", "MILD", "1",
  "STUDY001", "SUBJ001", 2, "Nausea", "Nausea", "2023-03-01", NA, "Y", "N", "NOT RELATED", "MODERATE", "2",
  "STUDY001", "SUBJ002", 1, "Fever", "Fever", "2023-01-06", "2023-01-08", "N", "Y", "PROBABLY RELATED", "SEVERE", "3",
  "STUDY001", "SUBJ003", 1, "Rash", "Rash", "2023-02-10", "2023-02-15", "N", "N", "NOT RELATED", "MILD", "1",
  "STUDY001", "SUBJ004", 1, "Dizziness", "Dizziness", "2023-01-16", "2023-01-16", "N", "Y", "DEFINITELY RELATED", "SEVERE", "3",
  "STUDY001", "SUBJ004", 2, "Fatigue", "Fatigue", "2023-06-25", NA, "Y", "N", "NOT RELATED", "MODERATE", "2", # Post-treatment emergence
  "STUDY001", "SUBJ005", 1, "Cough", "Cough", "2023-01-20", "2023-01-22", "N", "N", "POSSIBLY RELATED", "MILD", "1"
) %>%
  mutate(
    AESTDTC = ymd(AESTDTC),
    AEENDTC = ymd(AEENDTC)
  )

# Simulate SDTM.EX (Exposure) - for more detailed treatment emergence, though DM has RFXSTDTC/RFXENDTC
# For simplicity, we'll primarily use DM's RFXSTDTC/RFXENDTC for treatment emergence in this example.
sdtm_ex <- tibble::tribble(
  ~STUDYID, ~USUBJID, ~EXSEQ, ~EXSTDTC, ~EXENDTC,
  "STUDY001", "SUBJ001", 1, "2023-01-01", "2023-03-30",
  "STUDY001", "SUBJ002", 1, "2023-01-05", "2023-04-15",
  "STUDY001", "SUBJ003", 1, "2023-01-10", "2023-05-01",
  "STUDY001", "SUBJ004", 1, "2023-01-15", "2023-06-20",
  "STUDY001", "SUBJ005", 1, "2023-01-20", "2023-07-05"
) %>%
  mutate(
    EXSTDTC = ymd(EXSTDTC),
    EXENDTC = ymd(EXENDTC)
  )

cat("SDTM.DM (first 5 rows):\n")
SDTM.DM (first 5 rows):
print(head(sdtm_dm))
# A tibble: 5 × 7
  STUDYID  USUBJID RFSTDTC    RFXSTDTC   RFXENDTC   ARM         ACTARM     
  <chr>    <chr>   <date>     <date>     <date>     <chr>       <chr>      
1 STUDY001 SUBJ001 2023-01-01 2023-01-01 2023-03-30 Treatment A Treatment A
2 STUDY001 SUBJ002 2023-01-05 2023-01-05 2023-04-15 Placebo     Placebo    
3 STUDY001 SUBJ003 2023-01-10 2023-01-10 2023-05-01 Treatment B Treatment B
4 STUDY001 SUBJ004 2023-01-15 2023-01-15 2023-06-20 Treatment A Treatment A
5 STUDY001 SUBJ005 2023-01-20 2023-01-20 2023-07-05 Placebo     Placebo    
cat("\nSDTM.AE (first 5 rows):\n")

SDTM.AE (first 5 rows):
print(head(sdtm_ae))
# A tibble: 6 × 12
  STUDYID  USUBJID AESEQ AETERM AEDECOD AESTDTC    AEENDTC    AEONGO AESER AEREL
  <chr>    <chr>   <dbl> <chr>  <chr>   <date>     <date>     <chr>  <chr> <chr>
1 STUDY001 SUBJ001     1 Heada… Headac… 2023-01-15 2023-01-17 N      N     POSS…
2 STUDY001 SUBJ001     2 Nausea Nausea  2023-03-01 NA         Y      N     NOT …
3 STUDY001 SUBJ002     1 Fever  Fever   2023-01-06 2023-01-08 N      Y     PROB…
4 STUDY001 SUBJ003     1 Rash   Rash    2023-02-10 2023-02-15 N      N     NOT …
5 STUDY001 SUBJ004     1 Dizzi… Dizzin… 2023-01-16 2023-01-16 N      Y     DEFI…
6 STUDY001 SUBJ004     2 Fatig… Fatigue 2023-06-25 NA         Y      N     NOT …
# ℹ 2 more variables: AESEV <chr>, AETOXGR <chr>

6.0.1 ADaM ADAE Derivation with dplyr

This section demonstrates the derivation of common ADAE variables using the dplyr package. We will join the necessary SDTM domains and then apply transformations to create analysis-ready variables.

6.0.1.1 Initial Join and Core Variables

First, we join SDTM.AE with SDTM.DM to bring in reference dates.

#| label: dplyr_core_vars
#| code-fold: false

adae_dplyr <- sdtm_ae %>%
  left_join(sdtm_dm %>%  select (STUDYID,USUBJID, RFSTDTC, RFXSTDTC, RFXENDTC, ARM, ACTARM), by = c("STUDYID", "USUBJID")) %>%
  mutate(
    ADJ = "Y", # All records in ADAE are adjusted for analysis
    ADVERSE_EVENT_SEQ = AESEQ, # Copy of SDTM AESEQ
    ASTDTM = AESTDTC, # Analysis start date/time (copy of AESTDTC)
    AENDTM = AEENDTC # Analysis end date/time (copy of AEENDTC)
  )

print(adae_dplyr %>% select(USUBJID, AETERM, ASTDTM, AENDTM, ADJ, ADVERSE_EVENT_SEQ, ARM, ACTARM) %>% head())
# A tibble: 6 × 8
  USUBJID AETERM    ASTDTM     AENDTM     ADJ   ADVERSE_EVENT_SEQ ARM     ACTARM
  <chr>   <chr>     <date>     <date>     <chr>             <dbl> <chr>   <chr> 
1 SUBJ001 Headache  2023-01-15 2023-01-17 Y                     1 Treatm… Treat…
2 SUBJ001 Nausea    2023-03-01 NA         Y                     2 Treatm… Treat…
3 SUBJ002 Fever     2023-01-06 2023-01-08 Y                     1 Placebo Place…
4 SUBJ003 Rash      2023-02-10 2023-02-15 Y                     1 Treatm… Treat…
5 SUBJ004 Dizziness 2023-01-16 2023-01-16 Y                     1 Treatm… Treat…
6 SUBJ004 Fatigue   2023-06-25 NA         Y                     2 Treatm… Treat…

6.0.2 Deriving Study Days (ADJSDY, ADJEDY) and Duration (ADJDAY)

Study days are calculated relative to the subject’s reference start date (RFSTDTC). Duration is the difference between analysis end and start dates.

adae_dplyr <- adae_dplyr %>%
  mutate(
    ADJSDY = as.numeric(difftime(ASTDTM, RFSTDTC, units = "days")) + 1, # +1 for inclusive day count
    ADJEDY = as.numeric(difftime(AENDTM, RFSTDTC, units = "days")) + 1,
    ADJDAY = as.numeric(difftime(AENDTM, ASTDTM, units = "days")) + 1 # Duration in days
  )

print(adae_dplyr %>% select(USUBJID, ASTDTM, RFSTDTC, ADJSDY, AENDTM, ADJEDY, ADJDAY) %>% head())
# A tibble: 6 × 7
  USUBJID ASTDTM     RFSTDTC    ADJSDY AENDTM     ADJEDY ADJDAY
  <chr>   <date>     <date>      <dbl> <date>      <dbl>  <dbl>
1 SUBJ001 2023-01-15 2023-01-01     15 2023-01-17     17      3
2 SUBJ001 2023-03-01 2023-01-01     60 NA             NA     NA
3 SUBJ002 2023-01-06 2023-01-05      2 2023-01-08      4      3
4 SUBJ003 2023-02-10 2023-01-10     32 2023-02-15     37      6
5 SUBJ004 2023-01-16 2023-01-15      2 2023-01-16      2      1
6 SUBJ004 2023-06-25 2023-01-15    162 NA             NA     NA
Note

Scenario: Missing Dates If ASTDTM or AENDTM are missing, ADJSDY, ADJEDY, and ADJDAY will correctly become NA. The +1 is standard for inclusive day calculations in clinical trials.

6.0.2.1 Deriving Analysis Flags (ONGO, SER, REL)

These flags are typically direct copies or simple transformations of their SDTM counterparts.

#| label: dplyr_analysis_flags
#| code-fold: false

adae_dplyr <- adae_dplyr %>%
  mutate(
    ONGO = AEONGO, # Copy of AEONGO
    SER = AESER,   # Copy of AESER
    REL = case_when( # Simplify relationship to 'Y' or 'N' for analysis
      toupper(AEREL) %in% c("DEFINITELY RELATED", "PROBABLY RELATED", "POSSIBLY RELATED") ~ "Y",
      toupper(AEREL) == "NOT RELATED" | toupper(AEREL) == "UNLIKELY RELATED" ~ "N",
      TRUE ~ NA_character_
    )
  )

print(adae_dplyr %>% select(USUBJID, AEONGO, ONGO, AESER, SER, AEREL, REL) %>% head())

6.0.3 Deriving Severity and Toxicity Grade (AESEVN, ATOXGR)

AESEVN provides a numeric representation of severity. ATOXGR is typically a direct copy.

#| label: dplyr_severity_tox
#| code-fold: false

adae_dplyr <- adae_dplyr %>%
  mutate(
    AESEVN = case_when(
      toupper(AESEV) == "MILD" ~ 1,
      toupper(AESEV) == "MODERATE" ~ 2,
      toupper(AESEV) == "SEVERE" ~ 3,
      TRUE ~ NA_real_ # Use NA_real_ for numeric NA
    ),
    ATOXGR = AETOXGR # Copy of AETOXGR
  )

print(adae_dplyr %>% select(USUBJID, AESEV, AESEVN, AETOXGR, ATOXGR) %>% head())

6.0.3.1 Deriving Treatment Emergence Flag (TRTEMFL)

TRTEMFL indicates if an event started during or after treatment exposure. This is a common and important flag in AE analysis.

#| label: dplyr_trtemfl
#| code-fold: false

adae_dplyr <- adae_dplyr %>%
  mutate(
    TRTEMFL = case_when(
      # Event started on or after treatment start AND before or on treatment end (+7 days buffer)
      ASTDTM >= RFXSTDTC & ASTDTM <= (RFXENDTC + days(7)) ~ "Y",
      # Event started before treatment start
      ASTDTM < RFXSTDTC ~ "N",
      TRUE ~ NA_character_ # Handle other cases, e.g., missing dates
    )
  )

print(adae_dplyr %>% select(USUBJID, ASTDTM, RFXSTDTC, RFXENDTC, TRTEMFL) %>% head())
Tip

Scenario: Treatment Emergence Window The definition of “treatment emergent” often includes a buffer period (e.g., 7 or 30 days) after the last dose to capture events potentially related to the treatment. This example uses a 7-day buffer.

6.0.4 ADaM ADAE Derivation with Admiral

The Admiral package provides a structured, function-based approach to ADaM derivations, often simplifying complex logic and ensuring CDISC compliance. It operates on ADaM-formatted input datasets.

6.0.4.1 Initial Setup and ADaM Input Structure

Admiral functions typically expect ADaM-like inputs. We’ll prepare our simulated SDTM data into a structure that Admiral can consume.

#| label: admiral_setup
#| code-fold: false

# Prepare input datasets for Admiral
# Admiral functions often expect specific variable names and types.
# We'll rename and select relevant columns from SDTM.AE and SDTM.DM
adsl <- sdtm_dm %>%
  rename(TRTSDT = RFXSTDTC, TRTEDT = RFXENDTC, RFSTDTC = RFSTDTC) %>%
  mutate(
    TRTSDTM = as_datetime(TRTSDT),
    TRTEDTM = as_datetime(TRTEDT),
    RFSTDTC = as_datetime(RFSTDTC) # Ensure RFSTDTC is datetime for consistency if used with DTM vars
  ) %>%
  select(STUDYID, USUBJID, RFSTDTC, TRTSDT, TRTEDT, TRTSDTM, TRTEDTM, ARM, ACTARM)

adsl_ae <- sdtm_ae %>%
  rename(
    ASTDT = AESTDTC,
    AENDT = AEENDTC
  ) %>%
  mutate(
    ASTDTM = as_datetime(ASTDT),
    AENDTM = as_datetime(AENDT)
  ) %>%
  # Add ADJ and ADVERSE_EVENT_SEQ early as they are simple copies
  mutate(
    ADJ = "Y",
    ADVERSE_EVENT_SEQ = AESEQ
  )

cat("ADSL (for Admiral input, first 5 rows):\n")
print(head(adsl))
cat("\nADSL_AE (for Admiral input, first 5 rows):\n")
print(head(adsl_ae))

6.0.4.2 Merging ADSL Variables (ARM, ACTARM)

Admiral provides derive_vars_merged() to bring variables from ADSL into the analysis dataset.

#| label: admiral_merge_adsl_vars
#| code-fold: false
library(admiral)
adsl_vars <- exprs(TRTSDT, TRTEDT, TRT01A, TRT01P, DTHDT, EOSDT)

#| label: admiral_merge_adsl_vars
#| code-fold: false

adae_admiral <- adsl_ae %>%
  derive_vars_merged(
    dataset_adsl = adsl,
    by_vars = exprs(STUDYID, USUBJID),
    new_vars = exprs(ARM, ACTARM)
  )

print(adae_admiral %>% select(USUBJID, AETERM, ARM, ACTARM) %>% head())

6.0.4.3 Deriving Study Days (ADY, ASTDY, AENDY) and Duration (ADUR)

Admiral provides dedicated functions for date and day derivations, often handling edge cases and conventions automatically.

#| label: admiral_study_days_duration
#| code-fold: false

adae_admiral <- adae_admiral %>%
  derive_vars_ady(
    dataset_adsl = adsl,
    date_var = ASTDTM,
    new_var = ASTDY,
    ref_date_var = RFSTDTC
  ) %>%
  derive_vars_ady(
    dataset_adsl = adsl,
    date_var = AENDTM,
    new_var = AENDY,
    ref_date_var = RFSTDTC
  ) %>%
  derive_vars_duration(
    new_var = ADUR,
    start_date = ASTDTM,
    end_date = AENDTM,
    in_unit = "days",
    out_unit = "days",
    add_one = TRUE # For inclusive duration
  ) %>%
  # Rename ADUR to ADJDAY for consistency with dplyr example
  rename(ADJDAY = ADUR)

print(adae_admiral %>% select(USUBJID, ASTDTM, ASTDY, AENDTM, AENDY, ADJDAY) %>% head())
Note

Admiral’s Approach to Dates Admiral functions like derive_vars_ady simplify date derivations by taking the ADSL dataset as an argument, allowing for consistent reference date usage. derive_vars_duration is a robust way to calculate duration.

6.0.4.4 Deriving Analysis Flags (ONGO, SER, REL)

Admiral offers functions to derive event flags based on CDISC rules.

#| label: admiral_analysis_flags
#| code-fold: false

adae_admiral <- adae_admiral %>%
  mutate(
    ONGO = AEONGO, # Direct copy
    SER = AESER   # Direct copy
  ) %>%
  derive_flag_value(
    new_var = REL,
    condition = AEREL %in% c("DEFINITELY RELATED", "PROBABLY RELATED", "POSSIBLY RELATED"),
    true_value = "Y",
    false_value = "N"
  )

print(adae_admiral %>% select(USUBJID, ONGO, SER, AEREL, REL) %>% head())

6.0.4.5 Deriving Severity and Toxicity Grade (AESEVN, ATOXGR)

Similar to dplyr, these often involve case_when use METACORE package or direct copying.

#| label: admiral_severity_tox
#| code-fold: false

adae_admiral <- adae_admiral %>%
  mutate(
    AESEVN = case_when(
      toupper(AESEV) == "MILD" ~ 1,
      toupper(AESEV) == "MODERATE" ~ 2,
      toupper(AESEV) == "SEVERE" ~ 3,
      TRUE ~ NA_real_
    ),
    ATOXGR = AETOXGR # Copy
  )

print(adae_admiral %>% select(USUBJID, AESEV, AESEVN, AETOXGR, ATOXGR) %>% head())

6.0.4.6 Deriving Treatment Emergence Flag (TRTEMFL)

Admiral has specific functions for treatment emergent flags, often considering complex rules like prior treatment, concomitant medications, and washout periods.

#| label: admiral_trtemfl
#| code-fold: false

adae_admiral <- adae_admiral %>%
  derive_var_trt_emerg(
    dataset_adsl = adsl,
    new_var = TRTEMFL,
    start_date = ASTDTM,
    end_date = AENDTM, # Admiral's function can use end date for ongoing events
    trt_start_date = TRTSDTM,
    trt_end_date = TRTEDTM,
    buffer_days_after_trt = 7
  )

print(adae_admiral %>% select(USUBJID, ASTDTM, TRTSDTM, TRTEDTM, TRTEMFL) %>% head())
Tip

Admiral’s derive_var_trt_emerg

This function is highly configurable and directly implements CDISC ADaM rules for treatment emergence, including handling of ongoing events and buffer periods. It’s a powerful and standardized way to derive this flag.

6.0.4.7 Deriving Analysis Record Indicator (ANLFL)

ANLFL indicates if a record is to be included in the primary analysis.

This is often a simple flag set to ‘Y’ for all records in ADaM.

#| label: admiral_anlfl
#| code-fold: false

adae_admiral <- adae_admiral %>%
  mutate(ANLFL = "Y") # All records are analysis records by default in ADAE
print(adae_admiral %>% select(USUBJID, AETERM, ANLFL) %>% head())

6.0.5 Deriving Worst Severity Flag (AWORSTFL)

AWORSTFL indicates the record with the worst severity for a given adverse event term within a subject.

This requires grouping and ordering.

#| label: admiral_aworstfl
#| code-fold: false

adae_admiral <- adae_admiral %>%
  # Ensure AESEVN is numeric for proper ordering
  mutate(AESEVN = as.numeric(AESEVN)) %>%
  group_by(USUBJID, AEDECOD) %>%
  arrange(desc(AESEVN), ASTDTM, .by_group = TRUE) %>% # Order by severity (desc), then start date
  mutate(AWORSTFL = if_else(row_number() == 1, "Y", NA_character_)) %>% # Flag the first record in each group
  ungroup()

print(adae_admiral %>% select(USUBJID, AEDECOD, AESEV, AESEVN, ASTDTM, AWORSTFL) %>% head(10))
Note

Worst Flag Logic The AWORSTFL derivation involves grouping by subject and decoded term, ordering by severity (and potentially date for ties), and then flagging the first record. This is a common pattern in ADaM.

6.0.5.0.1 Final ADAE Dataset Structure

Let’s select and order the final variables for both dplyr and Admiral derived datasets to compare their outputs.

#| label: final_adae_datasets
#| code-fold: false

# Define common final columns for comparison
adae_cols <- c(
  "STUDYID", "USUBJID", "ADVERSE_EVENT_SEQ", "ADJ",
  "ARM", "ACTARM", # Added ARM, ACTARM
  "AETERM", "AEDECOD", "AESEV", "AESEVN", "ATOXGR",
  "ASTDTM", "AENDTM", "ADJSDY", "ADJEDY", "ADJDAY",
  "ONGO", "SER", "REL", "TRTEMFL", "ANLFL", "AWORSTFL" # Added ANLFL, AWORSTFL
)

final_adae_dplyr <- adae_dplyr %>%
  select(all_of(adae_cols)) %>%
  # For dplyr, we need to manually derive AWORSTFL and ANLFL to match Admiral's output for comparison
  mutate(
    ANLFL = "Y",
    AESEVN = as.numeric(AESEVN) # Ensure numeric for worst flag
  ) %>%
  group_by(USUBJID, AEDECOD) %>%
  arrange(desc(AESEVN), ASTDTM, .by_group = TRUE) %>%
  mutate(AWORSTFL = if_else(row_number() == 1, "Y", NA_character_)) %>%
  ungroup() %>%
  # Re-select to ensure order is consistent after adding new columns
  select(all_of(adae_cols))


final_adae_admiral <- adae_admiral %>%
  select(all_of(adae_cols))

cat("Final ADAE (dplyr) - first 5 rows:\n")
print(head(final_adae_dplyr))
cat("\nFinal ADAE (Admiral) - first 5 rows:\n")
print(head(final_adae_admiral))

# Verify if the two datasets are identical (ignoring attribute differences)
# This is a simple check, more robust comparison might be needed for real data
all_equal_check <- all.equal(
  final_adae_dplyr %>% arrange(USUBJID, ADVERSE_EVENT_SEQ),
  final_adae_admiral %>% arrange(USUBJID, ADVERSE_EVENT_SEQ)
)
cat("\nAre the final ADAE datasets from dplyr and Admiral identical (ignoring order/attributes)? ", all_equal_check, "\n")

Comparison of dplyr and Admiral Approaches Both dplyr and Admiral can successfully derive an ADAE dataset, but they offer different advantages:

Note

dplyr Advantages Flexibility: Provides granular control over every step of the derivation, ideal for highly custom or non-standard derivations.

General Purpose: Part of the tidyverse, making it familiar to a wide range of R users for general data manipulation tasks.

Direct Control: You write the exact logic for each transformation, which can be beneficial for understanding every detail.

Tip

Admiral Advantages Standardization: Designed specifically for ADaM, ensuring derivations adhere to CDISC ADaM Implementation Guide (IG) rules and best practices.

Reproducibility: Encapsulates complex ADaM logic into well-tested functions, reducing the likelihood of errors and promoting consistency across studies.

Efficiency: Functions like derive_vars_ady, derive_var_trt_emerg, derive_vars_merged, and derive_flag_value handle multiple sub-steps (e.g., date parsing, handling missing values, applying buffers, merging) in a single call, making code more concise.

Readability (for ADaM developers): Once familiar with Admiral functions, the code becomes highly readable and self-documenting for ADaM-specific tasks.

Conclusion

The choice between dplyr and Admiral for ADaM derivation often depends on the project’s specific needs, team’s familiarity, and the complexity of the derivations.

For simple, one-off derivations or when extreme customization is required, dplyr offers unmatched flexibility.

For standardized, reproducible, and compliant ADaM dataset generation in a regulated environment, Admiral is the preferred choice due to its built-in CDISC logic and robust functions.

Ideally, a combination of both can be used: dplyr for initial data cleaning and reshaping, and Admiral for the core ADaM-specific derivations