---
title: "Intermediate rtables - Translating Shells To Layouts"
subtitle: Contributed by Johnson & Johnson Innovative Medicine
date: "2025-06-17"
author:
- Gabriel Becker
- Dan Hofstaedter
output:
    rmarkdown::html_document:
        theme: "spacelab"
        highlight: "kate"
        toc: true
        toc_float: true
vignette: >
  %\VignetteIndexEntry{Intermediate rtables - Translating Shells To Layouts}
  %\VignetteEncoding{UTF-8}
  %\VignetteEngine{knitr::rmarkdown}
editor_options:
  markdown:
      wrap: 72
  chunk_output_type: console
---

```{r, include = FALSE}
suggested_dependent_pkgs <- c("dplyr")
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  eval = all(vapply(
    suggested_dependent_pkgs,
    requireNamespace,
    logical(1),
    quietly = TRUE
  ))
)
```

```{r, echo=FALSE}
knitr::opts_chunk$set(comment = "#")
```


# Introduction

The first - and often largest - hurdle to creating a table via
`rtables` is translating the desired table structure (typically in the
form of a *table shell*) into an `rtables` *layout*. We will cover
that translation process in this vignette.

## A Table Shell

Table shells can come in various forms. We will begin with a table
shell which is essentially the entire table with desired formatting
indicated instead of values:

```{r init, echo = FALSE, results = "hidden"}
suppressPackageStartupMessages(library(rtables))
suppressPackageStartupMessages(library(dplyr))

fixed_shell <- function(tt) {
  mystr <- table_shell_str(tt)
  regex_hits <- gregexpr("[(]N=[[:digit:]]+[)]", mystr)[[1]]
  hit_lens <- attr(regex_hits, "match.length")
  if (regex_hits[1] > 0) {
    for (i in seq_along(regex_hits)) {
      start <- regex_hits[i]
      len <- hit_lens[i]
      substr(mystr, start, start + len - 1) <- padstr("(N=xx)", len, just = "center")
    }
  }
  cat(mystr)
}

knitr::opts_chunk$set(comment = "")
```


```{r, echo = FALSE, result = "asis"}
adsl <- ex_adsl %>%
  filter(SEX %in% c("M", "F") & RACE %in% c("ASIAN", "BLACK OR AFRICAN AMERICAN", "WHITE")) %>%
  mutate(
    BMEASIFL = factor(as.character(BMEASIFL),
      levels = c("Y", "N"),
      labels = c("Yes", "No")
    ),
    SEX = factor(as.character(SEX),
      levels = c("M", "F", "UNDIFFERENTIATED", "U"),
      labels = c("Male", "Female", "Undifferentiated", "Unknown")
    ),
    RACE = factor(as.character(RACE),
      levels = c("ASIAN", "BLACK OR AFRICAN AMERICAN", "WHITE"),
      labels = c("Asian", "Black", "White")
    )
  )


lyt <- basic_table(
  title = "Subject Response by Race and Sex; Treated Subjects",
  show_colcounts = TRUE
) %>%
  split_cols_by("STRATA1", split_fun = keep_split_levels(only = c("A", "B"))) %>%
  split_cols_by("ARM", split_fun = keep_split_levels(only = c("A: Drug X", "B: Placebo"))) %>%
  analyze("BMEASIFL", afun = counts_wpcts, var_labels = "All Patients", show_labels = "visible") %>%
  split_rows_by(
    var = "RACE",
    label_pos = "topleft",
    split_fun = keep_split_levels(only = c("Asian", "Black", "White"))
  ) %>%
  split_rows_by(
    var = "SEX",
    label_pos = "topleft",
    split_fun = keep_split_levels(only = c("Male", "Female"))
  ) %>%
  summarize_row_groups(
    var = "SEX",
    format = "xx"
  ) %>%
  analyze(
    vars = "BMEASIFL",
    afun = counts_wpcts
  )

result <- build_table(lyt, adsl)
fixed_shell(result)
```

We will use this shell to illustrate the translation process to an
rtables layout, and thus ultimately a table output.

## A *Brief* Review Of `rtables` Layouts

For an in-depth discussion of how constructing a layout works we refer
the reader to other documentation. That said, there are a couple
things to remember as we consider translating shells into layouts:

