---
title: "Example Clinical Trials Tables"
author: "Gabriel Becker and Adrian Waddell"
date: "`r Sys.Date()`"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Example Clinical Trials Tables}
  %\VignetteEncoding{UTF-8}
  %\VignetteEngine{knitr::rmarkdown}
editor_options: 
  chunk_output_type: console
---

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


```{css, echo=FALSE}
.reveal .r code {
    white-space: pre;
}
```


## Introduction

In this vignette we create a 

* demographic table
* adverse event table
* response table

using the `rtables` layout facility. That is, we demonstrate how the layout based tabulation framework can specify the structure and relations that are commonly found when analyzing clinical trials data.

Note that all the data is created using random number generators. All `ex_*` data which is currently attached to the `rtables` package were created using `random.cdisc.data` another R package that we intend to release as open source soon.

The packages used in this vignette are:

```{r, message=FALSE}
library(rtables)
library(tibble)
library(dplyr)
```

## Demographic Table

Demographic tables summarize the variables content for different population subsets (encoded in the columns).

One feature of `analyze` that we have not introduced in the previous vignette is that the analysis function `afun` can specify multiple rows with the `in_rows` function:

```{r}
ADSL <- ex_adsl  # Example ADSL dataset

basic_table() %>%
  split_cols_by("ARM") %>%
  analyze(vars = "AGE", afun = function(x) {
    in_rows(
      "Mean (sd)" = rcell(c(mean(x), sd(x)), format = "xx.xx (xx.xx)"),
      "Range" = rcell(range(x), format = "xx.xx - xx.xx")
    )
  }) %>%
  build_table(ADSL)
```

Multiple variables can be analyzed in one `analyze` call:

```{r}
basic_table() %>%
  split_cols_by("ARM") %>%
  analyze(vars = c("AGE", "BMRKR1"), afun = function(x) {
    in_rows(
      "Mean (sd)" = rcell(c(mean(x), sd(x)), format = "xx.xx (xx.xx)"),
      "Range" = rcell(range(x), format = "xx.xx - xx.xx")
    )
  }) %>%
  build_table(ADSL)
```

Hence, if `afun` can process different data vector types (i.e. variables selected from the data) then we are fairly close to a standard demographic table. Here is a function that either creates a count table or some number summary if the argument `x` is a factor or numeric, respectively:


```{r}
s_summary <- function(x) {
  if (is.numeric(x)) {
    in_rows(
      "n" = rcell(sum(!is.na(x)), format = "xx"),
      "Mean (sd)" = rcell(c(mean(x, na.rm = TRUE), sd(x, na.rm = TRUE)), format = "xx.xx (xx.xx)"),
      "IQR" = rcell(IQR(x, na.rm = TRUE), format = "xx.xx"),
      "min - max" = rcell(range(x, na.rm = TRUE), format = "xx.xx - xx.xx")
    )
  } else if (is.factor(x)) {
    
    vs <- as.list(table(x))
    do.call(in_rows, lapply(vs, rcell, format = "xx"))
    
  } else (
    stop("type not supported")
  )
}
```

Note we use `rcell`s to wrap the results in order to add formatting instructions for `rtables`. We can use `s_summary` outside the context of tabulation:

```{r}
s_summary(ADSL$AGE)
```

and 

```{r}
s_summary(ADSL$SEX)
```

We can now create a commonly used variant of the demographic table:
 
```{r}
lyt <- basic_table() %>% 
  split_cols_by(var = "ARM") %>%
  analyze(c("AGE", "SEX"), afun = s_summary) 

tbl <- build_table(lyt, ADSL)
tbl
```

Note that `analyze` can also be called multiple times in sequence:

```{r}
tbl2 <- basic_table() %>% 
  split_cols_by(var = "ARM") %>%
  analyze("AGE", s_summary) %>%
  analyze("SEX", s_summary) %>%
  build_table(ADSL) 

tbl2
```

which leads to the identical table as `tbl`:

```{r}
identical(tbl, tbl2)
```

```{r, echo=FALSE}
stopifnot(identical(tbl, tbl2))
```

In clinical trials analyses the number of patients per column is often referred to as `N` (rather than the overall population which outside of clinical trials is commonly referred to as `N`). Column `N`s are added using the `add_colcounts` function:

