30 Days of Pharmaverse
  • Week 1: SDTM Fundamentals
  • Week 2: Production SDTM
  • Week 3: ADaM Deep Dive
  • Week 4: Tables, Listings and Figures
  1. Day 25: gtsummary and tfrmt - ARD-Backed Production Tables
  • Day 22: Demography Table with gtsummary + gt
  • Day 23: ADCM and ADRS - Concomitant Meds and Oncology Response
  • Day 24: ARD-First Reporting with cards and cardx
  • Day 25: gtsummary and tfrmt - ARD-Backed Production Tables
  • Day 26: flextable and officer - Word and RTF Clinical Tables
  • Day 27: rtables, tern, and r2rtf - Structured Clinical Tables
  • Day 28: Tplyr - Declarative Clinical Table Programming
  • Day 29: ggsurvfit + gtsummary - Survival Plots and Clinical Figures
  • Day 30: Capstone - Full Clinical Reporting Workflow

On this page

  • 1 Overview
  • 2 Setup
  • 3 Part 1: gtsummary
    • 3.1 Step 1: Demographics Table
    • 3.2 Step 2: Convert to gt and Add Headers
    • 3.3 Step 3: Extract ARD from gtsummary
    • 3.4 Step 4: Lab Summary Table from ADLB
    • 3.5 Step 5: Logistic Regression Table
    • 3.6 Step 6: Merge Tables with tbl_merge
  • 4 Part 2: tfrmt
    • 4.1 Step 7: Build a tfrmt Format Spec
    • 4.2 Step 8: Prepare BDS Summary Data for tfrmt
    • 4.3 Step 9: Apply tfrmt Format Spec
    • 4.4 Step 10: Mock Table from tfrmt Spec
  • 5 Step 11: Export tfrmt table to HTML
  • 6 Validation Checks
  • 7 Key Takeaways
  • 8 Next Steps
  • 9 Resources

Day 25: gtsummary and tfrmt - ARD-Backed Production Tables

Flexible TLGs from ARD and format-spec driven TLF libraries

Back to Roadmap

1 Overview

Today covers two complementary approaches to clinical table production:

  • gtsummary - builds publication-quality tables directly from ADaM data, and can extract an ARD via gather_ard()
  • tfrmt - separates data preparation from display spec; one format spec applies to many tables

2 Setup

library(gtsummary)
library(tfrmt)
library(pharmaverseadam)
library(dplyr)
library(gt)

# Used explicitly later
library(purrr)
library(tidyr)

# Safety population
adsl_safe <- pharmaverseadam::adsl |>
  dplyr::filter(SAFFL == "Y")

# Lab data for tfrmt section
adlb <- pharmaverseadam::adlb |>
  dplyr::filter(SAFFL == "Y", !is.na(AVAL))

cat("ADSL Safety N:", nrow(adsl_safe), "\n")
ADSL Safety N: 254 
cat("ADLB records:", nrow(adlb), "\n")
ADLB records: 82547 
cat("\nADLB columns (first 30):\n")

ADLB columns (first 30):
print(utils::head(names(adlb), 30))
 [1] "STUDYID"  "DOMAIN"   "USUBJID"  "LBSEQ"    "LBTESTCD" "LBTEST"  
 [7] "LBCAT"    "LBORRES"  "LBORRESU" "LBORNRLO" "LBORNRHI" "LBSTRESC"
[13] "LBSTRESN" "LBSTRESU" "LBSTNRLO" "LBSTNRHI" "LBNRIND"  "LBBLFL"  
[19] "VISITNUM" "VISIT"    "VISITDY"  "LBDTC"    "LBDY"     "TRTSDT"  
[25] "TRTEDT"   "TRT01A"   "TRT01P"   "ADT"      "ADY"      "PARAMCD" 

3 Part 1: gtsummary

3.1 Step 1: Demographics Table