1. Individual rows are declared by `analyze*` calls
2. Individual columns are the result of column faceting
3. New faceting will be nested within existing faceting in the same dimension (row/col) by default
4. All row faceting structures must be terminated with at least one analysis (`analyze`)
5. Row faceting which occurs directly after an `analyze` will *not* be nested

With those in mind, we will now discuss how to translate shells into layouts.

# Translating

There are three aspects to a shell that we must translate:

1. Column faceting structure
2. Row faceting structure
3. Cell contents
   - Marginal content for row facet structure
   - Individual facet content
 
We will explore each portion of the translation process separately.

## Translating Column Structure

Our first task, translating column structure, revolves around
identifying faceting in the column dimension of a shell or desired
table.

Our shell gives us the following to indicate column structure:

```{r, echo = FALSE, result="asis"}
fixed_shell(result[0, ])
```

The easiest way to identify faceting is to look at column- or
row-labels and determine the scope (i.e., the set of individual
columns or rows) they apply to.

For example, we see that the `"A"` column label applies to a group of
multiple columns each of which represent an individual arm:

```{r}
fixed_shell(result[0, c("A", "*")])
```

Thus we have strata faceting with arm faceting nested within it.

Faceting most commonly represents partitioning the data being
tabulated by the values of a categorical variable, though `rtables`
supports a generalized concept of faceting where the data group can
overlap and need not be exhaustive.

For our table, the faceting is nested faceting by the `"STRATA1"`, and
`"ARM"` variables. We achieve this by repeated calls to
`split_cols_by` (with the default `nested = TRUE`), with the call declaring
the outermost faceting first:


```{r}
lyt_cols <- basic_table() |>
  split_cols_by("STRATA1", split_fun = keep_split_levels(only = c("A", "B"))) %>%
  split_cols_by("ARM", split_fun = keep_split_levels(only = c("A: Drug X", "B: Placebo")))

build_table(lyt_cols, adsl)
```

This is almost correct. To fully achieve our shell we need the column
counts to show up, which we do via the `show_colcounts` argument in
the relevant `split_cols_by` call:

```{r}
lyt_cols <- basic_table() |>
  split_cols_by("STRATA1", split_fun = keep_split_levels(only = c("A", "B"))) %>%
  split_cols_by("ARM",
    split_fun = keep_split_levels(only = c("A: Drug X", "B: Placebo")),
    show_colcounts = TRUE
  )

build_table(lyt_cols, adsl)
```


This is a relatively straightforward column structure. We will cover
more complex ones later. Nevertheless we have translated our shell's
column space into `rtables` layouting instructions.


## Translating Row Structure

Moving to the second aspect of translation, we will now translate the
row structure of our shell. Interpreting row structure is similar to
interpreting column structure with the caveat that individual rows do
not come from faceting, but rather from analysis (which is in charge
of populating the contents of the table's primary, non-marginal
cells).

Our row structure is slightly less trivial than our column
structure. We can see two sections in our shell, one that displays the
response (`"BMEASIFL"`) of all patients collectively (by the column
structure):


```{r, echo = FALSE}
fixed_shell(tt_at_path(result, "BMEASIFL"))
```

and one that subsets the patients before displaying the response
within each subset, and with some marginal rows for context.

```{r, echo = FALSE}
fixed_shell(tt_at_path(result, "RACE"))
```

Because none of the labels or cell-values from the all patients
portion of the table apply directly to the subset analysis portion -
and vice versa - we can treat these separately.

In point of fact, the first portion does not require any structure
beyond an analysis of the `"BMEASIFL" variable with a label, so we can
leave that for the third translation step.

We can illustrate this using a dummy analyze as follows:

```{r}
dummy_afun <- function(x, ...) in_rows("Analysis" = "-")
lyt_a <- basic_table() |>
  analyze("BMEASIFL",
    afun = dummy_afun,
    var_labels = "All Patients",
    show_labels = "visible"
  )