```{r}
basic_table() %>% 
  split_cols_by(var = "ARMCD") %>%
  add_colcounts() %>%
  analyze(c("AGE", "SEX"), s_summary) %>%
  build_table(ADSL) 
```

### Variations on the demographic table

We will now show a couple of variations of the demographic table that we developed above. These variations are in structure and not in analysis, hence they don't require a modification to the `s_summary` function.


We will start with a standard table analyzing the variables `AGE` and `BMRKR2` variables:

```{r}
basic_table() %>% 
  split_cols_by(var = "ARM") %>%
  add_colcounts() %>%
  analyze(c("AGE", "BMRKR2"), s_summary) %>%
  build_table(ADSL) 
```

Assume we would like to have this analysis carried out per gender encoded in the row space:

```{r, warning=FALSE}
basic_table() %>% 
  split_cols_by(var = "ARM") %>%
  add_colcounts() %>%
  split_rows_by("SEX") %>%
  analyze(c("AGE", "BMRKR2"), s_summary) %>%
  build_table(ADSL) 
```

We will now subset `ADSL` to include only males and females in the analysis in order to reduces the number of rows in the table:

```{r, warning=FALSE}
ADSL_M_F <- filter(ADSL, SEX %in% c("M", "F"))

basic_table() %>% 
  split_cols_by(var = "ARM") %>%
  add_colcounts() %>%
  split_rows_by("SEX") %>%
  analyze(c("AGE", "BMRKR2"), s_summary) %>%
  build_table(ADSL_M_F) 
```

Note that the `UNDIFFERENTIATED` and `U` levels still show up in the table. This is because tabulation respects the factor levels and level order, exactly as the `split` and `table` function do. If empty levels should be dropped then `rtables` needs to know that at splitting time via the `split_fun` argument in `split_rows_by`. There are a number of predefined functions. For this example `drop_split_levels` is required to drop the empty levels at splitting time. Splitting is a big topic and will be eventually addressed in a specific package vignette.
 
 
```{r}
basic_table() %>% 
  split_cols_by(var = "ARM") %>%
  add_colcounts() %>%
  split_rows_by("SEX", split_fun = drop_split_levels, child_labels = "visible") %>%
  analyze(c("AGE", "BMRKR2"), s_summary) %>%
  build_table(ADSL_M_F) 
```

In the table above the labels `M` and `F` are not very descriptive. You can add the full label as follows:

```{r}
ADSL_M_F_l <- ADSL_M_F %>% 
  mutate(lbl_sex = case_when(
    SEX == "M" ~ "Male",
    SEX == "F" ~ "Female",
    SEX == "U" ~ "Unknown",
    SEX == "UNDIFFERENTIATED" ~ "Undifferentiated"
  ))

basic_table() %>% 
  split_cols_by(var = "ARM") %>%
  add_colcounts() %>%
  split_rows_by("SEX", labels_var = "lbl_sex", split_fun = drop_split_levels, child_labels = "visible") %>%
  analyze(c("AGE", "BMRKR2"), s_summary) %>%
  build_table(ADSL_M_F_l)
```

For the next table variation we only stratify by gender for the `AGE` analysis. To do this the `nested` argument has to be set to `FALSE`  in `analyze` call:

```{r}
basic_table() %>% 
  split_cols_by(var = "ARM") %>%
  add_colcounts() %>%
  split_rows_by("SEX", labels_var = "lbl_sex", split_fun = drop_split_levels, child_labels = "visible") %>%
  analyze("AGE", s_summary, show_labels = "visible") %>%
  analyze("BMRKR2", s_summary, nested = FALSE,  show_labels = "visible") %>%
  build_table(ADSL_M_F_l) 
```

Once we split the rows into groups (`Male` and `Female` here) one might want to summarize groups: usually by showing count and column percentages. This is especially important if we have missing data. For example if we create the above table but add missing data to the `AGE` variable:

```{r}
insert_NAs <- function(x) {
  x[sample(c(TRUE, FALSE), length(x), TRUE, prob = c(0.2, 0.8))] <- NA
  x
}

set.seed(1)
ADSL_NA <- ADSL_M_F_l %>% 
  mutate(AGE = insert_NAs(AGE))

basic_table() %>% 
  split_cols_by(var = "ARM") %>%
  add_colcounts() %>%
  split_rows_by("SEX", labels_var = "lbl_sex", split_fun = drop_split_levels, child_labels = "visible") %>%
  analyze("AGE", s_summary) %>%
  analyze("BMRKR2", s_summary, nested = FALSE,  show_labels = "visible") %>%
  build_table(filter(ADSL_NA, SEX %in% c("M", "F"))) 
```

