Skip to content

Add simplify_ard() to flatten the ARD of stratified, stacked and split tables - #326

Open
Melkiades wants to merge 8 commits into
mainfrom
325_simplify_ard@main
Open

Melkiades wants to merge 8 commits into
mainfrom
325_simplify_ard@main

Conversation

@Melkiades

Copy link
Copy Markdown
Contributor

What changes are proposed in this pull request?

Statistics are identified by groups, variables, context and stat_name. context is part of the key because some tables report two statistics with the same name for the same rows (subject and event counts in tbl_hierarchical_rate_and_count()), so compare_ard() needs the same keys:

cards::compare_ard(
  x, y,
  keys = c(cards::all_ard_groups(), cards::all_ard_variables(), "context", "stat_name")
)

The split duplication itself is a gtsummary issue (tbl_split_by_rows() keeps the full $cards and $tbls in every piece); simplify_ard() works around it until that is fixed upstream.

Reference GitHub issue associated with pull request. closes #325


Pre-review Checklist (if item does not apply, mark is as complete)

  • All GitHub Action workflows pass with a ✅
  • PR branch has pulled the most recent updates from master branch: usethis::pr_merge_main()
  • If a bug was fixed, a unit test was added.
  • Code coverage is suitable for any new functions/features (generally, 100% coverage for new code): devtools::test_coverage()
  • Request a reviewer

@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Unit Tests Summary

    1 files    368 suites   2m 0s ⏱️
  368 tests   368 ✅ 0 💤 0 ❌
1 070 runs  1 070 ✅ 0 💤 0 ❌

Results for commit 0ee41c5.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Unit Test Performance Difference

Test Suite $Status$ Time on main $±Time$ $±Tests$ $±Skipped$ $±Failures$ $±Errors$
simplify_ard 👶 $+0.00$ $+3$ $0$ $0$ $0$
Additional test case details
Test Suite $Status$ Time on main $±Time$ Test Case
simplify_ard 👶 $+0.00$ simplify_ard_.deduplicate_works
simplify_ard 👶 $+0.00$ simplify_ard_adds_a_tbl_id1_group_to_stacks_of_the_same_statistics
simplify_ard 👶 $+0.00$ simplify_ard_adds_strata_as_group_columns
simplify_ard 👶 $+0.00$ simplify_ard_errors_on_different_values_for_the_same_statistic
simplify_ard 👶 $+0.00$ simplify_ard_flattens_nested_ARD_lists
simplify_ard 👶 $+0.00$ simplify_ard_keeps_the_type_of_the_strata_levels
simplify_ard 👶 $+0.00$ simplify_ard_matches_the_strata_levels_of_tbl_strata_to_their_variables
simplify_ard 👶 $+0.00$ simplify_ard_messaging
simplify_ard 👶 $+0.00$ simplify_ard_names_nested_tbl_id_groups_by_level_and_aligns_them
simplify_ard 👶 $+0.00$ simplify_ard_numbers_strata_as_cards_ard_strata_
simplify_ard 👶 $+0.00$ simplify_ard_output_matches_the_same_ARD_built_with_cards
simplify_ard 👶 $+0.00$ simplify_ard_reads_factor_levels_with_and_
simplify_ard 👶 $+0.00$ simplify_ard_reads_the_strata_from_the_names_of_the_pieces_when_df_strata_does_not_fit
simplify_ard 👶 $+0.00$ simplify_ard_removes_the_ARD_copies_of_split_tables
simplify_ard 👶 $+0.00$ simplify_ard_returns_a_card_simplified_and_does_not_simplify_twice
simplify_ard 👶 $+0.00$ simplify_ard_returns_an_empty_ARD_for_tables_without_ARD
simplify_ard 👶 $+5.86$ simplify_ard_works_with_a_single_table
tbl_ada_summary 💔 $2.06$ $+12.94$ tbl_ada_summary_errors_on_missing_or_invalid_inputs
tbl_baseline_chg 💚 $13.17$ $-9.28$ tbl_baseline_chg_works
tbl_hierarchical_incidence_rate 💚 $17.53$ $-3.34$ tbl_hierarchical_incidence_rate_runs_with_minimum_arguments
tbl_hierarchical_rate_by_grade 💔 $0.81$ $+1.81$ tbl_hierarchical_rate_by_grade_works
tbl_mmrm 💚 $4.97$ $-1.92$ se_calculates_standard_error_correctly
tbl_rmpt 💔 $2.43$ $+1.57$ tbl_rmpt_works_with_default_parameters_Example_1_
tbl_roche_subgroups 💔 $0.25$ $+6.62$ tbl_roche_subgroups_time_to_event_NULL_works
tbl_shift 💚 $8.52$ $-7.30$ tbl_shift_strata_location_

