library(arframe)
library(pharmaverseadam)
library(dplyr, warn.conflicts = FALSE)
library(tidyr)
# Chemistry panel — 4 hepatic/renal parameters
param_subset <- c("ALT", "AST", "BILI", "CREAT")
adlb_saf <- pharmaverseadam::adlb |>
blank_to_na() |>
filter(
SAFFL == "Y",
TRT01A != "Screen Failure",
PARCAT1 == "CHEMISTRY",
PARAMCD %in% param_subset
)
adsl_saf <- pharmaverseadam::adsl |>
blank_to_na() |>
filter(SAFFL == "Y", TRT01A != "Screen Failure")
arm_levels <- c("Placebo", "Xanomeline Low Dose", "Xanomeline High Dose")
arm_n <- adsl_saf |> count(TRT01A) |> pull(n, name = TRT01A)
arm_n <- arm_n[arm_levels]
# Reference categories as factors (ensures 0-count levels appear)
bnrind_levels <- c("LOW", "NORMAL", "HIGH")
anrind_levels <- c("Low", "Normal", "High")
adlb_saf <- adlb_saf |>
mutate(
BNRIND = factor(BNRIND, levels = bnrind_levels),
ANRIND = factor(ANRIND, levels = c("LOW", "NORMAL", "HIGH"))
)Laboratory Shift Table
Shift from Baseline to Worst Post-Baseline
Setup
See Prerequisites for installation instructions.
Data Preparation
# ── Baseline reference indicator (one record per subject per param) ──
baseline <- adlb_saf |>
filter(ABLFL == "Y") |>
select(USUBJID, PARAMCD, PARAM, TRT01A, BNRIND)
# ── Worst post-baseline: rank LOW/HIGH above NORMAL, pick most extreme ──
# Severity rank: HIGH=2, LOW=2, NORMAL=1 (tie-break: take HIGH over LOW)
post_bl <- adlb_saf |>
filter(is.na(ABLFL) | ABLFL != "Y", !is.na(ANRIND), ANRIND != "") |>
mutate(
anrind_rank = case_when(
ANRIND == "HIGH" ~ 3L,
ANRIND == "LOW" ~ 2L,
ANRIND == "NORMAL" ~ 1L,
TRUE ~ 0L
)
) |>
group_by(USUBJID, PARAMCD) |>
slice_max(anrind_rank, n = 1, with_ties = FALSE) |>
ungroup() |>
select(USUBJID, PARAMCD, worst_ANRIND = ANRIND)
# ── Join baseline indicator to worst post-baseline ──
shift_subj <- baseline |>
inner_join(post_bl, by = c("USUBJID", "PARAMCD")) |>
filter(!is.na(BNRIND), BNRIND != "", BNRIND %in% c("LOW", "NORMAL", "HIGH"))
# ── n (%) helper — denominator is subjects with that baseline category per arm ──
n_pct <- function(n, denom) {
ifelse(denom == 0, "0 (0.0)", sprintf("%d (%.1f)", n, n / denom * 100))
}
# ── Cross-tabulate BNRIND x worst_ANRIND per arm, then pivot wide ──
# Column naming: <arm_no_spaces>_<post_category>
shift_counts <- shift_subj |>
count(PARAMCD, PARAM, TRT01A, BNRIND, worst_ANRIND) |>
filter(TRT01A %in% arm_levels)
# Denominators: subjects with each baseline category per arm per param
denom_df <- shift_subj |>
filter(TRT01A %in% arm_levels) |>
count(PARAMCD, TRT01A, BNRIND, name = "denom")
shift_pct <- shift_counts |>
left_join(denom_df, by = c("PARAMCD", "TRT01A", "BNRIND")) |>
mutate(
value = n_pct(n, denom),
col_key = paste0(gsub(" ", "_", TRT01A), "_", worst_ANRIND)
) |>
select(PARAMCD, PARAM, BNRIND, col_key, value) |>
pivot_wider(names_from = col_key, values_from = value)
# ── Ensure all arm×post-category columns exist ──
all_col_keys <- unlist(lapply(arm_levels, function(a) {
paste0(gsub(" ", "_", a), "_", c("LOW", "NORMAL", "HIGH"))
}))
for (col in all_col_keys) {
if (!col %in% names(shift_pct)) shift_pct[[col]] <- NA_character_
}
# ── Final layout: one row per PARAM × BNRIND ──
shift_wide <- shift_pct |>
mutate(
BNRIND = factor(BNRIND, levels = c("LOW", "NORMAL", "HIGH")),
PARAMCD = factor(PARAMCD, levels = param_subset)
) |>
arrange(PARAMCD, BNRIND) |>
mutate(
BNRIND = as.character(BNRIND),
PARAMCD = as.character(PARAMCD),
bnrind_label = paste0(BNRIND, " (Baseline)")
) |>
mutate(across(all_of(all_col_keys), ~ replace_na(.x, "0 (0.0)"))) |>
select(PARAMCD, PARAM, bnrind_label, all_of(all_col_keys))The shift table cross-tabulates a baseline reference indicator against the worst post-baseline reference indicator. This is a derived analysis not directly supported by cards ARD functions, so the dplyr path is the primary approach for this table type.
# ── Same derivation logic, expressed more compactly ──
baseline_c <- adlb_saf |>
filter(ABLFL == "Y", !is.na(BNRIND), BNRIND != "") |>
select(USUBJID, PARAMCD, PARAM, TRT01A, BNRIND)
worst_post_c <- adlb_saf |>
filter(is.na(ABLFL) | ABLFL != "Y", !is.na(ANRIND), ANRIND != "") |>
mutate(
anrind_rank = case_when(
ANRIND == "HIGH" ~ 3L,
ANRIND == "LOW" ~ 2L,
ANRIND == "NORMAL" ~ 1L,
TRUE ~ 0L
)
) |>
group_by(USUBJID, PARAMCD) |>
slice_max(anrind_rank, n = 1, with_ties = FALSE) |>
ungroup() |>
select(USUBJID, PARAMCD, worst_ANRIND = ANRIND)
shift_cards_raw <- baseline_c |>
inner_join(worst_post_c, by = c("USUBJID", "PARAMCD")) |>
filter(TRT01A %in% arm_levels, BNRIND %in% c("LOW", "NORMAL", "HIGH")) |>
count(PARAMCD, PARAM, TRT01A, BNRIND, worst_ANRIND) |>
left_join(
baseline_c |>
filter(TRT01A %in% arm_levels, BNRIND %in% c("LOW", "NORMAL", "HIGH")) |>
count(PARAMCD, TRT01A, BNRIND, name = "denom"),
by = c("PARAMCD", "TRT01A", "BNRIND")
) |>
mutate(
value = ifelse(denom == 0, "0 (0.0)", sprintf("%d (%.1f)", n, n / denom * 100)),
col_key = paste0(gsub(" ", "_", TRT01A), "_", worst_ANRIND)
) |>
select(PARAMCD, PARAM, BNRIND, col_key, value) |>
pivot_wider(names_from = col_key, values_from = value)
# Ensure all columns exist and final layout
for (col in all_col_keys) {
if (!col %in% names(shift_cards_raw)) shift_cards_raw[[col]] <- NA_character_
}
shift_cards <- shift_cards_raw |>
mutate(
BNRIND = factor(BNRIND, levels = c("LOW", "NORMAL", "HIGH")),
PARAMCD = factor(PARAMCD, levels = param_subset)
) |>
arrange(PARAMCD, BNRIND) |>
mutate(
BNRIND = as.character(BNRIND),
PARAMCD = as.character(PARAMCD),
bnrind_label = paste0(BNRIND, " (Baseline)")
) |>
mutate(across(all_of(all_col_keys), ~ replace_na(.x, "0 (0.0)"))) |>
select(PARAMCD, PARAM, bnrind_label, all_of(all_col_keys))arframe Pipeline
The rendered table below uses the dplyr data prep (shift_wide). The cards tab produces an equivalent shift_cards — swap it in to use the cards path instead.
Each row is a baseline reference category. Columns are treatment arm × post-baseline reference category, with arm-level spans. One page per parameter:
# Column specs: arm × post-baseline category
shift_cols <- unlist(lapply(arm_levels, function(a) {
setNames(
lapply(c("LOW", "NORMAL", "HIGH"), function(cat) {
fr_col(tools::toTitleCase(tolower(cat)), align = "decimal")
}),
paste0(gsub(" ", "_", a), "_", c("LOW", "NORMAL", "HIGH"))
)
}), recursive = FALSE)
shift_wide |>
fr_table() |>
fr_titles(
"Table 14.3.6",
"Laboratory Shift Table — Shift from Baseline to Worst Post-Baseline",
"Safety Population"
) |>
fr_cols(
PARAMCD = fr_col(visible = FALSE),
PARAM = fr_col(visible = FALSE),
bnrind_label = fr_col("Baseline\nReference Range", width = 1.8),
!!!shift_cols,
.n = arm_n
) |>
fr_spans(
!!!setNames(
lapply(arm_levels, function(a) {
paste0(gsub(" ", "_", a), "_", c("LOW", "NORMAL", "HIGH"))
}),
arm_levels
)
) |>
fr_header(bold = TRUE, align = "center") |>
fr_rows(page_by = "PARAM") |>
fr_footnotes(
"Worst post-baseline is the most extreme reference range indicator (High or Low) recorded after baseline; Normal if no abnormal values observed.",
"Denominator for percentages is the number of subjects with the specified baseline reference range category.",
"Reference range indicators derived from laboratory normal ranges (ANRIND, BNRIND).",
"Safety Population: all randomised subjects who received at least one dose."
)Rendered Table
Table 14.3.6
Laboratory Shift Table — Shift from Baseline to Worst Post-Baseline
Safety Population
Alanine Aminotransferase (U/L)
| Placebo (N=86) | Xanomeline Low Dose (N=96) | Xanomeline High Dose (N=72) | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| Baseline Reference Range | Low | Normal | High | Low | Normal | High | Low | Normal | High | ||
| LOW (Baseline) | 0 | 0 | 0 | 0 | 1 (100.0) | 0 | 0 | 0 | 0 | ||
| NORMAL (Baseline) | 1 (1.2) | 73 ( 91.2) | 6 ( 7.5) | 5 (5.6) | 75 ( 84.3) | 9 ( 10.1) | 0 | 61 (89.7) | 7 ( 10.3) | ||
| HIGH (Baseline) | 0 | 1 ( 25.0) | 3 ( 75.0) | 0 | 0 | 3 (100.0) | 0 | 0 | 4 (100.0) | ||
Worst post-baseline is the most extreme reference range indicator (High or Low) recorded after baseline; Normal if no abnormal values observed.
Denominator for percentages is the number of subjects with the specified baseline reference range category.
Reference range indicators derived from laboratory normal ranges (ANRIND, BNRIND).
Safety Population: all randomised subjects who received at least one dose.
/opt/quarto/share/rmd/rmd.R
01APR2026 09:54:05
Table 14.3.6
Laboratory Shift Table — Shift from Baseline to Worst Post-Baseline
Safety Population
Aspartate Aminotransferase (U/L)
| Placebo (N=86) | Xanomeline Low Dose (N=96) | Xanomeline High Dose (N=72) | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| Baseline Reference Range | Low | Normal | High | Low | Normal | High | Low | Normal | High | ||
| NORMAL (Baseline) | 0 | 70 ( 89.7) | 8 ( 10.3) | 0 | 76 ( 89.4) | 9 ( 10.6) | 1 ( 1.4) | 63 (91.3) | 5 ( 7.2) | ||
| HIGH (Baseline) | 0 | 2 ( 33.3) | 4 ( 66.7) | 0 | 3 ( 37.5) | 5 ( 62.5) | 0 | 1 (33.3) | 2 ( 66.7) | ||
Worst post-baseline is the most extreme reference range indicator (High or Low) recorded after baseline; Normal if no abnormal values observed.
Denominator for percentages is the number of subjects with the specified baseline reference range category.
Reference range indicators derived from laboratory normal ranges (ANRIND, BNRIND).
Safety Population: all randomised subjects who received at least one dose.
/opt/quarto/share/rmd/rmd.R
01APR2026 09:54:05
Table 14.3.6
Laboratory Shift Table — Shift from Baseline to Worst Post-Baseline
Safety Population
Bilirubin (umol/L)
| Placebo (N=86) | Xanomeline Low Dose (N=96) | Xanomeline High Dose (N=72) | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| Baseline Reference Range | Low | Normal | High | Low | Normal | High | Low | Normal | High | ||
| NORMAL (Baseline) | 0 | 78 ( 95.1) | 4 ( 4.9) | 0 | 88 ( 98.9) | 1 ( 1.1) | 0 | 66 (95.7) | 3 ( 4.3) | ||
| HIGH (Baseline) | 0 | 0 | 2 (100.0) | 0 | 2 ( 66.7) | 1 ( 33.3) | 0 | 0 | 3 (100.0) | ||
Worst post-baseline is the most extreme reference range indicator (High or Low) recorded after baseline; Normal if no abnormal values observed.
Denominator for percentages is the number of subjects with the specified baseline reference range category.
Reference range indicators derived from laboratory normal ranges (ANRIND, BNRIND).
Safety Population: all randomised subjects who received at least one dose.
/opt/quarto/share/rmd/rmd.R
01APR2026 09:54:05
Table 14.3.6
Laboratory Shift Table — Shift from Baseline to Worst Post-Baseline
Safety Population
Creatinine (umol/L)
| Placebo (N=86) | Xanomeline Low Dose (N=96) | Xanomeline High Dose (N=72) | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| Baseline Reference Range | Low | Normal | High | Low | Normal | High | Low | Normal | High | ||
| LOW (Baseline) | 0 | 2 (100.0) | 0 | 0 | 0 | 0 | 1 (100.0) | 0 | 0 | ||
| NORMAL (Baseline) | 1 (1.2) | 72 ( 88.9) | 8 ( 9.9) | 1 (1.1) | 82 ( 93.2) | 5 ( 5.7) | 2 ( 3.0) | 56 (84.8) | 8 ( 12.1) | ||
| HIGH (Baseline) | 0 | 0 | 1 (100.0) | 0 | 0 | 5 (100.0) | 0 | 0 | 5 (100.0) | ||
Worst post-baseline is the most extreme reference range indicator (High or Low) recorded after baseline; Normal if no abnormal values observed.
Denominator for percentages is the number of subjects with the specified baseline reference range category.
Reference range indicators derived from laboratory normal ranges (ANRIND, BNRIND).
Safety Population: all randomised subjects who received at least one dose.
/opt/quarto/share/rmd/rmd.R
01APR2026 09:54:05