Date: 2025-06-17

# Tabulation

#### 2025-06-17

## `junco` Tabulation

The `junco` R package provides functions to create analyses for clinical trials in `R`. It is considered as an add-on/alternative to the `tern` package, which is the first package available in the `NEST` framework for analysis functions with main focus for clinical trials. The core functionality for tabulation is built on the more general purpose `rtables` package. New users should first begin by reading the “Introduction to `tern`” and “Introduction to `rtables`” vignettes.

The packages used in this vignette are:

    library(rtables)
    library(junco)
    library(tern)
    library(dplyr)

The datasets used in this vignette are:

    adsl <- ex_adsl
    adae <- ex_adae
    advs <- ex_advs

Some common data manipulation on/from these datasets, such as adding extra variables and defining some tabulation settings, like treatment variable, control group for the table.

    trtvar <- "ARM"
    ctrl_grp <- "B: Placebo"

    non_ctrl_grp <- setdiff(levels(adsl[[trtvar]]), ctrl_grp)

    adsl$colspan_trt <- factor(ifelse(adsl[[trtvar]] == ctrl_grp, " ", "Active Study Agent"),
      levels = c("Active Study Agent", " ")
    )

    adsl$rrisk_header <- "Risk Difference (%) (95% CI)"
    adsl$rrisk_label <- paste(adsl[[trtvar]], paste("vs", ctrl_grp))
    adsl$rrisk_header_vs <- "Difference in Mean Change (95% CI)"

    # define colspan_trt_map
    colspan_trt_map <- create_colspan_map(adsl,
      non_active_grp = ctrl_grp,
      non_active_grp_span_lbl = " ",
      active_grp_span_lbl = "Active Study Agent",
      colspan_var = "colspan_trt",
      trt_var = trtvar
    )

    # define reference group specification
    ref_path <- c("colspan_trt", " ", trtvar, ctrl_grp)

    adae[["TRTEMFL"]] <- "Y"

    # add adsl variables to adae
    adae <- adae %>% left_join(., adsl)
    advs <- advs %>% left_join(., adsl)

    advs[advs[["ABLFL"]] == "Y", "CHG"] <- NA

## `junco` Analysis Functions

The `junco` analysis functions are used in combination with the `rtables` layout functions, `rtables::analyze` and `rtables::summarize_row_groups`, in the pipeline which creates the `rtables` table. They apply some statistical logic to the layout of the `rtables` table. The table layout is materialized with the `rtables::build_table` function and the data.

The `junco` analysis functions are functions that can be applied as an `afun` in either `rtables::analyze` or as a `cfun` in `rtables::summarize_row_groups` function. This is a slightly different approach to `tern`, where the table layout is constructed using `analyze` functions, which are wrappers around `rtables::analyze`.

Just like `tern` analyze functions, the `junco` analysis functions offer various methods useful from the perspective of clinical trials and other statistical projects.

Examples of the `junco` analysis functions are `a_freq_j`, `a_freq_subcol_j`, `a_summarize_aval_chg_diff_j` or `a_summarize_ex_j`.

A complete list of analysis functions can be found in the \[`junco`\] package documentation.

## Tabulation Examples using `a_freq_j`

We present an example usage of `a_freq_j` for the very common AE table in clinical trials.

The standard table of adverse events is a summary by system organ class and preferred term. For frequency counts by preferred term, if there are multiple occurrences of the same AE in an individual we count them only once.

With `junco` package, this table can be created with several calls to the same `a_freq_j` function in a tabulation pipeline using `rtables::analyze` and `rtables::summarize_row_groups`.

In next paragraph, we’ll describe the difference versus using `tern` package, for similar AE table. Differences in how to define the layout as well as differences in the resulting table.

    ## extra args for a_freq_j controlling specification of
    ## reference group, denominator used to calculate percentages,
    ## and other details
    extra_args_rr <- list(
      denom = "n_altdf",
      riskdiff = TRUE,
      ref_path = ref_path,
      method = "wald",
      .stats = c("count_unique_fraction")
    )

Using `junco` analysis function `a_freq_j` we define the layout, using 2 `analyze` calls (for overall summary - TRTEMFL and preferred term - AEDECOD), and 1 `summarize_row_groups` call (for system organ class - AEBODSYS)

Note that the same specifications, `extra_args_rr` can be re-used in the first and second `analyze` call, as well as in the in-between call to `summarize_row_groups`.

