ARD + Tables

Daniel D. Sjoberg and Becca Krouse

Workshop outline

  1. Introduction to the Analysis Results Standard and ARDs with {cards}

  2. Introduction to the {cardx} Package and ARD Extras

  3. ARD to Tables with {gtsummary} and {tfrmt}

Tables in the {pharmaverse}

Tables in the {pharmaverse}

Data frame-first packages

Tables in the {pharmaverse}

ARD-first packages

Introduction to {gtsummary}

How it started

  • Began to address reproducible issues while working in academia

  • Goal was to build a package to summarize study results with code that was both simple and customizable

  • First release in May 2019

How it’s going

  • The stats

    • 1,300,000+ installations from CRAN
    • 1100+ GitHub stars
    • 300+ contributors
    • 50+ code contributors
  • Won the 2021 American Statistical Association (ASA) Innovation in Programming Award

  • Won the 2024 Posit Pharma Table Contest

{gtsummary} overview

  • Create tabular summaries with sensible defaults but highly customizable
  • Types of summaries:
    • Demographic- or “Table 1”-types
    • Cross-tabulation
    • Regression models
    • Survival data
    • Nested summaries (AE tables)
    • Custom tables
  • Stack and/or merge any table type
  • Use themes to standardize across tables
  • Choose from different print engines

{gtsummary} runs on ARDs!

Simple Summary Table

library(gtsummary)
ADSL <- cards::ADSL |> dplyr::filter(startsWith(ARM, "Xanomeline"))
ADAE <- cards::ADAE |> dplyr::filter(startsWith(TRTA, "Xanomeline"))

tbl <- ADSL |> 
  tbl_summary(
    by = ARM, 
    include = c(AGE, RACE),
    type = AGE ~ "continuous2",
    statistic = all_continuous() ~ c("{mean} ({sd})", "{min}, {max}")
  ) |> 
  add_overall()
Characteristic Overall
N = 1681
Xanomeline High Dose
N = 841
Xanomeline Low Dose
N = 841
Age


    Mean (SD) 75 (8) 74 (8) 76 (8)
    Min, Max 51, 88 56, 88 51, 88
Race


    AMERICAN INDIAN OR ALASKA NATIVE 1 (0.6%) 1 (1.2%) 0 (0.0%)
    BLACK OR AFRICAN AMERICAN 15 (8.9%) 9 (10.7%) 6 (7.1%)
    WHITE 152 (90.5%) 74 (88.1%) 78 (92.9%)
1 n (%)

Simple Summary Table

But where are the ARDs?!

gather_ard(tbl) |> 
  purrr::pluck("tbl_summary") |> 
  cards::apply_fmt_fn()
   group1 group1_level variable variable_level stat_name stat_label  stat stat_fmt
1     ARM    Xanomeli…     RACE      AMERICAN…         n          n     1        1
2     ARM    Xanomeli…     RACE      AMERICAN…         N          N    84       84
3     ARM    Xanomeli…     RACE      AMERICAN…         p          % 0.012      1.2
4     ARM    Xanomeli…     RACE      BLACK OR…         n          n     9        9
5     ARM    Xanomeli…     RACE      BLACK OR…         N          N    84       84
6     ARM    Xanomeli…     RACE      BLACK OR…         p          % 0.107     10.7
7     ARM    Xanomeli…     RACE          WHITE         n          n    74       74
8     ARM    Xanomeli…     RACE          WHITE         N          N    84       84
9     ARM    Xanomeli…     RACE          WHITE         p          % 0.881     88.1
10    ARM    Xanomeli…     RACE      AMERICAN…         n          n     0        0
  • Because the ARD is created as a byproduct of every gtsummary function, QC is a breeze.

  • Simply compare the ARD above to the ARD created with {cards}, which allows you to compare both the underlying numeric values and the rounded/formatted value that appears in the table.

AE Summary Table

tbl <- ADAE |> 
  dplyr::filter(AESOC %in% c("EYE DISORDERS", "VASCULAR DISORDERS")) |> 
  tbl_hierarchical(
    by = TRTA,
    variables = c(AESOC, AETERM),
    id = USUBJID,
    denominator = ADSL |> dplyr::rename(TRTA = ARM)
  )


Primary System Organ Class
    Reported Term for the Adverse Event
Xanomeline High Dose
N = 841
Xanomeline Low Dose
N = 841
EYE DISORDERS 1 (1.2%) 2 (2.4%)
    CONJUNCTIVAL HAEMORRHAGE 0 (0.0%) 1 (1.2%)
    VISION BLURRED 1 (1.2%) 1 (1.2%)