build_table(lyt_a, adsl)
```

While we do not have the individual rows we desired, as that is left
to step 3 of translation, we can see that we have successfully created
the first portion of the row *structure*.

Note that in most tables the column and row structure are orthogonal
and so we do not need to worry about columns when we are translating
the row structure.

Also note we *could* say that there is a facet there which contains all the
patients and has the name/label `"All Patients"`; this would result in
an equivalent table from an output perspective but there isn't really
any benefit to the added layouting instructions that would be
required, so we will not do so here.

The second portion of the table contains labels and rows which *do*
apply to multiple individual rows.

We see that the `"Asian"` label, for example, applies across the
corresponding `"Male"` and `"Female"` labels/marginal rows, each of
which in turn applies to a group of individual rows (`"Yes"`, and
`"No"`).

Thus we can recreate this section via nested faceting, this time with 

```{r}
lyt_b <- basic_table() |>
  split_rows_by("RACE") |>
  split_rows_by("SEX") |>
  analyze("BMEASIFL", afun = dummy_afun)

head(build_table(lyt_b, adsl), 30)
```

We are almost there, but we see extra `"SEX"` values that weren't in
our shell. We can prevent this with the `keep_split_levels` function
provided by `rtables`:

```{r}
lyt_b2 <- basic_table() |>
  split_rows_by("RACE") |>
  split_rows_by("SEX", split_fun = keep_split_levels(only = c("Male", "Female"))) |>
  analyze("BMEASIFL", afun = dummy_afun)


build_table(lyt_b2, adsl)
```

Finally, we can combine the two sections by simply combining the relevant layout instructions:


```{r}
lyt_b3 <- basic_table() |>
  analyze("BMEASIFL",
    afun = dummy_afun,
    var_labels = "All Patients",
    show_labels = "visible"
  ) |>
  split_rows_by("RACE") |>
  split_rows_by("SEX", split_fun = keep_split_levels(only = c("Male", "Female"))) |>
  analyze("BMEASIFL", afun = dummy_afun)

build_table(lyt_b3, adsl)
```

Note here that row split instructions which directly follow an
`analyze` call will automatically be non-nested, so we do not need to
specify `nested = FALSE` in the `"RACE"` split, though doing so would
not harm anything.

We can convince ourselves that treating the column and row structure
separately by combining the layouting instructions for both to receive
something equivalent in structure (i.e., up individual rows and marginal cell contents) to our
shell:


```{r}
lyt_struct <- basic_table() |>
  split_cols_by("STRATA1", split_fun = keep_split_levels(only = c("A", "B"))) |>
  split_cols_by("ARM",
    split_fun = keep_split_levels(only = c("A: Drug X", "B: Placebo")),
    show_colcounts = TRUE
  ) |>
  analyze("BMEASIFL",
    afun = dummy_afun,
    var_labels = "All Patients",
    show_labels = "visible"
  ) |>
  split_rows_by("RACE") |>
  split_rows_by("SEX", split_fun = keep_split_levels(only = c("Male", "Female"))) |>
  analyze("BMEASIFL", afun = dummy_afun)

build_table(lyt_struct, adsl)
```

We can see that the marginal cells for `"Male"` and `"Female"` within
each race are not present, but we will handle those in the third
translation step.


## Translating Cell Contents

Finally, we will finish our translation with the third step:
translating cell contents.

Tables can contain up to two types of rows with non-empty cells as
reckoned by the `rtables` conceptual model: individual analysis rows,
and marginal group summary rows (called *content* rows by the
`rtables` internals).

Analysis rows are declared via `analyze` during layout construction;
an analysis function (the `afun` argument) specifying how *all cells
within a single facet pane should be simultaneously created*.

We see in our shell that we want two rows whenever we analyze
`BMEASIFL` response: one for `"Yes"` and one for `"No"`.

Most analysis functions provided by `rtables` or extensions like
`tern` or `junco` will automatically generate multiple rows when
analyzing a categorical variable (i.e., factor):


```{r}
rw_lyt <- basic_table() |>
  analyze("BMEASIFL",
    var_labels = "All Patients",
    show_labels = "visible"
  )

