Records for the same subject and preferred term whose date ranges overlap, touch, or are separated by no more than gap_days are collapsed into a single episode carrying the earliest onset, the latest resolution, the worst severity, the worst causality and the number of records collapsed.

collapse_ae_episodes(
  adae,
  gap_days = 1,
  by = c("USUBJID", "AEDECOD"),
  start_var = "ASTDT",
  end_var = "AENDT",
  severity_var = "AESEV",
  causality_var = "AEREL",
  severity_levels = c("MILD", "MODERATE", "SEVERE", "LIFE THREATENING", "FATAL"),
  causality_levels = c("NONE", "NOT RELATED", "UNLIKELY", "REMOTE", "POSSIBLE",
    "PROBABLE", "RELATED", "DEFINITE")
)

Arguments

adae

ADaM adverse event data frame, e.g. ADAE.

gap_days

Maximum number of days between one episode's end and the next record's start for the two to be considered the same episode. 1 (the default) merges records on consecutive days.

by

Character vector of grouping variables. Default c("USUBJID", "AEDECOD").

start_var, end_var

Onset and resolution date columns. Date, POSIXct, ISO date character ("2021-01-05") or numeric study days are accepted. Defaults "ASTDT" / "AENDT".

severity_var, causality_var

Severity and causality columns, or NULL to skip. Defaults "AESEV" / "AEREL".

severity_levels, causality_levels

Character vectors ordering the severity and causality vocabularies from least to most severe.

Value

A tibble::tibble, one row per episode, with the by variables and EPISODE (sequence within the by group), start_var (earliest onset), end_var (latest resolution, NA if any record is ongoing), severity_var, causality_var, NRECORD (records collapsed) and ONGOING (logical). A plain data frame, ready to hand on to admiral.

Interval merging rules

Within each by group records are sorted by start date and merged left to right. A record starts a new episode when

start > max(end of all earlier records in the episode) + gap_days

so with the default gap_days = 1:

  • overlapping (05-10 Jan and 08-15 Jan) merge, episode 05-15 Jan;

  • adjacent (01-03 Feb and 04-06 Feb, i.e. no clear day between) merge;

  • gapped (01-02 Mar and 10-12 Mar) stay separate;

  • open-ended (a record with a missing end date, i.e. still ongoing) absorbs every later record in the group; the episode end is NA and ONGOING is TRUE;

  • gap_days = 0 merges only records that overlap or touch.

Merging uses a running maximum end date, not the previous record's end, so a long record that swallows several short ones is handled correctly.

Records with a missing start date cannot be positioned on the timeline; each is returned as its own single-record episode, sorted last within its group, and a warning reports how many. A record whose end date precedes its start date is treated as ending on its start date.

Worst severity and causality

"Worst" is the value with the highest rank in severity_levels / causality_levels (compared upper-case, whitespace-trimmed). Values not in the level list rank below every listed value and trigger a warning; missing values never win unless every record in the episode is missing.

Replaces this SAS idiom

The %AE_EPISODE / "collapse continuing AEs" DATA step: PROC SORT by USUBJID AEDECOD ASTDT, then a BY-group RETAIN of _prev_end with IF ASTDT - _prev_end > 1 THEN episode + 1; and a second pass for MAX(AESEVN). The RETAIN version is the derivation most often quietly wrong, because it compares against the previous record's end date rather than the running maximum. See sas_note().

See also

Examples

adae <- utils::read.csv(
  system.file("extdata", "adae_example.csv", package = "admiralease")
)
collapse_ae_episodes(adae)
#> ! 1 record with a missing ASTDT cannot be merged; kept as single-record episode.
#> # A tibble: 7 × 9
#>   USUBJID  AEDECOD   EPISODE ASTDT      AENDT      AESEV   AEREL NRECORD ONGOING
#>   <chr>    <chr>       <int> <date>     <date>     <chr>   <chr>   <int> <lgl>  
#> 1 AE01-001 ERYTHEMA        1 2021-01-05 2021-01-15 MODERA… POSS…       2 FALSE  
#> 2 AE01-001 HEADACHE        1 2021-03-01 2021-03-02 MILD    NONE        1 FALSE  
#> 3 AE01-001 HEADACHE        2 2021-03-10 2021-03-12 SEVERE  PROB…       1 FALSE  
#> 4 AE01-002 DIARRHOEA       1 2021-04-01 NA         SEVERE  PROB…       2 TRUE   
#> 5 AE01-002 NAUSEA          1 2021-02-01 2021-02-06 MODERA… POSS…       2 FALSE  
#> 6 AE01-003 DIZZINESS       1 NA         2021-06-05 MODERA… POSS…       1 FALSE  
#> 7 AE01-003 FATIGUE         1 2021-05-01 2021-05-25 SEVERE  PROB…       3 FALSE  

# Only merge records that actually overlap or touch
collapse_ae_episodes(adae, gap_days = 0)
#> ! 1 record with a missing ASTDT cannot be merged; kept as single-record episode.
#> # A tibble: 9 × 9
#>   USUBJID  AEDECOD   EPISODE ASTDT      AENDT      AESEV   AEREL NRECORD ONGOING
#>   <chr>    <chr>       <int> <date>     <date>     <chr>   <chr>   <int> <lgl>  
#> 1 AE01-001 ERYTHEMA        1 2021-01-05 2021-01-15 MODERA… POSS…       2 FALSE  
#> 2 AE01-001 HEADACHE        1 2021-03-01 2021-03-02 MILD    NONE        1 FALSE  
#> 3 AE01-001 HEADACHE        2 2021-03-10 2021-03-12 SEVERE  PROB…       1 FALSE  
#> 4 AE01-002 DIARRHOEA       1 2021-04-01 NA         SEVERE  PROB…       2 TRUE   
#> 5 AE01-002 NAUSEA          1 2021-02-01 2021-02-03 MILD    REMO…       1 FALSE  
#> 6 AE01-002 NAUSEA          2 2021-02-04 2021-02-06 MODERA… POSS…       1 FALSE  
#> 7 AE01-003 DIZZINESS       1 NA         2021-06-05 MODERA… POSS…       1 FALSE  
#> 8 AE01-003 FATIGUE         1 2021-05-01 2021-05-20 MODERA… POSS…       2 FALSE  
#> 9 AE01-003 FATIGUE         2 2021-05-21 2021-05-25 SEVERE  PROB…       1 FALSE  

# Realistic data, if available
if (rlang::is_installed("pharmaverseadam")) {
  ep <- collapse_ae_episodes(pharmaverseadam::adae)
  c(records = nrow(pharmaverseadam::adae), episodes = nrow(ep))
}
#>  records episodes 
#>     1191      867