Here it is not easy to see how many females and males there are in each arm as `n` represents the number of non-missing data elements in the variables. Groups within rows that are defined by splitting can be summarized with `summarize_row_groups`, for example:

```{r}
basic_table() %>% 
  split_cols_by(var = "ARM") %>%
  add_colcounts() %>%
  split_rows_by("SEX", labels_var = "lbl_sex", split_fun = drop_split_levels) %>%
  summarize_row_groups()  %>% 
  analyze("AGE", s_summary) %>%
  analyze("BMRKR2", afun = s_summary, nested = FALSE,  show_labels = "visible") %>%
  build_table(filter(ADSL_NA, SEX %in% c("M", "F"))) 
```

There are a couple of things to note here. 

* Group summaries produce "content" rows. Visually it's impossible to distinguish data rows from content rows. Their difference is justified (and it's an important design decision) because when we paginate tables the content rows are by default repeated if a group gets divided via pagination.
* Conceptually the content rows summarize the patient population which is analyzed and hence is often the count & group percentages (default behavior of `summarize_row_groups`). 

We can recreate this default behavior (count percentage) by defining a `cfun` for illustrative purposes here as it results in the same table as above:

```{r}
basic_table() %>% 
  split_cols_by(var = "ARM") %>%
  add_colcounts() %>%
  split_rows_by("SEX", labels_var = "lbl_sex", split_fun = drop_split_levels) %>%
  summarize_row_groups(cfun = function(df, labelstr, .N_col, ...) {
    in_rows(
      rcell(nrow(df) * c(1, 1/.N_col), format = "xx (xx.xx%)"),
      .labels = labelstr
    )
  })  %>% 
  analyze("AGE", s_summary) %>%
  analyze("BEP01FL", afun = s_summary, nested = FALSE,  show_labels = "visible") %>%
  build_table(filter(ADSL_NA, SEX %in% c("M", "F"))) 
```

Note that `cfun` differs from `afun` (which is used in `analyze`)  in that `cfun` does not operate on variables but rather on `data.frame`s or `tibble`s which are passed via the `df` argument (`afun` can optionally request `df` too). Further, `cfun` gives the default group label (factor level from splitting) as an argument to `labelstr` and hence it could be modified:

```{r}
basic_table() %>% 
  split_cols_by(var = "ARM") %>%
  split_rows_by("SEX", labels_var = "lbl_sex", split_fun = drop_split_levels, child_labels = "hidden") %>%
  summarize_row_groups(cfun = function(df, labelstr, .N_col, ...) {
    in_rows(
       rcell(nrow(df) * c(1, 1/.N_col), format = "xx (xx.xx%)"),
       .labels = paste0(labelstr, ": count (perc.)")
    )
  })  %>% 
  analyze("AGE", s_summary) %>%
  analyze("BEP01FL", s_summary, nested = FALSE, show_labels = "visible") %>%
  build_table(filter(ADSL_NA, SEX %in% c("M", "F"))) 
```

### Using Layouts

Layouts have a couple of advantages over tabulating the tables directly:

* the creation of layouts requires the analyst to describe the problem in an abstract way
   - i.e. they separate the analyses description from the actual data
* referencing variable names happens via strings (no non-standard evaluation (NSE) is needed, though this is arguably either feature or a short coming)
* layouts can be reused

Here is an example that demonstrates the reusability of layouts:

```{r}
lyt <- basic_table() %>% 
  split_cols_by("ARM") %>%
  add_colcounts() %>%
  analyze(c("AGE", "SEX"), afun = s_summary)

lyt
```

We can now build a table for `ADSL`

```{r}
build_table(lyt, ADSL)
```

or for all patients that are older than 18:

```{r}
build_table(lyt, ADSL %>% filter(AGE > 18))
```


## Adverse Events

There are a number of different adverse event tables. We will now present two tables that show adverse events by id and then by grade and by id. 