VASCULAR DISORDERS 2 (2.4%) 3 (3.6%)
    HOT FLUSH 0 (0.0%) 1 (1.2%)
    HYPERTENSION 1 (1.2%) 1 (1.2%)
    HYPOTENSION 0 (0.0%) 1 (1.2%)
    WOUND HAEMORRHAGE 1 (1.2%) 0 (0.0%)
1 n (%)

tbl_merge()/tbl_stack()

tbl_merge() for side-by-side tables

library(survival)

tbl_uvreg <- cards::ADTTE |> 
  tbl_uvregression(
    y = Surv(AVAL, 1 - CNSR),
    include = c(TRTA, AGE),
    method = coxph,
    exponentiate = TRUE, 
    hide_n = TRUE)
tbl_uvreg
Characteristic HR 95% CI p-value
Actual Treatment


    Placebo
    Xanomeline High Dose 5.03 3.18, 7.94 <0.0001
    Xanomeline Low Dose 4.15 2.65, 6.50 <0.0001
Age 0.99 0.97, 1.01 0.2494
Abbreviations: CI = Confidence Interval, HR = Hazard Ratio
tbl_reg <- 
  coxph(
    Surv(AVAL, 1-CNSR)~TRTA+AGE, 
    data = cards::ADTTE) |> 
  tbl_regression(exponentiate=TRUE)




tbl_reg
Characteristic HR 95% CI p-value
Actual Treatment


    Placebo
    Xanomeline High Dose 5.08 3.21, 8.03 <0.0001
    Xanomeline Low Dose 4.22 2.69, 6.63 <0.0001
Age 0.99 0.97, 1.00 0.1326
Abbreviations: CI = Confidence Interval, HR = Hazard Ratio

tbl_merge() for side-by-side tables

# combine the tables side by side
tbl <- list(tbl_uvreg, tbl_reg) |> 
  tbl_merge(tab_spanner = c("**Univariable**", "**Multivariable**"))


Characteristic
Univariable
Multivariable
HR 95% CI p-value HR 95% CI p-value
Actual Treatment





    Placebo

    Xanomeline High Dose 5.03 3.18, 7.94 <0.0001 5.08 3.21, 8.03 <0.0001
    Xanomeline Low Dose 4.15 2.65, 6.50 <0.0001 4.22 2.69, 6.63 <0.0001
Age 0.99 0.97, 1.01 0.2494 0.99 0.97, 1.00 0.1326
Abbreviations: CI = Confidence Interval, HR = Hazard Ratio

tbl_stack() to combine vertically

# stack the two tables 
tbl <- list(tbl_uvreg, tbl_reg) |> 
  tbl_stack(group_header = c("Univariable", "Multivariable")) 
HR 95% CI p-value
Univariable
Actual Treatment


    Placebo
    Xanomeline High Dose 5.03 3.18, 7.94 <0.0001
    Xanomeline Low Dose 4.15 2.65, 6.50 <0.0001
Age 0.99 0.97, 1.01 0.2494
Multivariable
Actual Treatment


    Placebo
    Xanomeline High Dose 5.08 3.21, 8.03 <0.0001
    Xanomeline Low Dose 4.22 2.69, 6.63 <0.0001
Age 0.99 0.97, 1.00 0.1326
Abbreviations: CI = Confidence Interval, HR = Hazard Ratio

ARD-first tables

ARD-first Tables

Similar to functions that accept a data frame, the package exports functions with nearly identical APIs that accept an ARD.

tbl_summary()

tbl_continuous()

tbl_wide_summary()
tbl_ard_summary()

tbl_ard_continuous()

tbl_ard_wide_summary()

ARD-first Tables

We can use the skills we learned earlier today to create ARDs for simple and complex gtsummary tables.

tbl <- ADSL |> 
  ard_stack( 
    .by = ARM,
    ard_continuous(variables = AGE),
    ard_categorical(variables = RACE),
    # add this for best-looking tables
    .attributes = TRUE
  ) |> 
  tbl_ard_summary(
    by = ARM,
    type = all_continuous() ~ "continuous2",
    statistic = all_continuous() ~ c("{mean} ({sd})", "{min} - {max}")
  ) |> 
  modify_caption("**Table 1. Subject Demographics**")

ARD-first Tables

Table 1. Subject Demographics
Characteristic Xanomeline High Dose1 Xanomeline Low Dose1
Age

    Mean (SD) 74.4 (7.9) 75.7 (8.3)
    Min - Max 56.0 - 88.0 51.0 - 88.0