tbl_demo <- adsl_safe |>
  gtsummary::tbl_summary(
    by       = TRT01A,
    include  = c(AGE, AGEGR1, SEX, RACE, ETHNIC),
    type     = list(AGE ~ "continuous2"),
    statistic = list(
      gtsummary::all_continuous2() ~ c("{mean} ({sd})", "{median} ({p25}, {p75})"),
      gtsummary::all_categorical() ~ "{n} ({p}%)"
    ),
    digits  = gtsummary::all_continuous() ~ 1,
    missing = "no"
  ) |>
  gtsummary::add_overall(last = TRUE) |>
  gtsummary::add_n() |>
  gtsummary::modify_header(label = "**Characteristic**") |>
  gtsummary::bold_labels() |>
  gtsummary::add_stat_label()

tbl_demo
Table 1
Characteristic N Placebo
N = 86
Xanomeline High Dose
N = 72
Xanomeline Low Dose
N = 96
Overall
N = 254
Age 254



    Mean (SD)
75.2 (8.6) 73.8 (7.9) 76.0 (8.1) 75.1 (8.2)
    Median (Q1, Q3)
76.0 (69.0, 82.0) 75.5 (70.0, 79.0) 78.0 (71.0, 82.0) 77.0 (70.0, 81.0)
Pooled Age Group 1, n (%) 254



    >64
72 (84%) 61 (85%) 88 (92%) 221 (87%)
    18-64
14 (16%) 11 (15%) 8 (8.3%) 33 (13%)
Sex, n (%) 254



    F
53 (62%) 35 (49%) 55 (57%) 143 (56%)
    M
33 (38%) 37 (51%) 41 (43%) 111 (44%)
Race, n (%) 254



    AMERICAN INDIAN OR ALASKA NATIVE
0 (0%) 1 (1.4%) 0 (0%) 1 (0.4%)
    BLACK OR AFRICAN AMERICAN
8 (9.3%) 9 (13%) 6 (6.3%) 23 (9.1%)
    WHITE
78 (91%) 62 (86%) 90 (94%) 230 (91%)
Ethnicity, n (%) 254



    HISPANIC OR LATINO
3 (3.5%) 3 (4.2%) 6 (6.3%) 12 (4.7%)
    NOT HISPANIC OR LATINO
83 (97%) 69 (96%) 90 (94%) 242 (95%)

3.2 Step 2: Convert to gt and Add Headers

tbl_demo |>
  gtsummary::as_gt() |>
  gt::tab_header(
    title    = "Table 14.1.1",
    subtitle = "Demographic and Baseline Characteristics (Safety Population)"
  ) |>
  gt::tab_source_note(
    source_note = "Source: pharmaverseadam::adsl | Population: Safety (SAFFL=Y)"
  )
Table 2
Table 14.1.1
Demographic and Baseline Characteristics (Safety Population)
Characteristic N Placebo
N = 86
Xanomeline High Dose
N = 72
Xanomeline Low Dose
N = 96
Overall
N = 254
Age 254



    Mean (SD)
75.2 (8.6) 73.8 (7.9) 76.0 (8.1) 75.1 (8.2)
    Median (Q1, Q3)
76.0 (69.0, 82.0) 75.5 (70.0, 79.0) 78.0 (71.0, 82.0) 77.0 (70.0, 81.0)
Pooled Age Group 1, n (%) 254



    >64
72 (84%) 61 (85%) 88 (92%) 221 (87%)
    18-64
14 (16%) 11 (15%) 8 (8.3%) 33 (13%)
Sex, n (%) 254



    F
53 (62%) 35 (49%) 55 (57%) 143 (56%)
    M
33 (38%) 37 (51%) 41 (43%) 111 (44%)
Race, n (%) 254



    AMERICAN INDIAN OR ALASKA NATIVE
0 (0%) 1 (1.4%) 0 (0%) 1 (0.4%)
    BLACK OR AFRICAN AMERICAN
8 (9.3%) 9 (13%) 6 (6.3%) 23 (9.1%)
    WHITE
78 (91%) 62 (86%) 90 (94%) 230 (91%)
Ethnicity, n (%) 254



    HISPANIC OR LATINO