For the overall summary, we add as extra specification to restrict to TRTEMFL = “Y” values and the label to show in the row.

    lyt <- basic_table(
      top_level_section_div = " ",
      colcount_format = "N=xx"
    ) %>%
      ## main columns
      split_cols_by("colspan_trt", split_fun = trim_levels_to_map(map = colspan_trt_map)) %>%
      split_cols_by(trtvar, show_colcounts = TRUE) %>%
      ## risk diff columns, note nested = FALSE
      split_cols_by("rrisk_header", nested = FALSE) %>%
      split_cols_by(trtvar,
        labels_var = "rrisk_label", split_fun = remove_split_levels(ctrl_grp),
        show_colcounts = FALSE
      ) %>%
      analyze("TRTEMFL",
        afun = a_freq_j,
        extra_args = append(extra_args_rr, list(val = "Y", label = "Number of subjects with AE"))
      ) %>%
      split_rows_by("AEBODSYS",
        split_label = "System Organ Class",
        split_fun = trim_levels_in_group("AEDECOD"),
        label_pos = "topleft",
        section_div = c(" "),
        nested = FALSE
      ) %>%
      summarize_row_groups("AEBODSYS",
        cfun = a_freq_j,
        extra_args = extra_args_rr
      ) %>%
      analyze("AEDECOD",
        afun = a_freq_j,
        extra_args = extra_args_rr
      )

Now we just build and view the resulting table.

    tbl <- build_table(lyt, adae, alt_counts_df = adsl)
    head(tbl, 10)
    #>                                   Active Study Agent
    #>                               A: Drug X    C: Combination   B: Placebo                 Risk Difference (%) (95% CI)
    #> System Organ Class              N=134          N=132           N=134      A: Drug X vs B: Placebo   C: Combination vs B: Placebo
    #> ————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
    #> Number of subjects with AE   122 (91.0%)    120 (90.9%)     123 (91.8%)      -0.7 (-7.5, 6.0)             -0.9 (-7.6, 5.9)
    #>
    #> cl A.1                       78 (58.2%)      89 (67.4%)     75 (56.0%)       2.2 (-9.6, 14.1)            11.5 (-0.1, 23.1)
    #>   dcd A.1.1.1.1              50 (37.3%)      63 (47.7%)     45 (33.6%)       3.7 (-7.7, 15.2)             14.1 (2.5, 25.8)
    #>   dcd A.1.1.1.2              48 (35.8%)      50 (37.9%)     48 (35.8%)       0.0 (-11.5, 11.5)            2.1 (-9.5, 13.7)
    #>
    #> cl B.1                       47 (35.1%)      43 (32.6%)     49 (36.6%)      -1.5 (-13.0, 10.0)           -4.0 (-15.4, 7.4)
    #>   dcd B.1.1.1.1              47 (35.1%)      43 (32.6%)     49 (36.6%)      -1.5 (-13.0, 10.0)           -4.0 (-15.4, 7.4)
    #>
    #> cl B.2                       79 (59.0%)      85 (64.4%)     74 (55.2%)       3.7 (-8.1, 15.6)             9.2 (-2.6, 20.9)
    #>   dcd B.2.1.2.1              49 (36.6%)      52 (39.4%)     44 (32.8%)       3.7 (-7.7, 15.1)             6.6 (-5.0, 18.1)
    #>   dcd B.2.2.3.1              48 (35.8%)      51 (38.6%)     54 (40.3%)       -4.5 (-16.1, 7.1)           -1.7 (-13.4, 10.1)
    #>
    #> cl C.1                       43 (32.1%)      43 (32.6%)     46 (34.3%)       -2.2 (-13.5, 9.0)           -1.8 (-13.1, 9.6)

### Minor differences between current table and a similar table using `tern` analyze functions.

