{cardx} & other extras

Daniel D. Sjoberg and Becca Krouse

Workshop outline

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}

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

  • Also includes function inputs; useful for re-use, e.g. we record 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.

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

  • Update code for a two-sample t-test
pharmaverseadam::adsl |> 
  dplyr::filter(ARM %in% c("Xanomeline High Dose", "Xanomeline Low Dose")) |>
  cards::ard_complex(
    variables = AGE,
    statistic = ~ list(t_test = \(x, data, ...) broom::tidy(t.test(x ~ data$ARM)))
  ) |> 
  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

Building your own ARD functions

See the full vignette for details on creating both one-off ARDs and functions for creating ARDs.

{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(), glmtoolbox::glmgee(), 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::vgam(), VGAM::vglm() (and more)

{cardx} Regression Example

library(survival)
library(gtsummary)

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

# put model in a summary table
tbl <- tbl_regression(mod, exponentiate = TRUE) |> 
  add_n(location = c('label', 'level')) |> 
  add_nevent(location = c('label', 'level')) |> 
  bold_labels()
Characteristic N Event N HR 95% CI p-value
Description of Planned Arm 254 6


    Placebo 86 3
    Xanomeline High Dose 84 2 1.94 0.27, 14.1 0.5
    Xanomeline Low Dose 84 1 0.65 0.06, 7.47 0.7
Age 254 6 1.10 0.95, 1.29 0.2
Abbreviations: CI = Confidence Interval, HR = Hazard Ratio

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

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

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"]
                },
...