tlf-01-disposition.Rmd
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:
## # 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.
## # 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.
## # 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:
adsl
.n_rand
.n_disc
.n_reason
. format it by r2rtf
.n_complete
.n_rand
, n_disc
, n_reason
and n_complete
and format it by r2rtf
.