arframe TFL Gallery
  1. Tables
  2. Serious Adverse Events by SOC and PT
  • 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. Serious Adverse Events by SOC and PT

Serious Adverse Events by SOC and PT

SAEs by System Organ Class and Preferred Term

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_sae <- pharmaverseadam::adae |>
  blank_to_na() |>
  filter(SAFFL == "Y", TRTEMFL == "Y", AESER == "Y")

arm_levels <- c("Placebo", "Xanomeline Low Dose", "Xanomeline High Dose")
arm_n <- adsl_saf |>
  filter(TRT01A %in% arm_levels) |>
  count(TRT01A) |>
  pull(n, name = TRT01A)
arm_n <- arm_n[arm_levels]
N_total <- nrow(adsl_saf)
n_vec <- c(arm_n, Total = N_total)

Data Preparation

  • dplyr
  • cards
n_pct <- function(n, denom) sprintf("%d (%.1f)", n, n / denom * 100)

# ── Any SAE row ──
any_sae_arm <- adae_sae |>
  distinct(USUBJID, TRT01A) |>
  count(TRT01A) |>
  filter(TRT01A %in% arm_levels) |>
  mutate(value = mapply(n_pct, n, arm_n[TRT01A])) |>
  select(TRT01A, value) |>
  pivot_wider(names_from = TRT01A, values_from = value)

any_sae_total <- n_distinct(adae_sae$USUBJID)

any_sae_row <- bind_cols(
  tibble(soc = "ANY SAE", pt = "ANY SAE", row_type = "overall"),
  any_sae_arm,
  tibble(Total = n_pct(any_sae_total, N_total))
)

# ── SOC-level ──
soc_arm <- adae_sae |>
  distinct(USUBJID, TRT01A, AEBODSYS) |>
  count(TRT01A, AEBODSYS) |>
  filter(TRT01A %in% arm_levels) |>
  mutate(value = mapply(n_pct, n, arm_n[TRT01A])) |>
  select(TRT01A, AEBODSYS, value) |>
  pivot_wider(names_from = TRT01A, values_from = value)

soc_total <- adae_sae |>
  distinct(USUBJID, AEBODSYS) |>
  count(AEBODSYS) |>
  mutate(Total = n_pct(n, N_total)) |>
  select(AEBODSYS, Total)

soc_wide <- left_join(soc_arm, soc_total, by = "AEBODSYS") |>
  mutate(soc = AEBODSYS, pt = AEBODSYS, row_type = "soc", .before = 1) |>
  select(-AEBODSYS)

# ── PT-level ──
pt_arm <- adae_sae |>
  distinct(USUBJID, TRT01A, AEBODSYS, AEDECOD) |>
  count(TRT01A, AEBODSYS, AEDECOD) |>
  filter(TRT01A %in% arm_levels) |>
  mutate(value = mapply(n_pct, n, arm_n[TRT01A])) |>
  select(TRT01A, AEBODSYS, AEDECOD, value) |>
  pivot_wider(names_from = TRT01A, values_from = value)

pt_total <- adae_sae |>
  distinct(USUBJID, AEBODSYS, AEDECOD) |>
  count(AEBODSYS, AEDECOD) |>
  mutate(Total = n_pct(n, N_total)) |>
  select(AEBODSYS, AEDECOD, Total)

pt_wide <- left_join(pt_arm, pt_total, by = c("AEBODSYS", "AEDECOD")) |>
  mutate(soc = AEBODSYS, pt = AEDECOD, row_type = "pt", .before = 1) |>
  select(-AEBODSYS, -AEDECOD)

# ── Sort by frequency and interleave ──
soc_order <- adae_sae |>
  distinct(USUBJID, AEBODSYS) |>
  count(AEBODSYS, name = "soc_n") |>
  arrange(desc(soc_n)) |>
  pull(AEBODSYS)

sae_wide <- bind_rows(
  any_sae_row,
  bind_rows(lapply(soc_order, function(s) {
    bind_rows(
      filter(soc_wide, soc == s),
      filter(pt_wide, soc == s) |>
        left_join(
          adae_sae |> distinct(USUBJID, AEBODSYS, AEDECOD) |>
            count(AEBODSYS, AEDECOD, name = "pt_n"),
          by = c("soc" = "AEBODSYS", "pt" = "AEDECOD")
        ) |>
        arrange(desc(pt_n)) |>
        select(-pt_n)
    )
  }))
) |>
  mutate(across(where(is.character) & !c(soc, pt, row_type),
                ~ replace_na(.x, "0 (0.0)")))