Here is a similar table generated with the tern functions (`summarize_occurrences` and `count_occurrences`), as close as possible to our target AE table we produced before in `tbl`.

    lyt_tern <- basic_table(
      top_level_section_div = " ",
      colcount_format = "N=xx"
    ) %>%
      ## main columns
      split_cols_by(trtvar,
        show_colcounts = TRUE,
        split_fun = add_riskdiff(arm_x = ctrl_grp, arm_y = non_ctrl_grp)
      ) %>%
      analyze_num_patients(
        vars = "USUBJID",
        .stats = c("unique"),
        .labels = c(
          unique = "Total number of patients with at least one adverse event"
        ),
        riskdiff = TRUE
      ) %>%
      split_rows_by("AEBODSYS",
        split_label = "System Organ Class",
        split_fun = trim_levels_in_group("AEDECOD"),
        label_pos = "topleft",
        section_div = c(" "),
        nested = FALSE
      ) %>%
      summarize_occurrences(
        var = "AEBODSYS",
        denom = "N_col",
        riskdiff = TRUE,
        .stats = c("count_fraction")
      ) %>%
      count_occurrences(
        vars = "AEDECOD",
        denom = "N_col",
        riskdiff = TRUE,
        .stats = c("count_fraction")
      )

    tbl_tern <- build_table(lyt_tern, adae, alt_counts_df = adsl)
    head(tbl_tern, 10)
    #>                                                                                                         Risk Difference (%) (95% CI)   Risk Difference (%) (95% CI)
    #>                                                             A: Drug X    B: Placebo    C: Combination     B: Placebo vs. A: Drug X     B: Placebo vs. C: Combination
    #> System Organ Class                                            N=134         N=134          N=132                   N=268                           N=266
    #> ————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
    #> Total number of patients with at least one adverse event   122 (91.0%)   123 (91.8%)    120 (90.9%)           0.7 (-6.0 - 7.5)               0.9 (-5.9 - 7.6)
    #>
    #> cl A.1                                                     78 (58.2%)     75 (56%)       89 (67.4%)          -2.2 (-14.1 - 9.6)             -11.5 (-23.1 - 0.1)
    #>   dcd A.1.1.1.1                                            50 (37.3%)    45 (33.6%)      63 (47.7%)          -3.7 (-15.2 - 7.7)            -14.1 (-25.8 - -2.5)
    #>   dcd A.1.1.1.2                                            48 (35.8%)    48 (35.8%)      50 (37.9%)          0.0 (-11.5 - 11.5)             -2.1 (-13.7 - 9.5)
    #>
    #> cl B.1                                                     47 (35.1%)    49 (36.6%)      43 (32.6%)          1.5 (-10.0 - 13.0)              4.0 (-7.4 - 15.4)
    #>   dcd B.1.1.1.1                                            47 (35.1%)    49 (36.6%)      43 (32.6%)          1.5 (-10.0 - 13.0)              4.0 (-7.4 - 15.4)
    #>
    #> cl B.2                                                      79 (59%)     74 (55.2%)      85 (64.4%)          -3.7 (-15.6 - 8.1)             -9.2 (-20.9 - 2.6)
    #>   dcd B.2.1.2.1                                            49 (36.6%)    44 (32.8%)      52 (39.4%)          -3.7 (-15.1 - 7.7)             -6.6 (-18.1 - 5.0)
    #>   dcd B.2.2.3.1                                            48 (35.8%)    54 (40.3%)      51 (38.6%)          4.5 (-7.1 - 16.1)              1.7 (-10.1 - 13.4)
    #>
    #> cl C.1                                                     43 (32.1%)    46 (34.3%)      43 (32.6%)          2.2 (-9.0 - 13.5)               1.8 (-9.6 - 13.1)

The main differences in the resulting table are

- the comparison against the reference group is reversed (ie B: Placebo vs. A: Drug X instead of A: Drug X vs. B: Placebo).
- the extra column spanner for active treatment group is not present in the `tern` version, and cannot be added.

### a_freq_j supports various methods for the risk difference column.

One of the statistical options to control with the usage of `a_freq_j` is the method for the risk difference calculation for the risk difference columns. All methods available in ´tern::estimate_proportion_diff´ can be supported. Eg, switching to `waldcc`, or others can be done. There is no option to switch methods using tern `count_occurrences` layouts, only `wald` method is available.

    extra_args_rr[["method"]] <- "waldcc"

    tbl2 <- basic_table(
      top_level_section_div = " ",
      colcount_format = "N=xx"
    ) %>%
      ## main columns
      split_cols_by("colspan_trt", split_fun = trim_levels_to_map(map = colspan_trt_map)) %>%
      split_cols_by(trtvar, show_colcounts = TRUE) %>%
      ## risk diff columns, note nested = FALSE
      split_cols_by("rrisk_header", nested = FALSE) %>%
      split_cols_by(trtvar,
        labels_var = "rrisk_label", split_fun = remove_split_levels(ctrl_grp),
        show_colcounts = FALSE
      ) %>%
      split_rows_by("AEBODSYS",
        split_label = "System Organ Class",
        split_fun = trim_levels_in_group("AEDECOD"),
        label_pos = "topleft",
        section_div = c(" "),
        nested = FALSE
      ) %>%
      summarize_row_groups("AEBODSYS",
        cfun = a_freq_j,
        extra_args = extra_args_rr
      ) %>%
      analyze("AEDECOD",
        afun = a_freq_j,
        extra_args = extra_args_rr
      ) %>%
      build_table(adae, alt_counts_df = adsl)

    head(tbl2, 10)
    #>                          Active Study Agent
    #>                      A: Drug X    C: Combination   B: Placebo                Risk Difference (%) (95% CI)
    #> System Organ Class     N=134          N=132          N=134      A: Drug X vs B: Placebo   C: Combination vs B: Placebo
    #> ——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
    #> cl A.1               78 (58.2%)     89 (67.4%)     75 (56.0%)      2.2 (-10.4, 14.8)           11.5 (-0.9, 23.8)
    #>   dcd A.1.1.1.1      50 (37.3%)     63 (47.7%)     45 (33.6%)      3.7 (-8.5, 15.9)             14.1 (1.7, 26.6)
    #>   dcd A.1.1.1.2      48 (35.8%)     50 (37.9%)     48 (35.8%)      0.0 (-12.2, 12.2)           2.1 (-10.3, 14.4)
    #>
    #> cl B.1               47 (35.1%)     43 (32.6%)     49 (36.6%)     -1.5 (-13.7, 10.7)           -4.0 (-16.2, 8.2)
    #>   dcd B.1.1.1.1      47 (35.1%)     43 (32.6%)     49 (36.6%)     -1.5 (-13.7, 10.7)           -4.0 (-16.2, 8.2)
    #>
    #> cl B.2               79 (59.0%)     85 (64.4%)     74 (55.2%)      3.7 (-8.9, 16.3)             9.2 (-3.3, 21.7)
    #>   dcd B.2.1.2.1      49 (36.6%)     52 (39.4%)     44 (32.8%)      3.7 (-8.4, 15.9)             6.6 (-5.7, 18.8)
    #>   dcd B.2.2.3.1      48 (35.8%)     51 (38.6%)     54 (40.3%)      -4.5 (-16.8, 7.9)           -1.7 (-14.2, 10.8)
    #>
    #> cl C.1               43 (32.1%)     43 (32.6%)     46 (34.3%)      -2.2 (-14.3, 9.8)           -1.8 (-13.8, 10.3)
    #>   dcd C.1.1.1.3      43 (32.1%)     43 (32.6%)     46 (34.3%)      -2.2 (-14.3, 9.8)           -1.8 (-13.8, 10.3)

