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 22: Demography Table with gtsummary + gt
  • 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 Welcome to Week 4: TLFs
  • 2 Setup
  • 3 Step 1: Check Available Columns
  • 4 Step 2: Define Analysis Population
  • 5 Step 3: Select Variables for Table 1
  • 6 Step 4: Build Summary Table with gtsummary
  • 7 Step 5: Convert to gt Table
  • 8 Step 6: Explore Disposition Variables
  • 9 Step 7: Disposition Summary Table
  • 10 Step 8: Disposition gt Table
  • 11 Validation Checks
  • 12 Summary
  • 13 Export Reference (not run)
  • 14 Key Takeaways
  • 15 Next Steps
  • 16 Resources

Day 22: Demography Table with gtsummary + gt

First TLF from ADSL

Back to Roadmap

1 Welcome to Week 4: TLFs

Week 4 moves from building ADaMs to producing TLFs.

Today we build two classic tables using pharmaverseadam::adsl:

  • Table 14.1.1 Demography and Baseline Characteristics
  • Table 14.2.1 Disposition Summary

2 Setup

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

adsl <- pharmaverseadam::adsl

cat("Loaded ADSL:", nrow(adsl), "subjects\n")
Loaded ADSL: 306 subjects
cat("Variables:", ncol(adsl), "\n")
Variables: 54 

3 Step 1: Check Available Columns

# Always check what exists before selecting
names(adsl)
 [1] "STUDYID"  "USUBJID"  "SUBJID"   "RFSTDTC"  "RFENDTC"  "RFXSTDTC"
 [7] "RFXENDTC" "RFICDTC"  "RFPENDTC" "DTHDTC"   "DTHFL"    "SITEID"  
[13] "AGE"      "AGEU"     "SEX"      "RACE"     "ETHNIC"   "ARMCD"   
[19] "ARM"      "ACTARMCD" "ACTARM"   "COUNTRY"  "DMDTC"    "DMDY"    
[25] "TRT01P"   "TRT01A"   "TRTSDTM"  "TRTSTMF"  "TRTEDTM"  "TRTETMF" 
[31] "TRTSDT"   "TRTEDT"   "TRTDURD"  "SCRFDT"   "EOSDT"    "EOSSTT"  
[37] "FRVDT"    "RANDDT"   "DTHDT"    "DTHDTF"   "DTHADY"   "LDDTHELD"
[43] "DTHCAUS"  "DTHDOM"   "DTHCGR1"  "LSTALVDT" "SAFFL"    "RACEGR1" 
[49] "AGEGR1"   "REGION1"  "LDDTHGR1" "DTH30FL"  "DTHA30FL" "DTHB30FL"

4 Step 2: Define Analysis Population

adsl_saf <- adsl %>%
  dplyr::filter(SAFFL == "Y")

cat("Safety population:", nrow(adsl_saf), "subjects\n")
Safety population: 254 subjects
adsl_saf %>%
  dplyr::count(TRT01A)
# A tibble: 3 × 2
  TRT01A                   n
  <chr>                <int>
1 Placebo                 86
2 Xanomeline High Dose    72
3 Xanomeline Low Dose     96

5 Step 3: Select Variables for Table 1

# Only columns confirmed in pharmaverseadam::adsl
adsl_tab <- adsl_saf %>%
  dplyr::select(TRT01A, AGE, SEX, RACE, ETHNIC, AGEGR1)

adsl_tab %>%
  head(5)
# A tibble: 5 × 6
  TRT01A                 AGE SEX   RACE  ETHNIC                 AGEGR1
  <chr>                <dbl> <chr> <chr> <chr>                  <chr> 
1 Placebo                 63 F     WHITE HISPANIC OR LATINO     18-64 
2 Placebo                 64 M     WHITE HISPANIC OR LATINO     18-64 
3 Xanomeline High Dose    71 M     WHITE NOT HISPANIC OR LATINO >64   
4 Xanomeline Low Dose     74 M     WHITE NOT HISPANIC OR LATINO >64   
5 Xanomeline High Dose    77 F     WHITE NOT HISPANIC OR LATINO >64   

