New Function

ard_hierarchical_stack() is a function similar to ard_stack() that simplifies the process of creating an ARD (analysis ready dataset) for nested/hierarchical tables. The expected function call will look something like:

ard_hierarchal_stack(
     .data = adae,
     .by = ARM,
     .overall = TRUE,
     .id = USUBJID,
     .overallrow = TRUE,
     ard_hierarchical(
            variables = AEBODSYS,
            denominator = adsl
     ),
... 
)

The function will take a single “common” dataset to pass over the various ard_*() calls. One quality-of-life improvement this function allows for is the ability to easily calculate the various statistics stratified by treatment types and over all treatment types using the .overall argument. Of course, one could achieve the same result by using the functions in the cards package by themselves. The process is outlined below.

  • The .overall argument is used to indicate whether or not the user wishes to include the “total”/“overall” column’s data. This argument will ignore the .by argument when performing calculations
  • .overallrow is used to indicate whether or not top level calculations should be performed. e.g. total AE per ARM or number of subjects with at least one AE
  • .id is an argument that will be passed to ard_hierarchical() and is used to check whether or not there are any duplicates in the variable provided

Using Existing Functions

For this example, the data will come from the chevron package.

# Load data
adsl <- chevron::syn_data$adsl
adae <- chevron::syn_data$adae

We will be recreating this AET02 table as an ARD.

# Ensure character variables are converted to factors and empty strings and NAs are explicit missing levels.
adsl <- df_explicit_na(adsl)
adae <- df_explicit_na(adae) %>%
  var_relabel(
    AEBODSYS = "MedDRA System Organ Class",
    AEDECOD = "MedDRA Preferred Term"
  ) %>%
  filter(ANL01FL == "Y")

# Define the split function
split_fun <- drop_split_levels

lyt <- basic_table(show_colcounts = TRUE) %>%
  split_cols_by(var = "ACTARM") %>%
  add_overall_col(label = "All Patients") %>%
  analyze_num_patients(
    vars = "USUBJID",
    .stats = c("unique", "nonunique"),
    .labels = c(
      unique = "Total number of patients with at least one adverse event",
      nonunique = "Overall total number of events"
    )
  ) %>%
  split_rows_by(
    "AEBODSYS",
    child_labels = "visible",
    nested = FALSE,
    split_fun = split_fun,
    label_pos = "topleft",
    split_label = obj_label(adae$AEBODSYS)
  ) %>%
  summarize_num_patients(
    var = "USUBJID",
    .stats = c("unique", "nonunique"),
    .labels = c(
      unique = "Total number of patients with at least one adverse event",
      nonunique = "Total number of events"
    )
  ) %>%
  count_occurrences(
    vars = "AEDECOD",
    .indent_mods = -1L
  ) %>%
  append_varlabels(adae, "AEDECOD", indent = 1L)

result <- build_table(lyt, df = adae, alt_counts_df = adsl) 

#saved result as tt_export.html
includeHTML("tt_export.html")

rtable