### Creation of Subgroup tables with `a_freq_j`

The junco function `a_freq_j` also supports the creation of subgroup tables, which is demonstrated in below table.

    extra_args_rr_common <- list(
      denom = "n_altdf",
      denom_by = "SEX"
    )

    extra_args_rr <- append(
      extra_args_rr_common,
      list(
        riskdiff = FALSE,
        extrablankline = TRUE,
        .stats = c("n_altdf"),
        label_fstr = "Gender: %s"
      )
    )

    extra_args_rr2 <- append(
      extra_args_rr_common,
      list(
        riskdiff = TRUE,
        ref_path = ref_path,
        method = "wald",
        .stats = c("count_unique_denom_fraction"),
        na_str = rep("NA", 3)
      )
    )

    tbl <- basic_table(
      top_level_section_div = " ",
      colcount_format = "N=xx"
    ) %>%
      ## main columns
      split_cols_by("colspan_trt", split_fun = trim_levels_to_map(map = colspan_trt_map)) %>%
      split_cols_by(trtvar, show_colcounts = TRUE) %>%
      ## risk diff columns, note nested = FALSE
      split_cols_by("rrisk_header", nested = FALSE) %>%
      split_cols_by(trtvar,
        labels_var = "rrisk_label", split_fun = remove_split_levels(ctrl_grp),
        show_colcounts = FALSE
      ) %>%
      split_rows_by("SEX", split_fun = drop_split_levels) %>%
      summarize_row_groups("SEX",
        cfun = a_freq_j,
        extra_args = extra_args_rr
      ) %>%
      split_rows_by("TRTEMFL",
        split_fun = keep_split_levels("Y"),
        indent_mod = -1L,
        section_div = c(" ")
      ) %>%
      summarize_row_groups("TRTEMFL",
        cfun = a_freq_j,
        extra_args = append(extra_args_rr2, list(label = "Subjects with >=1 AE", extrablankline = TRUE))
      ) %>%
      split_rows_by("AEBODSYS",
        split_label = "System Organ Class",
        split_fun = trim_levels_in_group("AEDECOD"),
        label_pos = "topleft",
        section_div = c(" "),
        nested = TRUE
      ) %>%
      summarize_row_groups("AEBODSYS",
        cfun = a_freq_j,
        extra_args = extra_args_rr2
      ) %>%
      analyze("AEDECOD",
        afun = a_freq_j,
        extra_args = extra_args_rr2
      ) %>%
      build_table(adae, alt_counts_df = adsl)

    head(tbl, 30)
    #>                              Active Study Agent
    #>                          A: Drug X     C: Combination    B: Placebo                  Risk Difference (%) (95% CI)
    #> System Organ Class         N=134           N=132            N=134       A: Drug X vs B: Placebo   C: Combination vs B: Placebo
    #> ——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
    #> Gender: F                   79               66              77
    #>
    #> Subjects with >=1 AE   72/79 (91.1%)   61/66 (92.4%)    73/77 (94.8%)      -3.7 (-11.7, 4.3)           -2.4 (-10.5, 5.7)
    #>
    #>   cl A.1               53/79 (67.1%)   42/66 (63.6%)    48/77 (62.3%)      4.8 (-10.2, 19.7)           1.3 (-14.6, 17.2)
    #>     dcd A.1.1.1.1      34/79 (43.0%)   32/66 (48.5%)    30/77 (39.0%)      4.1 (-11.3, 19.5)            9.5 (-6.7, 25.8)
    #>     dcd A.1.1.1.2      32/79 (40.5%)   24/66 (36.4%)    31/77 (40.3%)      0.2 (-15.2, 15.6)           -3.9 (-19.9, 12.1)
    #>
    #>   cl B.1               28/79 (35.4%)   21/66 (31.8%)    32/77 (41.6%)      -6.1 (-21.4, 9.1)           -9.7 (-25.5, 6.0)
    #>     dcd B.1.1.1.1      28/79 (35.4%)   21/66 (31.8%)    32/77 (41.6%)      -6.1 (-21.4, 9.1)           -9.7 (-25.5, 6.0)
    #>
    #>   cl B.2               46/79 (58.2%)   40/66 (60.6%)    43/77 (55.8%)      2.4 (-13.1, 17.9)           4.8 (-11.4, 20.9)
    #>     dcd B.2.1.2.1      29/79 (36.7%)   19/66 (28.8%)    30/77 (39.0%)     -2.3 (-17.5, 13.0)           -10.2 (-25.6, 5.3)
    #>     dcd B.2.2.3.1      30/79 (38.0%)   24/66 (36.4%)    30/77 (39.0%)     -1.0 (-16.3, 14.3)           -2.6 (-18.5, 13.3)
    #>
    #>   cl C.1               30/79 (38.0%)   25/66 (37.9%)    35/77 (45.5%)      -7.5 (-22.9, 8.0)           -7.6 (-23.7, 8.6)
    #>     dcd C.1.1.1.3      30/79 (38.0%)   25/66 (37.9%)    35/77 (45.5%)      -7.5 (-22.9, 8.0)           -7.6 (-23.7, 8.6)
    #>
    #>   cl C.2               23/79 (29.1%)   28/66 (42.4%)    33/77 (42.9%)     -13.7 (-28.7, 1.2)           -0.4 (-16.7, 15.8)
    #>     dcd C.2.1.2.1      23/79 (29.1%)   28/66 (42.4%)    33/77 (42.9%)     -13.7 (-28.7, 1.2)           -0.4 (-16.7, 15.8)
    #>
    #>   cl D.1               45/79 (57.0%)   39/66 (59.1%)    38/77 (49.4%)      7.6 (-8.0, 23.2)             9.7 (-6.6, 26.0)
    #>     dcd D.1.1.1.1      25/79 (31.6%)   26/66 (39.4%)    28/77 (36.4%)     -4.7 (-19.6, 10.1)           3.0 (-12.9, 19.0)
    #>     dcd D.1.1.4.2      30/79 (38.0%)   26/66 (39.4%)    21/77 (27.3%)      10.7 (-3.9, 25.3)           12.1 (-3.3, 27.5)
    #>
    #>   cl D.2               26/79 (32.9%)   32/66 (48.5%)    40/77 (51.9%)     -19.0 (-34.3, -3.8)          -3.5 (-19.9, 13.0)
    #>     dcd D.2.1.5.3      26/79 (32.9%)   32/66 (48.5%)    40/77 (51.9%)     -19.0 (-34.3, -3.8)          -3.5 (-19.9, 13.0)
    #>
    #> Gender: M                   51               60              55
    #>
    #> Subjects with >=1 AE   46/51 (90.2%)   53/60 (88.3%)    48/55 (87.3%)      2.9 (-9.1, 14.9)            1.1 (-10.9, 13.0)
    #>
    #>   cl A.1               24/51 (47.1%)   41/60 (68.3%)    25/55 (45.5%)      1.6 (-17.4, 20.6)            22.9 (5.2, 40.5)
    #>     dcd A.1.1.1.1      16/51 (31.4%)   29/60 (48.3%)    15/55 (27.3%)      4.1 (-13.2, 21.4)            21.1 (3.8, 38.3)
    #>     dcd A.1.1.1.2      15/51 (29.4%)   22/60 (36.7%)    15/55 (27.3%)      2.1 (-15.0, 19.3)            9.4 (-7.6, 26.3)
    #>
    #>   cl B.1               18/51 (35.3%)   21/60 (35.0%)    16/55 (29.1%)      6.2 (-11.6, 24.0)           5.9 (-11.1, 22.9)
    #>     dcd B.1.1.1.1      18/51 (35.3%)   21/60 (35.0%)    16/55 (29.1%)      6.2 (-11.6, 24.0)           5.9 (-11.1, 22.9)

