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 24: ARD-First Reporting with cards and cardx
  • 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 Step 1: Confirm Available Variables
  • 4 Step 2: Continuous Summary ARD
  • 5 Step 3: Categorical Summary ARD
  • 6 Step 4: Missing Values ARD
  • 7 Step 5: T-test ARD (cardx)
  • 8 Step 6: Chi-Square Test ARD (cardx)
  • 9 Step 7: Wilcoxon Rank-Sum ARD (cardx)
  • 10 Step 8: Combine Summary ARDs
  • 11 Step 9: Inspect the Descriptive ARD
  • 12 Step 10: Extract Specific Statistics
  • 13 Step 11: Format Statistics from ARD
  • 14 Step 12: ARD-Backed Summary Table
  • 15 Step 13: ARD for Regression (cardx)
  • 16 Step 14: Validation Checks
  • 17 Summary
  • 18 Export ARD as CSV
  • 19 Key Takeaways
  • 20 Next Steps
  • 21 Resources

Day 24: ARD-First Reporting with cards and cardx

CDISC Analysis Results Data - computation decoupled from presentation

Back to Roadmap

1 Overview

The Analysis Results Dataset (ARD) is an emerging CDISC standard that stores statistical results in a tidy, machine-readable format - reusable across tables, figures, and reports without re-running analyses.

Today’s packages:

  • cards - core ARD objects: continuous, categorical summaries
  • cardx - extended ARD: t-tests, chi-square, regression models
  • pharmaverseadam - reference ADaM datasets as input

2 Setup

library(cards)
library(cardx)
library(pharmaverseadam)
library(dplyr)

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

cat("Safety population N:", nrow(adsl_safe), "\n")
Safety population N: 254 
cat("Treatment arms:\n")
Treatment arms:
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

3 Step 1: Confirm Available Variables