This time we won't use the `ADAE` dataset from `random.cdisc.data` but rather generate a dataset on the fly (see [Adrian's 2016 Phuse paper](https://github.com/waddella/phuse2016_adverse_events)):

```{r}
set.seed(1)

lookup <- tribble(
  ~AEDECOD,                          ~AEBODSYS,                                         ~AETOXGR,
  'HEADACHE',                        "NERVOUS SYSTEM DISORDERS",                        "5",
  'BACK PAIN',                       "MUSCULOSKELETAL AND CONNECTIVE TISSUE DISORDERS", "2",
  'GINGIVAL BLEEDING',               "GASTROINTESTINAL DISORDERS",                      "1",
  'HYPOTENSION',                     "VASCULAR DISORDERS",                              "3",
  'FAECES SOFT',                     "GASTROINTESTINAL DISORDERS",                      "2",
  'ABDOMINAL DISCOMFORT',            "GASTROINTESTINAL DISORDERS",                      "1",
  'DIARRHEA',                        "GASTROINTESTINAL DISORDERS",                      "1",
  'ABDOMINAL FULLNESS DUE TO GAS',   "GASTROINTESTINAL DISORDERS",                      "1",
  'NAUSEA (INTERMITTENT)',           "GASTROINTESTINAL DISORDERS",                      "2",
  'WEAKNESS',                        "MUSCULOSKELETAL AND CONNECTIVE TISSUE DISORDERS", "3",
  'ORTHOSTATIC HYPOTENSION',         "VASCULAR DISORDERS",                              "4"
)

normalize <- function(x) x/sum(x)
weightsA <- normalize(c(0.1, dlnorm(seq(0, 5, length.out = 25), meanlog = 3)))
weightsB <- normalize(c(0.2, dlnorm(seq(0, 5, length.out = 25))))

N_pop <- 300
ADSL2 <- data.frame(
  USUBJID = seq(1, N_pop, by = 1),
  ARM = sample(c('ARM A', 'ARM B'), N_pop, TRUE),
  SEX = sample(c('F', 'M'), N_pop, TRUE),
  AGE = 20 + rbinom(N_pop, size=40, prob=0.7)
)
                                      
l.adae <- mapply(ADSL2$USUBJID, ADSL2$ARM, ADSL2$SEX, ADSL2$AGE, FUN = function(id, arm, sex, age) {
  n_ae <- sample(0:25, 1, prob = if (arm == "ARM A") weightsA else weightsB)
  i <- sample(1:nrow(lookup), size = n_ae, replace = TRUE, prob = c(6, rep(1, 10))/16)
  lookup[i, ] %>% 
    mutate(
      AESEQ = seq_len(n()),
      USUBJID = id, ARM = arm, SEX = sex, AGE = age
    )
}, SIMPLIFY = FALSE)

ADAE2 <- do.call(rbind, l.adae)
ADAE2 <- ADAE2 %>% 
  mutate(
    ARM = factor(ARM, levels = c("ARM A", "ARM B")),
    AEDECOD = as.factor(AEDECOD),
    AEBODSYS = as.factor(AEBODSYS), 
    AETOXGR = factor(AETOXGR, levels = as.character(1:5))
  ) %>% 
  select(USUBJID, ARM, AGE, SEX, AESEQ, AEDECOD, AEBODSYS, AETOXGR)
  
ADAE2
```

### Adverse Events By ID

We start by defining an events summary function:

```{r}
s_events_patients <- function(x, labelstr, .N_col) {
  in_rows(
    "Total number of patients with at least one event" = 
      rcell(length(unique(x)) * c(1, 1/.N_col), format = "xx (xx.xx%)"),
    
    "Total number of events" = rcell(length(x), format = "xx")
  )
}
```

So, for a population of `5` patients where

* one patient has 2 AEs
* one patient has 1 AE
* three patients have no AEs

we would get the following summary:

```{r}
s_events_patients(x = c("id 1", "id 1", "id 2"), .N_col = 5)
```

The `.N_col` argument is a special keyword argument which `build_table` passes the population size for each respective column. For a list of keyword arguments for the functions passed to `afun` in `analyze` refer to the documentation with `?analyze`.

We now use the `s_events_patients` summary function in a tabulation:

```{r}
basic_table() %>% 
  split_cols_by("ARM") %>%
  add_colcounts() %>%
  analyze("USUBJID", s_events_patients) %>%
  build_table(ADAE2)
```

Note that the column `N`'s are wrong as by default they are set to the number of rows per group (i.e. number of AEs per arm here). This also affects the percentages. For this table we are interested in the number of patients per column/arm which is usually taken from `ADSL` (variable `ADSL2` here):

```{r}
N_per_arm <- table(ADSL2$ARM)
N_per_arm
```

Since this information is not "pre-data" it needs to go to the table creation function `build_table`:

```{r}
basic_table() %>% 
  split_cols_by("ARM") %>%
  add_colcounts() %>%
  analyze("USUBJID", s_events_patients) %>%
  build_table(ADAE2, col_counts = N_per_arm)
```

We next calculate this information per system organ class:

```{r}
l <- basic_table() %>% 
  split_cols_by("ARM") %>%
  add_colcounts() %>%
  analyze("USUBJID", s_events_patients) %>%
  split_rows_by("AEBODSYS", child_labels = "visible", nested = FALSE)  %>%
  summarize_row_groups("USUBJID", cfun = s_events_patients)
  build_table(l, ADAE2, col_counts = N_per_arm)
```

We now have to the add a count table of `AEDECOD` for each `AEBODSYS`. The default `analyze` behavior for a factor is to create the count table per level (using `rtab_inner`):

```{r}
tbl1 <- basic_table() %>% 
  split_cols_by("ARM") %>%
  add_colcounts() %>%
  split_rows_by("AEBODSYS", child_labels = "visible", indent_mod = 1)  %>%
  summarize_row_groups("USUBJID", cfun = s_events_patients) %>%
  analyze("AEDECOD", indent_mod = -1) %>%
  build_table(ADAE2, col_counts = N_per_arm)

tbl1
```

The `indent_mod` argument enables relative indenting changes if the tree structure of the table does not result in the desired indentation by default.

This table so far is however not the usual adverse event table as it counts the total number of events and not the number of subjects one or more events for a particular term. To get the correct table we need to write a custom analysis function:

```{r}
table_count_once_per_id <- function(df, termvar = "AEDECOD", idvar = "USUBJID") {

  x <- df[[termvar]]
  id <- df[[idvar]]
 
  counts <- table(x[!duplicated(id)])
  
  in_rows(
    .list = as.vector(counts),
    .labels = names(counts)
  )
}

table_count_once_per_id(ADAE2)
```

So the desired AE table is:

```{r}
basic_table() %>%
  split_cols_by("ARM") %>%
  add_colcounts() %>%
  split_rows_by("AEBODSYS", child_labels = "visible", indent_mod = 1)  %>%
  summarize_row_groups("USUBJID", cfun = s_events_patients) %>%
  analyze("AEDECOD", afun = table_count_once_per_id, show_labels = "hidden", indent_mod = -1) %>%
  build_table(ADAE2, col_counts = N_per_arm)
```

Note that we are missing the overall summary in the first two rows. This can be added with another `analyze` call and then setting `nested` to `FALSE` in the subsequent `summarize_row_groups` call:

```{r}
tbl <- basic_table() %>% 
  split_cols_by("ARM") %>%
  add_colcounts() %>%
  analyze("USUBJID", afun = s_events_patients) %>% 
  split_rows_by("AEBODSYS", child_labels = "visible", nested = FALSE, indent_mod = 1)  %>%
  summarize_row_groups("USUBJID", cfun = s_events_patients) %>%
  analyze("AEDECOD", table_count_once_per_id, show_labels = "hidden", indent_mod = -1) %>%
  build_table(ADAE2, col_counts = N_per_arm)

tbl
```

Finally, if we wanted to prune the 0 counts row we can do that with the `trim_rows` function:

```{r}
trim_rows(tbl)
```

Pruning is a larger topic with a separate `rtables` package vignette.

### Adverse Events By ID and By Grade

The adverse events table by ID and by grade shows how many patients had at least one adverse event per grade for different subsets of the data (e.g. defined by system organ class).

For this table we do not show the zero count grades. Note that we add the "overall" groups with a custom split function. 

```{r}
table_count_grade_once_per_id <- function(df, labelstr = "", gradevar = "AETOXGR", idvar = "USUBJID", grade_levels = NULL) {
  
  id <- df[[idvar]]
  grade <- df[[gradevar]]
  
  if (!is.null(grade_levels)) {
    stopifnot(all(grade %in% grade_levels))
    grade <- factor(grade, levels = grade_levels)
  }
  
  id_sel <- !duplicated(id)
  
  in_rows(
      "--Any Grade--" = sum(id_sel),
      .list =  as.list(table(grade[id_sel]))
    )
}

table_count_grade_once_per_id(ex_adae, grade_levels = 1:5)
```

All of the layouting concepts needed to create this table have already been introduced so far:

```{r}
basic_table() %>% 
  split_cols_by("ARM") %>%
  add_colcounts() %>%
  analyze("AETOXGR", 
          afun = table_count_grade_once_per_id, 
          extra_args = list(grade_levels = 1:5),
          var_labels = "- Any adverse events -", show_labels = "visible") %>%
  split_rows_by("AEBODSYS", child_labels = "visible", nested = FALSE, indent_mod = 1) %>%
  summarize_row_groups(cfun = table_count_grade_once_per_id, format = "xx", indent_mod = 1) %>%
  split_rows_by("AEDECOD", child_labels = "visible", indent_mod = -2)  %>%
  analyze("AETOXGR", 
          afun = table_count_grade_once_per_id, 
          extra_args = list(grade_levels = 1:5), show_labels = "hidden") %>%
  build_table(ADAE2, col_counts = N_per_arm)
```

## Response Table

The response table that we will create here is composed of 3 parts:

1. Binary response table
1. Unstratified analysis comparison vs. control group
1. Multinomial response table

Let's start with the first part which is fairly simple to derive:

```{r}
ADRS_BESRSPI <- ex_adrs %>%
  filter(PARAMCD == "BESRSPI") %>%
  mutate(
    rsp = factor(AVALC %in% c("CR", "PR"), levels = c(TRUE, FALSE), labels = c("Responders", "Non-Responders")),
    is_rsp = (rsp == "Responders")
  )

s_proportion <- function(x, .N_col) {
   in_rows(.list = lapply(as.list(table(x)), function(xi) rcell(xi * c(1, 1/.N_col), format = "xx.xx (xx.xx%)")))
}

basic_table() %>%
  split_cols_by("ARMCD", ref_group = "ARM A") %>%
  add_colcounts() %>%
  analyze("rsp", s_proportion, show_labels = "hidden") %>%
  build_table(ADRS_BESRSPI)
```


Note that we did set the `ref_group` argument in `split_cols_by` which for the current table had no effect as we only use the cell data for the responder and non-responder counting. The `ref_group` argument is needed for the part 2. and 3. of the table.

We will now look the implementation of part "2. Unstratified analysis comparison vs. control group." Let's start with the analysis function:

```{r}
s_unstratified_response_analysis <- function(x, .ref_group, .in_ref_col) {
  
  if (.in_ref_col) {
    return(in_rows(
        "Difference in Response Rates (%)" = rcell(numeric(0)),
        "95% CI (Wald, with correction)" = rcell(numeric(0)),
        "p-value (Chi-Squared Test)" = rcell(numeric(0)),
        "Odds Ratio (95% CI)" = rcell(numeric(0))
    ))
  }
  
  fit <- stats::prop.test(
    x = c(sum(x), sum(.ref_group)),
    n = c(length(x), length(.ref_group)),
    correct = FALSE
  )
  
  fit_glm <- stats::glm(
    formula = rsp ~ group,
    data = data.frame(
      rsp = c(.ref_group, x), 
      group = factor(rep(c("ref", "x"), times = c(length(.ref_group), length(x))), levels = c("ref", "x"))
    ),
    family = binomial(link = "logit")
  )

  in_rows(
      "Difference in Response Rates (%)" = non_ref_rcell((mean(x) - mean(.ref_group)) * 100,
                                                         .in_ref_col, format = "xx.xx") ,
      "95% CI (Wald, with correction)" = non_ref_rcell(fit$conf.int * 100,
                                                       .in_ref_col, format = "(xx.xx, xx.xx)"),
      "p-value (Chi-Squared Test)" = non_ref_rcell(fit$p.value,
                                                   .in_ref_col, format = "x.xxxx | (<0.0001)"),
      "Odds Ratio (95% CI)" = non_ref_rcell(c(
          exp(stats::coef(fit_glm)[-1]),
          exp(stats::confint.default(fit_glm, level = .95)[-1, , drop = FALSE])
      ),
      .in_ref_col, format = "xx.xx (xx.xx - xx.xx)")
  )
}

s_unstratified_response_analysis(
  x = ADRS_BESRSPI %>% filter(ARM == "A: Drug X") %>% pull(is_rsp), 
  .ref_group = ADRS_BESRSPI %>% filter(ARM == "B: Placebo") %>% pull(is_rsp),
  .in_ref_col = FALSE
)
```

Hence we can now add the next vignette to the table:

```{r}
basic_table() %>%
  split_cols_by("ARMCD", ref_group = "ARM A") %>%
  add_colcounts() %>%
  analyze("rsp", s_proportion, show_labels = "hidden") %>%
  analyze("is_rsp", s_unstratified_response_analysis, show_labels = "visible", var_labels = "Unstratified Response Analysis") %>%
  build_table(ADRS_BESRSPI)
```

Next we will add part 3. the "multinomial response table". To do so, we are adding a row-split by response level, and then doing the same thing as we did for the binary response table above.

```{r}
s_prop <- function(df, .N_col) {
  in_rows(
    "95% CI (Wald, with correction)" = rcell(binom.test(nrow(df), .N_col)$conf.int * 100, format = "(xx.xx, xx.xx)")
  )
}

s_prop(
  df = ADRS_BESRSPI %>% filter(ARM == "A: Drug X", AVALC == "CR"), 
  .N_col = sum(ADRS_BESRSPI$ARM == "A: Drug X")
)
```

We can now create the final response table with all three parts:

```{r}
basic_table() %>%
  split_cols_by("ARMCD", ref_group = "ARM A") %>%
  add_colcounts() %>%
  analyze("rsp", s_proportion, show_labels = "hidden") %>%
  analyze("is_rsp", s_unstratified_response_analysis, 
          show_labels = "visible", var_labels = "Unstratified Response Analysis") %>%
  split_rows_by(
    var = "AVALC",
    split_fun = reorder_split_levels(neworder = c("CR", "PR", "SD", "NON CR/PD", "PD", "NE"), drlevels = TRUE), 
    nested = FALSE
  ) %>%
  summarize_row_groups() %>%
  analyze("AVALC", afun = s_prop) %>%
  build_table(ADRS_BESRSPI)
```

In case the we wanted to rename the levels of `AVALC` and remove the CI for `NE` we could do that as follows:

```{r}
rsp_label <- function(x) {
  rsp_full_label <- c(
    CR          = "Complete Response (CR)",
    PR          = "Partial Response (PR)",
    SD          = "Stable Disease (SD)",
    `NON CR/PD` = "Non-CR or Non-PD (NON CR/PD)",
    PD          = "Progressive Disease (PD)",
    NE          = "Not Evaluable (NE)",
    Missing     = "Missing",
    `NE/Missing` = "Missing or unevaluable"
  )
  stopifnot(all(x %in% names(rsp_full_label)))
  rsp_full_label[x]
}


tbl <- basic_table() %>%
  split_cols_by("ARMCD", ref_group = "ARM A") %>%
  add_colcounts() %>%
  analyze("rsp", s_proportion, show_labels = "hidden") %>%
  analyze("is_rsp", s_unstratified_response_analysis, 
          show_labels = "visible", var_labels = "Unstratified Response Analysis") %>%
  split_rows_by(
    var = "AVALC",
    split_fun = keep_split_levels(c("CR", "PR", "SD", "PD"), reorder = TRUE), 
    nested = FALSE
  ) %>%
  summarize_row_groups(cfun = function(df, labelstr, .N_col) {
    in_rows(nrow(df) * c(1, 1/.N_col), .formats = "xx (xx.xx%)", .labels = rsp_label(labelstr))
  }) %>%
  analyze("AVALC", afun = s_prop) %>%
  analyze("AVALC", afun = function(x, .N_col) {
    in_rows(rcell(sum(x == "NE") * c(1, 1/.N_col), format = "xx.xx (xx.xx%)"), .labels = rsp_label("NE"))
  }, nested = FALSE) %>%
  build_table(ADRS_BESRSPI)

tbl
```

Note that the table is missing the rows gaps to make it more readable. The row spacing feature is on the `rtables` roadmap and will be implemented in future.