# Ensure all arm columns exist (some arms may have zero SAEs)
for (a in arm_levels) {
  if (!a %in% names(sae_wide)) sae_wide[[a]] <- "0 (0.0)"
}
sae_ard <- ard_stack_hierarchical(
  data        = adae_sae,
  variables   = c(AEBODSYS, AEDECOD),
  by          = TRT01A,
  denominator = adsl_saf,
  id          = USUBJID,
  overall     = TRUE,
  over_variables = TRUE
) |>
  sort_ard_hierarchical(sort = "descending")

sae_wide_cards <- fr_wide_ard(
  sae_ard,
  statistic = "{n} ({p}%)",
  decimals  = c(p = 1),
  label     = c(
    "..ard_hierarchical_overall.." = "ANY SAE",
    AEBODSYS = "System Organ Class",
    AEDECOD  = "Preferred Term"
  )
)

arframe Pipeline

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

sae_wide |>
  fr_table() |>
  fr_titles(
    "Table 14.3.2",
    "Patients With Serious Adverse Events by System Organ Class and Preferred Term",
    "Safety Population"
  ) |>
  fr_cols(
    soc      = fr_col(visible = FALSE),
    pt       = fr_col("System Organ Class\n  Preferred Term", width = 3.5),
    row_type = fr_col(visible = FALSE),
    !!!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  = "soc",
    indent_by = "pt"
  ) |>
  fr_styles(
    fr_row_style(rows = fr_rows_matches("row_type", value = "soc"), bold = TRUE),
    fr_row_style(rows = fr_rows_matches("row_type", value = "overall"), bold = TRUE)
  ) |>
  fr_footnotes(
    "SAE = Serious Adverse Event (any AE that results in death, is life-threatening, requires hospitalization, etc.).",
    "Subjects counted once per SOC and once per PT.",
    "Percentages based on N per treatment arm (Safety Population).",
    "SOC sorted by descending frequency; PT sorted within SOC by descending frequency."
  )

Rendered Table

Table 14.3.2
Patients With Serious Adverse Events by System Organ Class and Preferred Term
Safety Population
System Organ Class
Preferred Term
Xanomeline High Dose
(N=72)
Xanomeline Low Dose
(N=96)
Total
(N=254)
Placebo
(N=86)
ANY SAE1 (1.4)2 (2.1)3 (1.2)0
NERVOUS SYSTEM DISORDERS1 (1.4)2 (2.1)3 (1.2)0
SYNCOPE0 2 (2.1)2 (0.8)0
PARTIAL SEIZURES WITH SECONDARY GENERALISATION1 (1.4)0 1 (0.4)0
SAE = Serious Adverse Event (any AE that results in death, is life-threatening, requires hospitalization, etc.).
Subjects counted once per SOC and once per PT.
Percentages based on N per treatment arm (Safety Population).
SOC sorted by descending frequency; PT sorted within SOC by descending frequency.
/opt/quarto/share/rmd/rmd.R 01APR2026 09:52:21
Source Code
---
title: "Serious Adverse Events by SOC and PT"
subtitle: "SAEs by System Organ Class and Preferred Term"
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_sae <- pharmaverseadam::adae |>
  blank_to_na() |>
  filter(SAFFL == "Y", TRTEMFL == "Y", AESER == "Y")

arm_levels <- c("Placebo", "Xanomeline Low Dose", "Xanomeline High Dose")
arm_n <- adsl_saf |>
  filter(TRT01A %in% arm_levels) |>
  count(TRT01A) |>
  pull(n, name = TRT01A)