# Always verify before use - BMIBL is NOT in pharmaverseadam::adsl
names(adsl_safe)
 [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"

Note: pharmaverseadam::adsl does not contain BMIBL. We use AGE as the only continuous variable.


4 Step 2: Continuous Summary ARD

ard_cont <- cards::ard_continuous(
  adsl_safe,
  by        = TRT01A,
  variables = AGE,
  statistic = ~ cards::continuous_summary_fns(c("mean", "sd", "median", "p25", "p75", "min", "max"))
)

ard_cont %>%
  dplyr::select(group1, group1_level, variable, stat_name, stat_label, stat)
   group1 group1_level variable stat_name stat_label   stat
1  TRT01A      Placebo      AGE      mean       Mean 75.209
2  TRT01A      Placebo      AGE        sd         SD   8.59
3  TRT01A      Placebo      AGE    median     Median     76
4  TRT01A      Placebo      AGE       p25         Q1     69
5  TRT01A      Placebo      AGE       p75         Q3     82
6  TRT01A      Placebo      AGE       min        Min     52
7  TRT01A      Placebo      AGE       max        Max     89
8  TRT01A    Xanomeli…      AGE      mean       Mean 73.778
9  TRT01A    Xanomeli…      AGE        sd         SD  7.944
10 TRT01A    Xanomeli…      AGE    median     Median   75.5

ARD Structure:

Column Meaning
group1 Grouping variable (TRT01A)
group1_level Level of grouping variable
variable Analysis variable (AGE)
stat_name Statistic name (mean, sd, …)
stat_label Human-readable label
stat Computed value (list column)

5 Step 3: Categorical Summary ARD

ard_cat <- cards::ard_categorical(
  adsl_safe,
  by        = TRT01A,
  variables = c(SEX, RACE, AGEGR1)
)

ard_cat %>%
  dplyr::select(group1, group1_level, variable, variable_level, stat_name, stat_label, stat) %>%
  head(12)
   group1 group1_level variable variable_level stat_name stat_label  stat
1  TRT01A      Placebo      SEX              F         n          n    53
2  TRT01A      Placebo      SEX              F         N          N    86
3  TRT01A      Placebo      SEX              F         p          % 0.616
4  TRT01A      Placebo      SEX              M         n          n    33
5  TRT01A      Placebo      SEX              M         N          N    86
6  TRT01A      Placebo      SEX              M         p          % 0.384
7  TRT01A      Placebo     RACE      AMERICAN…         n          n     0
8  TRT01A      Placebo     RACE      AMERICAN…         N          N    86
9  TRT01A      Placebo     RACE      AMERICAN…         p          %     0
10 TRT01A      Placebo     RACE      BLACK OR…         n          n     8
11 TRT01A      Placebo     RACE      BLACK OR…         N          N    86
12 TRT01A      Placebo     RACE      BLACK OR…         p          % 0.093

6 Step 4: Missing Values ARD

# Note: keep ard_missing separate - do not combine with ard_cont/cat
# (overlapping stat_names cause duplicates in bind_ard)
ard_miss <- cards::ard_missing(
  adsl_safe,
  by        = TRT01A,
  variables = c(AGE, SEX, RACE, AGEGR1, ETHNIC)
)

ard_miss %>%
  dplyr::select(group1, group1_level, variable, stat_name, stat_label, stat)
   group1 group1_level variable stat_name stat_label stat
1  TRT01A      Placebo      AGE     N_obs  Vector L…   86
2  TRT01A      Placebo      AGE    N_miss  N Missing    0
3  TRT01A      Placebo      AGE N_nonmiss  N Non-mi…   86
4  TRT01A      Placebo      AGE    p_miss  % Missing    0
5  TRT01A      Placebo      AGE p_nonmiss  % Non-mi…    1
6  TRT01A      Placebo      SEX     N_obs  Vector L…   86
7  TRT01A      Placebo      SEX    N_miss  N Missing    0
8  TRT01A      Placebo      SEX N_nonmiss  N Non-mi…   86
9  TRT01A      Placebo      SEX    p_miss  % Missing    0
10 TRT01A      Placebo      SEX p_nonmiss  % Non-mi…    1

7 Step 5: T-test ARD (cardx)

ard_t <- cardx::ard_stats_t_test(
  adsl_safe,
  by        = TRT01A,
  variables = AGE
)

ard_t %>%
  dplyr::select(group1, variable, stat_name, stat_label, stat)
   group1 variable   stat_name stat_label  stat
1  TRT01A      AGE    estimate  Mean Dif…      
2  TRT01A      AGE   estimate1  Group 1 …      
3  TRT01A      AGE   estimate2  Group 2 …      
4  TRT01A      AGE   statistic  t Statis…      
5  TRT01A      AGE     p.value    p-value      
6  TRT01A      AGE   parameter  Degrees …      
7  TRT01A      AGE    conf.low  CI Lower…      
8  TRT01A      AGE   conf.high  CI Upper…      
9  TRT01A      AGE      method     method      
10 TRT01A      AGE alternative  alternat…      
11 TRT01A      AGE          mu    H0 Mean     0
12 TRT01A      AGE      paired  Paired t… FALSE
13 TRT01A      AGE   var.equal  Equal Va… FALSE
14 TRT01A      AGE  conf.level  CI Confi…  0.95

8 Step 6: Chi-Square Test ARD (cardx)

ard_chi <- cardx::ard_stats_chisq_test(
  adsl_safe,
  by        = TRT01A,
  variables = SEX
)

ard_chi %>%
  dplyr::select(group1, variable, stat_name, stat_label, stat)
  group1 variable        stat_name stat_label                        stat
1 TRT01A      SEX        statistic  X-square…                       2.761
2 TRT01A      SEX          p.value    p-value                       0.251
3 TRT01A      SEX        parameter  Degrees …                           2
4 TRT01A      SEX           method     method                   Pearson'…
5 TRT01A      SEX          correct    correct                        TRUE
6 TRT01A      SEX                p          p rep, 1/length(x), length(x)
7 TRT01A      SEX        rescale.p  rescale.p                       FALSE
8 TRT01A      SEX simulate.p.value  simulate…                       FALSE
9 TRT01A      SEX                B          B                        2000

9 Step 7: Wilcoxon Rank-Sum ARD (cardx)

ard_wilcox <- cardx::ard_stats_wilcox_test(
  adsl_safe,
  by        = TRT01A,
  variables = AGE
)

ard_wilcox %>%
  dplyr::select(group1, variable, stat_name, stat_label, stat)
   group1 variable   stat_name stat_label  stat
1  TRT01A      AGE   statistic  X-square…      
2  TRT01A      AGE     p.value    p-value      
3  TRT01A      AGE      method     method      
4  TRT01A      AGE alternative  alternat…      
5  TRT01A      AGE          mu         mu     0
6  TRT01A      AGE      paired  Paired t… FALSE
7  TRT01A      AGE       exact      exact      
8  TRT01A      AGE     correct    correct  TRUE
9  TRT01A      AGE    conf.int   conf.int FALSE
10 TRT01A      AGE  conf.level  CI Confi…  0.95
11 TRT01A      AGE    tol.root   tol.root     0
12 TRT01A      AGE digits.rank  digits.r…   Inf

10 Step 8: Combine Summary ARDs

# bind_ard() with .update = TRUE resolves duplicate stat_names across ARD objects
# Keep inferential (t-test, chisq, wilcox) separate from descriptive (cont, cat)
ard_descriptive <- cards::bind_ard(
  ard_cont,
  ard_cat,
  .update = TRUE
)

ard_inferential <- cards::bind_ard(
  ard_t,
  ard_chi,
  ard_wilcox,
  .update = TRUE
)

cat("Descriptive ARD rows:", nrow(ard_descriptive), "\n")
Descriptive ARD rows: 84 
cat("Inferential ARD rows:", nrow(ard_inferential), "\n")
Inferential ARD rows: 28 
cat("\nDescriptive contexts:\n")

Descriptive contexts:
ard_descriptive %>% dplyr::count(context)
    context
1 categori…
2 continuo…
cat("\nInferential contexts:\n")

Inferential contexts:
ard_inferential %>% dplyr::count(context)
    context
1 stats_ch…
2 stats_t_…
3 stats_wi…

11 Step 9: Inspect the Descriptive ARD

ard_descriptive %>%
  dplyr::select(group1, group1_level, variable, context, stat_name, stat_label, stat) %>%
  head(20)
   group1 group1_level variable stat_name stat_label   stat
1  TRT01A      Placebo      AGE      mean       Mean 75.209
2  TRT01A      Placebo      AGE        sd         SD   8.59
3  TRT01A      Placebo      AGE    median     Median     76
4  TRT01A      Placebo      AGE       p25         Q1     69
5  TRT01A      Placebo      AGE       p75         Q3     82
6  TRT01A      Placebo      AGE       min        Min     52
7  TRT01A      Placebo      AGE       max        Max     89
8  TRT01A    Xanomeli…      AGE      mean       Mean 73.778
9  TRT01A    Xanomeli…      AGE        sd         SD  7.944
10 TRT01A    Xanomeli…      AGE    median     Median   75.5
11 TRT01A    Xanomeli…      AGE       p25         Q1     70
12 TRT01A    Xanomeli…      AGE       p75         Q3     79
13 TRT01A    Xanomeli…      AGE       min        Min     56
14 TRT01A    Xanomeli…      AGE       max        Max     88
15 TRT01A    Xanomeli…      AGE      mean       Mean 75.958
16 TRT01A    Xanomeli…      AGE        sd         SD  8.114
17 TRT01A    Xanomeli…      AGE    median     Median     78
18 TRT01A    Xanomeli…      AGE       p25         Q1     71
19 TRT01A    Xanomeli…      AGE       p75         Q3     82
20 TRT01A    Xanomeli…      AGE       min        Min     51

12 Step 10: Extract Specific Statistics

# Pull mean AGE per treatment arm
ard_descriptive %>%
  dplyr::filter(
    variable  == "AGE",
    stat_name == "mean",
    context   == "continuous"
  ) %>%
  dplyr::select(group1_level, stat_name, stat) %>%
  dplyr::mutate(stat = round(unlist(stat), 1))
  group1_level stat_name stat
1      Placebo      mean 75.2
2    Xanomeli…      mean 73.8
3    Xanomeli…      mean   76

13 Step 11: Format Statistics from ARD

ard_formatted <- ard_descriptive %>%
  dplyr::filter(
    variable  == "AGE",
    context   == "continuous",
    stat_name %in% c("mean", "sd", "median", "p25", "p75")
  ) %>%
  cards::apply_fmt_fn()

ard_formatted %>%
  dplyr::select(group1_level, stat_name, stat_label, stat_fmt)
   group1_level stat_name stat_label stat_fmt
1       Placebo      mean       Mean     75.2
2       Placebo        sd         SD      8.6
3       Placebo    median     Median     76.0
4       Placebo       p25         Q1     69.0
5       Placebo       p75         Q3     82.0
6     Xanomeli…      mean       Mean     73.8
7     Xanomeli…        sd         SD      7.9
8     Xanomeli…    median     Median     75.5
9     Xanomeli…       p25         Q1     70.0
10    Xanomeli…       p75         Q3     79.0
11    Xanomeli…      mean       Mean     76.0
12    Xanomeli…        sd         SD      8.1
13    Xanomeli…    median     Median     78.0
14    Xanomeli…       p25         Q1     71.0
15    Xanomeli…       p75         Q3     82.0

14 Step 12: ARD-Backed Summary Table

mean_age <- ard_descriptive %>%
  dplyr::filter(variable == "AGE", stat_name == "mean", context == "continuous") %>%
  dplyr::mutate(treatment = as.character(group1_level), mean_age = round(unlist(stat), 1)) %>%
  dplyr::select(treatment, mean_age)

sd_age <- ard_descriptive %>%
  dplyr::filter(variable == "AGE", stat_name == "sd", context == "continuous") %>%
  dplyr::mutate(treatment = as.character(group1_level), sd_age = round(unlist(stat), 1)) %>%
  dplyr::select(treatment, sd_age)

age_summary <- dplyr::left_join(mean_age, sd_age, by = "treatment") %>%
  dplyr::mutate(`Mean (SD)` = paste0(mean_age, " (", sd_age, ")")) %>%
  dplyr::select(Treatment = treatment, `Mean (SD)`)

age_summary
data frame with 0 columns and 3 rows

15 Step 13: ARD for Regression (cardx)

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()
)