3 (3.5%) 3 (4.2%) 6 (6.3%) 12 (4.7%)
    NOT HISPANIC OR LATINO
83 (97%) 69 (96%) 90 (94%) 242 (95%)
Source: pharmaverseadam::adsl | Population: Safety (SAFFL=Y)

3.3 Step 3: Extract ARD from gtsummary

ard_from_gtsumm <- tbl_demo |>
  gtsummary::gather_ard() |>
  purrr::pluck("tbl_summary")

cat("ARD rows extracted from gtsummary:", nrow(ard_from_gtsumm), "\n")
ARD rows extracted from gtsummary: 193 
ard_from_gtsumm |>
  dplyr::select(group1, group1_level, variable, stat_name, stat_label, stat) |>
  head(10)
   group1 group1_level variable stat_name stat_label  stat
1  TRT01A      Placebo      SEX         n          n    53
2  TRT01A      Placebo      SEX         N          N    86
3  TRT01A      Placebo      SEX         p          % 0.616
4  TRT01A      Placebo      SEX         n          n    33
5  TRT01A      Placebo      SEX         N          N    86
6  TRT01A      Placebo      SEX         p          % 0.384
7  TRT01A      Placebo     RACE         n          n     0
8  TRT01A      Placebo     RACE         N          N    86
9  TRT01A      Placebo     RACE         p          %     0
10 TRT01A      Placebo     RACE         n          n     8

3.4 Step 4: Lab Summary Table from ADLB

# Summarise ALT at baseline per treatment
# (Do not assume AVALU exists in pharmaverseadam::adlb)
adlb_alt_bl <- adlb |>
  dplyr::filter(PARAMCD == "ALT", ABLFL == "Y") |>
  dplyr::select(TRT01A, AVAL)

cat("ALT baseline records:", nrow(adlb_alt_bl), "\n")
ALT baseline records: 254 
tbl_lab <- adlb_alt_bl |>
  gtsummary::tbl_summary(
    by       = TRT01A,
    include  = AVAL,
    type     = list(AVAL ~ "continuous2"),
    statistic = list(
      gtsummary::all_continuous2() ~ c(
        "{mean} ({sd})",
        "{median} ({p25}, {p75})",
        "{min}, {max}"
      )
    ),
    digits  = gtsummary::all_continuous() ~ 1,
    missing = "no",
    label   = list(AVAL ~ "ALT")
  ) |>
  gtsummary::add_overall(last = TRUE) |>
  gtsummary::add_n() |>
  gtsummary::bold_labels()

tbl_lab
Table 3
Characteristic N Placebo
N = 86
Xanomeline High Dose
N = 72
Xanomeline Low Dose
N = 96
Overall
N = 254
ALT 254



    Mean (SD)
17.5 (8.3) 19.1 (10.1) 18.1 (8.3) 18.1 (8.8)
    Median (Q1, Q3)
15.0 (13.0, 21.0) 16.0 (14.0, 21.5) 16.0 (14.0, 19.5) 16.0 (13.0, 20.0)
    Min, Max
7.0, 55.0 6.0, 62.0 5.0, 70.0 5.0, 70.0

3.5 Step 5: Logistic Regression Table

# Logistic regression: SEX ~ AGE + TRT01A
adsl_model <- adsl_safe |>
  dplyr::mutate(SEX_BIN = dplyr::if_else(SEX == "M", 1L, 0L))

logit_mod <- glm(
  SEX_BIN ~ AGE + TRT01A,
  data   = adsl_model,
  family = binomial()
)

tbl_reg <- logit_mod |>
  gtsummary::tbl_regression(
    exponentiate = TRUE,
    label        = list(AGE ~ "Age (years)", TRT01A ~ "Treatment Arm")
  ) |>
  gtsummary::add_n() |>
  gtsummary::add_nevent() |>
  gtsummary::bold_p(t = 0.05) |>
  gtsummary::bold_labels()

tbl_reg
Table 4
Characteristic N Event N OR 95% CI p-value
Age (years) 254 111 0.98 0.95, 1.01 0.3
Treatment Arm 254 111


    Placebo

