arframe TFL Gallery
  1. Tables
  2. Overall Safety Summary
  • Getting Started
    • Installation

  • Tables
    • Study Conduct
    • Enrollment by Country and Site

    • Study Population
    • Demographics Summary
    • Medical History
    • Prior Medication
    • Disposition Summary
    • Analysis Populations

    • Extent of Exposure
    • Concomitant Medications
    • Extent of Exposure

    • Safety
    • Adverse Events by System Organ Class and Preferred Term
    • AEs Related to Study Drug
    • Common Adverse Events
    • Adverse Events by Grade / Intensity
    • Overall Safety Summary
    • Adverse Events with Event Counts
    • Exposure-Adjusted Adverse Events
    • Adverse Events by Subgroup
    • Serious Adverse Events by SOC and PT
    • AEs Leading to Study Drug Discontinuation
    • Death Summary
    • Vital Signs
    • Laboratory Results - Chemistry
    • Laboratory Shift Table
    • Laboratory Worst Toxicity Grade
    • Laboratory Marked Abnormalities
    • Electrocardiogram Summary

    • Efficacy
    • Time to Event Summary
    • Best Overall Response

  • Listings
    • Adverse Event Listing
    • Demographic Characteristics Listing
    • Medical History Listing
    • Vital Signs Listing
    • Laboratory Test Results Listing
    • Concomitant Medications Listing

  • Figures
    • Kaplan-Meier Plot
    • Swimmer Plot
    • Waterfall Plot

On this page

  • Setup
  • Data Preparation
  • arframe Pipeline
  • Rendered Table
  1. Tables
  2. Overall Safety Summary

Overall Safety Summary

Overview of Adverse Events

Setup

See Prerequisites for installation instructions.

library(arframe)
library(pharmaverseadam)
library(dplyr, warn.conflicts = FALSE)
library(tidyr)
library(cards)

adsl_saf <- pharmaverseadam::adsl |>
  blank_to_na() |>
  filter(SAFFL == "Y", TRT01A != "Screen Failure")

adae <- pharmaverseadam::adae |>
  blank_to_na() |>
  filter(SAFFL == "Y", TRTEMFL == "Y")

arm_levels <- c("Placebo", "Xanomeline Low Dose", "Xanomeline High Dose")
arm_n <- setNames(
  vapply(arm_levels, function(a) sum(adsl_saf$TRT01A == a), integer(1L)),
  arm_levels
)
N_total <- nrow(adsl_saf)
n_vec   <- c(arm_n, Total = N_total)

Data Preparation

  • dplyr
  • cards
# ── Helper: count unique subjects with a condition, per arm + Total ──
ae_flag_row <- function(ae_data, adsl_data, condition, row_label,
                        arm_levels, N_total) {
  condition <- rlang::enquo(condition)

  flag_subjs <- ae_data |>
    filter(!!condition) |>
    distinct(USUBJID, TRT01A)

  by_arm <- flag_subjs |>
    count(TRT01A) |>
    complete(TRT01A = arm_levels, fill = list(n = 0L)) |>
    left_join(
      adsl_data |> count(TRT01A, name = "N"),
      by = "TRT01A"
    ) |>
    mutate(pct = sprintf("%d (%.1f)", n, n / N * 100)) |>
    select(TRT01A, pct) |>
    pivot_wider(names_from = TRT01A, values_from = pct)

  total_n <- n_distinct(flag_subjs$USUBJID)
  by_arm |>
    mutate(
      category   = "",
      stat_label = row_label,
      Total      = sprintf("%d (%.1f)", total_n, total_n / N_total * 100),
      .before    = 1
    )
}

# ── Severity helper: count by max severity per subject ──
severity_rows <- function(ae_data, adsl_data, arm_levels, N_total) {
  sev_order <- c("MILD", "MODERATE", "SEVERE")

  max_sev <- ae_data |>
    mutate(sev_n = match(AESEV, sev_order)) |>
    group_by(USUBJID, TRT01A) |>
    summarise(max_sev = sev_order[max(sev_n, na.rm = TRUE)], .groups = "drop")

  lapply(sev_order, function(s) {
    subjs <- max_sev |> filter(max_sev == s) |> distinct(USUBJID, TRT01A)

    by_arm <- subjs |>
      count(TRT01A) |>
      complete(TRT01A = arm_levels, fill = list(n = 0L)) |>
      left_join(
        adsl_data |> count(TRT01A, name = "N"),
        by = "TRT01A"
      ) |>
      mutate(pct = sprintf("%d (%.1f)", n, n / N * 100)) |>
      select(TRT01A, pct) |>
      pivot_wider(names_from = TRT01A, values_from = pct)

    total_n <- n_distinct(subjs$USUBJID)
    by_arm |>
      mutate(
        category   = "Maximum Severity",
        stat_label = tools::toTitleCase(tolower(s)),
        Total      = sprintf("%d (%.1f)", total_n, total_n / N_total * 100),
        .before    = 1
      )
  }) |> bind_rows()
}