ard_logit <- cardx::ard_regression(
  logit_mod,
  exponentiate = TRUE
)

ard_logit %>%
  dplyr::select(variable, stat_name, stat_label, stat) %>%
  head(12)
   variable stat_name stat_label      stat
1       AGE      term       term       AGE
2       AGE var_label      Label       Age
3       AGE var_class      Class   numeric
4       AGE  var_type       Type continuo…
5       AGE     label  Level La…       Age
6       AGE     n_obs     N Obs.       254
7       AGE   n_event   N Events       111
8       AGE  estimate  Coeffici…     0.983
9       AGE std.error  Standard…     0.016
10      AGE statistic  statistic    -1.117
11      AGE   p.value    p-value     0.264
12      AGE  conf.low  CI Lower…     0.953

16 Step 14: Validation Checks

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

=== ARD Validation ===
# Check 1: No error records in descriptive ARD
check1 <- ard_descriptive %>%
  dplyr::filter(!sapply(error, is.null))
cat("Check 1 - Descriptive ARD error records:", nrow(check1), "\n")
Check 1 - Descriptive ARD error records: 0 
# Check 2: AGE mean stat is numeric
mean_stat <- ard_descriptive %>%
  dplyr::filter(variable == "AGE", stat_name == "mean", context == "continuous") %>%
  dplyr::pull(stat)
