arframe TFL Gallery
  1. Tables
  2. AEs Leading to Study Drug Discontinuation
  • 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. AEs Leading to Study Drug Discontinuation

AEs Leading to Study Drug Discontinuation

Treatment-Emergent AEs Leading to Discontinuation by SOC and PT

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")

# AEs where outcome was fatal or serious — proxy for discontinuation
# pharmaverseadam AEACN is all NA, so we use AESER as proxy
adae_disc <- 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)

# ── Overall row ──
any_disc_arm <- adae_disc |>
  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_disc_total <- n_distinct(adae_disc$USUBJID)

any_disc_row <- bind_cols(
  tibble(soc = "PATIENTS WITH AT LEAST ONE AE LEADING TO DISCONTINUATION",
         pt = "PATIENTS WITH AT LEAST ONE AE LEADING TO DISCONTINUATION",
         row_type = "overall"),
  any_disc_arm,
  tibble(Total = n_pct(any_disc_total, N_total))
)

# ── SOC-level ──
soc_arm <- adae_disc |>
  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_disc |>
  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_disc |>
  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_disc |>
  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 and interleave ──
soc_order <- adae_disc |>
  distinct(USUBJID, AEBODSYS) |>
  count(AEBODSYS, name = "soc_n") |>
  arrange(desc(soc_n)) |>
  pull(AEBODSYS)

ae_disc_wide <- bind_rows(
  any_disc_row,
  bind_rows(lapply(soc_order, function(s) {
    bind_rows(
      filter(soc_wide, soc == s),
      filter(pt_wide, soc == s) |>
        left_join(
          adae_disc |> 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
for (a in arm_levels) {
  if (!a %in% names(ae_disc_wide)) ae_disc_wide[[a]] <- "0 (0.0)"
}
disc_ard <- ard_stack_hierarchical(
  data        = adae_disc,
  variables   = c(AEBODSYS, AEDECOD),
  by          = TRT01A,
  denominator = adsl_saf,
  id          = USUBJID,
  overall     = TRUE,
  over_variables = TRUE
) |>
  sort_ard_hierarchical(sort = "descending")

ae_disc_cards <- fr_wide_ard(
  disc_ard,
  statistic = "{n} ({p}%)",
  decimals  = c(p = 1),
  label     = c(
    "..ard_hierarchical_overall.." = "PATIENTS WITH AT LEAST ONE AE LEADING TO DISCONTINUATION",
    AEBODSYS = "System Organ Class",
    AEDECOD  = "Preferred Term"
  )
)

arframe Pipeline

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

ae_disc_wide |>
  fr_table() |>
  fr_titles(
    "Table 14.3.2",
    "Patients With Adverse Events Leading to Study Drug Discontinuation",
    "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(
    "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.",
    "Note: AEACN unavailable in this dataset; SAEs used as proxy for discontinuation."
  )

Rendered Table

Table 14.3.2
Patients With Adverse Events Leading to Study Drug Discontinuation
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)
PATIENTS WITH AT LEAST ONE AE LEADING TO DISCONTINUATION1 (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
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.
Note: AEACN unavailable in this dataset; SAEs used as proxy for discontinuation.
/opt/quarto/share/rmd/rmd.R 01APR2026 09:51:50
Source Code
---
title: "AEs Leading to Study Drug Discontinuation"
subtitle: "Treatment-Emergent AEs Leading to Discontinuation by SOC and PT"
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")

# AEs where outcome was fatal or serious — proxy for discontinuation
# pharmaverseadam AEACN is all NA, so we use AESER as proxy
adae_disc <- 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)

# ── Overall row ──
any_disc_arm <- adae_disc |>
  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_disc_total <- n_distinct(adae_disc$USUBJID)

any_disc_row <- bind_cols(
  tibble(soc = "PATIENTS WITH AT LEAST ONE AE LEADING TO DISCONTINUATION",
         pt = "PATIENTS WITH AT LEAST ONE AE LEADING TO DISCONTINUATION",
         row_type = "overall"),
  any_disc_arm,
  tibble(Total = n_pct(any_disc_total, N_total))
)

# ── SOC-level ──
soc_arm <- adae_disc |>
  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_disc |>
  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_disc |>
  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_disc |>
  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 and interleave ──
soc_order <- adae_disc |>
  distinct(USUBJID, AEBODSYS) |>
  count(AEBODSYS, name = "soc_n") |>
  arrange(desc(soc_n)) |>
  pull(AEBODSYS)

ae_disc_wide <- bind_rows(
  any_disc_row,
  bind_rows(lapply(soc_order, function(s) {
    bind_rows(
      filter(soc_wide, soc == s),
      filter(pt_wide, soc == s) |>
        left_join(
          adae_disc |> 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
for (a in arm_levels) {
  if (!a %in% names(ae_disc_wide)) ae_disc_wide[[a]] <- "0 (0.0)"
}
```

### cards

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

ae_disc_cards <- fr_wide_ard(
  disc_ard,
  statistic = "{n} ({p}%)",
  decimals  = c(p = 1),
  label     = c(
    "..ard_hierarchical_overall.." = "PATIENTS WITH AT LEAST ONE AE LEADING TO DISCONTINUATION",
    AEBODSYS = "System Organ Class",
    AEDECOD  = "Preferred Term"
  )
)
```

:::


## arframe Pipeline

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

```{r}
#| label: pipeline
#| eval: false
ae_disc_wide |>
  fr_table() |>
  fr_titles(
    "Table 14.3.2",
    "Patients With Adverse Events Leading to Study Drug Discontinuation",
    "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(
    "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.",
    "Note: AEACN unavailable in this dataset; SAEs used as proxy for discontinuation."
  )
```


## Rendered Table

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

Open-source TFL reference collection

 

CDISC Pilot Study (CDISCPILOT01) • pharmaverseadam datasets