Skip to contents

Suppose you need to create the table below, and need an ARD representation of the results to get started. Here, we will review an examples for creating a basic demographics table.

To get started, load the {cards} package.

Demographics

Characteristic Placebo
N = 86
Xanomeline Low Dose
N = 84
Xanomeline High Dose
N = 84
Age


    Median (IQR) 76 (69, 82) 78 (71, 82) 76 (71, 80)
    Mean (SD) 75 (9) 76 (8) 74 (8)
    Range 52 - 89 51 - 88 56 - 88
Age Group, n (%)


    <65 14 (16%) 8 (10%) 11 (13%)
    65-80 42 (49%) 47 (56%) 55 (65%)
    >80 30 (35%) 29 (35%) 18 (21%)
Female, n (%) 53 (62%) 50 (60%) 40 (48%)

The table above has three types of data summaries: a continuous variable summary for AGE, a categorical variable summary for AGEGR1, and a dichotomous variable summary for SEX.

Continuous Summaries

To get a continuous variable summary, we will use the ard_continuous() function from the {cards} package.

df_continuous_ard <-
  ard_continuous(
    ADSL,
    by = ARM,
    variables = AGE,
    statistic = ~ continuous_summary_fns(c("median", "p25", "p75", "mean", "sd", "min", "max"))
  )
df_continuous_ard |> head(5)
#> {cards} data frame: 5 x 10
#>   group1 group1_level variable stat_name stat_label   stat
#> 1    ARM      Placebo      AGE    median     Median     76
#> 2    ARM      Placebo      AGE       p25  25th Per…     69
#> 3    ARM      Placebo      AGE       p75  75th Per…     82
#> 4    ARM      Placebo      AGE      mean       Mean 75.209
#> 5    ARM      Placebo      AGE        sd         SD   8.59
#>  4 more variables: context, fmt_fn, warning, error

Categorical Summaries

To get the categorical variable summary, we will use the ard_categorical() function.

df_categorical_ard <-
  ard_categorical(
    ADSL,
    by = ARM,
    variables = AGEGR1
  )
df_categorical_ard |> head(5)
#> {cards} data frame: 5 x 11
#>   group1 group1_level variable variable_level stat_name stat_label  stat
#> 1    ARM      Placebo   AGEGR1            <65         n          n    14
#> 2    ARM      Placebo   AGEGR1            <65         N          N    86
#> 3    ARM      Placebo   AGEGR1            <65         p          % 0.163
#> 4    ARM    Xanomeli…   AGEGR1            <65         n          n    11
#> 5    ARM    Xanomeli…   AGEGR1            <65         N          N    84
#>  4 more variables: context, fmt_fn, warning, error

Dichotomous Summaries

To get the dichotomous variable summary, we will use ard_dichotomous(). In this case, we want to show the Female ("F") level of the SEX variable and specify this with the values argument.

df_dichotomous_ard <-
  ard_dichotomous(
    ADSL,
    by = ARM,
    variables = SEX,
    value = list(SEX = "F")
  )
df_dichotomous_ard |> head(5)
#> {cards} data frame: 5 x 11
#>   group1 group1_level variable variable_level stat_name stat_label  stat
#> 1    ARM      Placebo      SEX              F         n          n    53
#> 2    ARM      Placebo      SEX              F         N          N    86
#> 3    ARM      Placebo      SEX              F         p          % 0.616
#> 4    ARM    Xanomeli…      SEX              F         n          n    40
#> 5    ARM    Xanomeli…      SEX              F         N          N    84
#>  4 more variables: context, fmt_fn, warning, error

Combine Results

As a last step, you can combine all of these objects into a single object using bind_ard(), which is similar to dplyr::bind_rows() and includes additional structural checks for our results.

bind_ard(
  df_continuous_ard,
  df_categorical_ard,
  df_dichotomous_ard
)
#> {cards} data frame: 57 x 11
#>    group1 group1_level variable variable_level stat_name stat_label   stat
#> 1     ARM      Placebo      AGE                   median     Median     76
#> 2     ARM      Placebo      AGE                      p25  25th Per…     69
#> 3     ARM      Placebo      AGE                      p75  75th Per…     82
#> 4     ARM      Placebo      AGE                     mean       Mean 75.209
#> 5     ARM      Placebo      AGE                       sd         SD   8.59
#> 6     ARM      Placebo      AGE                      min        Min     52
#> 7     ARM      Placebo      AGE                      max        Max     89
#> 8     ARM    Xanomeli…      AGE                   median     Median     76
#> 9     ARM    Xanomeli…      AGE                      p25  25th Per…   70.5
#> 10    ARM    Xanomeli…      AGE                      p75  75th Per…     80
#>  47 more rows
#>  Use `print(n = ...)` to see more rows
#>  4 more variables: context, fmt_fn, warning, error

Shortcut

The ard_stack() function provides a shortcut to perform the calculations above in a single step.

In the example below, the data and by arguments are passed to each subsequent ard_*() function call. Moreover, we will also be returned the univariate tabulation of the by variable, which would be used to add counts to the header row of the table.

