Metadata-driven wrappers over admiral for the three derivations that get rewritten as study-specific code on every study:

Function Does Replaces the SAS
grade_lab() Lab toxicity grades against published NCI-CTCAE / DAIDS criteria %LABGRADE / %CTCAE nested IF/ELSE cascade
collapse_ae_episodes() Collapses AE records into episodes by interval merging %AE_EPISODE RETAIN _prev_end DATA step
derive_pop_flags() Population flags from a rules table %POPFLAG IF trtsdt NE . THEN saffl = 'Y';

Supporting exports: grading_table() and pop_rules() validate the metadata and print it; sas_note() prints the SAS idiom each derivation replaces.

Every function returns a plain tibble, so the result hands straight back to admiral, rtables or xportr. No study-specific parameter, threshold or flag name is hardcoded anywhere in the package.

Install

# from source
install.packages("admiralease", repos = NULL, type = "source")

Worked example

Runs as-is: the inputs are the fixtures shipped with the package.

library(admiralease)
ex <- function(f) system.file("extdata", f, package = "admiralease")

# 1. Lab toxicity grading -----------------------------------------------
# Default: NCI-CTCAE v5 *laboratory* criteria (admiral::atoxgr_criteria_ctcv5).
# ATOXDSCL/ATOXDSCH must already hold the criteria term for each record.
adlb <- utils::read.csv(ex("adlb_example.csv"))

graded <- grade_lab(adlb)
graded[, c("PARAMCD", "AVAL", "ATOXGRL", "ATOXGRH", "ATOXGR")]
#>    PARAMCD  AVAL ATOXGRL ATOXGRH ATOXGR
#> 1      ALT  20.0      NA       0      0
#> 2      ALT 120.0      NA       2      2
#> 3      ALT    NA      NA      NA     NA   <- missing stays missing, not "0"
#> 4    POTAS   5.4       0       0      0   <- exactly on the threshold: not graded
#> 5    POTAS   5.8       0       2      2
#> 6    POTAS   3.2       2       0     -2   <- low toxicity carried as negative
#> 7    POTAS   1.9       4       0     -4
#> 8   SODIUM 128.0       3       0     -3
#> 9      HGB   6.0      NA      NA     NA   <- mmol/L, criterion checks g/L
#> 10    PLAT 400.0       0      NA      0
#> 11    PLAT  60.0       2      NA     -2
#> 12    BILI  40.0      NA       2      2
#> 13   CREAT 130.0      NA       2      2
#> 14     ALB  30.0       1      NA     -1
#> 15    HGBC   9.0      NA      NA     NA
#> 16    PHOS   0.7      NA      NA     NA
#> 17   ANISO   1.0      NA      NA     NA   <- term not in the table: NA, not "0"

# The criteria sets differ, and they differ where the standards differ.
data.frame(
  PARAMCD = adlb$PARAMCD,
  ctcv4 = grade_lab(adlb, "ctcv4")$ATOXGR,
  ctcv5 = grade_lab(adlb, "ctcv5")$ATOXGR,
  uscv  = grade_lab(adlb, "ctcv5_uscv")$ATOXGR
)[c(14, 15, 16), ]
#>    PARAMCD ctcv4 ctcv5 uscv
#> 14     ALB    -1    -1   NA   <- g/L: no US-conventional criterion matches
#> 15    HGBC    NA    NA   -2   <- g/dL: only the US-conventional table grades it
#> 16    PHOS    -2    NA   NA   <- Hypophosphatemia was dropped in CTCAE v5

# 2. AE episodes ---------------------------------------------------------
adae <- utils::read.csv(ex("adae_example.csv"))

collapse_ae_episodes(adae, gap_days = 1)
#> # A tibble: 7 x 9
#>   USUBJID  AEDECOD   EPISODE ASTDT      AENDT      AESEV    AEREL    NRECORD ONGOING
#>   AE01-001 ERYTHEMA        1 2021-01-05 2021-01-15 MODERATE POSSIBLE       2 FALSE
#>   AE01-001 HEADACHE        1 2021-03-01 2021-03-02 MILD     NONE           1 FALSE
#>   AE01-001 HEADACHE        2 2021-03-10 2021-03-12 SEVERE   PROBABLE       1 FALSE
#>   AE01-002 DIARRHOEA       1 2021-04-01 NA         SEVERE   PROBABLE       2 TRUE
#>   AE01-002 NAUSEA          1 2021-02-01 2021-02-06 MODERATE POSSIBLE       2 FALSE
#>   AE01-003 DIZZINESS       1 NA         2021-06-05 MODERATE POSSIBLE       1 FALSE
#>   AE01-003 FATIGUE         1 2021-05-01 2021-05-25 SEVERE   PROBABLE       3 FALSE