MedDRA System Organ Class A: Drug X B: Placebo C: Combination All Patients
MedDRA Preferred Term (N=15) (N=15) (N=15) (N=45)
Total number of patients with at least one adverse event 13 (86.7%) 14 (93.3%) 15 (100%) 42 (93.3%)
Overall total number of events 58 59 99 216
cl A.1
Total number of patients with at least one adverse event 7 (46.7%) 6 (40.0%) 10 (66.7%) 23 (51.1%)
Total number of events 8 11 16 35
dcd A.1.1.1.1 3 (20.0%) 1 (6.7%) 6 (40.0%) 10 (22.2%)
dcd A.1.1.1.2 5 (33.3%) 6 (40.0%) 6 (40.0%) 17 (37.8%)
cl B.1
Total number of patients with at least one adverse event 5 (33.3%) 6 (40.0%) 8 (53.3%) 19 (42.2%)
Total number of events 6 6 12 24
dcd B.1.1.1.1 5 (33.3%) 6 (40.0%) 8 (53.3%) 19 (42.2%)
cl B.2
Total number of patients with at least one adverse event 11 (73.3%) 8 (53.3%) 10 (66.7%) 29 (64.4%)
Total number of events 18 15 20 53
dcd B.2.1.2.1 5 (33.3%) 6 (40.0%) 5 (33.3%) 16 (35.6%)
dcd B.2.2.3.1 8 (53.3%) 6 (40.0%) 7 (46.7%) 21 (46.7%)
cl C.1
Total number of patients with at least one adverse event 4 (26.7%) 4 (26.7%) 5 (33.3%) 13 (28.9%)
Total number of events 4 9 10 23
dcd C.1.1.1.3 4 (26.7%) 4 (26.7%) 5 (33.3%) 13 (28.9%)
cl C.2
Total number of patients with at least one adverse event 6 (40.0%) 4 (26.7%) 8 (53.3%) 18 (40.0%)
Total number of events 6 4 12 22
dcd C.2.1.2.1 6 (40.0%) 4 (26.7%) 8 (53.3%) 18 (40.0%)
cl D.1
Total number of patients with at least one adverse event 9 (60.0%) 5 (33.3%) 11 (73.3%) 25 (55.6%)
Total number of events 13 9 19 41
dcd D.1.1.1.1 4 (26.7%) 4 (26.7%) 7 (46.7%) 15 (33.3%)
dcd D.1.1.4.2 6 (40.0%) 2 (13.3%) 7 (46.7%) 15 (33.3%)
cl D.2
Total number of patients with at least one adverse event 2 (13.3%) 5 (33.3%) 7 (46.7%) 14 (31.1%)
Total number of events 3 5 10 18
dcd D.2.1.5.3 2 (13.3%) 5 (33.3%) 7 (46.7%) 14 (31.1%)

Patients Per Arm

We’ll start from the top of the table. To retreive the total number of patients by the treatment arm and the overall number of patients, all we need is the adsl dataset. The statistic n is chosen because that’s all we really need for this top row in this example. By default the function will give you the number per category (n), the proportion of observations in the category (p), and the total number of observations in the dataset (N).

To calculate the total number of patients in the dataset, I opted to transform the arm to be the same for all patients. You’re welcome to choose another method or function if it’s more intuitive another way.

# By ARM
card_patients_by_arm <- adsl |> 
  cards::ard_categorical(variables = ARM, statistic = everything() ~ c("n"))

## display
card_patients_by_arm %>%
  kable(format = "html") %>%
  kable_styling() %>%
  kableExtra::scroll_box(width = "100%")
variable variable_level context stat_name stat_label stat fmt_fn warning error
ARM 1 categorical n n 15 0 NULL NULL
ARM 2 categorical n n 15 0 NULL NULL
ARM 3 categorical n n 15 0 NULL NULL
# Overall 
card_patient_total <- adsl |> 
  mutate(ARM = "total") |>
  cards::ard_categorical(variables = ARM, statistic = everything() ~ c("n"))

## display
card_patient_total %>%
  kable(format = "html") %>%
  kable_styling() %>%
  kableExtra::scroll_box(width = "100%")
variable variable_level context stat_name stat_label stat fmt_fn warning error
ARM total categorical n n 45 0 NULL NULL

Number of Patients With at Least One AE

Moving on, we’ll be calculating the values for the second row. In order to return the correct values, we’ll have to do a minor transformation on the data so we only have one observation per patient per AE. In the first place, if they are included in this dataset, they already have at least one AE.

Similarly to last time, ARM was changed to only have one level for the calculation over all treatment levels.

# By ARM
card_any_ae <- adae |> 
  dplyr::slice(1L, .by = USUBJID) |> 
  dplyr::mutate(any_ae = TRUE) |> 
  cards::ard_dichotomous(by = ARM, variable = any_ae, denominator = adsl)