Variables used:

  • AGE (continuous)
  • SEX, RACE, ETHNIC, AGEGR1 (categorical)

6 Step 4: Build Summary Table with gtsummary

var_types <- list(
  AGE    ~ "continuous2",
  SEX    ~ "categorical",
  RACE   ~ "categorical",
  ETHNIC ~ "categorical",
  AGEGR1 ~ "categorical"
)

tbl_demo <- adsl_tab %>%
  gtsummary::tbl_summary(
    by = TRT01A,
    type = var_types,
    statistic = list(
      gtsummary::all_continuous()  ~ 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()

tbl_demo
Characteristic N Placebo
N = 861
Xanomeline High Dose
N = 721
Xanomeline Low Dose
N = 961
Overall
N = 2541
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)
Sex 254



    F
53 (62%) 35 (49%) 55 (57%) 143 (56%)
    M
33 (38%) 37 (51%) 41 (43%) 111 (44%)
Race 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 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%)
Pooled Age Group 1 254



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

Features:

  • Continuous: mean (SD) and median (Q1, Q3)
  • Categorical: n (%)
  • Columns: each treatment arm + overall
  • add_n() shows N per column header

7 Step 5: Convert to gt Table

gt_demo <- tbl_demo %>%
  gtsummary::as_gt() %>%
  gt::tab_header(
    title    = "Table 14.1.1",
    subtitle = "Summary of Demographic and Baseline Characteristics (Safety Population)"
  ) %>%
  gt::tab_source_note(
    source_note = "Source: pharmaverseadam::adsl | Population: Safety (SAFFL=Y)"
  )

gt_demo
Table 14.1.1
Summary of Demographic and Baseline Characteristics (Safety Population)
Characteristic N Placebo
N = 861
Xanomeline High Dose
N = 721
Xanomeline Low Dose
N = 961
Overall
N = 2541
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)
Sex 254



    F
53 (62%) 35 (49%) 55 (57%) 143 (56%)
    M
33 (38%) 37 (51%) 41 (43%) 111 (44%)
Race 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 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%)
Pooled Age Group 1 254



    >64
72 (84%) 61 (85%) 88 (92%) 221 (87%)
    18-64
14 (16%) 11 (15%) 8 (8.3%) 33 (13%)
1 n (%)
Source: pharmaverseadam::adsl | Population: Safety (SAFFL=Y)

8 Step 6: Explore Disposition Variables

# EOSSTT = End of Study Status (confirmed in adsl)
# DTHFL = Death Flag (confirmed in adsl)

adsl_saf %>%
  dplyr::count(EOSSTT)
# A tibble: 2 × 2
  EOSSTT           n
  <chr>        <int>
1 COMPLETED      110
2 DISCONTINUED   144

9 Step 7: Disposition Summary Table

disp_tab <- adsl_saf %>%
  dplyr::select(TRT01A, EOSSTT, DTHFL)

tbl_disp <- disp_tab %>%
  gtsummary::tbl_summary(
    by = TRT01A,
    type = list(
      EOSSTT ~ "categorical",
      DTHFL  ~ "categorical"
    ),
    statistic = gtsummary::all_categorical() ~ "{n} ({p}%)",
    missing = "no"
  ) %>%
  gtsummary::add_overall(last = TRUE) %>%
  gtsummary::add_n()

tbl_disp
Characteristic N Placebo
N = 861
Xanomeline High Dose
N = 721
Xanomeline Low Dose
N = 961
Overall
N = 2541
End of Study Status 254



    COMPLETED
58 (67%) 27 (38%) 25 (26%) 110 (43%)
    DISCONTINUED
28 (33%) 45 (63%) 71 (74%) 144 (57%)
Subject Death Flag 3



    Y
2 (100%) 0 (NA%) 1 (100%) 3 (100%)
1 n (%)

