Disposition

Following the ICH E3 guidance, we need to summarize accounting of all patients who entered the study, in Section 10.1, Disposition of Patients. The disposition of patients reports the numbers of patients who were randomized, and who entered and completed each phase of the study, as well as the reasons for all post-randomization discontinuations, grouped by treatment and by major reason (lost to follow-up, adverse event, poor compliance etc.).

library(esubdemo)
## Warning in eval(ei, envir): The current R version is not the same with the
## current project in 4.1.0
library(haven) # Read SAS data
library(dplyr) # Manipulate data
library(tidyr) # Manipulate data
library(r2rtf) # Reporting in RTF format

The first step is to read relevant datasets into R. For disposition table, all the required information is saved in the ADSL dataset. We can use haven package to read the dataset.

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

We illustrate how to prepare a report data for a simplified disposition of patients table using variables below:

  • USUBJID: Unique subject identifier
  • TRT01P: Planed treatment
  • TRT01PN: Planned treatment encoding
  • DISCONFL: Discontinued from study flag
  • DCREASCD: Discontinued from study reason
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 calculated patients in the analysis population.

n_rand <- adsl %>%
  group_by(TRT01PN) %>%
  summarize(n = n()) %>%
  pivot_wider(
    names_from = TRT01PN,
    names_prefix = "n_",
    values_from = n
  ) %>%
  mutate(row = "Patients in population")
n_rand
## # A tibble: 1 × 4
##     n_0  n_54  n_81 row                   
##   <int> <int> <int> <chr>                 
## 1    86    84    84 Patients in population

In the code below, we calculate number and percentage of patients who discontinued the study.

Here we use formatC() to customize the numeric value to be 1 decimal with fixed width of 5.

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 number and percentage of patients who completed/discontinued the study in different reasons.

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)
## `summarise()` has grouped output by 'TRT01PN'. You can override using the
## `.groups` argument.
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 number and percentage of patients who complete the study. We split n_reason because we need 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 number and percentage of patients who discontinued the study in different reasons. Here we use paste0(" ", row) to add some leading space for individual reasons for study discontinuation.

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 combined individual rows into the whole 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 "Patients 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"

We start to define the format of the output. We highlighted items that is not discussed in previous discussion.

rtf_title() defines the 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 Patients") %>%
  # 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")

In conclusion, the procedure to generate a AE summary table as shown in the above example is listed as follows:

  • Step 1: Read data into R, i.e. adsl.
  • Step 2: Count patients in the analysis population and name the dataset as n_rand.
  • Step 3: Count the number and percentage of patients who discontinued the study, and name the dataset as n_disc.
  • Step 4: Count the number and percentage of of patients who discontinued the study in different reasons, and name the dataset as n_reason. format it by r2rtf.
  • Step 5: Count the number and percentage of of patients who completed the study, and name the dataset as n_complete.
  • Step 6: Rowly bind n_rand, n_disc, n_reason and n_complete and format it by r2rtf.