# ── Build all rows matching FDA Table 6 ──
ae_wide <- bind_rows(
  ae_flag_row(adae, adsl_saf, TRUE,
              "Any TEAE", arm_levels, N_total),
  ae_flag_row(adae, adsl_saf, AESER == "Y",
              "Any Serious AE (SAE)", arm_levels, N_total),
  ae_flag_row(adae, adsl_saf, AESDTH == "Y",
              "Any AE Leading to Death", arm_levels, N_total),
  ae_flag_row(adae, adsl_saf, AEREL %in% c("POSSIBLE", "PROBABLE"),
              "Any AE Related to Study Drug", arm_levels, N_total),
  severity_rows(adae, adsl_saf, arm_levels, N_total)
) |>
  mutate(across(
    all_of(c(arm_levels, "Total")),
    ~ replace_na(.x, "0 (0.0)")
  ))
# Build one row per subject with dichotomous AE flags
ae_subj <- adsl_saf |>
  select(USUBJID, TRT01A) |>
  left_join(
    adae |>
      group_by(USUBJID) |>
      summarise(
        any_teae    = any(TRTEMFL == "Y", na.rm = TRUE),
        any_sae     = any(AESER == "Y", na.rm = TRUE),
        any_death   = any(AESDTH == "Y", na.rm = TRUE),
        any_related = any(AEREL %in% c("POSSIBLE", "PROBABLE"), na.rm = TRUE),
        max_sev     = if (all(is.na(AESEV))) NA_character_
                      else c("MILD", "MODERATE", "SEVERE")[
                        max(match(AESEV, c("MILD", "MODERATE", "SEVERE")),
                            na.rm = TRUE)],
        .groups = "drop"
      ),
    by = "USUBJID"
  ) |>
  mutate(
    across(c(any_teae, any_sae, any_death, any_related),
           ~ replace_na(.x, FALSE)),
    max_sev = factor(max_sev, levels = c("MILD", "MODERATE", "SEVERE"))
  )

ae_ard <- ard_stack(
  data = ae_subj,
  .by  = "TRT01A",
  ard_dichotomous(
    variables = c(any_teae, any_sae, any_death, any_related),
    value     = list(
      any_teae    = TRUE,
      any_sae     = TRUE,
      any_death   = TRUE,
      any_related = TRUE
    )
  ),
  ard_categorical(variables = "max_sev"),
  .overall = TRUE
)

ae_wide_cards <- fr_wide_ard(
  ae_ard,
  statistic = list(
    dichotomous = "{n} ({p}%)",
    categorical = "{n} ({p}%)"
  ),
  decimals = c(p = 1),
  label = c(
    any_teae    = "Any TEAE",
    any_sae     = "Any Serious AE (SAE)",
    any_death   = "Any AE Leading to Death",
    any_related = "Any AE Related to Study Drug",
    max_sev     = "Maximum Severity"
  )
)

arframe Pipeline

The rendered table below uses the dplyr data prep (ae_wide). The cards tab produces an equivalent ae_wide_cards — swap it in to use the cards path instead.

ae_wide |>
  fr_table() |>
  fr_titles(
    "Table 14.3.1",
    "Overview of Adverse Events",
    "Safety Population"
  ) |>
  fr_cols(
    category   = fr_col(visible = FALSE),
    stat_label = fr_col("Event", width = 3.2),
    !!!setNames(
      lapply(arm_levels, function(a) fr_col(a, align = "decimal")),
      arm_levels
    ),
    Total = fr_col("Total", align = "decimal"),
    .n = n_vec
  ) |>
  fr_header(bold = TRUE, align = "center") |>
  fr_rows(
    group_by    = list(cols = "category", label = "stat_label"),
    blank_after = "category",
    group_style = list(bold = TRUE)
  ) |>
  fr_styles(
    fr_row_style(rows = 1L, bold = TRUE)
  ) |>
  fr_footnotes(
    "TEAE = Treatment-Emergent Adverse Event (onset on or after first dose of study drug).",
    "Related = Investigator-assessed relationship of Possible or Probable.",
    "Maximum severity = worst severity across all TEAEs per subject.",
    "Subjects counted once per category. Percentages based on N per treatment group.",
    "CDISCPILOT01 Safety Population."
  )

Rendered Table