arm_n <- arm_n[arm_levels]
N_total <- nrow(adsl_saf)
n_vec <- c(arm_n, Total = N_total)
```


## Data Preparation

::: {.panel-tabset}

### dplyr

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

n_pct <- function(n, denom) sprintf("%d (%.1f)", n, n / denom * 100)

# ── Any SAE row ──
any_sae_arm <- adae_sae |>
  distinct(USUBJID, TRT01A) |>
  count(TRT01A) |>
  filter(TRT01A %in% arm_levels) |>
  mutate(value = mapply(n_pct, n, arm_n[TRT01A])) |>
  select(TRT01A, value) |>
  pivot_wider(names_from = TRT01A, values_from = value)

any_sae_total <- n_distinct(adae_sae$USUBJID)

any_sae_row <- bind_cols(
  tibble(soc = "ANY SAE", pt = "ANY SAE", row_type = "overall"),
  any_sae_arm,
  tibble(Total = n_pct(any_sae_total, N_total))
)

# ── SOC-level ──
soc_arm <- adae_sae |>
  distinct(USUBJID, TRT01A, AEBODSYS) |>
  count(TRT01A, AEBODSYS) |>
  filter(TRT01A %in% arm_levels) |>
  mutate(value = mapply(n_pct, n, arm_n[TRT01A])) |>
  select(TRT01A, AEBODSYS, value) |>
  pivot_wider(names_from = TRT01A, values_from = value)

soc_total <- adae_sae |>
  distinct(USUBJID, AEBODSYS) |>
  count(AEBODSYS) |>
  mutate(Total = n_pct(n, N_total)) |>
  select(AEBODSYS, Total)

soc_wide <- left_join(soc_arm, soc_total, by = "AEBODSYS") |>
  mutate(soc = AEBODSYS, pt = AEBODSYS, row_type = "soc", .before = 1) |>
  select(-AEBODSYS)

# ── PT-level ──
pt_arm <- adae_sae |>
  distinct(USUBJID, TRT01A, AEBODSYS, AEDECOD) |>
  count(TRT01A, AEBODSYS, AEDECOD) |>
  filter(TRT01A %in% arm_levels) |>
  mutate(value = mapply(n_pct, n, arm_n[TRT01A])) |>
  select(TRT01A, AEBODSYS, AEDECOD, value) |>
  pivot_wider(names_from = TRT01A, values_from = value)

pt_total <- adae_sae |>
  distinct(USUBJID, AEBODSYS, AEDECOD) |>
  count(AEBODSYS, AEDECOD) |>
  mutate(Total = n_pct(n, N_total)) |>
  select(AEBODSYS, AEDECOD, Total)

pt_wide <- left_join(pt_arm, pt_total, by = c("AEBODSYS", "AEDECOD")) |>
  mutate(soc = AEBODSYS, pt = AEDECOD, row_type = "pt", .before = 1) |>
  select(-AEBODSYS, -AEDECOD)

# ── Sort by frequency and interleave ──
soc_order <- adae_sae |>
  distinct(USUBJID, AEBODSYS) |>
  count(AEBODSYS, name = "soc_n") |>
  arrange(desc(soc_n)) |>
  pull(AEBODSYS)

sae_wide <- bind_rows(
  any_sae_row,
  bind_rows(lapply(soc_order, function(s) {
    bind_rows(
      filter(soc_wide, soc == s),
      filter(pt_wide, soc == s) |>
        left_join(
          adae_sae |> distinct(USUBJID, AEBODSYS, AEDECOD) |>
            count(AEBODSYS, AEDECOD, name = "pt_n"),
          by = c("soc" = "AEBODSYS", "pt" = "AEDECOD")
        ) |>
        arrange(desc(pt_n)) |>
        select(-pt_n)
    )
  }))
) |>
  mutate(across(where(is.character) & !c(soc, pt, row_type),
                ~ replace_na(.x, "0 (0.0)")))

# Ensure all arm columns exist (some arms may have zero SAEs)
for (a in arm_levels) {
  if (!a %in% names(sae_wide)) sae_wide[[a]] <- "0 (0.0)"
}
```

### cards

```{r}
#| label: cards-code
sae_ard <- ard_stack_hierarchical(
  data        = adae_sae,
  variables   = c(AEBODSYS, AEDECOD),
  by          = TRT01A,
  denominator = adsl_saf,
  id          = USUBJID,
  overall     = TRUE,
  over_variables = TRUE
) |>
  sort_ard_hierarchical(sort = "descending")

sae_wide_cards <- fr_wide_ard(
  sae_ard,
  statistic = "{n} ({p}%)",
  decimals  = c(p = 1),
  label     = c(
    "..ard_hierarchical_overall.." = "ANY SAE",
    AEBODSYS = "System Organ Class",
    AEDECOD  = "Preferred Term"
  )
)
```

:::


## arframe Pipeline

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

```{r}
#| label: pipeline
#| eval: false
sae_wide |>
  fr_table() |>
  fr_titles(
    "Table 14.3.2",
    "Patients With Serious Adverse Events by System Organ Class and Preferred Term",
    "Safety Population"
  ) |>
  fr_cols(
    soc      = fr_col(visible = FALSE),
    pt       = fr_col("System Organ Class\n  Preferred Term", width = 3.5),
    row_type = fr_col(visible = FALSE),
    !!!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  = "soc",
    indent_by = "pt"
  ) |>
  fr_styles(
    fr_row_style(rows = fr_rows_matches("row_type", value = "soc"), bold = TRUE),
    fr_row_style(rows = fr_rows_matches("row_type", value = "overall"), bold = TRUE)
  ) |>
  fr_footnotes(
    "SAE = Serious Adverse Event (any AE that results in death, is life-threatening, requires hospitalization, etc.).",
    "Subjects counted once per SOC and once per PT.",
    "Percentages based on N per treatment arm (Safety Population).",
    "SOC sorted by descending frequency; PT sorted within SOC by descending frequency."
  )
```


## Rendered Table

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

Open-source TFL reference collection

 

CDISC Pilot Study (CDISCPILOT01) • pharmaverseadam datasets