2 Disposition

Following ICH E3 guidance, a summary table needs to be provided to include all participants who entered the study in Section 10.1, Disposition of Participants.

The disposition of participants table reports the numbers of participants who were randomized, and who entered and completed each phase of the study. In addition, the reasons for all post-randomization discontinuations, grouped by treatment and by major reason (lost to follow-up, adverse event, poor compliance, etc.) are reported.

library(haven) # Read SAS data
library(dplyr) # Manipulate data
library(tidyr) # Manipulate data
library(r2rtf) # Reporting in RTF format

In this chapter, we show how to create a typical disposition table.

The first step is to read in the relevant datasets into R. For a disposition table, all the required information is saved in a Subject-level Analysis Dataset (ADSL). This dataset is provided in sas7bdat format, which is a SAS data format currently used in many clinical trial analysis and reporting. The haven package is able to read the dataset, while maintaining its attributes (e.g., variable labels).

adsl <- read_sas("data-adam/adsl.sas7bdat")

The following variables are used in the preparation of a simplified disposition of participants table:

  • USUBJID: unique subject identifier
  • TRT01P: planned treatment
  • TRT01PN: planned treatment numeric encoding
  • DISCONFL: discontinued from study flag
  • DCREASCD: discontinued from study reason coded
adsl %>%
  select(USUBJID, TRT01P, TRT01PN, DISCONFL, DCREASCD) %>%
  head(4)
#> # A tibble: 4 × 5
#>   USUBJID     TRT01P               TRT01PN DISCONFL DCREASCD        
#>   <chr>       <chr>                  <dbl> <chr>    <chr>           
#> 1 01-701-1015 Placebo                    0 ""       Completed       
#> 2 01-701-1023 Placebo                    0 "Y"      Adverse Event   
#> 3 01-701-1028 Xanomeline High Dose      81 ""       Completed       
#> 4 01-701-1033 Xanomeline Low Dose       54 "Y"      Sponsor Decision

In the code below, we calculate the number of participants in the analysis population by treatment arms.

n_rand <- adsl %>%
  group_by(TRT01PN) %>%
  summarize(n = n()) %>%
  pivot_wider(
    names_from = TRT01PN,
    names_prefix = "n_",
    values_from = n
  ) %>%
  mutate(row = "Participants in population")

n_rand
#> # A tibble: 1 × 4
#>     n_0  n_54  n_81 row                       
#>   <int> <int> <int> <chr>                     
#> 1    86    84    84 Participants in population
n_disc <- adsl %>%
  group_by(TRT01PN) %>%
  summarize(
    n = sum(DISCONFL == "Y"),
    pct = formatC(n / n() * 100,
      digits = 1, format = "f", width = 5
    )
  ) %>%
  pivot_wider(
    names_from = TRT01PN,
    values_from = c(n, pct)
  ) %>%
  mutate(row = "Discontinued")

n_disc
#> # A tibble: 1 × 7
#>     n_0  n_54  n_81 pct_0   pct_54  pct_81  row         
#>   <int> <int> <int> <chr>   <chr>   <chr>   <chr>       
#> 1    28    59    57 " 32.6" " 70.2" " 67.9" Discontinued

In the code below, we calculate the number and percentage of participants who completed/discontinued the study for different reasons by treatment arms.

n_reason <- adsl %>%
  group_by(TRT01PN) %>%
  mutate(n_total = n()) %>%
  group_by(TRT01PN, DCREASCD) %>%
  summarize(
    n = n(),
    pct = formatC(n / unique(n_total) * 100,
      digits = 1, format = "f", width = 5
    )
  ) %>%
  pivot_wider(
    id_cols = DCREASCD,
    names_from = TRT01PN,
    values_from = c(n, pct),
    values_fill = list(n = 0, pct = "  0.0")
  ) %>%
  rename(row = DCREASCD)

n_reason
#> # A tibble: 10 × 7
#>    row                  n_0  n_54  n_81 pct_0   pct_54  pct_81 
#>    <chr>              <int> <int> <int> <chr>   <chr>   <chr>  
#>  1 Adverse Event          8    44    40 "  9.3" " 52.4" " 47.6"
#>  2 Completed             58    25    27 " 67.4" " 29.8" " 32.1"
#>  3 Death                  2     1     0 "  2.3" "  1.2" "  0.0"
#>  4 I/E Not Met            1     0     2 "  1.2" "  0.0" "  2.4"
#>  5 Lack of Efficacy       3     0     1 "  3.5" "  0.0" "  1.2"
#>  6 Lost to Follow-up      1     1     0 "  1.2" "  1.2" "  0.0"
#>  7 Physician Decision     1     0     2 "  1.2" "  0.0" "  2.4"
#>  8 Protocol Violation     1     1     1 "  1.2" "  1.2" "  1.2"
#>  9 Sponsor Decision       2     2     3 "  2.3" "  2.4" "  3.6"
#> 10 Withdrew Consent       9    10     8 " 10.5" " 11.9" "  9.5"

In the code below, we calculate the number and percentage of participants who complete the study by treatment arms. We split n_reason because we want to customize the row order of the table.

n_complete <- n_reason %>%
  filter(row == "Completed")