Race

    AMERICAN INDIAN OR ALASKA NATIVE 1 (1.2%) 0 (0.0%)
    BLACK OR AFRICAN AMERICAN 9 (10.7%) 6 (7.1%)
    WHITE 74 (88.1%) 78 (92.9%)
1 n (%)

What About Other Tables?

While our examples have focused on simple demographics tables, the ARD structure is general and any statistic can be presented.

ADSL |> 
  cardx::ard_stats_t_test_onesample(by = c(ARM, SEX), variables = AGE) |> 
  cards::update_ard_fmt_fn(stat_names = "p.value", 
                           fmt_fn = label_style_pvalue(prepend_p = TRUE)) |> 
  tbl_ard_continuous(
    by = ARM, 
    include = SEX,
    variable = AGE,
    statistic = ~"{estimate} ({conf.low}, {conf.high}; {p.value})"
  ) |> 
  modify_footnote(all_stat_cols() ~ "Mean (95% CI; p-value)")
Characteristic Xanomeline High Dose1 Xanomeline Low Dose1
SEX

    F 74.7 (72.2, 77.1; p<0.001) 75.7 (73.4, 78.0; p<0.001)
    M 74.1 (71.6, 76.6; p<0.001) 75.6 (72.6, 78.7; p<0.001)
1 Mean (95% CI; p-value)

ARD-first Table Shells

Characteristic Drug A
N = xx1
Drug B
N = xx1
Patient Age, yrs

    Mean (SD) xx.x (xx.x) xx.x (xx.x)
    Min - Max xx.x - xx.x xx.x - xx.x
Grade

    I xx (xx.x%) xx (xx.x%)
    II xx (xx.x%) xx (xx.x%)
    III xx (xx.x%) xx (xx.x%)
1 n (%)

as_gtsummary()

For tables that just don’t fit any mold, use as_gtsummary().

  • Create a data frame of the data summary convert to gtsummary
ADAE[1:5,] |> 
  dplyr::select(USUBJID, AESOC, AETERM) |> 
  dplyr::mutate(
    .by = USUBJID, 
    USUBJID = ifelse(dplyr::row_number() == 1, USUBJID, NA)
  ) |> 
  as_gtsummary() |> 
  modify_header(USUBJID = "Patient ID") |> 
  modify_column_alignment(everything(), "left")

as_gtsummary()

Patient ID Primary System Organ Class Reported Term for the Adverse Event
01-701-1028 GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS APPLICATION SITE ERYTHEMA

GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS APPLICATION SITE PRURITUS
01-701-1034 GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS APPLICATION SITE PRURITUS

GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS FATIGUE
01-701-1097 SKIN AND SUBCUTANEOUS TISSUE DISORDERS ERYTHEMA

{gtsummary} print engines

{gtsummary} print engines

In Closing

{gtsummary} website

http://www.danieldsjoberg.com/gtsummary/

Package Authors/Contributors

Daniel D. Sjoberg

Joseph Larmarange

Michael Curry

Jessica Lavery

Karissa Whiting

Emily C. Zabor

Xing Bai

Esther Drill

Jessica Flynn

Margie Hannum

Stephanie Lobaugh

Shannon Pileggi

Amy Tin

Gustavo Zapata Wainberg

Other Contributors

@abduazizR, @ablack3, @ABohynDOE, @ABorakati, @adilsonbauhofer, @aghaynes, @ahinton-mmc, @aito123, @akarsteve, @akefley, @albamrt, @albertostefanelli, @alecbiom, @alexandrayas, @alexis-catherine, @AlexZHENGH, @alnajar, @amygimma, @anaavu, @anddis, @andrader, @Andrzej-Andrzej, @angelgar, @arbet003, @arnmayer, @aspina7, @AurelienDasre, @awcm0n, @ayogasekaram, @barretmonchka, @barthelmes, @bc-teixeira, @bcjaeger, @BeauMeche, @benediktclaus, @benwhalley, @berg-michael, @bhattmaulik, @BioYork, @blue-abdur, @brachem-christian, @brianmsm, @browne123, @bwiernik, @bx259, @calebasaraba, @CarolineXGao, @CharlyMarie, @ChongTienGoh, @Chris-M-P, @chrisleitzinger, @cjprobst, @ClaudiaCampani, @clmawhorter, @CodieMonster, @coeusanalytics, @coreysparks, @CorradoLanera, @crystalluckett-sanofi, @ctlamb, @dafxy, @DanChaltiel, @DanielPark-MGH, @davideyre, @davidgohel, @davidkane9, @DavisVaughan, @dax44, @dchiu911, @ddsjoberg, @DeFilippis, @denis-or, @dereksonderegger, @derekstein, @DesiQuintans, @dieuv0, @dimbage, @discoleo, @djbirke, @dmenne, @DrDinhLuong, @edelarua, @edrill, @Eduardo-Auer, @ElfatihHasabo, @emilyvertosick, @eokoshi, @ercbk, @eremingt, @erikvona, @eugenividal, @eweisbrod, @fdehrich, @feizhadj, @fh-jsnider, @fh-mthomson, @FrancoisGhesquiere, @ge-generation, @Generalized, @ghost, @giorgioluciano, @giovannitinervia9, @gjones1219, @gorkang, @GuiMarthe, @gungorMetehan, @hass91, @hescalar, @HichemLa, @hichew22, @hr70, @huftis, @hughjonesd, @iaingallagher, @ilyamusabirov, @IndrajeetPatil, @irene9116, @IsadoraBM, @j-tamad, @jalavery, @jaromilfrossard, @JBarsotti, @jbtov, @jeanmanguy, @jemus42, @jenifav, @jennybc, @JeremyPasco, @jerrodanzalone, @JesseRop, @jflynn264, @jhchou, @jhelvy, @jhk0530, @jjallaire, @jkylearmstrong, @jmbarajas, @jmbarbone, @JoanneF1229, @joelgautschi, @johnryan412, @JohnSodling, @jonasrekdalmathisen, @JonGretar, @jordan49er, @jsavinc, @jthomasmock, @juseer, @jwilliman, @karissawhiting, @karl-an, @kendonB, @kentm4, @klh281, @kmdono02, @kristyrobledo, @kwakuduahc1, @lamberp6, @lamhine, @larmarange, @ledermanr, @leejasme, @leslem, @levossen, @lngdet, @longjp, @lorenzoFabbri, @loukesio, @love520lfh, @lspeetluk, @ltin1214, @ltj-github, @lucavd, @LucyMcGowan, @LuiNov, @lukejenner6, @maciekbanas, @maia-sh, @malcolmbarrett, @mariamaseng, @Marsus1972, @martsobm, @Mathicaa, @matthieu-faron, @maxanes, @mayazadok2, @mbac, @mdidish, @medewitt, @meenakshi-kushwaha, @melindahiggins2000, @MelissaAssel, @Melkiades, @mfansler, @michaelcurry1123, @mikemazzucco, @mlamias, @mljaniczek, @moleps, @monitoringhsd, @motocci, @mrmvergeer, @msberends, @mvuorre, @myamortor, @myensr, @MyKo101, @nalimilan, @ndunnewind, @nikostr, @ningyile, @O16789, @oliviercailloux, @oranwutang, @palantre, @parmsam, @Pascal-Schmidt, @PaulC91, @paulduf, @pedersebastian, @perlatex, @pgseye, @philippemichel, @philsf, @polc1410, @Polperobis, @postgres-newbie, @proshano, @raphidoc, @RaviBot, @rawand-hanna, @rbcavanaugh, @remlapmot, @rich-iannone, @RiversPharmD, @rmgpanw, @roaldarbol, @roman2023, @ryzhu75, @s-j-choi, @sachijay, @saifelayan, @sammo3182, @samrodgersmelnick, @samuele-mercan, @sandhyapc, @sbalci, @sda030, @shah-in-boots, @shannonpileggi, @shaunporwal, @shengchaohou, @ShixiangWang, @simonpcouch, @slb2240, @slobaugh, @spiralparagon, @Spring75xx, @StaffanBetner, @steenharsted, @stenw, @Stephonomon, @storopoli, @stratopopolis, @strengejacke, @szimmer, @tamytsujimoto, @TAOS25, @TarJae, @themichjam, @THIB20, @tibirkrajc, @tjmeyers, @tldrcharlene, @tormodb, @toshifumikuroda, @TPDeramus, @UAB-BST-680, @uakimix, @uriahf, @Valja64, @viola-hilbert, @violet-nova, @vvm02, @will-gt, @xkcococo, @xtimbeau, @yatirbe, @yihunzeleke, @yonicd, @yoursdearboy, @YousufMohammed2002, @yuryzablotski, @zabore, @zachariae, @zaddyzad, @zawkzaw, @zdz2101, @zeyunlu, @zhangkaicr, @zhaohongxin0, @zheer-kejlberg, @zhengnow, @zhonghua723, @zlkrvsm, @zongell-star, and @Zoulf001.

Connect!