build_table(rw_lyt, adsl)
```

Further, recall that the faceting does the work of identifying subsets
and applying our analyses within those facets/subsets
automatically. Thus by applying the *structural* layout instructions
we translated above, we get something that is getting pretty close to
our desired table:


```{r}
rw_lyt_struct <- basic_table() |>
  split_cols_by("STRATA1", split_fun = keep_split_levels(only = c("A", "B"))) |>
  split_cols_by("ARM",
    split_fun = keep_split_levels(only = c("A: Drug X", "B: Placebo")),
    show_colcounts = TRUE
  ) |>
  analyze("BMEASIFL",
    var_labels = "All Patients",
    show_labels = "visible"
  ) |>
  split_rows_by("RACE") |>
  split_rows_by("SEX", split_fun = keep_split_levels(only = c("Male", "Female"))) |>
  analyze("BMEASIFL")

build_table(rw_lyt_struct, adsl)
```

Two aspects remain before we have matched our desired shell: our
marginal counts in the the individual gender rows within each race are
missing, and our analysis rows contain only counts rather than
matching the desired `"xx (xx.x%)"` format of count and percent.

`rtables` provides a (*very*) simple afun to calculate count percent
values (`counts_wpcts`) which we can use for illustration purposes
here.  We will see later that it is not flexible enough to meet a
study team's full set of needs and more complex afuns will be used in
practice in production.

```{r}
rw_lyt_structb <- basic_table() |>
  split_cols_by("STRATA1", split_fun = keep_split_levels(only = c("A", "B"))) |>
  split_cols_by("ARM",
    split_fun = keep_split_levels(only = c("A: Drug X", "B: Placebo")),
    show_colcounts = TRUE
  ) |>
  analyze("BMEASIFL",
    afun = counts_wpcts,
    var_labels = "All Patients",
    show_labels = "visible"
  ) |>
  split_rows_by("RACE") |>
  split_rows_by("SEX", split_fun = keep_split_levels(only = c("Male", "Female"))) |>
  analyze("BMEASIFL", afun = counts_wpcts)

build_table(rw_lyt_structb, adsl)
```

Now, all we need is the marginal gender counts. We do this by adding
`summarize_row_groups` *directly after* the relevant row faceting
(`split_rows_by`) instruction in the layout. This function can accept
a fully custom function (the `cfun` argument), but for our purposes,
we can control whether the percent is included in the default group
summary with the `format` argument.

```{r}
lyt_final <- basic_table() |>
  split_cols_by("STRATA1", split_fun = keep_split_levels(only = c("A", "B"))) |>
  split_cols_by("ARM",
    split_fun = keep_split_levels(only = c("A: Drug X", "B: Placebo")),
    show_colcounts = TRUE
  ) |>
  analyze("BMEASIFL",
    afun = counts_wpcts,
    var_labels = "All Patients",
    show_labels = "visible"
  ) |>
  split_rows_by("RACE") |>
  split_rows_by("SEX", split_fun = keep_split_levels(only = c("Male", "Female"))) |>
  summarize_row_groups(format = "xx") |>
  analyze("BMEASIFL", afun = counts_wpcts)

build_table(lyt_final, adsl)
```

Thus, we have fully translated our shell into an `rtables` declarative
layout and realized our desired table output.

In the remainder of this vignette we will walk through a number of
shells with more complex structural elements and how to translate them
into `rtables` layouts.

# Spanning Column Headers

Some shells will call for spanning labels in column space which do not
directly reflect a categorical variable in the raw data, but rather
represent groups of levels in a variable, e.g., trial arms.

For example, we might have the following column structure in a shell:


```{r, echo = FALSE}
span_map <- data.frame(
  ARM = c("A: Drug X", "B: Placebo", "C: Combination"),
  span_label = c("Active Treatment", " ", "Active Treatment")
)

lyt_span <- basic_table() |>
  split_cols_by("span_label", split_fun = trim_levels_to_map(span_map)) |>
  split_cols_by("ARM", show_colcounts = TRUE)

adsl2 <- adsl
adsl2$span_label <- "Active Treatment"
adsl2$span_label[adsl2$ARM == "B: Placebo"] <- " "