This table cannot be generated using the tern `count_occurrences` methods.

## Other junco features : Extra Statistics have been added to some tern statistical functions

    tern::get_stats("summarize_ancova")
    #> [1] "n"              "lsmean"         "lsmean_diff"    "lsmean_diff_ci"
    #> [5] "pval"
    tern::get_stats("analyze_vars_numeric")
    #>  [1] "n"               "sum"             "mean"            "sd"
    #>  [5] "se"              "mean_sd"         "mean_se"         "mean_ci"
    #>  [9] "mean_sei"        "mean_sdi"        "mean_pval"       "median"
    #> [13] "mad"             "median_ci"       "quantiles"       "iqr"
    #> [17] "range"           "min"             "max"             "median_range"
    #> [21] "cv"              "geom_mean"       "geom_sd"         "geom_mean_sd"
    #> [25] "geom_mean_ci"    "geom_cv"         "median_ci_3d"    "mean_ci_3d"
    #> [29] "geom_mean_ci_3d"

In junco, we have added extra stats for

- ancova: se for lsmean, combined lsmean-CI (3d stat), combined lsmean_diff-CI (3d stat)
- analyze_vars_numeric: combined version of mean-CI, median-CI, geom_mean-CI (available in tern >= 0.9.6)
- similar extra stats for Kaplan-Meier/survival and other methods