10 Step 8: Disposition gt Table

gt_disp <- tbl_disp %>%
  gtsummary::as_gt() %>%
  gt::tab_header(
    title    = "Table 14.2.1",
    subtitle = "Subject Disposition Summary (Safety Population)"
  ) %>%
  gt::tab_source_note(
    source_note = "Source: pharmaverseadam::adsl | Population: Safety (SAFFL=Y)"
  )

gt_disp
Table 14.2.1
Subject Disposition Summary (Safety Population)
Characteristic N Placebo
N = 861
Xanomeline High Dose
N = 721
Xanomeline Low Dose
N = 961
Overall
N = 2541
End of Study Status 254



    COMPLETED
58 (67%) 27 (38%) 25 (26%) 110 (43%)
    DISCONTINUED
28 (33%) 45 (63%) 71 (74%) 144 (57%)
Subject Death Flag 3



    Y
2 (100%) 0 (NA%) 1 (100%) 3 (100%)
1 n (%)
Source: pharmaverseadam::adsl | Population: Safety (SAFFL=Y)

11 Validation Checks

cat("\n=== Table Validation ===\n\n")

=== Table Validation ===
# Check 1: Safety N
cat("Check 1 - Safety N:", nrow(adsl_saf), "\n")
Check 1 - Safety N: 254 
# Check 2: All SAFFL subjects have TRT01A
check2 <- adsl_saf %>% dplyr::filter(is.na(TRT01A))
cat("Check 2 - Missing TRT01A:", nrow(check2), "\n")
Check 2 - Missing TRT01A: 0 
# Check 3: EOSSTT completeness
check3 <- adsl_saf %>% dplyr::filter(is.na(EOSSTT))
cat("Check 3 - Missing EOSSTT:", nrow(check3), "\n")
Check 3 - Missing EOSSTT: 0 
cat("\n✓ Validation complete\n")

✓ Validation complete

12 Summary

cat("\n=== Day 22 Summary ===\n\n")

=== Day 22 Summary ===
cat("Safety N:", nrow(adsl_saf), "\n\n")
Safety N: 254 
cat("Treatment arms:\n")
Treatment arms:
adsl_saf %>% dplyr::count(TRT01A)
# A tibble: 3 × 2
  TRT01A                   n
  <chr>                <int>
1 Placebo                 86
2 Xanomeline High Dose    72
3 Xanomeline Low Dose     96
cat("\nEnd of study status:\n")

End of study status:
adsl_saf %>% dplyr::count(EOSSTT)
# A tibble: 2 × 2
  EOSSTT           n
  <chr>        <int>
1 COMPLETED      110
2 DISCONTINUED   144

13 Export Reference (not run)

# Export gtsummary table to Word/RTF (covered in a later day)
# library(flextable)
# library(officer)
#
# ft_demo <- gtsummary::as_flex_table(tbl_demo)
# doc <- officer::read_docx() %>%
#   officer::body_add_flextable(ft_demo)
# print(doc, target = "table14_1_1_demography.docx")

14 Key Takeaways

  1. Always run names(adsl) first to confirm which columns exist
  2. pharmaverseadam::adsl confirmed variables: AGE, SEX, RACE, ETHNIC, AGEGR1, EOSSTT, DTHFL, TRT01A
  3. gtsummary::tbl_summary() handles both continuous and categorical in one call
  4. gtsummary::as_gt() converts to gt for headers, footnotes, and final formatting
  5. No DCSREAS, BMIBL, or adtte in pharmaverseadam - always verify before use

15 Next Steps

Day 23: AE tables with pharmaverseadam::adae and gt Day 24: KM curves with ggsurvfit Day 25: ARD-first workflow with cards


16 Resources

  • gtsummary: https://www.danieldsjoberg.com/gtsummary/
  • gt clinical case study: https://gt.rstudio.com/articles/case-study-clinical-tables.html
  • pharmaverseadam datasets: https://pharmaverse.github.io/pharmaverseadam/

End of Day 22


 

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