tbl_colspans <- build_table(lyt_span, adsl2)
fixed_shell(tbl_colspans)
```

Here we see the "Active Treatment" label spanning arms A and C, while
no label appears above the column for arm B. There are a couple things
to decode here that will collapse this column structure into a nested
faceting structure as we saw above.

Most importantly, while uneven splitting *is* possible with `rtables`,
including in column space, we can get our desired output by allowing
the B arm to have an invisible spanning label which is simply a single
space (`" "`). Viewing the structure this way, we can see that we have
two levels of faceting, one which splits between so called active
treatments and the remaining arms, and within that, we facet on
individual arm.

This brings us to our second issue: we don't have a variable for
active vs non-active treatments. There are a few ways to address this,
but the most user-friendly way is simply to create one as a
preprocessing step on the data before we make our table:

```{r}
adsl_forspans <- adsl
adsl_forspans$span_label <- "Active Treatment"
adsl_forspans$span_label[adsl_forspans$ARM == "B: Placebo"] <- " "

qtable(adsl_forspans, "ARM", "span_label")
```

With that we can build a table with the desired nested splitting:

```{r}
lyt_cspan <- basic_table() |>
  split_cols_by("span_label") |>
  split_cols_by("ARM", show_colcounts = TRUE)

build_table(lyt_cspan, adsl_forspans)
```

So we are getting close, but our individual arm columns are not only
showing up under their correct spanning label (though we see that the
*data* are being siphoned under the correct labels by the column
counts).

This type of non-full-factorial nesting is common; we often only want
facets that *make logical sense* within a nested faceting structure,
while wanting to omit any that don't (e.g., in our table, the Active
Treatment - Placebo facet).

`rtables` provides multiple ways to declare this behavior in the form
of both full *split functions* and *split function behavior building
blocks*, the latter being for use within `make_split_fun`. For now, we
will use a built-in full split function as we will be covering
`make_split_fun` in a different vignette.

Our two options for split functions are `trim_levels_in_group` and
`trim_levels_to_map`; the former is empirical and will keep all
combinations which are *observed in the data*, omitting any that
aren't. The latter requires us to provide a map of all combinations to
be displayed, but is more robust to sparse data (e.g., a data snapshot
from an in-flight trial) and allows for displaying zero counts for
unobserved but desired combinations.

Other than being empirical and declarative, respectively,
`trim_levels_in_group` and `trim_levels_to_map` behave similarly: when
used while splitting on a variable (the "outer variable"), the
observations and factor levels of of another ("inner") variable are
restricted independently within each facet for the outer variable.

In our case, our outer variable is `"span_label"`, while our inner
variable would be `"ARM"`. Thus we want to restrict the levels of
`"ARM"` within each facet of `"span_label"`. For our toy example here,
the two split functions will be equivalent, but we will use
`trim_levels_to_map` as it is more robust and appropriate for more
cases of production use.

Thus we need to create our map, a `data.frame` that contains the two
variables with each desired combination as a separate row:

```{R}
span_label_map <- tribble(
  ~span_label, ~ARM,
  "Active Treatment", "A: Drug X",
  "Active Treatment", "C: Combination",
  " ", "B: Placebo",
)

lyt_cspan_final <- basic_table() |>
  split_cols_by("span_label",
    split_fun = trim_levels_to_map(span_label_map)
  ) |>
  split_cols_by("ARM", show_colcounts = TRUE)

build_table(lyt_cspan_final, adsl_forspans)
```

Thus we have again achieved a "table" matching our desired shell. We
can consider only the column structure because in this case as
previously the column structure, row structure, and analysis are all
orthogonal. We will see an example where that isn't fully the case below

**Note:** in the general case, the level map used in
`trim_levels_to_map` will be a function of the data dictionaries for
the relevant variables within your study, thus for combinations of
actual variables these maps should not require manual construction as
we did above.

# Heterogeneous Column Structures (e.g., Risk Difference Columns)

In our previous examples, the column structure was simple nested
faceting, both in the case of faceting on two variables from the data,
and in the case we wanted spanning labels.

While this simple nesting structure is relatively common,
particularly for column structure, it does not fit the shells for all
tables we might need to create. One example of this is risk difference
columns, as found in modern FDA guidance for Adverse Event (AE)
tables.

In this section we will translate a shell with both spanning headers
and risk difference columns into a layout. To avoid subtleties about
counting we will analyze the `BMRKR2` variable in our synthetic
`ADSL` dataset rather than going for a realistic AE table. These
counting issues and realistic AE tables will be addressed elsewhere in
this series of vignettes.

## Risk Difference Columns

Many tables call for "risk difference", or comparison columns, in
addition to those used for the primary counts. When combined with
spanning labels, the column structure of our shell would look
something like:

```{r, echo = FALSE}
adsl2$rr_header <- "Risk Differences"
adsl2$rr_label <- paste(adsl2$ARM, "vs B: Placebo")
lyt_rr <- basic_table() |>
  split_cols_by("span_label", split_fun = trim_levels_to_map(span_map)) |>
  split_cols_by("ARM", show_colcounts = TRUE) |>
  split_cols_by("rr_header", nested = FALSE) |>
  split_cols_by("ARM", labels_var = "rr_label", split_fun = remove_split_levels("B: Placebo"))