## display
card_any_ae %>%
  kable(format = "html") %>%
  kable_styling() %>%
  kableExtra::scroll_box(width = "100%", height = "200px")
group1 group1_level variable variable_level context stat_name stat_label stat fmt_fn warning error
ARM 1 any_ae TRUE dichotomous n n 13 0 NULL NULL
ARM 1 any_ae TRUE dichotomous N N 15 0 NULL NULL
ARM 1 any_ae TRUE dichotomous p % 0.8666667 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 2 any_ae TRUE dichotomous n n 14 0 NULL NULL
ARM 2 any_ae TRUE dichotomous N N 15 0 NULL NULL
ARM 2 any_ae TRUE dichotomous p % 0.9333333 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 3 any_ae TRUE dichotomous n n 15 0 NULL NULL
ARM 3 any_ae TRUE dichotomous N N 15 0 NULL NULL
ARM 3 any_ae TRUE dichotomous p % 1 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
# Overall
adsl_total <- adsl |>
  dplyr::mutate(ARM = "total")

card_any_ae_all <- adae |>
  dplyr::slice(1L, .by = USUBJID) |>
  dplyr:: mutate(any_ae = TRUE, ARM = "total") |>
  cards::ard_dichotomous(by = ARM, variable = any_ae, denominator = adsl_total)

## display
card_any_ae_all %>%
  kable(format = "html") %>%
  kable_styling() %>%
  kableExtra::scroll_box(width = "100%", height = "200px")
group1 group1_level variable variable_level context stat_name stat_label stat fmt_fn warning error
ARM total any_ae TRUE dichotomous n n 42 0 NULL NULL
ARM total any_ae TRUE dichotomous N N 45 0 NULL NULL
ARM total any_ae TRUE dichotomous p % 0.9333333 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL

Overall Total Number of Events

For the total number of events in the data, we’ll be using the ard_hierarchical_count() function over the ARM variable.

# By ARM
card_ae_count <-adae |> 
  cards::ard_hierarchical_count(variables = ARM)

## display
card_ae_count %>%
  kable(format = "html") %>%
  kable_styling() %>%
  kableExtra::scroll_box(width = "100%", height = "200px")
variable variable_level context stat_name stat_label stat fmt_fn warning error
ARM 1 hierarchical_count n n 58 0 NULL NULL
ARM 2 hierarchical_count n n 59 0 NULL NULL
ARM 3 hierarchical_count n n 99 0 NULL NULL
# Overall
card_ae_count_all <- adae |> 
  mutate(ARM = "total") |>
  cards::ard_hierarchical_count(variables = ARM)

## display
card_ae_count_all %>%
  kable(format = "html") %>%
  kable_styling() %>%
  kableExtra::scroll_box(width = "100%")
variable variable_level context stat_name stat_label stat fmt_fn warning error
ARM total hierarchical_count n n 216 0 NULL NULL

Total Number of Patients With At Least One AE By SOC

Now we’re moving into the actual hierarchical portion of the table; the part split into summaries by SOC. For the total number of patients with at least one AE, the process is similar to before: modify the data so we only have one observation per subject per treatment arm and body system.

For the calculation done over all treatment arms, we just need to remove the by argument in ard_hierarchical().

# By ARM
card_by_soc_1ae <- adae |> 
  # keep one AE per SOC
  slice(1L, .by = c(USUBJID, ARM, AEBODSYS)) |> 
  cards::ard_hierarchical(
    by = ARM, 
    variables = AEBODSYS,
    denominator = adsl
  ) |> 
  filter(stat_name %in% c("n", "p"))

## display
head(card_by_soc_1ae, 10) %>%
  kable(format = "html") %>%
  kable_styling() %>%
  kableExtra::scroll_box(width = "100%", height = "300px")