## Tabulation Examples using `a_summarize_aval_chg_diff_j`

    multivars <- c("AVAL", "AVAL", "CHG")

    extra_args_3col <- list(
      format_na_str = rep(NA, 3),
      ref_path = ref_path,
      ancova = FALSE,
      comp_btw_group = TRUE,
      multivars = multivars
    )

    lyt_vs_p1 <- basic_table(
      show_colcounts = FALSE,
      colcount_format = "N=xx"
    ) %>%
      ### first columns
      split_cols_by("colspan_trt",
        split_fun = trim_levels_to_map(map = colspan_trt_map),
        show_colcounts = FALSE
      ) %>%
      split_cols_by(trtvar,
        show_colcounts = TRUE, colcount_format = "N=xx"
      ) %>%
      ## set up a 3 column split
      split_cols_by_multivar(multivars,
        varlabels = c("n/N (%)", "Mean (95% CI)", "Mean Change From Baseline (95% CI)")
      ) %>%
      split_rows_by("PARAM",
        label_pos = "topleft",
        split_label = "Parameter",
        section_div = " ",
        split_fun = drop_split_levels
      ) %>%
      ## note the child_labels = hidden for AVISIT, these labels will be taken care off by
      ## applying function summarize_aval_chg_diff further in the layout
      split_rows_by("AVISIT",
        label_pos = "topleft",
        split_label = "Study Visit",
        split_fun = drop_split_levels,
        child_labels = "hidden"
      )

    lyt_vs <- lyt_vs_p1 %>%
      ### restart for the rrisk_header columns - note the nested = FALSE option
      ### also note the child_labels = "hidden" in both PARAM and AVISIT
      split_cols_by("rrisk_header_vs", nested = FALSE) %>%
      split_cols_by(trtvar,
        split_fun = remove_split_levels(ctrl_grp),
        labels_var = "rrisk_label",
        show_colcounts = TRUE,
        colcount_format = "N=xx"
      ) %>%
      ### difference columns : just 1 column & analysis needs to be done on change
      split_cols_by_multivar(multivars[3],
        varlabels = c(" ")
      ) %>%
      ### the variable passed here in analyze is not used (STUDYID), it is a dummy var passing,
      ### the function a_summarize_aval_chg_diff_j grabs the required vars from cols_by_multivar calls
      analyze("STUDYID",
        afun = a_summarize_aval_chg_diff_j,
        extra_args = extra_args_3col
      )

    result_vs <- build_table(lyt_vs, advs, alt_counts_df = adsl)