Results for commit c7d9106

♻️ This comment has been updated with latest results.

Comment thread R/simplify_ard.R Resolved
Comment thread R/simplify_ard.R Resolved
Comment thread R/simplify_ard.R Resolved
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

badge

Code Coverage Summary

Filename                                 Stmts    Miss  Cover    Missing
-------------------------------------  -------  ------  -------  ------------------------------------------------------------------------------------------------
R/add_blank_rows.R                          63       0  100.00%
R/add_difference_row.R                     101       0  100.00%
R/add_forest_utils.R                        97      10  89.69%   76-79, 94-100
R/add_forest.R                             139       0  100.00%
R/add_hierarchical_count_row.R              33       0  100.00%
R/add_label_column.R                        26       0  100.00%
R/add_proportion_difference.R              150       1  99.33%   245
R/add_proportion_odds_ratio.R               57       0  100.00%
R/adjust_stat_columns_wrap.R                29       1  96.55%   59
R/annotate_gg_km.R                         141       0  100.00%
R/annotate_gg_pkc.R                        100       0  100.00%
R/annotate_gg.R                             94       0  100.00%
R/ard_tabulate_abnormal_by_baseline.R       65       0  100.00%
R/crane-package.R                            2       2  0.00%    26-27
R/deprecated.R                              21      21  0.00%    18-54
R/df_add_poolings.R                         41       0  100.00%
R/get_cox_pairwise_df.R                    163       0  100.00%
R/gg_km_utils.R                             39      14  64.10%   20-37
R/gg_km.R                                  143      37  74.13%   55-58, 75, 102, 176-181, 184-187, 197-199, 204-205, 239-241, 248-251, 255, 266-270, 283, 285-287
R/gg_lineplot.R                            113       0  100.00%
R/gg_mmrm_lineplot.R                       102       1  99.02%   106
R/gg_pkc_boxplot.R                         195       5  97.44%   108, 136-139
R/gg_pkc_lineplot.R                        118       0  100.00%
R/gg_utils.R                               222       0  100.00%
R/label_roche.R                             72       0  100.00%
R/modify_header_rm_md.R                     18       2  88.89%   35-36
R/modify_split_caption.R                    37       0  100.00%
R/modify_zero_recode.R                      20       1  95.00%   64
R/pk_helper.R                              499       0  100.00%
R/reverse_difference_ci.R                   33       0  100.00%
R/simplify_ard.R                           198       5  97.47%   318, 413, 439, 601, 619
R/tbl_ada_summary.R                        200       4  98.00%   277-280
R/tbl_baseline_chg.R                       188       0  100.00%
R/tbl_coxph.R                               90       1  98.89%   229
R/tbl_hierarchical_incidence_rate.R        291       4  98.63%   430-433
R/tbl_hierarchical_rate_and_count.R        339      13  96.17%   343, 425, 446-456
R/tbl_hierarchical_rate_by_grade.R         317       3  99.05%   169-171
R/tbl_listing.R                             36       0  100.00%
R/tbl_mmrm.R                               344       6  98.26%   452-456, 548
R/tbl_null_report.R                         10       0  100.00%
R/tbl_proportion.R                         141      13  90.78%   100-103, 107-111, 114-117
R/tbl_rmpt.R                               157      12  92.36%   299-304, 316-321
R/tbl_roche_subgroups.R                    157       0  100.00%
R/tbl_roche_summary.R                       64       0  100.00%
R/tbl_shift.R                              111       0  100.00%
R/tbl_survfit_quantiles.R                  154       0  100.00%
R/tbl_survfit_times.R                       92       0  100.00%
R/tbl_with_pools.R                          79       1  98.73%   317
R/theme_gtsummary_roche.R                   87       1  98.85%   61
R/utils.R                                   42       0  100.00%
TOTAL                                     6030     158  97.38%

Diff against main