tbl_rr_shell <- build_table(lyt_rr, adsl2)
fixed_shell(tbl_rr_shell)
```

We see that the first portion of the column structure is the same, but
we now have the risk difference structure in addition. There are a
number of different ways to model risk difference columns but we will
do so as a separate nested substructure. Thus as we did with the
"Active Treatment" spanning label, we will create and then facet on a
variable that gives us the "Risk Differences" label.

We can build up this substructure separately and then combine it with
the structure we created above to match the full shell.


```{r}
adsl_rr <- adsl_forspans
adsl_rr$rr_header <- "Risk Differences"

lyt_only_rr <- basic_table() |>
  split_cols_by("rr_header") |>
  split_cols_by("ARM")

build_table(lyt_only_rr, adsl_rr)
```

This is getting close there are two issues: first, we don't want a
placebo column (which would nonsensically compare placebo against
itself), and the labels are simply the individual arms rather than the
pair of arms being compared as in our shell.

We can restrict the facets generated using the `remove_split_levels`
(or sibling `keep_split_levels`) split function provided by
`rtables`. In addition the `split_*_by` functions accept the
`labels_var` argument which specifies an additional variable which
should be used for the *labels* (not names) of the facets
generated. With preprocessing to create such a variable, and combining
these two approaches, we can achieve the risk difference structure:

```{r}
adsl_rr$rr_label <- paste(adsl_rr$ARM, "vs B: Placebo")

lyt_only_rr2 <- basic_table() |>
  split_cols_by("rr_header") |>
  split_cols_by("ARM",
    split_fun = remove_split_levels("B: Placebo"),
    labels_var = "rr_label"
  )

build_table(lyt_only_rr2, adsl_rr)
```

To combine our two sections of column structure, we simply combine the
sets of layouting instructions and add `nested = FALSE` to our split
on `"rr_header"`:


```{r}
lyt_rr_cols <- basic_table() |>
  split_cols_by("span_label",
    split_fun = trim_levels_to_map(span_label_map)
  ) |>
  split_cols_by("ARM", show_colcounts = TRUE) |>
  split_cols_by("rr_header", nested = FALSE) |>
  split_cols_by("ARM",
    split_fun = remove_split_levels("B: Placebo"),
    labels_var = "rr_label"
  )