ard_stack(
  data = ADSL,
  .by = ARM,
  ard_continuous(
    variables = AGE,
    statistic = ~ continuous_summary_fns(c("median", "p25", "p75", "mean", "sd", "min", "max"))
  ),
  ard_categorical(variables = AGEGR1),
  ard_dichotomous(variables = SEX, value = list(SEX = "F"))
)
#> {cards} data frame: 66 x 11
#>    group1 group1_level variable variable_level stat_name stat_label   stat
#> 1     ARM      Placebo      AGE                   median     Median     76
#> 2     ARM      Placebo      AGE                      p25  25th Per…     69
#> 3     ARM      Placebo      AGE                      p75  75th Per…     82
#> 4     ARM      Placebo      AGE                     mean       Mean 75.209
#> 5     ARM      Placebo      AGE                       sd         SD   8.59
#> 6     ARM      Placebo      AGE                      min        Min     52
#> 7     ARM      Placebo      AGE                      max        Max     89
#> 8     ARM    Xanomeli…      AGE                   median     Median     76
#> 9     ARM    Xanomeli…      AGE                      p25  25th Per…   70.5
#> 10    ARM    Xanomeli…      AGE                      p75  75th Per…     80
#>  56 more rows
#>  Use `print(n = ...)` to see more rows
#>  4 more variables: context, fmt_fn, warning, error

Adverse Events

Next, we will review several examples for creating basic adverse events (AE) tables.

Participant-level summaries

A common type of AE table contains participant-level summaries. Here, we are reporting the number and percentage of subjects with at least one AE by system organ class and preferred term.

Placebo Xanomeline Low Dose Xanomeline High Dose
ANY BODY SYSTEM  65 (76%)  77 (92%)  76 (90%)
CARDIAC DISORDERS  12 (14%)  13 (15%)  15 (18%)
GASTROINTESTINAL DISORDERS  17 (20%)  14 (17%)  20 (24%)
GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS  21 (24%)  47 (56%)  40 (48%)
APPLICATION SITE PRURITUS   6 ( 7%)  22 (26%)  22 (26%)
APPLICATION SITE ERYTHEMA   3 ( 3%)  12 (14%)  15 (18%)
APPLICATION SITE IRRITATION   3 ( 3%)   9 (11%)   9 (11%)
INFECTIONS AND INFESTATIONS  16 (19%)   9 (11%)  13 (15%)
NERVOUS SYSTEM DISORDERS   8 ( 9%)  20 (24%)  25 (30%)
DIZZINESS   2 ( 2%)   8 (10%)  11 (13%)
RESPIRATORY, THORACIC AND MEDIASTINAL DISORDERS   8 ( 9%)   9 (11%)  10 (12%)
SKIN AND SUBCUTANEOUS TISSUE DISORDERS  20 (23%)  39 (46%)  40 (48%)
PRURITUS   8 ( 9%)  21 (25%)  26 (31%)
ERYTHEMA   8 ( 9%)  14 (17%)  14 (17%)
RASH   5 ( 6%)  13 (15%)   9 (11%)

For the computations below, we will not only make use of the ADAE dataset, but we will also rely on ADSL for the full study population. To match between them, we need to do a small data manipulation on the naming of the treatment variable.

# rename trt variable
adsl <- ADSL |>
  dplyr::rename("TRTA" = "TRT01A")

To get these participant level summaries, we will take a multi-step approach by calculating number and percent of subjects with: (1) at least one AE, (2) at least one AE by system organ class, and (3) at least one AE by system organ class and preferred term.

Because ADAE potentially contains multiple records per subject for a given AE, we must collapse the data so that it is subject-level. For the first step, we must reduce the data down to 1 record per subject with any AE (note that your data may contain derived flags to assist with this step):

adae_subj <- ADAE |>
  dplyr::select(USUBJID, TRTA) |>
  unique()

We then pass this dataset to ard_categorical() to compute the number and percentage of subjects by treatment arm. To ensure our denominator is the full study population, we pass our adsl data to the denominator argument:

# any ae
ard_subj_any <- ard_categorical(
  data = adae_subj,
  variables = TRTA,
  statistic = ~ c("n", "p"),
  denominator = adsl
)

For the second step, we create a new dataset of 1 record per subject with any AE by system organ class:

adae_soc <- ADAE |>
  dplyr::select(USUBJID, TRTA, AEBODSYS) |>
  unique()

We then pass this dataset to ard_categorical() to compute the number and percentage of subjects within each system organ class by treatment arm.

ard_subj_soc <- ard_categorical(
  data = adae_soc,
  by = TRTA,
  variables = AEBODSYS,
  statistic = ~ c("n", "p"),
  denominator = adsl
)

Finally, we create another dataset of 1 record per subject with any AE by system organ class and preferred term.

adae_pt <- ADAE |>
  dplyr::select(USUBJID, TRTA, AEBODSYS, AETERM) |>
  unique()

This time, we will use ard_hierarchical() to compute the number and percentage of subjects within each preferred term observed within each system organ class by treatment arm. The use of ard_hierarchical() ensures we keep the nested structure of preferred terms within system organ classes.