check2 <- all(sapply(mean_stat, is.numeric))
cat("Check 2 - AGE mean is numeric:", check2, "\n")
Check 2 - AGE mean is numeric: TRUE 
# Check 3: SEX counts per treatment arm
cat_n <- ard_descriptive %>%
  dplyr::filter(variable == "SEX", stat_name == "n", context == "categorical") %>%
  dplyr::mutate(val = unlist(stat)) %>%
  dplyr::group_by(group1_level) %>%
  dplyr::summarise(total = sum(val), .groups = "drop")
cat("Check 3 - SEX counts per arm:\n")
Check 3 - SEX counts per arm:
print(cat_n)
# A tibble: 3 × 2
  group1_level total
  <list>       <int>
1 <chr [1]>       86
2 <chr [1]>       72
3 <chr [1]>       96
cat("\n✓ ARD validation complete\n")

✓ ARD validation complete

17 Summary

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

=== Day 24 Summary ===
cat("Descriptive ARD contexts:\n")
Descriptive ARD contexts:
ard_descriptive %>% dplyr::count(context) %>% print()
    context
1 categori…
2 continuo…
cat("\nInferential ARD contexts:\n")

Inferential ARD contexts:
ard_inferential %>% dplyr::count(context) %>% print()
    context
1 stats_ch…
2 stats_t_…
3 stats_wi…
cat("\nVariables covered: AGE, SEX, RACE, AGEGR1, ETHNIC\n")