group1 group1_level variable variable_level context stat_name stat_label stat fmt_fn warning error
ARM 1 AEBODSYS 1 hierarchical n n 7 0 NULL NULL
ARM 1 AEBODSYS 1 hierarchical p % 0.4666667 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 2 AEBODSYS 1 hierarchical n n 6 0 NULL NULL
ARM 2 AEBODSYS 1 hierarchical p % 0.4 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 3 AEBODSYS 1 hierarchical n n 10 0 NULL NULL
ARM 3 AEBODSYS 1 hierarchical p % 0.6666667 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 1 AEBODSYS 2 hierarchical n n 5 0 NULL NULL
ARM 1 AEBODSYS 2 hierarchical p % 0.3333333 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 2 AEBODSYS 2 hierarchical n n 6 0 NULL NULL
ARM 2 AEBODSYS 2 hierarchical p % 0.4 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
# Overall 
card_by_soc_1ae_all <- adae |> 
  # keep one AE per SOC
  slice(1L, .by = c(USUBJID, ARM, AEBODSYS)) |> 
  cards::ard_hierarchical(
    variables = AEBODSYS,
    denominator = adsl
  ) |> 
  filter(stat_name %in% c("n", "p"))

## display
head(card_by_soc_1ae_all, 10) %>%
  kable(format = "html") %>%
  kable_styling() %>%
  kableExtra::scroll_box(width = "100%", height = "300px")
variable variable_level context stat_name stat_label stat fmt_fn warning error
AEBODSYS 1 hierarchical n n 23 0 NULL NULL
AEBODSYS 1 hierarchical p % 0.5111111 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
AEBODSYS 2 hierarchical n n 19 0 NULL NULL
AEBODSYS 2 hierarchical p % 0.4222222 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
AEBODSYS 3 hierarchical n n 29 0 NULL NULL
AEBODSYS 3 hierarchical p % 0.6444444 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
AEBODSYS 4 hierarchical n n 13 0 NULL NULL
AEBODSYS 4 hierarchical p % 0.2888889 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
AEBODSYS 5 hierarchical n n 18 0 NULL NULL
AEBODSYS 5 hierarchical p % 0.4 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL

Total Number of Events By SOC

To find the counts of the total number of events by SOC, we’ll be using the ard_hierarchical_count() function but this time by AEBODSYS.

# By ARM  
card_count_soc_all_event <- adae |> 
  cards::ard_hierarchical_count(
    by = ARM, 
    variables = AEBODSYS
  )

## display
card_count_soc_all_event %>%
  kable(format = "html") %>%
  kable_styling() %>%
  kableExtra::scroll_box(width = "100%", height = "300px")
group1 group1_level variable variable_level context stat_name stat_label stat fmt_fn warning error
ARM 1 AEBODSYS 1 hierarchical_count n n 8 0 NULL NULL
ARM 2 AEBODSYS 1 hierarchical_count n n 11 0 NULL NULL
ARM 3 AEBODSYS 1 hierarchical_count n n 16 0 NULL NULL
ARM 1 AEBODSYS 2 hierarchical_count n n 6 0 NULL NULL
ARM 2 AEBODSYS 2 hierarchical_count n n 6 0 NULL NULL
ARM 3 AEBODSYS 2 hierarchical_count n n 12 0 NULL NULL
ARM 1 AEBODSYS 3 hierarchical_count n n 18 0 NULL NULL
ARM 2 AEBODSYS 3 hierarchical_count n n 15 0 NULL NULL
ARM 3 AEBODSYS 3 hierarchical_count n n 20 0 NULL NULL
ARM 1 AEBODSYS 4 hierarchical_count n n 4 0 NULL NULL
ARM 2 AEBODSYS 4 hierarchical_count n n 9 0 NULL NULL
ARM 3 AEBODSYS 4 hierarchical_count n n 10 0 NULL NULL
ARM 1 AEBODSYS 5 hierarchical_count n n 6 0 NULL NULL
ARM 2 AEBODSYS 5 hierarchical_count n n 4 0 NULL NULL
ARM 3 AEBODSYS 5 hierarchical_count n n 12 0 NULL NULL
ARM 1 AEBODSYS 6 hierarchical_count n n 13 0 NULL NULL
ARM 2 AEBODSYS 6 hierarchical_count n n 9 0 NULL NULL
ARM 3 AEBODSYS 6 hierarchical_count n n 19 0 NULL NULL
ARM 1 AEBODSYS 7 hierarchical_count n n 3 0 NULL NULL
ARM 2 AEBODSYS 7 hierarchical_count n n 5 0 NULL NULL
ARM 3 AEBODSYS 7 hierarchical_count n n 10 0 NULL NULL
# Overall
card_count_soc_all_event_total <- adae |> 
  cards::ard_hierarchical_count(
    variables = AEBODSYS
  )