Table 14.3.1
Overview of Adverse Events
Safety Population
EventTotal
(N=254)
Placebo
(N=86)
Xanomeline High Dose
(N=72)
Xanomeline Low Dose
(N=96)
Any TEAE217 (85.4)65 (75.6)68 (94.4)84 (87.5)
Any Serious AE (SAE) 3 ( 1.2) 0 1 ( 1.4) 2 ( 2.1)
Any AE Leading to Death 3 ( 1.2) 2 ( 2.3) 0 1 ( 1.0)
Any AE Related to Study Drug184 (72.4)43 (50.0)64 (88.9)77 (80.2)
Maximum Severity
Mild 77 (30.3)36 (41.9)20 (27.8)21 (21.9)
Moderate111 (43.7)24 (27.9)40 (55.6)47 (49.0)
Severe 29 (11.4) 5 ( 5.8) 8 (11.1)16 (16.7)
TEAE = Treatment-Emergent Adverse Event (onset on or after first dose of study drug).
Related = Investigator-assessed relationship of Possible or Probable.
Maximum severity = worst severity across all TEAEs per subject.
Subjects counted once per category. Percentages based on N per treatment group.
CDISCPILOT01 Safety Population.
/opt/quarto/share/rmd/rmd.R 01APR2026 09:52:11
Source Code
---
title: "Overall Safety Summary"
subtitle: "Overview of Adverse Events"
execute:
  echo: true
  eval: true
---


```{r}
#| label: prereqs
#| include: false
library(arframe)
fr_theme(hlines = "header", font_family = "Courier New")

blank_to_na <- function(df) {
  df[] <- lapply(df, function(x) {
    if (is.character(x)) x[x == ""] <- NA_character_
    x
  })
  df
}
```

## Setup

See [Prerequisites](../install.qmd) for installation instructions.

```{r}
#| label: setup
library(arframe)
library(pharmaverseadam)
library(dplyr, warn.conflicts = FALSE)
library(tidyr)
library(cards)

adsl_saf <- pharmaverseadam::adsl |>
  blank_to_na() |>
  filter(SAFFL == "Y", TRT01A != "Screen Failure")

adae <- pharmaverseadam::adae |>
  blank_to_na() |>
  filter(SAFFL == "Y", TRTEMFL == "Y")

arm_levels <- c("Placebo", "Xanomeline Low Dose", "Xanomeline High Dose")
arm_n <- setNames(
  vapply(arm_levels, function(a) sum(adsl_saf$TRT01A == a), integer(1L)),
  arm_levels
)
N_total <- nrow(adsl_saf)
n_vec   <- c(arm_n, Total = N_total)
```


## Data Preparation

::: {.panel-tabset}

### dplyr

```{r}
#| label: dplyr-code

# ── Helper: count unique subjects with a condition, per arm + Total ──
ae_flag_row <- function(ae_data, adsl_data, condition, row_label,
                        arm_levels, N_total) {
  condition <- rlang::enquo(condition)

  flag_subjs <- ae_data |>
    filter(!!condition) |>
    distinct(USUBJID, TRT01A)

  by_arm <- flag_subjs |>
    count(TRT01A) |>
    complete(TRT01A = arm_levels, fill = list(n = 0L)) |>
    left_join(
      adsl_data |> count(TRT01A, name = "N"),
      by = "TRT01A"
    ) |>
    mutate(pct = sprintf("%d (%.1f)", n, n / N * 100)) |>
    select(TRT01A, pct) |>
    pivot_wider(names_from = TRT01A, values_from = pct)

  total_n <- n_distinct(flag_subjs$USUBJID)
  by_arm |>
    mutate(
      category   = "",
      stat_label = row_label,
      Total      = sprintf("%d (%.1f)", total_n, total_n / N_total * 100),
      .before    = 1
    )
}

# ── Severity helper: count by max severity per subject ──
severity_rows <- function(ae_data, adsl_data, arm_levels, N_total) {
  sev_order <- c("MILD", "MODERATE", "SEVERE")

  max_sev <- ae_data |>
    mutate(sev_n = match(AESEV, sev_order)) |>
    group_by(USUBJID, TRT01A) |>
    summarise(max_sev = sev_order[max(sev_n, na.rm = TRUE)], .groups = "drop")

  lapply(sev_order, function(s) {
    subjs <- max_sev |> filter(max_sev == s) |> distinct(USUBJID, TRT01A)

    by_arm <- subjs |>
      count(TRT01A) |>
      complete(TRT01A = arm_levels, fill = list(n = 0L)) |>
      left_join(
        adsl_data |> count(TRT01A, name = "N"),
        by = "TRT01A"
      ) |>
      mutate(pct = sprintf("%d (%.1f)", n, n / N * 100)) |>
      select(TRT01A, pct) |>
      pivot_wider(names_from = TRT01A, values_from = pct)

    total_n <- n_distinct(subjs$USUBJID)
    by_arm |>
      mutate(
        category   = "Maximum Severity",
        stat_label = tools::toTitleCase(tolower(s)),
        Total      = sprintf("%d (%.1f)", total_n, total_n / N_total * 100),
        .before    = 1
      )
  }) |> bind_rows()
}

# ── Build all rows matching FDA Table 6 ──
ae_wide <- bind_rows(
  ae_flag_row(adae, adsl_saf, TRUE,
              "Any TEAE", arm_levels, N_total),
  ae_flag_row(adae, adsl_saf, AESER == "Y",
              "Any Serious AE (SAE)", arm_levels, N_total),
  ae_flag_row(adae, adsl_saf, AESDTH == "Y",
              "Any AE Leading to Death", arm_levels, N_total),
  ae_flag_row(adae, adsl_saf, AEREL %in% c("POSSIBLE", "PROBABLE"),
              "Any AE Related to Study Drug", arm_levels, N_total),
  severity_rows(adae, adsl_saf, arm_levels, N_total)
) |>
  mutate(across(
    all_of(c(arm_levels, "Total")),
    ~ replace_na(.x, "0 (0.0)")
  ))
```

