Advanced rtables - Structure-Conditional Behavior In `afun`s With `.spl_context`
Contributed by Johnson & Johnson Innovative Medicine
Gabriel Becker
Dan Hofstaedter
2025-10-22
Source:vignettes/guided_advanced_afuns_spl_context.Rmd
guided_advanced_afuns_spl_context.Rmd#> Loading required package: formatters
#>
#> Attaching package: 'formatters'
#> The following object is masked from 'package:base':
#>
#> %||%
#>
#> Attaching package: 'rtables'
#> The following object is masked from 'package:utils':
#>
#> str
Split Context
The split context (i.e., the optional .spl_context
argument to a/c functions) provides analysis functions the ability to
know what substructure of the table it is calculating cell values for,
the data (sub)set corresponding to that substructure, and the steps -
both in terms of faceting structure and corresponding data subsetting -
taken by the tabulation engine to arrive where it is now.
This allows us to program custom analysis functions which have behavior conditional on which row or column facet they are currently calculating cell values for, as we will see further down in this document.
The split context is a data.frame with one row
per faceting step in row space up to and including the one the analysis
function is being called within, and the following columns which vary
across context rows:
-
split(character) - name of the split represented by each row of the split context -
value(character) - string representation of the value of the split for each row of the context -
full_parent_df(listofdata.frames) - the full data (across all columns) corresponding to each row faceting step -
all_cols_n(integer) - the observation count for each row faceting step (across all columns) -
<column names>(onelistcolumn per column in the table) - logical vectors corresponding to the subset offull_parent_dffor the named column for each faceting step. Named bynames(col_exprs(tab)).
In addition, the context contains the following columns which are constant across context rows:
-
cur_col_id(character) - identifier for the current column -
cur_col_expr(listofexpresssionobjects) - symbolic expression for subset corresponding to current column. -
cur_col_n(integer) - column count for the current column -
cur_col_split(listofcharacter) - vector of split names from the path which resolves to the current column. -
cur_col_split_value(listofcharacter) - vector of split values from the path which resolves to the current column.
Interleaving cur_col_split and
cur_col_split_value will recreate the full unique column
path for the current column.
Designing Conditional Behavior in afuns
Recall that table contents are (typically) calculated by repeated calling the analysis or content function for a given row facet - once per individual column within the table structure.
Conditional-On-Column Behavior in afuns
If we want our table to have different types of content in
different cells of the same row, we need an afun that
- can determine where in column space it is calculating cells for, and
- implements two or more behaviors which it selects between based on (1)
Note it is mandatory that the calls to our analysis or content function result in the same number of rows within each column. This can involve padding the results with blank cells in some columns.
Determining Column-Space Position Within An afun
We can use the cur_col_* elements of the split context -
all of which are constant across rows - to determine where in the column
structure we are creating cells for, as we saw in the Translating
Shells portion of the intermediate guided tour. In that function we
used cur_col_id to indicate column, but using
cur_col_split and/or cur_col_split_value is
more robust, as follows:
in_risk_diff <- function(spl_context) {
any(grepl("Risk Differences", spl_context$cur_col_split_value[1]))
}We can use the first element of the cur_col_id column of
the split context because as noted above, the column information columns
are constant across rows in the context.
The cur_col_id value in the split context is currently
computed by pasting the split values of the column path to the current
column.
General Template For Column Aware afun
Assuming two desired behaviors depending on column position (e.g.,
primary or risk difference column), a general template for a
conditional-on-column afun is:
col_condition <- function(spl_context) {
## return TRUE or FALSE
}
col_cond_afun_template1 <- function(df, .var, ..., .spl_context) {
## shared processing
if (col_condition(.spl_context)) {
## alternate behavior
## data processing
## value calculation
## determine cell formats, etc
} else {
## primary behavior
## data processing
## value calculation
## determine cell formats, etc
}
## label calculation, etc if necessary
in_rows(val_list, .labels = lbl_vector, .formats = format_vector)
}Or, alternatively if we have two existing afuns that
each fully encapsulate the desired behavior for one of the
conditions,
col_cond_afun_template2 <- function(df, .var, ..., .spl_context) {
if (col_condition(.spl_context)) {
alt_behavior_afun(df, .var, ..., .spl_context = .spl_context)
} else {
main_behavior_afun(df, .var, ..., .spl_context = .spl_context)
}
}We note that both of the approaches above would be straightforward to
extend to more than two conditional behaviors by utilizing a condition
function which could return more than two values, and a
switch call or
if/ifelse/else block. We leave
this as an exercise for the reader.
Using Row Faceting Information Within afuns
The split context is a data frame with a row for each preceding row faceting (splitting) step; in particular, as described above, we have access to the full data, split name, and split value for each of these steps.
Some illustrative examples of code extracting information from the split context are:
|+——————————————————+|+————–+|+——————+|+————————————————+| |
.split_context$full_parent_df[[1]] | first | root (no
faceting) | full data (that was passed to build_table | |
.split_context$split[NROW(.split_context)] | last | current
facet | name of the current split (typically a var name) | |
.split_context$split_value[NROW(.split_context) - 1] |
second to last | parent facet | facet value of parent facet |
We often want to retrieve reference group information for use in model fitting or risk difference calculations. In practice this translates to a different column facet’s intersection with our current row facet than the column we are currently operating within.
Given a ref_path, which can be passed as an extra
argument in the analyze call if it’s constant or set as an
extra argument on each split value by a custom split function if not
(see junco’s grouped_cols_w_diffs function for
an example of this), we can extract the relevant data.
We will use the fact that the column subsetting vectors are included in the split context by their “col ids”, which currently are constructed by pasting the split values (only) collapsed with “.”:
basic_get_ref <- function(ref_path, spl_context) {
facet_dat <- spl_context$full_parent_df[[NROW(spl_context)]]
ref_col_id <- paste(ref_path[seq(2, length(ref_path), by = 2)])
ref_subset_vec <- spl_context[[ref_col_id]][[NROW(spl_context)]]
ref_dat <- facet_dat[ref_subset_vec, ]
list(ref_group = ref_dat, in_ref_col = ref_col_id == spl_context$cur_col_id[[1]])
}We can see that this is working via a diagnostic table that shows us what is coming out of that function:
diag_afun <- function(df, .spl_context, ref_path) {
ref_info <- basic_get_ref(ref_path, .spl_context)
in_rows(
data_dim = dim(df),
ref_dim = dim(ref_info$ref_group),
in_ref_col = ref_info$in_ref_col,
.formats = c(
data_dim = "xx, xx",
ref_dim = "xx, xx",
in_ref_col = "xx"
)
)
}
lyt <- basic_table() |>
split_cols_by("ARM") |>
split_rows_by("STRATA1") |>
split_rows_by("SEX", split_fun = keep_split_levels(c("F", "M"))) |>
analyze("AGE", diag_afun, extra_args = list(ref_path = c("ARM", "B: Placebo")))
build_table(lyt, ex_adsl)
# A: Drug X B: Placebo C: Combination
# ————————————————————————————————————————————————————————
# A
# F
# data_dim 21, 31 24, 31 18, 31
# ref_dim 24, 31 24, 31 24, 31
# in_ref_col FALSE TRUE FALSE
# M
# data_dim 16, 31 19, 31 20, 31
# ref_dim 19, 31 19, 31 19, 31
# in_ref_col FALSE TRUE FALSE
# B
# F
# data_dim 25, 31 27, 31 21, 31
# ref_dim 27, 31 27, 31 27, 31
# in_ref_col FALSE TRUE FALSE
# M
# data_dim 21, 31 17, 31 21, 31
# ref_dim 17, 31 17, 31 17, 31
# in_ref_col FALSE TRUE FALSE
# C
# F
# data_dim 33, 31 26, 31 27, 31
# ref_dim 26, 31 26, 31 26, 31
# in_ref_col FALSE TRUE FALSE
# M
# data_dim 14, 31 19, 31 19, 31
# ref_dim 19, 31 19, 31 19, 31
# in_ref_col FALSE TRUE FALSE