## display
card_count_soc_all_event_total %>%
  kable(format = "html") %>%
  kable_styling() %>%
  kableExtra::scroll_box(width = "100%", height = "300px")
variable variable_level context stat_name stat_label stat fmt_fn warning error
AEBODSYS 1 hierarchical_count n n 35 0 NULL NULL
AEBODSYS 2 hierarchical_count n n 24 0 NULL NULL
AEBODSYS 3 hierarchical_count n n 53 0 NULL NULL
AEBODSYS 4 hierarchical_count n n 23 0 NULL NULL
AEBODSYS 5 hierarchical_count n n 22 0 NULL NULL
AEBODSYS 6 hierarchical_count n n 41 0 NULL NULL
AEBODSYS 7 hierarchical_count n n 18 0 NULL NULL

Breakdown of AE By SOC

Finally, breaking the categories down further by AE types, we perform a similar operation to when we calculated the total number of patients with at least one adverse event by body system, just adding an extra nesting factor (AEDECOD) to the mix.

# By ARM
card_ae_rate <-adae |> 
  # keep one AE per subject
  slice(1L, .by = c(USUBJID, ARM, AEBODSYS, AEDECOD)) |> 
  cards::ard_hierarchical(
    by = ARM, 
    variables = c(AEBODSYS, AEDECOD),
    denominator = adsl
  ) |> 
  filter(stat_name %in% c("n", "p"))

## display
card_ae_rate %>%
  kable(format = "html") %>%
  kable_styling() %>%
  kableExtra::scroll_box(width = "100%", height = "300px")