- -
    Xanomeline High Dose

1.66 0.88, 3.16 0.12
    Xanomeline Low Dose

1.21 0.67, 2.21 0.5
Abbreviations: CI = Confidence Interval, OR = Odds Ratio

3.6 Step 6: Merge Tables with tbl_merge

tbl_merged <- gtsummary::tbl_merge(
  tbls        = list(tbl_demo, tbl_lab),
  tab_spanner = c("**Demographics**", "**ALT at Baseline**")
)

tbl_merged
Table 5
Characteristic
Demographics
ALT at Baseline
N Placebo
N = 86
Xanomeline High Dose
N = 72
Xanomeline Low Dose
N = 96
Overall
N = 254
N Placebo
N = 86
Xanomeline High Dose
N = 72
Xanomeline Low Dose
N = 96
Overall
N = 254
Age 254








    Mean (SD)
75.2 (8.6) 73.8 (7.9) 76.0 (8.1) 75.1 (8.2)




    Median (Q1, Q3)
76.0 (69.0, 82.0) 75.5 (70.0, 79.0) 78.0 (71.0, 82.0) 77.0 (70.0, 81.0)




Pooled Age Group 1, n (%) 254








    >64
72 (84%) 61 (85%) 88 (92%) 221 (87%)




    18-64
14 (16%) 11 (15%) 8 (8.3%) 33 (13%)




Sex, n (%) 254








    F
53 (62%) 35 (49%) 55 (57%) 143 (56%)




    M
33 (38%) 37 (51%) 41 (43%) 111 (44%)




Race, n (%) 254








    AMERICAN INDIAN OR ALASKA NATIVE
0 (0%) 1 (1.4%) 0 (0%) 1 (0.4%)




    BLACK OR AFRICAN AMERICAN
8 (9.3%) 9 (13%) 6 (6.3%) 23 (9.1%)




    WHITE
78 (91%) 62 (86%) 90 (94%) 230 (91%)




Ethnicity, n (%) 254








    HISPANIC OR LATINO
3 (3.5%) 3 (4.2%) 6 (6.3%) 12 (4.7%)




    NOT HISPANIC OR LATINO
83 (97%) 69 (96%) 90 (94%) 242 (95%)




ALT




254



    Mean (SD)





17.5 (8.3) 19.1 (10.1) 18.1 (8.3) 18.1 (8.8)
    Median (Q1, Q3)





15.0 (13.0, 21.0) 16.0 (14.0, 21.5) 16.0 (14.0, 19.5) 16.0 (13.0, 20.0)
    Min, Max





7.0, 55.0 6.0, 62.0 5.0, 70.0 5.0, 70.0

4 Part 2: tfrmt

4.1 Step 7: Build a tfrmt Format Spec

# tfrmt expects a long dataset with:
# - group columns, label column, column columns
# - param column (e.g., mean, sd)
# - value column (numeric)

my_fmt <- tfrmt::tfrmt(
  group  = vars(PARAMCD),
  label  = vars(VISIT),
  column = vars(TRT01A),
  param  = PARAM,
  value  = VALUE,
  body_plan = tfrmt::body_plan(
    tfrmt::frmt_structure(
      group_val = ".default",
      label_val = ".default",
      tfrmt::frmt_combine(
        "{mean} ({sd})",
        mean = tfrmt::frmt("xx.x"),
        sd   = tfrmt::frmt("xx.xx")
      )
    )
  )
)

my_fmt
$group
<list_of<quosure>>

[[1]]
<quosure>
expr: ^PARAMCD
env:  0x000001a124959b30


$label
<quosure>
expr: ^VISIT
env:  0x000001a1249ba200

$param
<quosure>
expr: ^PARAM
env:  0x000001a12496b008

$value
<quosure>
expr: ^VALUE
env:  0x000001a12496b008

$column
<list_of<quosure>>

[[1]]
<quosure>
expr: ^TRT01A
env:  0x000001a124a06c80