ard_subj_pt <- ard_hierarchical(
  data = adae_pt,
  by = TRTA,
  variables = c(AEBODSYS, AETERM),
  statistic = ~ c("n", "p"),
  denominator = adsl
)

All participant level summaries can be combined using bind_ard().

bind_ard(ard_subj_any, ard_subj_soc, ard_subj_pt)
#> {cards} data frame: 1596 x 13
#>    group1 group1_level group2 group2_level variable variable_level stat_name
#> 1    <NA>                <NA>                  TRTA        Placebo         n
#> 2    <NA>                <NA>                  TRTA        Placebo         p
#> 3    <NA>                <NA>                  TRTA      Xanomeli…         n
#> 4    <NA>                <NA>                  TRTA      Xanomeli…         p
#> 5    <NA>                <NA>                  TRTA      Xanomeli…         n
#> 6    <NA>                <NA>                  TRTA      Xanomeli…         p
#> 7    TRTA      Placebo   <NA>              AEBODSYS      CARDIAC …         n
#> 8    TRTA      Placebo   <NA>              AEBODSYS      CARDIAC …         p
#> 9    TRTA    Xanomeli…   <NA>              AEBODSYS      CARDIAC …         n
#> 10   TRTA    Xanomeli…   <NA>              AEBODSYS      CARDIAC …         p
#>    stat_label  stat
#> 1           n    69
#> 2           % 0.272
#> 3           n    79
#> 4           % 0.311
#> 5           n    77
#> 6           % 0.303
#> 7           n    13
#> 8           % 0.151
#> 9           n    18
#> 10          % 0.214
#>  1586 more rows
#>  Use `print(n = ...)` to see more rows
#>  4 more variables: context, fmt_fn, warning, error

Event-level Summaries

In addition to participant-level summaries, event-level summaries are often needed. For these types of tables, we report total counts of AEs , and therefore we can use the ADAE data directly. We will need to count AEs overall, by system organ class, and by preferred term (within system organ class).

Placebo Xanomeline Low Dose Xanomeline High Dose
ANY BODY SYSTEM 281 412 433
CARDIAC DISORDERS  26  30  30
GASTROINTESTINAL DISORDERS  26  22  36
GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS  46 118 124
APPLICATION SITE PRURITUS  10  32  35
APPLICATION SITE ERYTHEMA   3  20  23
APPLICATION SITE IRRITATION   7  18  16
INFECTIONS AND INFESTATIONS  35  16  20
NERVOUS SYSTEM DISORDERS  11  40  41
DIZZINESS   3  13  15
RESPIRATORY, THORACIC AND MEDIASTINAL DISORDERS  12  14  22
SKIN AND SUBCUTANEOUS TISSUE DISORDERS  45 111 104
PRURITUS  11  31  38
ERYTHEMA  12  22  22
RASH   9  18  15

For overall AE counts by treatment group, we can use ard_hierarchical_count() as such. As we don’t have a hierarchy, this is equivalent to ard_categorical() with statistic = ~ "n". We can rename the counts so they are distinguishable when stacked with the participate level counts.

# overall
ard_event_any <- ard_hierarchical_count(
  data = ADAE,
  variables = TRTA
)

For AE counts within preferred term and system organ class by treatment group, we can use ard_hierarchical() specifying our nesting as such:

ard_event_socpt <- ard_hierarchical_count(
  data = ADAE,
  by = TRTA,
  variables = c(AEBODSYS, AETERM)
)

These event-level summaries can be combined using bind_ard().

bind_ard(ard_event_any, ard_event_socpt)
#> {cards} data frame: 798 x 13
#>    group1 group1_level   group2 group2_level variable variable_level stat_name
#> 1    <NA>                  <NA>                  TRTA        Placebo         n
#> 2    <NA>                  <NA>                  TRTA      Xanomeli…         n
#> 3    <NA>                  <NA>                  TRTA      Xanomeli…         n
#> 4    TRTA      Placebo AEBODSYS    GASTROIN…   AETERM      ABDOMINA…         n
#> 5    TRTA    Xanomeli… AEBODSYS    GASTROIN…   AETERM      ABDOMINA…         n
#> 6    TRTA    Xanomeli… AEBODSYS    GASTROIN…   AETERM      ABDOMINA…         n
#> 7    TRTA      Placebo AEBODSYS    GASTROIN…   AETERM      ABDOMINA…         n
#> 8    TRTA    Xanomeli… AEBODSYS    GASTROIN…   AETERM      ABDOMINA…         n
#> 9    TRTA    Xanomeli… AEBODSYS    GASTROIN…   AETERM      ABDOMINA…         n
#> 10   TRTA      Placebo AEBODSYS    SURGICAL…   AETERM      ACROCHOR…         n
#>    stat_label stat
#> 1           n  301
#> 2           n  455
#> 3           n  435
#> 4           n    0
#> 5           n    1
#> 6           n    0
#> 7           n    1
#> 8           n    2
#> 9           n    3
#> 10          n    0
#>  788 more rows
#>  Use `print(n = ...)` to see more rows
#>  4 more variables: context, fmt_fn, warning, error