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)Overall Safety Summary
Overview of Adverse Events
Setup
See Prerequisites for installation instructions.
Data Preparation
# ── 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
| Event | Total (N=254) | Placebo (N=86) | Xanomeline High Dose (N=72) | Xanomeline Low Dose (N=96) |
|---|---|---|---|---|
| Any TEAE | 217 (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 Drug | 184 (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) |
| Moderate | 111 (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