$body_plan
[[1]]
Format Structure
  Group Values: ".default"
  Label Values: ".default"
  Param Values: "mean", "sd"
  Format: < frmt_combine | Expression: `{mean} ({sd})` >

attr(,"class")
[1] "body_plan"  "frmt_table"

attr(,"class")
[1] "tfrmt"

4.2 Step 8: Prepare BDS Summary Data for tfrmt

# Summarise ADLB: mean and SD of AVAL by PARAMCD, AVISIT, TRT01A
adlb_summary <- adlb |>
  dplyr::filter(PARAMCD %in% c("ALT", "AST", "BILI")) |>
  dplyr::filter(!is.na(AVISIT), !is.na(AVAL)) |>
  dplyr::group_by(PARAMCD, AVISIT, TRT01A) |>
  dplyr::summarise(
    mean = mean(AVAL, na.rm = TRUE),
    sd   = sd(AVAL, na.rm = TRUE),
    .groups = "drop"
  ) |>
  tidyr::pivot_longer(
    cols      = c(mean, sd),
    names_to  = "PARAM",
    values_to = "VALUE"
  ) |>
  dplyr::rename(VISIT = AVISIT)

cat("tfrmt input rows:", nrow(adlb_summary), "\n")
tfrmt input rows: 378 
head(adlb_summary, 10)
# A tibble: 10 × 5
   PARAMCD VISIT              TRT01A               PARAM VALUE
   <chr>   <chr>              <chr>                <chr> <dbl>
 1 ALT     Ambul Ecg Removal  Xanomeline Low Dose  mean  15   
 2 ALT     Ambul Ecg Removal  Xanomeline Low Dose  sd    NA   
 3 ALT     Baseline           Placebo              mean  17.6 
 4 ALT     Baseline           Placebo              sd     9.22
 5 ALT     Baseline           Xanomeline High Dose mean  19.2 
 6 ALT     Baseline           Xanomeline High Dose sd    10.3 
 7 ALT     Baseline           Xanomeline Low Dose  mean  18.1 
 8 ALT     Baseline           Xanomeline Low Dose  sd     8.74
 9 ALT     POST-BASELINE LAST Placebo              mean  16.6 
10 ALT     POST-BASELINE LAST Placebo              sd    10.8 

4.3 Step 9: Apply tfrmt Format Spec

tfrmt_gt <- tfrmt::print_to_gt(my_fmt, .data = adlb_summary)

tfrmt_gt
Xanomeline Low Dose Placebo Xanomeline High Dose
ALT


  Ambul Ecg Removal 15.0 (NA)
  Baseline 18.1 ( 8.74) 17.6 ( 9.22) 19.2 (10.25)
  POST-BASELINE LAST 19.1 ( 8.54) 16.6 (10.83) 18.9 ( 6.49)
  POST-BASELINE MAXIMUM 24.7 (13.16) 23.9 (18.05) 26.7 (16.13)
  POST-BASELINE MINIMUM 15.5 ( 7.52) 13.4 (10.41) 16.1 ( 5.60)
  Retrieval 13.0 (NA)
  Unscheduled 1.1 21.8 (11.82) 22.1 (13.78) 24.0 (18.58)
  Unscheduled 1.2 13.0 ( 2.83) 18.0 (NA) 14.0 (NA)
  Unscheduled 1.3 20.0 (NA) 11.0 (NA)
  Unscheduled 12.1 15.0 (NA) 12.0 (NA)
  Unscheduled 13.1 20.0 (NA)
  Unscheduled 4.1 14.0 ( 5.66) 95.0 (NA)
  Unscheduled 4.2 92.0 (NA)
  Unscheduled 5.1 16.0 (NA) 73.0 (NA)
  Unscheduled 6.1 17.0 (NA)
  Unscheduled 7.1 17.0 ( 2.83) 12.0 (NA)
  Unscheduled 8.2 13.0 (NA) 42.0 (NA)
  Unscheduled 9.2  9.0 (NA)
  Week 12 18.5 (12.68) 18.0 ( 9.16) 21.0 (10.18)
  Week 16 17.3 ( 7.51) 17.1 ( 7.39) 19.6 ( 7.61)
  Week 2 20.7 (10.40) 18.0 (12.53) 21.2 ( 8.87)
  Week 20 16.7 ( 6.33) 16.1 ( 6.56) 19.6 ( 6.82)
  Week 24 18.2 ( 9.17) 17.9 (15.61) 21.0 ( 8.70)
  Week 26 17.8 ( 9.51) 16.0 ( 5.98) 18.9 ( 7.02)
  Week 4 17.5 ( 7.66) 18.7 (12.91) 21.3 ( 9.51)
  Week 6 17.0 ( 7.98) 17.0 ( 9.92) 21.2 ( 9.49)
  Week 8 17.6 ( 7.86) 16.7 ( 9.34) 22.8 (17.49)