### cards

```{r}
#| label: cards-code

# Build one row per subject with dichotomous AE flags
ae_subj <- adsl_saf |>
  select(USUBJID, TRT01A) |>
  left_join(
    adae |>
      group_by(USUBJID) |>
      summarise(
        any_teae    = any(TRTEMFL == "Y", na.rm = TRUE),
        any_sae     = any(AESER == "Y", na.rm = TRUE),
        any_death   = any(AESDTH == "Y", na.rm = TRUE),
        any_related = any(AEREL %in% c("POSSIBLE", "PROBABLE"), na.rm = TRUE),
        max_sev     = if (all(is.na(AESEV))) NA_character_
                      else c("MILD", "MODERATE", "SEVERE")[
                        max(match(AESEV, c("MILD", "MODERATE", "SEVERE")),
                            na.rm = TRUE)],
        .groups = "drop"
      ),
    by = "USUBJID"
  ) |>
  mutate(
    across(c(any_teae, any_sae, any_death, any_related),
           ~ replace_na(.x, FALSE)),
    max_sev = factor(max_sev, levels = c("MILD", "MODERATE", "SEVERE"))
  )

ae_ard <- ard_stack(
  data = ae_subj,
  .by  = "TRT01A",
  ard_dichotomous(
    variables = c(any_teae, any_sae, any_death, any_related),
    value     = list(
      any_teae    = TRUE,
      any_sae     = TRUE,
      any_death   = TRUE,
      any_related = TRUE
    )
  ),
  ard_categorical(variables = "max_sev"),
  .overall = TRUE
)

ae_wide_cards <- fr_wide_ard(
  ae_ard,
  statistic = list(
    dichotomous = "{n} ({p}%)",
    categorical = "{n} ({p}%)"
  ),
  decimals = c(p = 1),
  label = c(
    any_teae    = "Any TEAE",
    any_sae     = "Any Serious AE (SAE)",
    any_death   = "Any AE Leading to Death",
    any_related = "Any AE Related to Study Drug",
    max_sev     = "Maximum Severity"
  )
)
```

:::


## arframe Pipeline

The rendered table below uses the **dplyr** data prep (`ae_wide`). The cards tab produces an equivalent `ae_wide_cards` — swap it in to use the cards path instead.

```{r}
#| label: pipeline
#| eval: false
ae_wide |>
  fr_table() |>
  fr_titles(
    "Table 14.3.1",
    "Overview of Adverse Events",
    "Safety Population"
  ) |>
  fr_cols(
    category   = fr_col(visible = FALSE),
    stat_label = fr_col("Event", width = 3.2),
    !!!setNames(
      lapply(arm_levels, function(a) fr_col(a, align = "decimal")),
      arm_levels
    ),
    Total = fr_col("Total", align = "decimal"),
    .n = n_vec
  ) |>
  fr_header(bold = TRUE, align = "center") |>
  fr_rows(
    group_by    = list(cols = "category", label = "stat_label"),
    blank_after = "category",
    group_style = list(bold = TRUE)
  ) |>
  fr_styles(
    fr_row_style(rows = 1L, bold = TRUE)
  ) |>
  fr_footnotes(
    "TEAE = Treatment-Emergent Adverse Event (onset on or after first dose of study drug).",
    "Related = Investigator-assessed relationship of Possible or Probable.",
    "Maximum severity = worst severity across all TEAEs per subject.",
    "Subjects counted once per category. Percentages based on N per treatment group.",
    "CDISCPILOT01 Safety Population."
  )
```

## Rendered Table

```{r}
#| label: table
#| echo: false
#| ref.label: pipeline
```

Open-source TFL reference collection

 

CDISC Pilot Study (CDISCPILOT01) • pharmaverseadam datasets