8  Specific AE

Following ICH E3 guidance, we need to summarize number of participants for each specific AE in Section 12.2, Adverse Events (AEs).

library(haven) # Read SAS data
library(dplyr) # Manipulate data
library(tidyr) # Manipulate data
library(r2rtf) # Reporting in RTF format

In this chapter, we illustrate how to summarize simplified specific AE information for a study.

The data used to summarize AE information is in adsl and adae datasets.

adsl <- read_sas("data-adam/adsl.sas7bdat")
adae <- read_sas("data-adam/adae.sas7bdat")

For illustration purpose, we only provide counts in the simplified table. The percentage of participants for each AE can be calculated as shown in Chapter 7.

Here, we focus on the analysis script for two advanced features for a table layout.

In the code below, we count the number of participants in each AE term by SOC and treatment arm, and we create a new variable order and set it as 0. The variable order can help with the data manipulation later.

fmt_num <- function(x, digits, width = digits + 4) {
  formatC(
    x,
    digits = digits,
    format = "f",
    width = width
  )
}
ana <- adae %>%
  mutate(
    AESOC = tools::toTitleCase(tolower(AESOC)),
    AEDECOD = tools::toTitleCase(tolower(AEDECOD))
  )

t1 <- ana %>%
  group_by(TRTAN, AESOC) %>%
  summarise(n = fmt_num(n_distinct(USUBJID), digits = 0)) %>%
  mutate(AEDECOD = AESOC, order = 0)

t1
#> # A tibble: 61 × 5
#> # Groups:   TRTAN [3]
#>    TRTAN AESOC                                               n     AEDECOD order
#>    <dbl> <chr>                                               <chr> <chr>   <dbl>
#>  1     0 Cardiac Disorders                                   "  1… Cardia…     0
#>  2     0 Ear and Labyrinth Disorders                         "   … Ear an…     0
#>  3     0 Eye Disorders                                       "   … Eye Di…     0
#>  4     0 Gastrointestinal Disorders                          "  1… Gastro…     0
#>  5     0 General Disorders and Administration Site Conditio… "  2… Genera…     0
#>  6     0 Hepatobiliary Disorders                             "   … Hepato…     0
#>  7     0 Infections and Infestations                         "  1… Infect…     0
#>  8     0 Injury, Poisoning and Procedural Complications      "   … Injury…     0
#>  9     0 Investigations                                      "  1… Invest…     0
#> 10     0 Metabolism and Nutrition Disorders                  "   … Metabo…     0
#> # ℹ 51 more rows

In the code below, we count the number of subjects in each AE term by SOC, AE term, and treatment arm. Here we also create a new variable order and set it as 1.

t2 <- ana %>%
  group_by(TRTAN, AESOC, AEDECOD) %>%
  summarise(n = fmt_num(n_distinct(USUBJID), digits = 0)) %>%
  mutate(order = 1)

t2
#> # A tibble: 373 × 5
#> # Groups:   TRTAN, AESOC [61]
#>    TRTAN AESOC             AEDECOD                              n      order
#>    <dbl> <chr>             <chr>                                <chr>  <dbl>
#>  1     0 Cardiac Disorders Atrial Fibrillation                  "   1"     1
#>  2     0 Cardiac Disorders Atrial Hypertrophy                   "   1"     1
#>  3     0 Cardiac Disorders Atrioventricular Block First Degree  "   1"     1
#>  4     0 Cardiac Disorders Atrioventricular Block Second Degree "   2"     1
#>  5     0 Cardiac Disorders Bradycardia                          "   1"     1
#>  6     0 Cardiac Disorders Bundle Branch Block Left             "   1"     1
#>  7     0 Cardiac Disorders Bundle Branch Block Right            "   1"     1
#>  8     0 Cardiac Disorders Cardiac Failure Congestive           "   1"     1
#>  9     0 Cardiac Disorders Myocardial Infarction                "   4"     1
#> 10     0 Cardiac Disorders Sinus Arrhythmia                     "   1"     1
#> # ℹ 363 more rows

We prepare reporting data for AE information using code below:

t_ae <- bind_rows(t1, t2) %>%
  pivot_wider(
    id_cols = c(AESOC, order, AEDECOD),
    names_from = TRTAN,
    names_prefix = "n_",
    values_from = n,
    values_fill = fmt_num(0, digits = 0)
  ) %>%
  arrange(AESOC, order, AEDECOD) %>%
  select(AESOC, AEDECOD, starts_with("n"))