AST


  Ambul Ecg Removal 31.0 (NA)
  Baseline 23.3 ( 7.99) 23.2 ( 7.50) 23.2 ( 6.71)
  POST-BASELINE LAST 23.8 ( 8.31) 22.7 (12.38) 22.1 ( 5.51)
  POST-BASELINE MAXIMUM 28.4 (13.95) 29.8 (20.46) 28.9 (13.21)
  POST-BASELINE MINIMUM 20.5 ( 6.44) 19.4 (11.74) 19.9 ( 4.28)
  Retrieval 21.0 (NA)
  Unscheduled 1.1 26.5 (13.35) 25.5 ( 9.12) 21.5 ( 8.62)
  Unscheduled 1.2 16.0 ( 1.41) 33.0 (NA) 18.0 (NA)
  Unscheduled 1.3 31.0 (NA) 17.0 (NA)
  Unscheduled 12.1 30.0 (NA) 25.0 (NA)
  Unscheduled 13.1 25.0 (NA)
  Unscheduled 4.1 21.0 ( 2.83) 115.0 (NA)
  Unscheduled 4.2 114.0 (NA)
  Unscheduled 5.1 23.0 (NA) 92.0 (NA)
  Unscheduled 6.1 21.0 (NA)
  Unscheduled 7.1 20.5 ( 3.54) 19.0 (NA)
  Unscheduled 8.2 19.0 (NA) 21.0 (NA)
  Unscheduled 9.2 12.0 (NA)
  Week 12 24.2 (15.87) 22.8 ( 7.64) 23.3 ( 6.11)
  Week 16 22.4 (10.34) 22.8 ( 6.42) 23.1 ( 5.78)
  Week 2 24.5 ( 8.00) 23.6 (12.35) 23.5 ( 4.93)
  Week 20 20.7 ( 5.74) 21.9 ( 5.90) 24.0 ( 6.90)
  Week 24 22.4 (10.78) 25.2 (21.02) 24.4 ( 7.29)
  Week 26 22.1 (11.85) 21.5 ( 6.99) 21.6 ( 5.71)
  Week 4 22.3 ( 6.75) 23.9 (14.93) 23.8 ( 5.85)
  Week 6 22.1 ( 6.11) 22.0 ( 6.40) 24.1 ( 7.84)
  Week 8 22.7 ( 5.95) 22.3 ( 7.05) 25.7 (13.33)