group1 group1_level group2 group2_level variable variable_level context stat_name stat_label stat fmt_fn warning error
ARM 1 AEBODSYS 1 AEDECOD 1 hierarchical n n 3 0 NULL NULL
ARM 1 AEBODSYS 1 AEDECOD 1 hierarchical p % 0.2 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 2 AEBODSYS 1 AEDECOD 1 hierarchical n n 1 0 NULL NULL
ARM 2 AEBODSYS 1 AEDECOD 1 hierarchical p % 0.06666667 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 3 AEBODSYS 1 AEDECOD 1 hierarchical n n 6 0 NULL NULL
ARM 3 AEBODSYS 1 AEDECOD 1 hierarchical p % 0.4 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 1 AEBODSYS 1 AEDECOD 2 hierarchical n n 5 0 NULL NULL
ARM 1 AEBODSYS 1 AEDECOD 2 hierarchical p % 0.3333333 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 2 AEBODSYS 1 AEDECOD 2 hierarchical n n 6 0 NULL NULL
ARM 2 AEBODSYS 1 AEDECOD 2 hierarchical p % 0.4 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 3 AEBODSYS 1 AEDECOD 2 hierarchical n n 6 0 NULL NULL
ARM 3 AEBODSYS 1 AEDECOD 2 hierarchical p % 0.4 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 1 AEBODSYS 2 AEDECOD 3 hierarchical n n 5 0 NULL NULL
ARM 1 AEBODSYS 2 AEDECOD 3 hierarchical p % 0.3333333 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 2 AEBODSYS 2 AEDECOD 3 hierarchical n n 6 0 NULL NULL
ARM 2 AEBODSYS 2 AEDECOD 3 hierarchical p % 0.4 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 3 AEBODSYS 2 AEDECOD 3 hierarchical n n 8 0 NULL NULL
ARM 3 AEBODSYS 2 AEDECOD 3 hierarchical p % 0.5333333 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 1 AEBODSYS 3 AEDECOD 4 hierarchical n n 5 0 NULL NULL
ARM 1 AEBODSYS 3 AEDECOD 4 hierarchical p % 0.3333333 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 2 AEBODSYS 3 AEDECOD 4 hierarchical n n 6 0 NULL NULL
ARM 2 AEBODSYS 3 AEDECOD 4 hierarchical p % 0.4 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 3 AEBODSYS 3 AEDECOD 4 hierarchical n n 5 0 NULL NULL
ARM 3 AEBODSYS 3 AEDECOD 4 hierarchical p % 0.3333333 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 1 AEBODSYS 3 AEDECOD 5 hierarchical n n 8 0 NULL NULL
ARM 1 AEBODSYS 3 AEDECOD 5 hierarchical p % 0.5333333 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 2 AEBODSYS 3 AEDECOD 5 hierarchical n n 6 0 NULL NULL
ARM 2 AEBODSYS 3 AEDECOD 5 hierarchical p % 0.4 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 3 AEBODSYS 3 AEDECOD 5 hierarchical n n 7 0 NULL NULL
ARM 3 AEBODSYS 3 AEDECOD 5 hierarchical p % 0.4666667 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 1 AEBODSYS 4 AEDECOD 6 hierarchical n n 4 0 NULL NULL
ARM 1 AEBODSYS 4 AEDECOD 6 hierarchical p % 0.2666667 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 2 AEBODSYS 4 AEDECOD 6 hierarchical n n 4 0 NULL NULL
ARM 2 AEBODSYS 4 AEDECOD 6 hierarchical p % 0.2666667 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 3 AEBODSYS 4 AEDECOD 6 hierarchical n n 5 0 NULL NULL
ARM 3 AEBODSYS 4 AEDECOD 6 hierarchical p % 0.3333333 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 1 AEBODSYS 5 AEDECOD 7 hierarchical n n 6 0 NULL NULL
ARM 1 AEBODSYS 5 AEDECOD 7 hierarchical p % 0.4 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 2 AEBODSYS 5 AEDECOD 7 hierarchical n n 4 0 NULL NULL
ARM 2 AEBODSYS 5 AEDECOD 7 hierarchical p % 0.2666667 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 3 AEBODSYS 5 AEDECOD 7 hierarchical n n 8 0 NULL NULL
ARM 3 AEBODSYS 5 AEDECOD 7 hierarchical p % 0.5333333 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 1 AEBODSYS 6 AEDECOD 8 hierarchical n n 4 0 NULL NULL
ARM 1 AEBODSYS 6 AEDECOD 8 hierarchical p % 0.2666667 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 2 AEBODSYS 6 AEDECOD 8 hierarchical n n 4 0 NULL NULL
ARM 2 AEBODSYS 6 AEDECOD 8 hierarchical p % 0.2666667 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 3 AEBODSYS 6 AEDECOD 8 hierarchical n n 7 0 NULL NULL
ARM 3 AEBODSYS 6 AEDECOD 8 hierarchical p % 0.4666667 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 1 AEBODSYS 6 AEDECOD 9 hierarchical n n 6 0 NULL NULL
ARM 1 AEBODSYS 6 AEDECOD 9 hierarchical p % 0.4 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 2 AEBODSYS 6 AEDECOD 9 hierarchical n n 2 0 NULL NULL
ARM 2 AEBODSYS 6 AEDECOD 9 hierarchical p % 0.1333333 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 3 AEBODSYS 6 AEDECOD 9 hierarchical n n 7 0 NULL NULL
ARM 3 AEBODSYS 6 AEDECOD 9 hierarchical p % 0.4666667 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 1 AEBODSYS 7 AEDECOD 10 hierarchical n n 2 0 NULL NULL
ARM 1 AEBODSYS 7 AEDECOD 10 hierarchical p % 0.1333333 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 2 AEBODSYS 7 AEDECOD 10 hierarchical n n 5 0 NULL NULL
ARM 2 AEBODSYS 7 AEDECOD 10 hierarchical p % 0.3333333 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
ARM 3 AEBODSYS 7 AEDECOD 10 hierarchical n n 7 0 NULL NULL
ARM 3 AEBODSYS 7 AEDECOD 10 hierarchical p % 0.4666667 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
# Overall 
card_ae_rate_total <- adae |> 
  # keep one AE per subject
  slice(1L, .by = c(USUBJID, ARM, AEBODSYS, AEDECOD)) |> 
  cards::ard_hierarchical(
    variables = c(AEBODSYS, AEDECOD),
    denominator = adsl
  ) |> 
  filter(stat_name %in% c("n", "p"))

