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)AEs Leading to Study Drug Discontinuation
Treatment-Emergent AEs Leading to Discontinuation by SOC and PT
Setup
See Prerequisites for installation instructions.
Data Preparation
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 DISCONTINUATION | 1 (1.4) | 2 (2.1) | 3 (1.2) | 0 |
| NERVOUS SYSTEM DISORDERS | 1 (1.4) | 2 (2.1) | 3 (1.2) | 0 |
| SYNCOPE | 0 | 2 (2.1) | 2 (0.8) | 0 |
| PARTIAL SEIZURES WITH SECONDARY GENERALISATION | 1 (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