Filename            Stmts    Miss  Cover
----------------  -------  ------  -------
R/simplify_ard.R     +198      +5  +97.47%
TOTAL                +198      +5  +0.00%

Results for commit: 0ee41c5

Minimum allowed coverage is 80%

♻️ This comment has been updated with latest results

@Melkiades Melkiades mentioned this pull request Oct 9, 2026
3 of 5 tasks
Melkiades added a commit that referenced this pull request Oct 12, 2026
**What changes are proposed in this pull request?**
* CI and test only, no user-facing change (no NEWS entry).

* `R-CMD-check.yaml`: install XQuartz on macOS. `gdtools` (used by
`flextable`) is linked against X11, which the macOS runners no longer
ship, so every example using flextable failed with "Library not loaded:
/opt/X11/lib/libXrender.1.dylib".
* `test-add_forest.R`: `expect_no_warning()` changed to
`expect_no_error()` when rendering the gt forest plot. R-devel warns
upstream on the out-of-range test values ("object length is not a
multiple of subscript length"); the warning does not come from crane
code.

Both failures are on `main` and block #326.


--------------------------------------------------------------------------------

Pre-review Checklist (if item does not apply, mark is as complete)
- [ ] **All** GitHub Action workflows pass with a ✅
- [x] PR branch has pulled the most recent updates from master branch:
`usethis::pr_merge_main()`
- [x] If a bug was fixed, a unit test was added.
- [x] Code coverage is suitable for any new functions/features
(generally, 100% coverage for new code): `devtools::test_coverage()`
- [ ] Request a reviewer

Co-authored-by: Melkiades <11279768+Melkiades@users.noreply.github.com>
Comment thread R/simplify_ard.R Resolved

@martincadek martincadek left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have run few test on the function on different scenarios and the examples and there are few issues:

  1. Strata levels swapped with two or more strata
tbl1 <- adsl |>
  dplyr::filter(AGEGR1 %in% c("<65", ">80")) |>
  tbl_strata(
    strata = c(SEX, AGEGR1),
    .tbl_fun = ~ tbl_summary(.x, by = ARM, include = AGE),
    .combine_with = "tbl_stack",
    .combine_args = list(quiet = TRUE)
  )

tbl1$df_strata

simplify_ard(tbl1) |> 
    dplyr::select(cards::all_ard_groups()) |>
    cards::unlist_ard_columns() |>
    dplyr::distinct()
# got:      group2 = AGEGR1 with levels "F", "M"
#           group3 = SEX    with levels "<65", ">80"
# expected: group2 = AGEGR1 with levels "<65", ">80"
#           group3 = SEX    with levels "F", "M"
  1. factor strata becomes character, automatic coecion
adsl_fct <- dplyr::mutate(adsl, SEX = factor(SEX))

tbl2 <- adsl_fct |>
  tbl_strata(
    strata = SEX,
    .tbl_fun = ~ tbl_summary(.x, by = ARM, include = AGE, type = AGE ~ "continuous", statistic = AGE ~ "{mean}"),
    .combine_with = "tbl_stack",
    .combine_args = list(quiet = TRUE)
  )
ard_tbl <- simplify_ard(tbl2) |>
  dplyr::filter(context == "summary", !is.na(group1))

ard_qc <- cards::ard_strata(
  adsl_fct,
  .strata = SEX,
  .f = \(df) cards::ard_summary(df, by = ARM, variables = AGE, statistic = ~ cards::continuous_summary_fns("mean"))
)

str(ard_tbl$group2_level[[1]]) # got: chr "F"
str(ard_qc$group2_level[[1]]) # cards: Factor w/ 2 levels "F","M"
  1. Stratum lost when a factor level contains a comma
adsl_comma <- dplyr::mutate(
  adsl,
  AGECAT = factor(ifelse(AGE < 70, "Young, <70", "Old, >=70"))
)

tbl3 <- adsl_comma |>
  tbl_strata(
    strata = AGECAT,
    .tbl_fun = ~ tbl_summary(.x, by = ARM, include = AGE),
    .combine_with = "tbl_stack",
    .combine_args = list(quiet = TRUE)
  )

# factor levels are not quoted
names(gather_ard(tbl3))

simplify_ard(tbl3) |> 
    dplyr::select(cards::all_ard_groups()) |>
    cards::unlist_ard_columns() |>
    dplyr::distinct()
# got:      group2 = tbl_id1 with levels "AGECAT=Old, >=70", "AGECAT=Young, <70"
# expected: group2 = AGECAT  with levels "Old, >=70", "Young, <70"
  1. stat is still a list?
tbl4 <- tbl_summary(adsl_fct, by = ARM, include = c(AGE, SEX))

ard4 <- simplify_ard(tbl4, .unlist = TRUE)
class(ard4$stat)

I would also suggest to consider naming group and group_level as strata and strata_level unless that collides with anything.

Melkiades and others added 2 commits November 5, 2026 13:40
`df_strata` keeps the strata columns in the order of the data, so with
`strata` in another order the levels went to the wrong variable. The levels
now come from `df_strata` by name and keep their type (factor, numeric).
The variable names are split from the names of the pieces with the levels
known, so factor levels with `,` or `=` are read right. The strata of one
call are numbered in the order of `strata`, as `cards::ard_strata()` does,
and those of nested calls come first, as with nested `ard_strata()` calls.

`.unlist` is removed: `stat` cannot be unlisted while the attributes rows
are in, so filter the statistics, then use `cards::unlist_ard_columns()`.
The `compare_ard()` snippet in the details uses the names of the example.
@Melkiades

Copy link
Copy Markdown
Contributor Author

Thanks for the careful checks. All four reproduce: three were real bugs, and the fourth showed that .unlist promised more than it can do. Fixed in 0ee41c5, with a test for each case.

1) Strata levels swapped. tbl_strata() keeps the columns of df_strata in the order of the data, not of strata. In cards::ADSL AGEGR1 comes before SEX, so df_strata starts with strata_2, and simplify_ard() read the columns by position. They are read by name now. This also showed that the numbering didn't follow cards: cards::ard_strata(.strata = c(SEX, AGEGR1)) puts SEX in group2 and AGEGR1 in group3, while the docs said innermost first. The strata of one call are now numbered in the order of strata, as in cards. Nested calls still put the inner stratum first, as nested ard_strata() calls do. Your example gives:

  group1 group1_level         group2 group2_level group3 group3_level
