library(arframe)
library(pharmaverseadam)
library(dplyr, warn.conflicts = FALSE)
library(tidyr)
library(cards)
# Safety population denominators
adsl_saf <- pharmaverseadam::adsl |>
blank_to_na() |>
filter(SAFFL == "Y", TRT01A != "Screen Failure")
# Post-baseline chemistry records (ANL01FL = "Y")
adlb_chem <- pharmaverseadam::adlb |>
blank_to_na() |>
filter(
SAFFL == "Y",
TRT01A != "Screen Failure",
PARCAT1 == "CHEMISTRY",
ANL01FL == "Y"
) |>
mutate(ANRIND = factor(ANRIND, levels = c("LOW", "NORMAL", "HIGH")))
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)Laboratory Marked Abnormalities
Subjects with Notable Laboratory Values
Setup
See Prerequisites for installation instructions.
Data Preparation
n_pct <- function(n, denom) sprintf("%d (%.1f)", n, n / denom * 100)
# ── PARAMCD -> PARAM lookup (1:1 in this dataset) ──
param_lookup <- adlb_chem |> distinct(PARAMCD, PARAM)
# ── Per-subject ever-abnormal flags (any post-baseline record) ──
# Note: drop PARAM here so complete() doesn't cross-join PARAMCD × PARAM
subj_flags <- adlb_chem |>
group_by(USUBJID, TRT01A, PARAMCD) |>
summarise(
ever_high = any(ANRIND == "HIGH", na.rm = TRUE),
ever_low = any(ANRIND == "LOW", na.rm = TRUE),
.groups = "drop"
)
# ── Helper: count subjects with a given flag, per arm + Total ──
count_abnorm <- function(flag_col) {
subj_flags |>
filter(.data[[flag_col]]) |>
distinct(USUBJID, TRT01A, PARAMCD) |>
count(PARAMCD, TRT01A) |>
complete(PARAMCD, TRT01A = arm_levels, fill = list(n = 0L)) |>
mutate(
total_n = vapply(PARAMCD, function(p) {
n_distinct(subj_flags$USUBJID[
subj_flags$PARAMCD == p & subj_flags[[flag_col]]
])
}, integer(1L)),
value = mapply(n_pct, n, arm_n[TRT01A]),
Total = n_pct(total_n, N_total)
) |>
select(PARAMCD, TRT01A, value, Total) |>
pivot_wider(names_from = TRT01A, values_from = value) |>
left_join(param_lookup, by = "PARAMCD")
}
high_wide <- count_abnorm("ever_high") |>
mutate(direction = "High", .before = 1)
low_wide <- count_abnorm("ever_low") |>
mutate(direction = "Low", .before = 1)
# ── Sort: parameters alphabetically, High before Low ──
lab_wide <- bind_rows(high_wide, low_wide) |>
arrange(PARAMCD, direction) |>
mutate(
criteria_label = paste0(PARAMCD, " \u2014 ", direction),
across(all_of(c(arm_levels, "Total")), ~ replace_na(.x, "0 (0.0)"))
)# One wide row per subject with flags for every PARAMCD × High/Low combo
subj_flags_all <- adsl_saf |>
select(USUBJID, TRT01A) |>
left_join(
adlb_chem |>
group_by(USUBJID, PARAMCD) |>
summarise(
ever_high = any(ANRIND == "HIGH", na.rm = TRUE),
ever_low = any(ANRIND == "LOW", na.rm = TRUE),
.groups = "drop"
) |>
pivot_wider(
names_from = PARAMCD,
values_from = c(ever_high, ever_low),
names_glue = "{PARAMCD}_{.value}"
),
by = "USUBJID"
) |>
mutate(across(where(is.logical), ~ replace_na(.x, FALSE)))
flag_cols <- grep("_ever_(high|low)$", names(subj_flags_all), value = TRUE)
flag_values <- setNames(as.list(rep(TRUE, length(flag_cols))), flag_cols)
lab_ard <- ard_stack(
data = subj_flags_all,
.by = "TRT01A",
ard_dichotomous(
variables = all_of(flag_cols),
value = flag_values
),
.overall = TRUE
)
# Build display labels matching PARAMCD — High / Low
flag_labels <- setNames(
vapply(flag_cols, function(fc) {
parts <- strsplit(fc, "_ever_")[[1]]
paste0(parts[1], " \u2014 ", tools::toTitleCase(parts[2]))
}, character(1L)),
flag_cols
)
lab_cards <- fr_wide_ard(
lab_ard,
statistic = list(dichotomous = "{n} ({p}%)"),
decimals = c(p = 1),
label = flag_labels
) |>
rename(criteria_label = variable)arframe Pipeline
The rendered table below uses the dplyr data prep (lab_wide). The cards tab produces an equivalent lab_cards — swap it in to use the cards path instead.
lab_wide |>
fr_table() |>
fr_titles(
"Table 14.3.4",
"Laboratory Marked Abnormalities",
"Subjects with Notable Laboratory Values \u2014 Safety Population"
) |>
fr_cols(
PARAMCD = fr_col(visible = FALSE),
PARAM = fr_col(visible = FALSE),
criteria_label = fr_col(visible = FALSE),
direction = fr_col("", width = 2.8),
!!!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 = "PARAMCD", label = "direction"),
blank_after = "PARAMCD",
group_style = list(bold = TRUE)
) |>
fr_footnotes(
"A subject is counted once per parameter and direction (High/Low) regardless of number of abnormal records.",
"High = post-baseline value above the normal reference range (ANRIND = HIGH).",
"Low = post-baseline value below the normal reference range (ANRIND = LOW).",
"Post-baseline records flagged by ANL01FL = Y. Chemistry parameters (PARCAT1 = CHEMISTRY) only.",
"Percentages based on N per treatment arm (Safety Population)."
)Rendered Table
Table 14.3.4
Laboratory Marked Abnormalities
Subjects with Notable Laboratory Values — Safety Population
| Total (N=254) | Placebo (N=86) | Xanomeline High Dose (N=72) | Xanomeline Low Dose (N=96) | |
|---|---|---|---|---|
| ALB | ||||
| High | 6 ( 2.4) | 4 ( 4.7) | 1 ( 1.4) | 1 ( 1.0) |
| Low | 35 (13.8) | 17 (19.8) | 5 ( 6.9) | 13 (13.5) |
| ALKPH | ||||
| High | 20 ( 7.9) | 8 ( 9.3) | 5 ( 6.9) | 7 ( 7.3) |
| Low | 9 ( 3.5) | 4 ( 4.7) | 3 ( 4.2) | 2 ( 2.1) |
| ALT | ||||
| High | 30 (11.8) | 9 (10.5) | 11 (15.3) | 10 (10.4) |
| Low | 6 ( 2.4) | 1 ( 1.2) | 0 | 5 ( 5.2) |
| AST | ||||
| High | 29 (11.4) | 12 (14.0) | 7 ( 9.7) | 10 (10.4) |
| BILI | ||||
| High | 13 ( 5.1) | 6 ( 7.0) | 5 ( 6.9) | 2 ( 2.1) |
| BUN | ||||
| High | 41 (16.1) | 11 (12.8) | 10 (13.9) | 20 (20.8) |
| CA | ||||
| High | 6 ( 2.4) | 1 ( 1.2) | 2 ( 2.8) | 3 ( 3.1) |
| Low | 28 (11.0) | 10 (11.6) | 10 (13.9) | 8 ( 8.3) |
| CHOLES | ||||
| High | 13 ( 5.1) | 5 ( 5.8) | 4 ( 5.6) | 4 ( 4.2) |
| Low | 15 ( 5.9) | 5 ( 5.8) | 5 ( 6.9) | 5 ( 5.2) |
| CK | ||||
| High | 42 (16.5) | 18 (20.9) | 13 (18.1) | 11 (11.5) |
| Low | 1 ( 0.4) | 1 ( 1.2) | 0 | 0 |
| CL | ||||
| High | 19 ( 7.5) | 11 (12.8) | 3 ( 4.2) | 5 ( 5.2) |
| CREAT | ||||
| High | 31 (12.2) | 9 (10.5) | 13 (18.1) | 9 ( 9.4) |
| Low | 5 ( 2.0) | 1 ( 1.2) | 3 ( 4.2) | 1 ( 1.0) |
| GGT | ||||
| High | 24 ( 9.4) | 7 ( 8.1) | 8 (11.1) | 9 ( 9.4) |
| Low | 9 ( 3.5) | 4 ( 4.7) | 1 ( 1.4) | 4 ( 4.2) |
| GLUC | ||||
| High | 9 ( 3.5) | 2 ( 2.3) | 6 ( 8.3) | 1 ( 1.0) |
| Low | 1 ( 0.4) | 0 | 0 | 1 ( 1.0) |
| PHOS | ||||
| High | 5 ( 2.0) | 2 ( 2.3) | 2 ( 2.8) | 1 ( 1.0) |
| Low | 2 ( 0.8) | 1 ( 1.2) | 1 ( 1.4) | 0 |
| POTAS | ||||
| High | 3 ( 1.2) | 1 ( 1.2) | 2 ( 2.8) | 0 |
| Low | 7 ( 2.8) | 3 ( 3.5) | 2 ( 2.8) | 2 ( 2.1) |
| PROT | ||||
| High | 16 ( 6.3) | 10 (11.6) | 2 ( 2.8) | 4 ( 4.2) |
| Low | 3 ( 1.2) | 1 ( 1.2) | 1 ( 1.4) | 1 ( 1.0) |
| SODIUM | ||||
| High | 27 (10.6) | 10 (11.6) | 11 (15.3) | 6 ( 6.2) |
| Low | 12 ( 4.7) | 4 ( 4.7) | 5 ( 6.9) | 3 ( 3.1) |
| URATE | ||||
| High | 15 ( 5.9) | 4 ( 4.7) | 7 ( 9.7) | 4 ( 4.2) |
| Low | 12 ( 4.7) | 5 ( 5.8) | 4 ( 5.6) | 3 ( 3.1) |
A subject is counted once per parameter and direction (High/Low) regardless of number of abnormal records.
High = post-baseline value above the normal reference range (ANRIND = HIGH).
Low = post-baseline value below the normal reference range (ANRIND = LOW).
Post-baseline records flagged by ANL01FL = Y. Chemistry parameters (PARCAT1 = CHEMISTRY) only.
Percentages based on N per treatment arm (Safety Population).
/opt/quarto/share/rmd/rmd.R
01APR2026 09:53:16