BILI


  Ambul Ecg Removal 10.3 (NA)
  Baseline  9.5 ( 3.91)  9.7 ( 3.96) 11.2 ( 5.63)
  POST-BASELINE LAST 10.3 ( 4.89) 11.2 (13.43) 10.9 ( 5.59)
  POST-BASELINE MAXIMUM 11.5 ( 4.75) 13.4 (13.27) 13.0 ( 7.37)
  POST-BASELINE MINIMUM  7.9 ( 3.76)  8.7 (12.29)  8.7 ( 3.79)
  Retrieval  6.8 (NA)
  Unscheduled 1.1 10.7 ( 7.24) 12.6 ( 7.37) 15.1 (10.23)
  Unscheduled 1.2 12.0 ( 9.67) 13.7 (NA) 10.3 (NA)
  Unscheduled 1.3  6.8 (NA)  5.1 (NA)
  Unscheduled 12.1  5.1 (NA) 17.1 (NA)
  Unscheduled 13.1 15.4 (NA)
  Unscheduled 4.1  6.8 ( 0.00) 124.8 (NA)
  Unscheduled 4.2 99.2 (NA)
  Unscheduled 5.1  5.1 (NA) 71.8 (NA)
  Unscheduled 6.1  6.8 (NA)
  Unscheduled 7.1 10.3 ( 0.00)  8.5 (NA)
  Unscheduled 8.2  5.1 (NA) 15.4 (NA)
  Unscheduled 9.2 22.2 (NA)
  Week 12  8.8 ( 4.15)  9.5 ( 3.56) 11.5 ( 6.16)
  Week 16  8.8 ( 3.91) 10.0 ( 3.63) 11.6 ( 4.89)
  Week 2  9.4 ( 4.06) 10.8 (12.26) 10.5 ( 4.04)
  Week 20  8.8 ( 4.51) 10.1 ( 5.19) 11.8 ( 8.69)
  Week 24 10.1 ( 4.44)  9.4 ( 3.39) 12.3 ( 6.52)
  Week 26 10.2 ( 6.21) 10.0 ( 4.73) 12.2 ( 6.82)
  Week 4  9.2 ( 3.84) 11.1 (13.57) 10.9 ( 5.62)
  Week 6  9.5 ( 4.03)  9.6 ( 3.78) 10.9 ( 4.89)
  Week 8  9.6 ( 4.72)  9.4 ( 3.89) 10.8 ( 5.21)

4.4 Step 10: Mock Table from tfrmt Spec

tfrmt::print_mock_gt(my_fmt, .data = adlb_summary)
Xanomeline Low Dose Placebo Xanomeline High Dose
ALT


  Ambul Ecg Removal xx.x (xx.xx)
  Baseline xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  POST-BASELINE LAST xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  POST-BASELINE MAXIMUM xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  POST-BASELINE MINIMUM xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Retrieval xx.x (xx.xx)
  Unscheduled 1.1 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Unscheduled 1.2 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Unscheduled 1.3 xx.x (xx.xx) xx.x (xx.xx)
  Unscheduled 12.1 xx.x (xx.xx) xx.x (xx.xx)
  Unscheduled 13.1 xx.x (xx.xx)
  Unscheduled 4.1 xx.x (xx.xx) xx.x (xx.xx)
  Unscheduled 4.2 xx.x (xx.xx)
  Unscheduled 5.1 xx.x (xx.xx) xx.x (xx.xx)
  Unscheduled 6.1 xx.x (xx.xx)
  Unscheduled 7.1 xx.x (xx.xx) xx.x (xx.xx)
  Unscheduled 8.2 xx.x (xx.xx) xx.x (xx.xx)
  Unscheduled 9.2 xx.x (xx.xx)
  Week 12 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Week 16 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Week 2 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Week 20 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Week 24 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Week 26 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Week 4 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Week 6 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Week 8 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
AST


  Ambul Ecg Removal xx.x (xx.xx)
  Baseline xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  POST-BASELINE LAST xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  POST-BASELINE MAXIMUM xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  POST-BASELINE MINIMUM xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Retrieval xx.x (xx.xx)
  Unscheduled 1.1 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Unscheduled 1.2 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Unscheduled 1.3 xx.x (xx.xx) xx.x (xx.xx)
  Unscheduled 12.1 xx.x (xx.xx) xx.x (xx.xx)
  Unscheduled 13.1 xx.x (xx.xx)
  Unscheduled 4.1 xx.x (xx.xx) xx.x (xx.xx)
  Unscheduled 4.2 xx.x (xx.xx)
  Unscheduled 5.1 xx.x (xx.xx) xx.x (xx.xx)
  Unscheduled 6.1 xx.x (xx.xx)
  Unscheduled 7.1 xx.x (xx.xx) xx.x (xx.xx)
  Unscheduled 8.2 xx.x (xx.xx) xx.x (xx.xx)
  Unscheduled 9.2 xx.x (xx.xx)
  Week 12 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Week 16 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Week 2 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Week 20 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Week 24 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Week 26 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Week 4 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Week 6 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Week 8 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