# 3. Population flags ----------------------------------------------------
adsl <- utils::read.csv(ex("adsl_example.csv"))
rules <- utils::read.csv(ex("pop_rules.csv"))

derive_pop_flags(adsl, rules)$SAFFL
#> [1] "Y" "Y" "Y" "Y" "N" "Y" "N" "Y"

Scope and limits of the toxicity grading

Read this before grading anything you will report.

  • The criteria are real, and they are laboratory criteria only. grade_lab() grades with the tables admiral ships as package data — atoxgr_criteria_ctcv5 (default), atoxgr_criteria_ctcv4, atoxgr_criteria_ctcv5_uscv, atoxgr_criteria_daids — by calling admiral::derive_var_atoxgr_dir(). This package does not implement any published toxicity standard itself.
  • This is not the full CTCAE term set. "ctcv5" covers 40 terms across 3 SOCs (Blood and lymphatic system disorders, Investigations, Metabolism and nutrition disorders). Non-laboratory CTCAE terms — nausea, fatigue, rash and the rest — are not covered and cannot be graded by this package. A term that is not in the table returns NA, never "0".
  • You must supply the term. ATOXDSCL / ATOXDSCH must already hold the criteria term for each record; this package does not guess terms from PARAMCD. Merge your own lookup on first, as admiral’s ADLB template does with admiral::derive_vars_merged(). Terms are matched by exact string, so the "daids" table needs DAIDS term names ("ALT, High"), not CTCAE ones.
  • Units are checked exactly. A result in a unit the chosen table does not list grades to NA, not "0". Use "ctcv5" for SI units and "ctcv5_uscv" for US conventional units.
  • demo_grading_table.csv is a demo. The threshold fixture in inst/extdata is in pharmaverseadam units. It is not NCI-CTCAE and must never be used to grade a study. It exists so the threshold path has a test fixture. Sponsor threshold tables are still fully supported — they simply carry no published standard of their own.
  • Not an independent validation of admiral. This package does not check admiral’s transcription of CTCAE or DAIDS against the source documents.

What the edge cases do

  • Grading. A value exactly on a threshold does not qualify (> THRESHOLD high, < THRESHOLD low), for both published criteria and sponsor thresholds. Missing results, terms absent from the table, and results whose unit the table does not cover are all NA, never "0". ATOXGR carries low toxicity as a negative grade, the admiral::derive_var_atoxgr() convention. The input row order and variable labels are returned intact — admiral::derive_var_atoxgr_dir() regroups rows by term and drops labels.
  • AE episodes. Merging compares each onset against the running maximum end date of the episode, not the previous record’s end date, which is the bug hiding in most RETAIN implementations. A missing end date means ongoing and absorbs later records; a missing onset date is kept as its own episode and reported.
  • Population flags. An unevaluable condition is an error naming the flag. A condition that evaluates to NA (e.g. AGE >= 65 with a missing age) sets "N" and reports the count.

Real data

With pharmaverseadam installed:

episodes <- collapse_ae_episodes(pharmaverseadam::adae)
c(records = nrow(pharmaverseadam::adae), episodes = nrow(episodes))
#>  records episodes
#>     1191      867

# pharmaverseadam::adlb is built by admiral's ADLB template with CTCAE v4.03,
# so "ctcv4" must reproduce its own ATOXGR exactly.
same <- function(a, b) (is.na(a) & is.na(b)) | (!is.na(a) & !is.na(b) & a == b)
mean(same(grade_lab(pharmaverseadam::adlb, "ctcv4")$ATOXGR,
          pharmaverseadam::adlb$ATOXGR))
#> [1] 1

Measured agreement with that dataset’s own ATOXGR, over all 83,652 rows (45,077 of which carry a CTCAE term):

grading all rows rows both sides grade
"ctcv4" 100.00% 100.00% (n = 42,553)
"ctcv5" 92.72% 97.10% (n = 37,549)
"ctcv5_uscv" 74.78% 95.17% (n = 22,548)
"daids" 49.13% — (n = 0)
demo_grading_table.csv 75.00% 96.56% (n = 25,003)

"ctcv4" is the like-for-like comparison and it is exact. The "ctcv5" shortfall is the standards genuinely disagreeing: 2,512 Hypophosphatemia and 2,492 Hyperglycemia records are graded by v4.03 and were dropped from v5, and 749 Creatinine increased records changed criterion — 5,753 of the 6,093 disagreements. "ctcv5_uscv" is lower because that dataset reports SI units, and "daids" grades nothing on it because DAIDS uses its own term names. The demo table’s 96.56% is the number 0.0.0.9000 reported as 96.6%; it measures a demo fixture against CTCAE v4.03 and means nothing about correctness.

License

MIT (c) Bhanoji Duppada