Variables covered: AGE, SEX, RACE, AGEGR1, ETHNIC
cat("Tests: t-test, chi-square, Wilcoxon rank-sum\n")
Tests: t-test, chi-square, Wilcoxon rank-sum
cat("Regression: logistic (AGE + TRT01A)\n")
Regression: logistic (AGE + TRT01A)

18 Export ARD as CSV

ard_export <- ard_descriptive %>%
  dplyr::mutate(
    stat_value     = sapply(stat, function(x) if (is.null(x)) NA_character_ else as.character(x)),
    group1_level   = as.character(group1_level),
    variable_level = sapply(variable_level, function(x) if (is.null(x)) NA_character_ else as.character(x))
  ) %>%
  dplyr::select(group1, group1_level, variable, variable_level,
                context, stat_name, stat_label, stat_value)

readr::write_csv(ard_export, "ard_day24.csv")
cat("✓ ard_day24.csv exported -", nrow(ard_export), "rows\n")
✓ ard_day24.csv exported - 84 rows

19 Key Takeaways

  1. cards::ard_continuous() and ard_categorical() build tidy ARDs from ADaM data
  2. cardx extends with inferential stats: t-test, chi-square, Wilcoxon, regression
  3. bind_ard(.update = TRUE) resolves duplicate stat_names across ARD objects
  4. Keep descriptive and inferential ARDs in separate bind_ard() calls to avoid duplicates
  5. ARD stat column is a list column - use unlist() or apply_fmt_fn() to extract values
  6. pharmaverseadam::adsl has no BMIBL - only AGE is available as continuous

20 Next Steps

Day 25: gtsummary + tfrmt - ARD-backed production tables Day 26: flextable + officer - Word/RTF clinical output Day 27: r2rtf - submission-ready RTF tables


21 Resources

  • cards CRAN: https://cran.r-project.org/package=cards
  • cardx documentation: https://insightsengineering.github.io/cardx/
  • CDISC ARS standard: https://www.cdisc.org/standards/foundational/ars
  • pharmaverse cardinal project: https://github.com/pharmaverse/cardinal

End of Day 24

 

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