1 ARM    Placebo              SEX    F            AGEGR1 <65
2 ARM    Xanomeline High Dose SEX    F            AGEGR1 <65

and compare_ard() against cards::ard_strata(.strata = c(SEX, AGEGR1)) is equal (also with three strata, none in the order of the data).

2) Factor strata become character. Right, and it made compare_ard() fail against the cards ARD, numeric strata too. The levels now come from df_strata with their type:

str(ard_tbl$group2_level[[1]])
#>  Factor w/ 2 levels "F","M": 1

compare_ard(ard_tbl, ard_qc) is equal with no conversion. One limit: tbl_strata_nested_stack() and lists from gather_ard() only have the levels as text, so there they stay character. The docs say so.

3) Stratum lost with a comma in a factor level. gtsummary quotes character levels in the names of the pieces, but not factor levels, so AGECAT=Young, <70 could not be split. The variable names are now split off with the levels from df_strata known, so , and = in a level are fine:

  group1 group1_level         group2 group2_level
1 ARM    Placebo              AGECAT Old, >=70

This also fixes a quieter variant: levels like "Young, age=<70" were read as two strata, " age" and AGECAT.

4) stat still a list. Yes: cards::unlist_ard_columns() keeps a column as a list when one value has more than one element, here the levels of the factor SEX in the attributes rows. When it can unlist, numbers and labels end up as character. So .unlist is gone. Filter the statistics, then unlist:

simplify_ard(tbl4) |>
  dplyr::filter(context != "attributes") |>
  cards::unlist_ard_columns()

gives a numeric stat. The docs explain it and the examples show it.

strata / strata_level instead of group / group_level: I'd keep the group columns. cards puts strata there too (ard_strata() writes group2), and compare_ard() and all_ard_groups() key on them. With the columns renamed, compare_ard() against the cards ARD errors with "Columns group2 and group2_level don't exist", and all_ard_groups() only sees group1.

@Melkiades

Copy link
Copy Markdown
Contributor Author

Thanks @martincadek!! very good catches! let me know what you think of last update

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add simplify_ard() to flatten the ARD of stratified, stacked and split tables

3 participants