t_ae
#> # A tibble: 265 × 5
#>   AESOC             AEDECOD             n_0    n_54   n_81  
#>   <chr>             <chr>               <chr>  <chr>  <chr> 
#> 1 Cardiac Disorders Cardiac Disorders   "  13" "  13" "  18"
#> 2 Cardiac Disorders Atrial Fibrillation "   1" "   1" "   3"
#> 3 Cardiac Disorders Atrial Flutter      "   0" "   1" "   1"
#> 4 Cardiac Disorders Atrial Hypertrophy  "   1" "   0" "   0"
#> # ℹ 261 more rows

We prepare reporting data for analysis population using code below:

count_by <- function(data, # Input data set
                     grp, # Group variable
                     var, # Analysis variable
                     var_label = var, # Analysis variable label
                     id = "USUBJID") { # Subject ID variable
  data <- data %>% rename(grp = !!grp, var = !!var, id = !!id)

  left_join(
    count(data, grp, var),
    count(data, grp, name = "tot"),
    by = "grp",
  ) %>%
    mutate(
      pct = fmt_num(100 * n / tot, digits = 1),
      n = fmt_num(n, digits = 0),
      npct = paste0(n, " (", pct, ")")
    ) %>%
    pivot_wider(
      id_cols = var,
      names_from = grp,
      values_from = c(n, pct, npct),
      values_fill = list(n = "0", pct = fmt_num(0, digits = 0))
    ) %>%
    mutate(var_label = var_label)
}
t_pop <- adsl %>%
  filter(SAFFL == "Y") %>%
  count_by("TRT01AN", "SAFFL",
    var_label = "Participants in population"
  ) %>%
  mutate(
    AESOC = "pop",
    AEDECOD = var_label
  ) %>%
  select(AESOC, AEDECOD, starts_with("n_"))

t_pop
#> # A tibble: 1 × 5
#>   AESOC AEDECOD                    n_0    n_54   n_81  
#>   <chr> <chr>                      <chr>  <chr>  <chr> 
#> 1 pop   Participants in population "  86" "  84" "  84"

The final report data is saved in tbl_ae_spec. We also add a blank row between population and AE information in the reporting table.

tbl_ae_spec <- bind_rows(
  t_pop,
  data.frame(AESOC = "pop"),
  t_ae
) %>%
  mutate(AEDECOD = ifelse(AEDECOD == AESOC,
    AEDECOD, paste0("  ", AEDECOD)
  ))

tbl_ae_spec
#> # A tibble: 267 × 5
#>   AESOC             AEDECOD                        n_0    n_54   n_81  
#>   <chr>             <chr>                          <chr>  <chr>  <chr> 
#> 1 pop               "  Participants in population" "  86" "  84" "  84"
#> 2 pop                <NA>                           <NA>   <NA>   <NA> 
#> 3 Cardiac Disorders "Cardiac Disorders"            "  13" "  13" "  18"
#> 4 Cardiac Disorders "  Atrial Fibrillation"        "   1" "   1" "   3"
#> # ℹ 263 more rows

We define the format of the output as below:

To obtain the nested layout, we use the page_by argument in the rtf_body function. By defining page_by="AESOC", r2rtf recognizes the variable as a group indicator.

After setting pageby_row = "first_row", the first row is displayed as group header. If a group of information is broken into multiple pages, the group header row is repeated on each page by default.

We can also customize the text format by providing a matrix that has the same dimension as the input dataset (i.e., tbl_ae_spec). In the code below, we illustrate how to display bold text for group headers to highlight the nested structure of the table layout.

n_row <- nrow(tbl_ae_spec)
n_col <- ncol(tbl_ae_spec)
id <- tbl_ae_spec$AESOC == tbl_ae_spec$AEDECOD
id <- ifelse(is.na(id), FALSE, id)

text_format <- ifelse(id, "b", "")
tbl_ae_spec %>%
  rtf_title(
    "Analysis of Participants With Specific Adverse Events",
    "(Safety Analysis Population)"
  ) %>%
  rtf_colheader(" | Placebo | Xanomeline Low Dose| Xanomeline High Dose",
    col_rel_width = c(3, rep(1, 3))
  ) %>%
  rtf_colheader(" | n |  n | n ",
    border_top = "",
    border_bottom = "single",
    col_rel_width = c(3, rep(1, 3))
  ) %>%
  rtf_body(
    col_rel_width = c(1, 3, rep(1, 3)),
    text_justification = c("l", "l", rep("c", 3)),
    text_format = matrix(text_format, nrow = n_row, ncol = n_col),
    page_by = "AESOC",
    pageby_row = "first_row"
  ) %>%
  rtf_footnote("Every subject is counted a single time for each applicable row and column.") %>%
  rtf_encode() %>%
  write_rtf("tlf/tlf_spec_ae.rtf")

More discussion on page_by, group_by and subline_by features can be found on the r2rtf package website.

The procedure to generate a baseline characteristics table can be summarized as follows: