{cardx} & other extras

Daniel D. Sjoberg and Becca Krouse

Workshop outline

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

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

  3. ARD to Tables with {gtsummary}

  4. ARD to Tables with {tfrmt}

{cardx} (read: extra cards)

{cardx}

  • Extension of the {cards} package, providing additional functions to create Analysis Results Datasets (ARDs)

  • The {cardx} package exports many ard_*() function for statistical methods.

cards and cardx package logos

{cardx}

  • Exports ARD frameworks for statistical analyses from many packages
  - {stats}
  - {car}
  - {effectsize}
  - {emmeans}
  - {geepack}
  - {lme4}
  - {parameters}
  - {smd}
  - {survey}
  - {survival}
  • This list is growing (rather quickly) 🌱

{cardx} t-test Example

  • We see the results like the mean difference, the confidence interval, and p-value as expected.

  • And we also see the function’s inputs, which is incredibly useful for re-use, e.g. we know the we did not use equal variances.

pharmaverseadam::adsl |> 
  dplyr::filter(ARM %in% c("Xanomeline High Dose", "Xanomeline Low Dose")) |>
  cardx::ard_stats_t_test(by = ARM, variables = AGE)
{cards} data frame: 14 x 9
   group1 variable   context   stat_name stat_label      stat
1     ARM      AGE stats_t_…    estimate  Mean Dif…    -1.286
2     ARM      AGE stats_t_…   estimate1  Group 1 …    74.381
3     ARM      AGE stats_t_…   estimate2  Group 2 …    75.667
4     ARM      AGE stats_t_…   statistic  t Statis…     -1.03
5     ARM      AGE stats_t_…     p.value    p-value     0.304
6     ARM      AGE stats_t_…   parameter  Degrees …   165.595
7     ARM      AGE stats_t_…    conf.low  CI Lower…     -3.75
8     ARM      AGE stats_t_…   conf.high  CI Upper…     1.179
9     ARM      AGE stats_t_…      method     method Welch Tw…
10    ARM      AGE stats_t_… alternative  alternat… two.sided
11    ARM      AGE stats_t_…          mu    H0 Mean         0
12    ARM      AGE stats_t_…      paired  Paired t…     FALSE
13    ARM      AGE stats_t_…   var.equal  Equal Va…     FALSE
14    ARM      AGE stats_t_…  conf.level  CI Confi…      0.95
ℹ 3 more variables: fmt_fn, warning, error

{cardx} t-test Example

  • What to do if a method you need is not implemented?

  • It’s simple to wrap existing frameworks to customize.

  • One-sample t-test example utilizing cards::ard_continuous().

pharmaverseadam::adsl |> 
  dplyr::filter(ARM %in% c("Xanomeline High Dose", "Xanomeline Low Dose")) |>
  cards::ard_continuous(
    variables = AGE,
    statistic = everything() ~ list(t_test = \(x) t.test(x) |> broom::tidy())
  ) |> 
  dplyr::mutate(context = "t_test_one_sample")
{cards} data frame: 8 x 8
  variable   context   stat_name stat_label      stat fmt_fn
1      AGE t_test_o…    estimate   estimate    75.024      1
2      AGE t_test_o…   statistic  statistic     120.2      1
3      AGE t_test_o…     p.value    p.value         0      1
4      AGE t_test_o…   parameter  parameter       167      1
5      AGE t_test_o…    conf.low   conf.low    73.792      1
6      AGE t_test_o…   conf.high  conf.high    76.256      1
7      AGE t_test_o…      method     method One Samp…   <fn>
8      AGE t_test_o… alternative  alternat… two.sided   <fn>
ℹ 2 more variables: warning, error

{cardx} t-test Example

  • How to modify if we need a two-sample test, or more generally accessing other columns in the data frame.
pharmaverseadam::adsl |> 
  dplyr::filter(ARM %in% c("Xanomeline High Dose", "Xanomeline Low Dose")) |>
  cards::ard_complex(
    variables = AGE,
    statistic = 
      ~ list(t_test = \(x, data, ...) t.test(x ~ data$ARM) |> broom::tidy())
  ) |> 
  dplyr::mutate(group1 = "ARM", context = "t_test_two_sample") |> 
  cards::tidy_ard_column_order()
{cards} data frame: 10 x 9
   group1 variable   context   stat_name stat_label      stat
1     ARM      AGE t_test_t…    estimate   estimate    -1.286
2     ARM      AGE t_test_t…   estimate1  estimate1    74.381
3     ARM      AGE t_test_t…   estimate2  estimate2    75.667
4     ARM      AGE t_test_t…   statistic  statistic     -1.03
5     ARM      AGE t_test_t…     p.value    p.value     0.304
6     ARM      AGE t_test_t…   parameter  parameter   165.595
7     ARM      AGE t_test_t…    conf.low   conf.low     -3.75
8     ARM      AGE t_test_t…   conf.high  conf.high     1.179
9     ARM      AGE t_test_t…      method     method Welch Tw…
10    ARM      AGE t_test_t… alternative  alternat… two.sided
ℹ 3 more variables: fmt_fn, warning, error

{cardx} Regression

  • Includes functionality to summarize nearly every type of regression model in the R ecosystem:

betareg::betareg(), biglm::bigglm(), brms::brm(), cmprsk::crr(), fixest::feglm(), fixest::femlm(), fixest::feNmlm(), fixest::feols(), gam::gam(), geepack::geeglm(), glmmTMB::glmmTMB(), lavaan::lavaan(), lfe::felm(), lme4::glmer.nb(), lme4::glmer(), lme4::lmer(), logitr::logitr(), MASS::glm.nb(), MASS::polr(), mgcv::gam(), mice::mira, mmrm::mmrm(), multgee::nomLORgee(), multgee::ordLORgee(), nnet::multinom(), ordinal::clm(), ordinal::clmm(), parsnip::model_fit, plm::plm(), pscl::hurdle(), pscl::zeroinfl(), rstanarm::stan_glm(), stats::aov(), stats::glm(), stats::lm(), stats::nls(), survey::svycoxph(), survey::svyglm(), survey::svyolr(), survival::cch(), survival::clogit(), survival::coxph(), survival::survreg(), tidycmprsk::crr(), VGAM::vglm() (and more)

{cardx} Regression Example

library(survival)

# build model
mod <- pharmaverseadam::adtte_onco |> 
  dplyr::filter(PARAM %in% "Progression Free Survival") |>
  coxph(ggsurvfit::Surv_CNSR() ~ ARM, data = _)

# put model in a summary table
tbl <- gtsummary::tbl_regression(mod, exponentiate = TRUE) |> 
  gtsummary::add_n(location = c('label', 'level')) |> 
  gtsummary::add_nevent(location = c('label', 'level'))


Characteristic N Event N HR1 95% CI1 p-value
Description of Planned Arm 254 6


    Placebo 86 3
    Xanomeline High Dose 84 2 3.00 0.39, 22.9 0.3
    Xanomeline Low Dose 84 1 1.27 0.11, 14.3 0.8
1 HR = Hazard Ratio, CI = Confidence Interval

{cardx} Regression Example

The cardx::ard_regression() does a lot for us in the background.

  • Identifies the variable from the regression terms (i.e. groups levels of the same variable)
  • Identifies reference groups from categorical covariates
  • Finds variable labels from the source data frames
  • Knows the total N of the model, the number of events, and can do the same for each level of categorical variables
  • Contextually aware of slopes, odds ratios, hazard ratios, and incidence rate ratios
  • And much much more.

{cardx} Exercise

  • Open exercises/03-cardx.R

  • Compute the demographic summaries as described

08:00

{cardx} Exercise Solution

Prompt: Compare AGE, SEX, RACE distributions across treatment arms (TRT01A)

A. First, compute the Kruskal-Wallis test for AGE by TRT01A

adsl <- pharmaverseadam::adsl |> dplyr::filter(SAFFL == "Y") 

kruskal_ard <-  
  cardx::ard_stats_kruskal_test( 
    data = adsl,  
    by = TRT01A, 
    variables = AGE 
  ) 

kruskal_ard
{cards} data frame: 4 x 9
  group1 variable   context stat_name stat_label      stat
1 TRT01A      AGE stats_kr… statistic  Kruskal-…     3.879
2 TRT01A      AGE stats_kr…   p.value    p-value     0.144
3 TRT01A      AGE stats_kr… parameter  Degrees …         2
4 TRT01A      AGE stats_kr…    method     method Kruskal-…
ℹ 3 more variables: fmt_fn, warning, error

{cardx} Exercise Solution: Demographic comparison with {cardx}

Second, compute the Chi-squared test for SEX, RACE by TRT01A

chisq_ard <-  
  cardx::ard_stats_chisq_test( 
    data = adsl,  
    by = TRT01A, 
    variables = c(SEX, RACE) 
  ) 
chisq_ard |> 
  dplyr::filter(!stat_name %in% c("p", "B")) |> 
  dplyr::select(-warning)
{cards} data frame: 14 x 8
   group1 variable   context        stat_name stat_label      stat
1  TRT01A      SEX stats_ch…        statistic  X-square…     2.761
2  TRT01A      SEX stats_ch…          p.value    p-value     0.251
3  TRT01A      SEX stats_ch…        parameter  Degrees …         2
4  TRT01A      SEX stats_ch…           method     method Pearson'…
5  TRT01A      SEX stats_ch…          correct    correct      TRUE
6  TRT01A      SEX stats_ch…        rescale.p  rescale.p     FALSE
7  TRT01A      SEX stats_ch… simulate.p.value  simulate…     FALSE
8  TRT01A     RACE stats_ch…        statistic  X-square…     4.577
9  TRT01A     RACE stats_ch…          p.value    p-value     0.334
10 TRT01A     RACE stats_ch…        parameter  Degrees …         4
11 TRT01A     RACE stats_ch…           method     method Pearson'…
12 TRT01A     RACE stats_ch…          correct    correct      TRUE
13 TRT01A     RACE stats_ch…        rescale.p  rescale.p     FALSE
14 TRT01A     RACE stats_ch… simulate.p.value  simulate…     FALSE
ℹ 2 more variables: fmt_fn, error

{cardx} Exercise Solution: Demographic comparison with {cardx}

Combine your results with cards::bind_ard() and subset the ARD to include the rows with p-values only

final_ard <-  
  cards::bind_ard( 
    kruskal_ard,  
    chisq_ard 
  ) |>  
  dplyr::filter(stat_name == "p.value") 

final_ard
{cards} data frame: 3 x 9
  group1 variable stat_name stat_label  stat   warning
1 TRT01A      AGE   p.value    p-value 0.144          
2 TRT01A      SEX   p.value    p-value 0.251          
3 TRT01A     RACE   p.value    p-value 0.334 Chi-squa…
ℹ 3 more variables: context, fmt_fn, error
final_ard$warning[[3]]
[1] "Chi-squared approximation may be incorrect"

When things go wrong 😱

What happens when statistics are un-calculable?

ard_gone_wrong <- 
  cards::ADSL |> 
  cards::ard_continuous(
    by = ARM,
    variable = AGEGR1,
    statistic = ~list(kurtosis = \(x) e1071::kurtosis(x))
  ) |> 
  cards::replace_null_statistic()
ard_gone_wrong
{cards} data frame: 3 x 10
  group1 group1_level variable stat_name stat_label stat   warning     error
1    ARM      Placebo   AGEGR1  kurtosis   kurtosis   NA argument… non-nume…
2    ARM    Xanomeli…   AGEGR1  kurtosis   kurtosis   NA argument… non-nume…
3    ARM    Xanomeli…   AGEGR1  kurtosis   kurtosis   NA argument… non-nume…
ℹ 2 more variables: context, fmt_fn
cards::print_ard_conditions(ard_gone_wrong)

Mock ARDs

Like mock tables, mock ARDs are often useful as well

cards::bind_ard(
  cards::mock_categorical(variables = list(AGEGR1 = c("<65", ">=65"))),
  cards::mock_continuous(variables = "AGE")
) |> 
  cards::apply_fmt_fn()
{cards} data frame: 14 x 10
   variable variable_level stat_name stat_label stat stat_fmt
1    AGEGR1            <65         n          n            xx
2    AGEGR1            <65         p          %          xx.x
3    AGEGR1            <65         N          N            xx
4    AGEGR1           >=65         n          n            xx
5    AGEGR1           >=65         p          %          xx.x
6    AGEGR1           >=65         N          N            xx
7       AGE                        N          N            xx
8       AGE                     mean       Mean          xx.x
9       AGE                       sd         SD          xx.x
10      AGE                   median     Median          xx.x
11      AGE                      p25         Q1          xx.x
12      AGE                      p75         Q3          xx.x
13      AGE                      min        Min          xx.x
14      AGE                      max        Max          xx.x
ℹ 4 more variables: context, fmt_fn, warning, error

Mock ARDs

cards::bind_ard(
  cards::mock_continuous(variables = "AGE", 
                         by = list(ARM = c("Drug A", "Drug B"))),
  cards::mock_categorical(variables = list(AGEGR1 = c("<65", ">=65")),
                          by = list(ARM = c("Drug A", "Drug B")))
) |> 
  gtsummary::tbl_ard_summary(
    by = ARM,
    type  = AGE ~ "continuous2",
    statistic = AGE ~ c("{N}", "{mean} ({sd})", "{median} ({p25}, {p75})")
  )
Characteristic Drug A1 Drug B1
AGE

    N xx xx
    Mean (SD) xx.x (xx.x) xx.x (xx.x)
    Median (Q1, Q3) xx.x (xx.x, xx.x) xx.x (xx.x, xx.x)
AGEGR1

    <65 xx (xx.x%) xx (xx.x%)
    >=65 xx (xx.x%) xx (xx.x%)
1 n (%)

Other ARD Representations

While the data frame-based ARD is easy to work with, language-agnostic representations are often useful.

YAML

cards::as_nested_list(ard) |> 
  yaml::as.yaml() |> 
  cat()
variable:
  AGE:
    group1:
      ARM:
        group1_level:
          Placebo:
            stat_name:
              'N':
                stat: 86
                stat_fmt: '86'
                warning: ~
                error: ~
                context: continuous
              mean:
                stat: 75.2093023
...

JSON

cards::as_nested_list(ard) |> 
  jsonlite::toJSON(pretty = TRUE)
{
  "variable": {
    "AGE": {
      "group1": {
        "ARM": {
          "group1_level": {
            "Placebo": {
              "stat_name": {
                "N": {
                  "stat": [86],
                  "stat_fmt": ["86"],
                  "warning": {},
                  "error": {},
                  "context": ["continuous"]
                },
...

Break

10:00