n_complete
#> # A tibble: 1 × 7
#>   row         n_0  n_54  n_81 pct_0   pct_54  pct_81 
#>   <chr>     <int> <int> <int> <chr>   <chr>   <chr>  
#> 1 Completed    58    25    27 " 67.4" " 29.8" " 32.1"

In the code below, we calculate the numbers and percentages of participants who discontinued the study for different reasons by treatment arms. For display purpose, paste0(" ", row) is used to add leading spaces to produce indentation in the final report.

n_reason <- n_reason %>%
  filter(row != "Completed") %>%
  mutate(row = paste0("    ", row))

n_reason
#> # A tibble: 9 × 7
#>   row                        n_0  n_54  n_81 pct_0   pct_54  pct_81 
#>   <chr>                    <int> <int> <int> <chr>   <chr>   <chr>  
#> 1 "    Adverse Event"          8    44    40 "  9.3" " 52.4" " 47.6"
#> 2 "    Death"                  2     1     0 "  2.3" "  1.2" "  0.0"
#> 3 "    I/E Not Met"            1     0     2 "  1.2" "  0.0" "  2.4"
#> 4 "    Lack of Efficacy"       3     0     1 "  3.5" "  0.0" "  1.2"
#> 5 "    Lost to Follow-up"      1     1     0 "  1.2" "  1.2" "  0.0"
#> 6 "    Physician Decision"     1     0     2 "  1.2" "  0.0" "  2.4"
#> 7 "    Protocol Violation"     1     1     1 "  1.2" "  1.2" "  1.2"
#> 8 "    Sponsor Decision"       2     2     3 "  2.3" "  2.4" "  3.6"
#> 9 "    Withdrew Consent"       9    10     8 " 10.5" " 11.9" "  9.5"

Now we combine individual rows into one table for reporting purpose. tbl_disp is used as input for r2rtf to create final report.

tbl_disp <- bind_rows(n_rand, n_complete, n_disc, n_reason) %>%
  select(row, ends_with(c("_0", "_54", "_81")))

tbl_disp
#> # A tibble: 12 × 7
#>    row                            n_0 pct_0    n_54 pct_54   n_81 pct_81 
#>    <chr>                        <int> <chr>   <int> <chr>   <int> <chr>  
#>  1 "Participants in population"    86  <NA>      84  <NA>      84  <NA>  
#>  2 "Completed"                     58 " 67.4"    25 " 29.8"    27 " 32.1"
#>  3 "Discontinued"                  28 " 32.6"    59 " 70.2"    57 " 67.9"
#>  4 "    Adverse Event"              8 "  9.3"    44 " 52.4"    40 " 47.6"
#>  5 "    Death"                      2 "  2.3"     1 "  1.2"     0 "  0.0"
#>  6 "    I/E Not Met"                1 "  1.2"     0 "  0.0"     2 "  2.4"
#>  7 "    Lack of Efficacy"           3 "  3.5"     0 "  0.0"     1 "  1.2"
#>  8 "    Lost to Follow-up"          1 "  1.2"     1 "  1.2"     0 "  0.0"
#>  9 "    Physician Decision"         1 "  1.2"     0 "  0.0"     2 "  2.4"
#> 10 "    Protocol Violation"         1 "  1.2"     1 "  1.2"     1 "  1.2"
#> 11 "    Sponsor Decision"           2 "  2.3"     2 "  2.4"     3 "  3.6"
#> 12 "    Withdrew Consent"           9 " 10.5"    10 " 11.9"     8 "  9.5"

In the below code, formatting of the final table is defined. Items that were not discussed in the previous sections, are highlighted below.

The rtf_title defines table title. We can provide a vector for the title argument. Each value is a separate line. The format can also be controlled by providing a vector input in text format.

tbl_disp %>%
  # Table title
  rtf_title("Disposition of Participants") %>%
  # First row of column header
  rtf_colheader(" | Placebo | Xanomeline Low Dose| Xanomeline High Dose",
    col_rel_width = c(3, rep(2, 3))
  ) %>%
  # Second row of column header
  rtf_colheader(" | n | (%) | n | (%) | n | (%)",
    col_rel_width = c(3, rep(c(0.7, 1.3), 3)),
    border_top = c("", rep("single", 6)),
    border_left = c("single", rep(c("single", ""), 3))
  ) %>%
  # Table body
  rtf_body(
    col_rel_width = c(3, rep(c(0.7, 1.3), 3)),
    text_justification = c("l", rep("c", 6)),
    border_left = c("single", rep(c("single", ""), 3))
  ) %>%
  # Encoding RTF syntax
  rtf_encode() %>%
  # Save to a file
  write_rtf("tlf/tbl_disp.rtf")

The procedure to generate a disposition table can be summarized as follows:

  • Step 1: Read subject level data (i.e., adsl) into R.
  • Step 2: Count participants in the analysis population and name the dataset n_rand.
  • Step 3: Calculate the number and percentage of participants who discontinued the study by treatment arm, and name the dataset n_disc.
  • Step 4: Calculate the numbers and percentages of participants who discontinued the study for different reasons by treatment arm, and name the dataset n_reason.
  • Step 5: Calculate the number and percentage of participants who completed the study by treatment arm, and name the dataset n_complete.
  • Step 6: Bind n_rand, n_disc, n_reason, and n_complete by row.
  • Step 7: Write the final table to RTF