arframe TFL Gallery
  1. Tables
  2. Laboratory Marked Abnormalities
  • 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. Laboratory Marked Abnormalities

Laboratory Marked Abnormalities

Subjects with Notable Laboratory Values

Setup

See Prerequisites for installation instructions.

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)

Data Preparation

  • dplyr
  • cards
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)
Low35 (13.8)17 (19.8) 5 ( 6.9)13 (13.5)
ALKPH
High20 ( 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
High30 (11.8) 9 (10.5)11 (15.3)10 (10.4)
Low 6 ( 2.4) 1 ( 1.2) 0 5 ( 5.2)
AST
High29 (11.4)12 (14.0) 7 ( 9.7)10 (10.4)
BILI
High13 ( 5.1) 6 ( 7.0) 5 ( 6.9) 2 ( 2.1)
BUN
High41 (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)
Low28 (11.0)10 (11.6)10 (13.9) 8 ( 8.3)
CHOLES
High13 ( 5.1) 5 ( 5.8) 4 ( 5.6) 4 ( 4.2)
Low15 ( 5.9) 5 ( 5.8) 5 ( 6.9) 5 ( 5.2)
CK
High42 (16.5)18 (20.9)13 (18.1)11 (11.5)
Low 1 ( 0.4) 1 ( 1.2) 0 0
CL
High19 ( 7.5)11 (12.8) 3 ( 4.2) 5 ( 5.2)
CREAT
High31 (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
High24 ( 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
High16 ( 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
High27 (10.6)10 (11.6)11 (15.3) 6 ( 6.2)
Low12 ( 4.7) 4 ( 4.7) 5 ( 6.9) 3 ( 3.1)
URATE
High15 ( 5.9) 4 ( 4.7) 7 ( 9.7) 4 ( 4.2)
Low12 ( 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
Source Code
---
title: "Laboratory Marked Abnormalities"
subtitle: "Subjects with Notable Laboratory Values"
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)

# 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)
```


## Data Preparation

::: {.panel-tabset}

### dplyr

```{r}
#| label: dplyr-code

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

### cards

```{r}
#| label: cards-code

# 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.

```{r}
#| label: pipeline
#| eval: false
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

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

Open-source TFL reference collection

 

CDISC Pilot Study (CDISCPILOT01) • pharmaverseadam datasets