BILI


  Ambul Ecg Removal xx.x (xx.xx)
  Baseline xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  POST-BASELINE LAST xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  POST-BASELINE MAXIMUM xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  POST-BASELINE MINIMUM xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Retrieval xx.x (xx.xx)
  Unscheduled 1.1 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Unscheduled 1.2 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Unscheduled 1.3 xx.x (xx.xx) xx.x (xx.xx)
  Unscheduled 12.1 xx.x (xx.xx) xx.x (xx.xx)
  Unscheduled 13.1 xx.x (xx.xx)
  Unscheduled 4.1 xx.x (xx.xx) xx.x (xx.xx)
  Unscheduled 4.2 xx.x (xx.xx)
  Unscheduled 5.1 xx.x (xx.xx) xx.x (xx.xx)
  Unscheduled 6.1 xx.x (xx.xx)
  Unscheduled 7.1 xx.x (xx.xx) xx.x (xx.xx)
  Unscheduled 8.2 xx.x (xx.xx) xx.x (xx.xx)
  Unscheduled 9.2 xx.x (xx.xx)
  Week 12 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Week 16 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Week 2 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Week 20 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Week 24 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Week 26 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Week 4 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Week 6 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)
  Week 8 xx.x (xx.xx) xx.x (xx.xx) xx.x (xx.xx)

5 Step 11: Export tfrmt table to HTML

tfrmt_gt |>
  gt::gtsave("tfrmt_lab_summary.html")

cat("✓ tfrmt_lab_summary.html exported\n")
✓ tfrmt_lab_summary.html exported

6 Validation Checks

cat("\n=== Day 25 Validation ===\n\n")

=== Day 25 Validation ===
# Check 1: Safety N by arm
cat("Check 1 - Safety N by arm:\n")
Check 1 - Safety N by arm:
print(adsl_safe |> dplyr::count(TRT01A))
# A tibble: 3 × 2
  TRT01A                   n
  <chr>                <int>
1 Placebo                 86
2 Xanomeline High Dose    72
3 Xanomeline Low Dose     96
# Check 2: ARD extracted from gtsummary has rows
cat("\nCheck 2 - ARD rows from gather_ard():", nrow(ard_from_gtsumm), "\n")

Check 2 - ARD rows from gather_ard(): 193 
# Check 3: tfrmt data has all 3 parameters
cat("\nCheck 3 - PARAMCD in tfrmt data:\n")

Check 3 - PARAMCD in tfrmt data:
print(adlb_summary |> dplyr::distinct(PARAMCD))
# A tibble: 3 × 1
  PARAMCD
  <chr>  
1 ALT    
2 AST    
3 BILI   
# Check 4: No NA VALUE in tfrmt input
cat("\nCheck 4 - NA VALUE rows:", nrow(adlb_summary |> dplyr::filter(is.na(VALUE))), "\n")

Check 4 - NA VALUE rows: 54 
cat("\n✓ Validation complete\n")

✓ Validation complete

7 Key Takeaways

  1. tbl_summary(by = TRT01A) is the workhorse for clinical demographics tables
  2. gather_ard() extracts a CDISC-compatible ARD from any gtsummary table
  3. tfrmt decouples format spec from data; print_mock_gt() helps you agree shells early
  4. Always validate dataset columns (e.g., do not assume AVALU exists)

8 Next Steps

Day 26: flextable + officer - Word/RTF clinical output


9 Resources

  • gtsummary documentation: https://www.danieldsjoberg.com/gtsummary/
  • tfrmt examples: https://gsk-biostatistics.github.io/tfrmt/articles/examples.html

End of Day 25


 

30 Days of Pharmaverse  ·  Disclaimer  ·  Indraneel Chakraborty  ·  © 2026