build_table(lyt_rr_cols, adsl_rr)
```

Note that because we used `show_colcounts` in our `split_cols_by` call
for `"ARM"`, rather than in `build_table`, we have counts for our main
arm columns but not for our comparison columns, as desired.

One caveat here, however, is that we will need a more sophisticated
analysis function because its behavior is no longer independent of
which facet it is in: it might generate e.g., counts for the primary
arm columns and then confidence intervals for our risk difference
columns.

Typically trial teams will be using pre-existing analysis functions
for this, but we will illustrate these can be constructed now.

## Column-structure Aware Analysis Functions

Our analysis function needs two "modes": the primary arm column mode
and the risk difference mode, and it needs to be able to distinguish
between them.

Analysis (and content, i.e., row group summary) functions can accept
the optional `.spl_context` argument to receive information where in
the faceting structure the facet they are currently populating is. We
will leave a detailed discussion of the full contents of the split
context to other documentation and simply use the portions we need
here.

In particular, we will use the `cur_col_id` column of `.spl_context`
to determine which section of the column structure we are under. Note
that due to the vagaries of the current implementation, this is
constructed of the *labels* for the column facets rather than their
names. This is the split/value pairs of each column split in order
concatenated together, so it suffices to define

```{r}
in_risk_diff <- function(spl_context) grepl("Risk Differences", spl_context$cur_col_id[1])
```


For simplicity, we will not worry about *calculating* risk differences
here, and simply write an analysis function that emits something
different to show that it can tell it is in "risk difference mode".

Thus a very simplistic afun is as follows:

```{r}
rr_afun <- function(x, .N_col, .spl_context) {
  xtbl <- table(x)
  if (in_risk_diff(.spl_context)) {
    armlabel <- tail(.spl_context$cur_col_split_val[[1]], 1) # last split value, ie arm
    armletter <- substr(armlabel, 1, 1)
    vals <- as.list(rep(paste(armletter, "vs B"), length(xtbl)))
    fmts <- rep("xx", length(xtbl))
  } else {
    vals <- lapply(xtbl, function(x) x * c(1, 1 / .N_col)) ## count and pct
    fmts <- rep("xx.x (xx.x%)", length(xtbl))
  }
  names(vals) <- names(xtbl)
  names(fmts) <- names(vals)
  in_rows(.list = vals, .formats = fmts)
}
```

With this we can create a table. We will analyze `BMRKR2` (biomarker
2) for the sake of brevity. This is an oversimplifaction, as typically
this would be, e.g., `AEDECOD` in an `adae` dataset, but this requires
more sophisticated calculation of counts and/or percents that is
important but not germane to this specific issue.

```{r}
lyt_rr_full <- basic_table() |>
  split_cols_by("span_label",
    split_fun = trim_levels_to_map(span_label_map)
  ) |>
  split_cols_by("ARM", show_colcounts = TRUE) |>
  split_cols_by("rr_header", nested = FALSE) |>
  split_cols_by("ARM",
    split_fun = remove_split_levels("B: Placebo"),
    labels_var = "rr_label"
  ) |>
  analyze("BMRKR2", afun = rr_afun)

build_table(lyt_rr_full, adsl_rr)
```

The blank space above the column counts is a known issue which we
expect to be resolved in a future release due to the fact that the
header construction/wrapping behavior is not accounting for the fact
that the two sections of the column structure are independent.

Note that while our analysis function was dependent on where in the
*column* structure we are, it remains independent of where in the
*row* faceting structure we are. Thus we can use our analysis function
within row faceting without changes:


```{r}
lyt_rr_full2 <- basic_table() |>
  split_cols_by("span_label",
    split_fun = trim_levels_to_map(span_label_map)
  ) |>
  split_cols_by("ARM", show_colcounts = TRUE) |>
  split_cols_by("rr_header", nested = FALSE) |>
  split_cols_by("ARM",
    split_fun = remove_split_levels("B: Placebo"),
    labels_var = "rr_label"
  ) |>
  split_rows_by("STRATA1") |>
  split_rows_by("SEX", split_fun = keep_split_levels(c("Female", "Male"))) |>
  analyze("BMRKR2", afun = rr_afun)

tbl <- build_table(lyt_rr_full2, adsl_rr)

cwidths <- propose_column_widths(tbl)
cwidths[cwidths > 15] <- 15
cat(export_as_txt(tbl, colwidths = cwidths)) ## for wrapping
```

A more complete exploration of creating production ready analysis
functions will be presented elsewhere in this vignette series.

# Mixed Nesting Levels

In practice, the row structure in most shells can be translated to a
layout using combinations of the methods shown above. Some shells,
however, essentially call for group summaries for all levels of a
categorical variable, but additionally call for analysis within those
groups *for only some levels of the variable*.

In clinical trial outputs we have seen this most commonly in
disposition tables, the shells of which might look something like:

```{r, echo = FALSE}
simple_two_tier_init <- function(df, .var, .N_col, inner_var, drill_down_levs) {
  outer_tbl <- table(df[[.var]])

  cells <- lapply(
    names(outer_tbl),
    function(nm) {
      cont_cell <- rcell(outer_tbl[nm] * c(1, 1 / .N_col), format = "xx (xx.x%)")
      if (nm %in% drill_down_levs) {
        inner_tbl <- table(df[[inner_var]])
        detail_cells <- lapply(
          names(inner_tbl),
          function(innm) rcell(inner_tbl[innm] * c(1, 1 / .N_col), format = "xx (xx.x%)", indent_mod = 1L)
        )
        names(detail_cells) <- names(inner_tbl)
      } else {
        detail_cells <- NULL
      }
      c(setNames(list(cont_cell), nm), detail_cells)
    }
  )

  in_rows(.list = unlist(cells, recursive = FALSE))
}