This is the resulting table.

    head(result_vs, 15)
    #>                                                                                             Active Study Agent                                                                                                                                                           Difference in Mean Change (95% CI)
    #>                                                            A: Drug X                                                                C: Combination                                                                B: Placebo                                   A: Drug X vs B: Placebo   C: Combination vs B: Placebo
    #> Parameter                                                    N=134                                                                       N=132                                                                       N=134                                              N=134                       N=132
    #>   Study Visit                  n/N (%)          Mean (95% CI)     Mean Change From Baseline (95% CI)       n/N (%)          Mean (95% CI)     Mean Change From Baseline (95% CI)       n/N (%)          Mean (95% CI)     Mean Change From Baseline (95% CI)
    #> —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
    #> Diastolic Blood Pressure
    #>   SCREENING                134/134 (100.0%)   50.0 (48.7, 51.2)                                        132/132 (100.0%)   50.2 (48.9, 51.5)                                        134/134 (100.0%)   50.8 (49.3, 52.2)
    #>   BASELINE                 134/134 (100.0%)   48.6 (47.2, 50.0)                                        132/132 (100.0%)   51.1 (49.8, 52.4)                                        134/134 (100.0%)   50.4 (49.1, 51.8)
    #>   WEEK 1 DAY 8             134/134 (100.0%)   50.3 (49.0, 51.5)            1.7 (-0.2, 3.5)             132/132 (100.0%)   48.9 (47.5, 50.2)           -2.3 (-4.0, -0.5)            134/134 (100.0%)   49.7 (48.4, 51.0)            -0.8 (-2.6, 1.1)                2.4 (-0.2, 5.0)             -1.5 (-4.1, 1.1)
    #>   WEEK 2 DAY 15            134/134 (100.0%)   50.8 (49.5, 52.2)             2.2 (0.2, 4.2)             132/132 (100.0%)   50.0 (48.5, 51.4)            -1.1 (-3.1, 0.9)            134/134 (100.0%)   49.7 (48.3, 51.2)            -0.7 (-2.9, 1.4)                3.0 (0.0, 5.9)              -0.4 (-3.3, 2.5)
    #>   WEEK 3 DAY 22            134/134 (100.0%)   50.7 (49.4, 52.0)             2.1 (0.3, 4.0)             132/132 (100.0%)   49.9 (48.6, 51.3)            -1.2 (-3.0, 0.7)            134/134 (100.0%)   49.1 (47.7, 50.4)            -1.3 (-3.2, 0.5)                3.5 (0.8, 6.1)              0.2 (-2.5, 2.8)
    #>   WEEK 4 DAY 29            134/134 (100.0%)   50.1 (48.7, 51.5)            1.5 (-0.4, 3.3)             132/132 (100.0%)   49.7 (48.3, 51.1)            -1.4 (-3.3, 0.5)            134/134 (100.0%)   49.6 (48.4, 50.8)            -0.8 (-2.6, 1.0)                2.3 (-0.3, 4.9)             -0.6 (-3.2, 2.0)
    #>   WEEK 5 DAY 36            134/134 (100.0%)   50.6 (49.3, 51.9)             2.0 (0.1, 3.9)             132/132 (100.0%)   49.1 (47.8, 50.4)           -2.0 (-3.9, -0.2)            134/134 (100.0%)   48.4 (47.0, 49.7)           -2.1 (-4.1, -0.1)                4.0 (1.3, 6.8)              0.1 (-2.6, 2.8)
    #>
    #> Pulse Rate
    #>   SCREENING                134/134 (100.0%)   49.6 (48.1, 51.1)                                        132/132 (100.0%)   49.3 (47.8, 50.8)                                        134/134 (100.0%)   49.4 (48.0, 50.8)
    #>   BASELINE                 134/134 (100.0%)   51.9 (50.5, 53.2)                                        132/132 (100.0%)   50.3 (48.6, 51.9)                                        134/134 (100.0%)   50.3 (48.8, 51.8)
    #>   WEEK 1 DAY 8             134/134 (100.0%)   50.1 (48.6, 51.5)            -1.8 (-3.8, 0.2)            132/132 (100.0%)   49.8 (48.5, 51.1)            -0.5 (-2.6, 1.7)            134/134 (100.0%)   49.3 (48.0, 50.6)            -1.0 (-3.0, 1.0)               -0.8 (-3.6, 2.0)             0.5 (-2.4, 3.5)
    #>   WEEK 2 DAY 15            134/134 (100.0%)   49.7 (48.2, 51.2)           -2.2 (-4.3, -0.0)            132/132 (100.0%)   49.1 (47.7, 50.4)            -1.2 (-3.1, 0.8)            134/134 (100.0%)   50.8 (49.5, 52.1)            0.6 (-1.5, 2.6)                -2.7 (-5.6, 0.2)             -1.8 (-4.5, 1.0)
    #>   WEEK 3 DAY 22            134/134 (100.0%)   50.5 (49.2, 51.7)            -1.4 (-3.3, 0.5)            132/132 (100.0%)   49.8 (48.6, 51.0)            -0.5 (-2.5, 1.6)            134/134 (100.0%)   49.9 (48.6, 51.3)            -0.4 (-2.2, 1.5)               -1.1 (-3.7, 1.6)             -0.1 (-2.9, 2.7)
    #>   WEEK 4 DAY 29            134/134 (100.0%)   49.0 (47.5, 50.4)           -2.9 (-4.8, -1.0)            132/132 (100.0%)   51.0 (49.7, 52.3)            0.7 (-1.3, 2.7)             134/134 (100.0%)   50.0 (48.5, 51.4)            -0.3 (-2.4, 1.7)               -2.6 (-5.4, 0.2)             1.0 (-1.8, 3.9)

The columns for the comparison between reference group is optional. The same table without these extra columns can be produced by specifying the argument `comp_btw_group` = `FALSE` and leaving out the 3 `split_cols_by` calls

- split_cols_by(“rrisk_header_vs”)
- split_cols_by(trtvar)
- split_cols_by_multivar(multivars[3])

    multivars <- c("AVAL", "AVAL", "CHG")

    extra_args_3col <- list(
      format_na_str = rep(NA, 3),
      ancova = FALSE,
      comp_btw_group = FALSE,
      multivars = multivars
    )

    lyt_vs2 <- lyt_vs_p1 %>%
      ### the variable passed here in analyze is not used (STUDYID), it is a dummy var passing,
      ### the function a_summarize_aval_chg_diff_j grabs the required vars from cols_by_multivar calls
      analyze("STUDYID",
        afun = a_summarize_aval_chg_diff_j,
        extra_args = extra_args_3col
      )

    result_vs2 <- build_table(lyt_vs2, advs, alt_counts_df = adsl)