## display
card_ae_rate_total %>%
  kable(format = "html") %>%
  kable_styling() %>%
  kableExtra::scroll_box(width = "100%", height = "300px")
group1 group1_level variable variable_level context stat_name stat_label stat fmt_fn warning error
AEBODSYS 1 AEDECOD 1 hierarchical n n 10 0 NULL NULL
AEBODSYS 1 AEDECOD 1 hierarchical p % 0.2222222 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
AEBODSYS 1 AEDECOD 2 hierarchical n n 17 0 NULL NULL
AEBODSYS 1 AEDECOD 2 hierarchical p % 0.3777778 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
AEBODSYS 2 AEDECOD 3 hierarchical n n 19 0 NULL NULL
AEBODSYS 2 AEDECOD 3 hierarchical p % 0.4222222 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
AEBODSYS 3 AEDECOD 4 hierarchical n n 16 0 NULL NULL
AEBODSYS 3 AEDECOD 4 hierarchical p % 0.3555556 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
AEBODSYS 3 AEDECOD 5 hierarchical n n 21 0 NULL NULL
AEBODSYS 3 AEDECOD 5 hierarchical p % 0.4666667 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
AEBODSYS 4 AEDECOD 6 hierarchical n n 13 0 NULL NULL
AEBODSYS 4 AEDECOD 6 hierarchical p % 0.2888889 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
AEBODSYS 5 AEDECOD 7 hierarchical n n 18 0 NULL NULL
AEBODSYS 5 AEDECOD 7 hierarchical p % 0.4 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
AEBODSYS 6 AEDECOD 8 hierarchical n n 15 0 NULL NULL
AEBODSYS 6 AEDECOD 8 hierarchical p % 0.3333333 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
AEBODSYS 6 AEDECOD 9 hierarchical n n 15 0 NULL NULL
AEBODSYS 6 AEDECOD 9 hierarchical p % 0.3333333 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL
AEBODSYS 7 AEDECOD 10 hierarchical n n 14 0 NULL NULL
AEBODSYS 7 AEDECOD 10 hierarchical p % 0.3111111 function (x) , {, res <- ifelse(is.na(x), NA_character_, str_trim(format(round5(x * , scale, digits = digits), nsmall = digits))), if (!is.null(width)) {, res <- ifelse(nchar(res) >= width | is.na(res), res, , paste0(strrep(” “, width - nchar(res)), res)), }, res, } NULL NULL

Conclusion

Finally, once you’re done, you’re welcome to stick them all together (or leave them as-is) and you have your complete ARD. As you can see, for most of these operations, while calculating the “overall”/“total” areas, we’d have to calculate everything a second time just with very minor modifications. While the hassle isn’t the end of the world, ard_hierarchal_stack() aims to make the process more streamlined and much simpler. All-in-all, the cards package offers an easy-to-use and intuitive way to chunk out tlg’s, which may come in handy for applications such as in QC pipelines.