two_tier_shell_lyt <- basic_table() |>
  split_cols_by("span_label",
    split_fun = trim_levels_to_map(span_label_map)
  ) |>
  split_cols_by("ARM", show_colcounts = TRUE) |>
  split_rows_by("RACE", split_fun = keep_split_levels(c("Asian", "Black"))) |>
  analyze("EOSSTT",
    afun = simple_two_tier_init,
    extra_args = list(
      inner_var = "DCSREAS",
      drill_down_levs = "DISCONTINUED"
    )
  )

two_tier_shell <- build_table(two_tier_shell_lyt, adsl_rr) ## don't need the rr bits
fixed_shell(two_tier_shell)
```

In this shell, the `COMPLETED`, `DISCONTINUED` and `ONGOING` rows are
siblings (derived from the `EOSSTT` variable), however only the
`DISCONTINUED` row acts as a group summary row for a facet containing
further analysis; the other two essentially act as individual rows.

This type of structure where individual analysis rows and facets/group
summary rows are direct siblings is not currently supported by the
`rtables` layouting and tabulation engines, and is *somewhat*
supported when created via, e.g., trimming rows of a created table.

The above said, we can arrive at a table which renders as desired
using the *two-tier analysis function* strategy.

The key to the two-tier analysis function strategy is to generate both
levels of row in the same analysis function and simply use indent
modifiers to differentiate them.

Below is a simple `afun` that implements this strategy. For the
purposes of this lesson readers can ignore the details of what this
function does if desired; analysis function design and implementation
will be covered in another vignette in the advanced section.


```{R}
simple_two_tier <- function(df, .var, .N_col, inner_var, drill_down_levs) {
  ## group EOSSTT counts
  outer_tbl <- table(df[[.var]])

  cells <- lapply(
    names(outer_tbl),
    function(nm) {
      ## simulated group summary rows
      cont_cell <- rcell(outer_tbl[nm] * c(1, 1 / .N_col),
        format = "xx (xx.x%)"
      )
      if (nm %in% drill_down_levs) {
        ## detail (DCSREAS) counts
        inner_tbl <- table(df[[inner_var]])
        ## note indent_mod
        detail_cells <- lapply(
          names(inner_tbl),
          function(innm) {
            rcell(inner_tbl[innm] * c(1, 1 / .N_col),
              format = "xx (xx.x%)",
              ## appearance of "detail drill-down"
              indent_mod = 1L
            )
          }
        )
        names(detail_cells) <- names(inner_tbl)
      } else {
        detail_cells <- NULL
      }
      c(setNames(list(cont_cell), nm), detail_cells)
    }
  )

  in_rows(.list = unlist(cells, recursive = FALSE))
}

lyt_two_tier <- basic_table() |>
  analyze("EOSSTT",
    afun = simple_two_tier,
    extra_args = list(inner_var = "DCSREAS", drill_down_levs = "DISCONTINUED")
  )

build_table(lyt_two_tier, adsl_rr)
```

As in other cases, we can add the row- and column- structure
orthogonally (provided the analysis behavior is truly orthogonal to
the faceting, as it is in this shell):

```{r}
lyt_two_tier_full <- basic_table() |>
  split_cols_by("span_label",
    split_fun = trim_levels_to_map(span_label_map)
  ) |>
  split_cols_by("ARM", show_colcounts = TRUE) |>
  split_rows_by("RACE", split_fun = keep_split_levels(c("Asian", "Black"))) |>
  analyze("EOSSTT",
    afun = simple_two_tier,
    extra_args = list(inner_var = "DCSREAS", drill_down_levs = "DISCONTINUED")
  )

build_table(lyt_two_tier_full, adsl_rr)
```

Thus we have created our desired output.