This is the resulting table without the difference between treatment group columns.

    head(result_vs2, 15)
    #>                                                                                             Active Study Agent
    #>                                                            A: Drug X                                                                C: Combination                                                                B: Placebo
    #> Parameter                                                    N=134                                                                       N=132                                                                       N=134
    #>   Study Visit                  n/N (%)          Mean (95% CI)     Mean Change From Baseline (95% CI)       n/N (%)          Mean (95% CI)     Mean Change From Baseline (95% CI)       n/N (%)          Mean (95% CI)     Mean Change From Baseline (95% CI)
    #> ————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
    #> Diastolic Blood Pressure
    #>   SCREENING                134/134 (100.0%)   50.0 (48.7, 51.2)                                        132/132 (100.0%)   50.2 (48.9, 51.5)                                        134/134 (100.0%)   50.8 (49.3, 52.2)
    #>   BASELINE                 134/134 (100.0%)   48.6 (47.2, 50.0)                                        132/132 (100.0%)   51.1 (49.8, 52.4)                                        134/134 (100.0%)   50.4 (49.1, 51.8)
    #>   WEEK 1 DAY 8             134/134 (100.0%)   50.3 (49.0, 51.5)            1.7 (-0.2, 3.5)             132/132 (100.0%)   48.9 (47.5, 50.2)           -2.3 (-4.0, -0.5)            134/134 (100.0%)   49.7 (48.4, 51.0)            -0.8 (-2.6, 1.1)
    #>   WEEK 2 DAY 15            134/134 (100.0%)   50.8 (49.5, 52.2)             2.2 (0.2, 4.2)             132/132 (100.0%)   50.0 (48.5, 51.4)            -1.1 (-3.1, 0.9)            134/134 (100.0%)   49.7 (48.3, 51.2)            -0.7 (-2.9, 1.4)
    #>   WEEK 3 DAY 22            134/134 (100.0%)   50.7 (49.4, 52.0)             2.1 (0.3, 4.0)             132/132 (100.0%)   49.9 (48.6, 51.3)            -1.2 (-3.0, 0.7)            134/134 (100.0%)   49.1 (47.7, 50.4)            -1.3 (-3.2, 0.5)
    #>   WEEK 4 DAY 29            134/134 (100.0%)   50.1 (48.7, 51.5)            1.5 (-0.4, 3.3)             132/132 (100.0%)   49.7 (48.3, 51.1)            -1.4 (-3.3, 0.5)            134/134 (100.0%)   49.6 (48.4, 50.8)            -0.8 (-2.6, 1.0)
    #>   WEEK 5 DAY 36            134/134 (100.0%)   50.6 (49.3, 51.9)             2.0 (0.1, 3.9)             132/132 (100.0%)   49.1 (47.8, 50.4)           -2.0 (-3.9, -0.2)            134/134 (100.0%)   48.4 (47.0, 49.7)           -2.1 (-4.1, -0.1)
    #>
    #> Pulse Rate
    #>   SCREENING                134/134 (100.0%)   49.6 (48.1, 51.1)                                        132/132 (100.0%)   49.3 (47.8, 50.8)                                        134/134 (100.0%)   49.4 (48.0, 50.8)
    #>   BASELINE                 134/134 (100.0%)   51.9 (50.5, 53.2)                                        132/132 (100.0%)   50.3 (48.6, 51.9)                                        134/134 (100.0%)   50.3 (48.8, 51.8)
    #>   WEEK 1 DAY 8             134/134 (100.0%)   50.1 (48.6, 51.5)            -1.8 (-3.8, 0.2)            132/132 (100.0%)   49.8 (48.5, 51.1)            -0.5 (-2.6, 1.7)            134/134 (100.0%)   49.3 (48.0, 50.6)            -1.0 (-3.0, 1.0)
    #>   WEEK 2 DAY 15            134/134 (100.0%)   49.7 (48.2, 51.2)           -2.2 (-4.3, -0.0)            132/132 (100.0%)   49.1 (47.7, 50.4)            -1.2 (-3.1, 0.8)            134/134 (100.0%)   50.8 (49.5, 52.1)            0.6 (-1.5, 2.6)
    #>   WEEK 3 DAY 22            134/134 (100.0%)   50.5 (49.2, 51.7)            -1.4 (-3.3, 0.5)            132/132 (100.0%)   49.8 (48.6, 51.0)            -0.5 (-2.5, 1.6)            134/134 (100.0%)   49.9 (48.6, 51.3)            -0.4 (-2.2, 1.5)
    #>   WEEK 4 DAY 29            134/134 (100.0%)   49.0 (47.5, 50.4)           -2.9 (-4.8, -1.0)            132/132 (100.0%)   51.0 (49.7, 52.3)            0.7 (-1.3, 2.7)             134/134 (100.0%)   50.0 (48.5, 51.4)            -0.3 (-2.4, 1.7)