diff --git a/DESCRIPTION b/DESCRIPTION index 72e6f0b3..b679033e 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -16,6 +16,7 @@ Authors@R: person("Aksel", "Thomsen", , "oath@novonordisk.com", role = "aut"), person("Shiyu", "Chen", , "shiyu.chen@atorusresearch.com", role = "aut"), person("Rammprasad", "Ganapathy", , "rammprasad.ganapathy@gene.com", role = "aut") + person("Alanah", "Jonas", , "alanah.x.jonas@gsk.com", role = "aut"), Description: This is not a package, but we just use this file to declare the dependencies of the site. URL: https://github.com/pharmaverse/examples @@ -26,6 +27,7 @@ Imports: cards, dplyr, filters, + forcats, gtsummary, lubridate, labelled, @@ -50,6 +52,7 @@ Imports: teal.modules.clinical, teal.modules.general, tern, + tfrmt, tidyr, xportr, logr, diff --git a/inst/WORDLIST b/inst/WORDLIST index a57298a7..9698f6a0 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -41,6 +41,7 @@ ae AE AEACN AEBDSYCD +aebodsys AEBODSYS AEDECOD AEDIS @@ -84,6 +85,7 @@ AESTDAT AESTDTC AESTDY AETERM +aeterm AETHNIC AETHNICN AFRLT @@ -164,6 +166,7 @@ bds BDS BILI BILIBL +bigN BLAS blq BLQ @@ -351,14 +354,19 @@ fansi FAS FASFL fastmap +fct FDA's fileext flextable flx fmt +fns +forcats formatters FORMN +freqs FRLTU +frmt FRVDT fs Garolini @@ -388,6 +396,7 @@ iconv ifelse iframe init +inorder inputId installedpackages ITT @@ -494,6 +503,7 @@ onco ONCO ontrtfl ONTRTFL +ord os othgrpvars outfile @@ -532,6 +542,7 @@ PCTEST PCTESTCD PCTPT PCTPTNUM +pcts pfs PFS pharma @@ -604,6 +615,7 @@ refdates refdt REGIONCAT REGIONx +relevel renderPlot renderUI renv @@ -713,6 +725,7 @@ tempfile TEMPLOC TESTCD tf +tfrmt tformat tgdt tgt diff --git a/tlg/adverse_events.R b/tlg/adverse_events.R index b5c71c8f..43bc0bd1 100644 --- a/tlg/adverse_events.R +++ b/tlg/adverse_events.R @@ -57,3 +57,112 @@ lyt <- basic_table(show_colcounts = TRUE) %>% result <- build_table(lyt, df = adae, alt_counts_df = adsl) result + +## ----r message=FALSE, warning=FALSE------------------------------------------- +library(pharmaverseadam) # for clinical trial data +library(dplyr) # for data manipulation +library(cards) # for creating analysis result displays +library(tfrmt) # for formatting tables in R + +## ----r------------------------------------------------------------------------ +# Filter to include only subjects marked as part of the safety population +adsl <- pharmaverseadam::adsl |> + filter(SAFFL == "Y") +# Load adverse event data +adae <- pharmaverseadam::adae |> + filter(SAFFL == "Y" & TRTEMFL == "Y") + +## ----r------------------------------------------------------------------------ +# Create an ARD that stacks hierarchical data of adverse events +# Grouping by treatment, system organ class, and preferred term +ae_ard <- ard_stack_hierarchical( + data = adae, + by = TRT01A, # Note: by variables must be present in the denominator dataset + variables = c(AEBODSYS, AETERM), + statistic = ~ c("n", "p"), # Calculate count and percentage + denominator = adsl, + id = USUBJID, + over_variables = TRUE, + overall = TRUE +) + +# Filter adae and adsl with trt01a set to "Total" and create a new ARD for the total column +adae2 <- adae |> + mutate(TRT01A = "Total") +adsl2 <- adsl |> + mutate(TRT01A = "Total") + +ae2_ard <- ard_stack_hierarchical( + data = adae2, + by = TRT01A, # Note: by variables must be present in the denominator dataset + variables = c(AEBODSYS, AETERM), + denominator = adsl2, + statistic = ~ c("n", "p"), + id = USUBJID, + over_variables = TRUE, + overall = TRUE +) |> + filter(group2 == "TRT01A" | variable == "TRT01A") # filter to stats we need + +## ----r------------------------------------------------------------------------ +ae3_ard <- bind_ard(ae_ard, ae2_ard) |> + # reshape the data + shuffle_card(fill_hierarchical_overall = "ANY EVENT") |> + # transform group-level freqs/pcts into a singular "bigN" row + prep_big_n(vars = "TRT01A") |> + # for nested variables, fill any missing values with "ANY EVENT" + prep_hierarchical_fill(vars = c("AEBODSYS", "AETERM"), fill = "ANY EVENT") |> + mutate(TRT01A = ifelse(TRT01A == "Overall TRT01A", "Total", TRT01A)) + +# create ordering columns, sort by AEBODSYS +ordering_aebodsys <- ae3_ard |> + filter(TRT01A == "Total", stat_name == "n", AETERM == "ANY EVENT") |> + arrange(desc(stat)) |> + mutate(ord1 = row_number()) |> + select(AEBODSYS, ord1) + +# sort by AETERM after AEBODSYS order +ordering_aeterm <- ae3_ard |> + filter(TRT01A == "Total", stat_name == "n") |> + group_by(AEBODSYS) |> + arrange(desc(stat)) |> + mutate(ord2 = row_number()) |> + select(AEBODSYS, AETERM, ord2) + +# join on our ordering columns and keep required columns +ae4_ard <- ae3_ard |> + full_join(ordering_aebodsys, by = "AEBODSYS") |> + full_join(ordering_aeterm, by = c("AEBODSYS", "AETERM")) |> + select(AEBODSYS, AETERM, ord1, ord2, stat, stat_name, TRT01A) + +## ----r------------------------------------------------------------------------ +AE_T01 <- tfrmt_n_pct( + n = "n", pct = "p", + pct_frmt_when = frmt_when( + "==1" ~ frmt("(100%)"), + ">=0.995" ~ frmt("(>99%)"), + "==0" ~ frmt(""), + "<=0.01" ~ frmt("(<1%)"), + "TRUE" ~ frmt("(xx.x%)", transform = ~ . * 100) + ) +) |> + tfrmt( + group = AEBODSYS, + label = AETERM, + param = stat_name, + value = stat, + column = TRT01A, + sorting_cols = c(ord1, ord2), + col_plan = col_plan( + "System Organ Class + Preferred Term" = AEBODSYS, Placebo, `Xanomeline High Dose`, `Xanomeline Low Dose`, + -ord1, -ord2 + ), + row_grp_plan = row_grp_plan(row_grp_structure( + group_val = ".default", element_block(post_space = " ") + )), + big_n = big_n_structure(param_val = "bigN", n_frmt = frmt(" (N=xx)")) + ) |> + print_to_gt(ae4_ard) + +AE_T01 diff --git a/tlg/adverse_events.qmd b/tlg/adverse_events.qmd index 830d2a40..ac656588 100644 --- a/tlg/adverse_events.qmd +++ b/tlg/adverse_events.qmd @@ -15,13 +15,23 @@ knitr::knit_hooks$set(purl = invisible_hook_purl) ## Introduction This guide will show you how pharmaverse packages, along with some from tidyverse, can be used to create an Adverse Events table, using the `{pharmaverseadam}` `ADSL` and `ADAE` data as an input. +## Multiple Approaches Shown + +This example includes: + +- **ARD-based approach**: `{cards}` + `{tfrmt}` - Creates structured Analysis Results Datasets following CDISC standards, then formats them into tables +- **Classic approach**: `{rtables}` + `{tern}` - Direct table creation from datasets using mature, stable packages + +In the examples below, we illustrate two general approaches for creating an Adverse events table. The first is the classic method of creating summary tables directly from a data set. The second utilizes Analysis Results Datasets—part of the emerging CDISC Analysis Results Standard. + +## {rtables} & {tern} The packages used with a brief description of their purpose are as follows: * [`{rtables}`](https://insightsengineering.github.io/rtables/): designed to create and display complex tables with R. * [`{tern}`](https://insightsengineering.github.io/tern/): contains analysis functions to create tables and graphs used for clinical trial reporting. -## Load Data and Required pharmaverse Package +### Load Data and Required pharmaverse Package After installation of packages, the first step is to load our pharmaverse packages and input data. Here, we are going to encode missing entries in a data frame `adsl` and `adae`. @@ -39,7 +49,7 @@ adae <- adae %>% df_explicit_na() ``` -## Start preprocessing +### Start preprocessing Now we will add some pre-processing to add labels ready for display in the table and how the output will be split. @@ -55,7 +65,7 @@ adae <- adae %>% split_fun <- drop_split_levels ``` -## Adverse Events table +### Adverse Events table Now we create the Adverse Events table. @@ -97,3 +107,153 @@ result <- build_table(lyt, df = adae, alt_counts_df = adsl) result ``` + +## {tfrmt} & {cards} + +In the example below, we will use the [{tfrmt}](https://github.com/GSK-Biostatistics/tfrmt) and [{cards}](https://insightsengineering.github.io/cards/) packages to create a adverse events tables. + +- The {cards} package creates Analysis Results Datasets (ARDs, which are a part of the [CDISC Analysis Results Standard](https://www.cdisc.org/standards/foundational/analysis-results-standard)). +- The {tfrmt} utilizes ARDs to create tables. + +In the example below, we first build an ARD with the needed summary statistics using {cards}. +Then, we use the ARD to build the adverse events table with {tfrmt}. + +### Load libraries + +```{r message=FALSE, warning=FALSE} +library(pharmaverseadam) # for clinical trial data +library(dplyr) # for data manipulation +library(cards) # for creating analysis result displays +library(tfrmt) # for formatting tables in R +``` + +------------------------------------------------------------------------ + +### Step 1: Import data + +Subset to safety population. + +```{r} +# Filter to include only subjects marked as part of the safety population +adsl <- pharmaverseadam::adsl |> + filter(SAFFL == "Y") +# Load adverse event data +adae <- pharmaverseadam::adae |> + filter(SAFFL == "Y" & TRTEMFL == "Y") +``` + +------------------------------------------------------------------------ + +### Step 2: Data Preparation + +#### Analysis + +Create an Analysis Result Display (ARD) using the {cards} package. + +```{r} +# Create an ARD that stacks hierarchical data of adverse events +# Grouping by treatment, system organ class, and preferred term +ae_ard <- ard_stack_hierarchical( + data = adae, + by = TRT01A, # Note: by variables must be present in the denominator dataset + variables = c(AEBODSYS, AETERM), + statistic = ~ c("n", "p"), # Calculate count and percentage + denominator = adsl, + id = USUBJID, + over_variables = TRUE, + overall = TRUE +) + +# Filter adae and adsl with trt01a set to "Total" and create a new ARD for the total column +adae2 <- adae |> + mutate(TRT01A = "Total") +adsl2 <- adsl |> + mutate(TRT01A = "Total") + +ae2_ard <- ard_stack_hierarchical( + data = adae2, + by = TRT01A, # Note: by variables must be present in the denominator dataset + variables = c(AEBODSYS, AETERM), + denominator = adsl2, + statistic = ~ c("n", "p"), + id = USUBJID, + over_variables = TRUE, + overall = TRUE +) |> + filter(group2 == "TRT01A" | variable == "TRT01A") # filter to stats we need +``` + + +### Tidy for table + + +Further refine the ARD for use in tfrmt + +```{r} +ae3_ard <- bind_ard(ae_ard, ae2_ard) |> + # reshape the data + shuffle_card(fill_hierarchical_overall = "ANY EVENT") |> + # transform group-level freqs/pcts into a singular "bigN" row + prep_big_n(vars = "TRT01A") |> + # for nested variables, fill any missing values with "ANY EVENT" + prep_hierarchical_fill(vars = c("AEBODSYS", "AETERM"), fill = "ANY EVENT") |> + mutate(TRT01A = ifelse(TRT01A == "Overall TRT01A", "Total", TRT01A)) + +# create ordering columns, sort by AEBODSYS +ordering_aebodsys <- ae3_ard |> + filter(TRT01A == "Total", stat_name == "n", AETERM == "ANY EVENT") |> + arrange(desc(stat)) |> + mutate(ord1 = row_number()) |> + select(AEBODSYS, ord1) + +# sort by AETERM after AEBODSYS order +ordering_aeterm <- ae3_ard |> + filter(TRT01A == "Total", stat_name == "n") |> + group_by(AEBODSYS) |> + arrange(desc(stat)) |> + mutate(ord2 = row_number()) |> + select(AEBODSYS, AETERM, ord2) + +# join on our ordering columns and keep required columns +ae4_ard <- ae3_ard |> + full_join(ordering_aebodsys, by = "AEBODSYS") |> + full_join(ordering_aeterm, by = c("AEBODSYS", "AETERM")) |> + select(AEBODSYS, AETERM, ord1, ord2, stat, stat_name, TRT01A) +``` + +------------------------------------------------------------------------ + +### Step 3: Creating the `{tfrmt}` Table + +```{r} +AE_T01 <- tfrmt_n_pct( + n = "n", pct = "p", + pct_frmt_when = frmt_when( + "==1" ~ frmt("(100%)"), + ">=0.995" ~ frmt("(>99%)"), + "==0" ~ frmt(""), + "<=0.01" ~ frmt("(<1%)"), + "TRUE" ~ frmt("(xx.x%)", transform = ~ . * 100) + ) +) |> + tfrmt( + group = AEBODSYS, + label = AETERM, + param = stat_name, + value = stat, + column = TRT01A, + sorting_cols = c(ord1, ord2), + col_plan = col_plan( + "System Organ Class + Preferred Term" = AEBODSYS, Placebo, `Xanomeline High Dose`, `Xanomeline Low Dose`, + -ord1, -ord2 + ), + row_grp_plan = row_grp_plan(row_grp_structure( + group_val = ".default", element_block(post_space = " ") + )), + big_n = big_n_structure(param_val = "bigN", n_frmt = frmt(" (N=xx)")) + ) |> + print_to_gt(ae4_ard) + +AE_T01 +``` diff --git a/tlg/demographic.R b/tlg/demographic.R index ea89ee17..3163bcad 100644 --- a/tlg/demographic.R +++ b/tlg/demographic.R @@ -81,3 +81,88 @@ lyt <- basic_table(show_colcounts = TRUE) |> result <- build_table(lyt, adsl2) result + +## ----r tfrmt-table------------------------------------------------------------ +library(cards) +library(forcats) +library(tfrmt) + +# build the ARD with the needed summary statistics using {cards} +ard <- + ard_stack( + adsl, + ard_continuous( + variables = AGE, + statistic = ~ continuous_summary_fns(c("N", "mean", "sd", "min", "max")) + ), + ard_categorical(variables = c(AGEGR1, SEX, RACE)), + .by = ACTARM, # split results by treatment arm + .overall = TRUE, + .total_n = TRUE + ) + +# tidy the ARD for use in {tfrmt} +ard_tbl <- + ard |> + # reshape the data + shuffle_card(fill_overall = "Total") |> + # transform group-level freqs/pcts into a singular "bigN" row + prep_big_n(vars = "ACTARM") |> + # consolidate vars into a single variable column + prep_combine_vars(vars = c("AGE", "AGEGR1", "SEX", "RACE")) |> + # coalesce categorical levels + continuous stats into a "label" + prep_label() |> + group_by(ACTARM, stat_variable) |> + mutate(across(c(variable_level, label), ~ ifelse(stat_name == "N", "n", .x))) |> + ungroup() |> + unique() |> + # sorting + mutate( + ord1 = fct_inorder(stat_variable) |> fct_relevel("SEX", after = 0) |> as.numeric(), + ord2 = ifelse(label == "n", 1, 2) + ) |> + # relabel the variables + mutate(stat_variable = case_when( + stat_variable == "AGE" ~ "Age (YEARS) at First Dose", + stat_variable == "AGEGR1" ~ "Age Group (YEARS) at First Dose", + stat_variable == "SEX" ~ "Sex", + stat_variable == "RACE" ~ "High Level Race", + .default = stat_variable + )) |> + # drop variables not needed + select(ACTARM, stat_variable, label, stat_name, stat, ord1, ord2) |> + # remove duplicates (extra denominators per variable level) + unique() + +# create a demographics table using {tfrmt} +DM_T01 <- tfrmt( + group = stat_variable, + label = label, + param = stat_name, + value = stat, + column = ACTARM, + sorting_cols = c(ord1, ord2), + body_plan = body_plan( + frmt_structure(group_val = ".default", label_val = ".default", frmt("xxx")), + frmt_structure( + group_val = ".default", label_val = ".default", + frmt_combine("{n} ({p}%)", + n = frmt("xxx"), + p = frmt("xx", transform = ~ . * 100) + ) + ) + ), + big_n = big_n_structure(param_val = "bigN", n_frmt = frmt(" (N=xx)")), + col_plan = col_plan( + -starts_with("ord") + ), + col_style_plan = col_style_plan( + col_style_structure(col = c("Placebo", "Xanomeline High Dose", "Xanomeline Low Dose", "Total"), align = "left") + ), + row_grp_plan = row_grp_plan( + row_grp_structure(group_val = ".default", element_block(post_space = " ")) + ) +) |> + print_to_gt(ard_tbl) + +DM_T01 diff --git a/tlg/demographic.qmd b/tlg/demographic.qmd index 4af3b9cc..b8993da9 100644 --- a/tlg/demographic.qmd +++ b/tlg/demographic.qmd @@ -16,9 +16,15 @@ knitr::opts_chunk$set(echo = TRUE) This guide will show you how pharmaverse packages, along with some from tidyverse, can be used to create a Demographic table, using the `{pharmaverseadam}` `ADSL` data as an input. -In the examples below, we illustrate two general approaches for creating a demographics table. -The first utilizes Analysis Results Datasets---part of the emerging [CDISC Analysis Results Standard](https://www.cdisc.org/standards/foundational/analysis-results-standard). +In the examples below, we illustrate three general approaches for creating a demographics table. +The first and third utilize Analysis Results Datasets---part of the emerging [CDISC Analysis Results Standard](https://www.cdisc.org/standards/foundational/analysis-results-standard). The second is the classic method of creating summary tables directly from a data set. +## Multiple Approaches Shown + +This example includes: + +- **ARD-based approaches**: `{cards}` + `{gtsummary}` or `{tfrmt}` - Creates structured Analysis Results Datasets following CDISC standards, then formats them into tables +- **Classic approach**: `{rtables}` + `{tern}` - Direct table creation from datasets using mature, stable packages ## Data preprocessing @@ -143,3 +149,100 @@ result <- build_table(lyt, adsl2) result ``` + +## {tfrmt} & {cards} + +In the example below, we will use the [{tfrmt}](https://github.com/GSK-Biostatistics/tfrmt) and [{cards}](https://insightsengineering.github.io/cards/) packages to create a demographics tables. + +- The {cards} package creates Analysis Results Datasets (ARDs, which are a part of the [CDISC Analysis Results Standard](https://www.cdisc.org/standards/foundational/analysis-results-standard)). +- The {tfrmt} utilizes ARDs to create tables. + +In the example below, we first build an ARD with the needed summary statistics using {cards}. +Then, we use the ARD to build the demographics table with {tfrmt}. + +```{r tfrmt-table} +#| message: false +library(cards) +library(forcats) +library(tfrmt) + +# build the ARD with the needed summary statistics using {cards} +ard <- + ard_stack( + adsl, + ard_continuous( + variables = AGE, + statistic = ~ continuous_summary_fns(c("N", "mean", "sd", "min", "max")) + ), + ard_categorical(variables = c(AGEGR1, SEX, RACE)), + .by = ACTARM, # split results by treatment arm + .overall = TRUE, + .total_n = TRUE + ) + +# tidy the ARD for use in {tfrmt} +ard_tbl <- + ard |> + # reshape the data + shuffle_card(fill_overall = "Total") |> + # transform group-level freqs/pcts into a singular "bigN" row + prep_big_n(vars = "ACTARM") |> + # consolidate vars into a single variable column + prep_combine_vars(vars = c("AGE", "AGEGR1", "SEX", "RACE")) |> + # coalesce categorical levels + continuous stats into a "label" + prep_label() |> + group_by(ACTARM, stat_variable) |> + mutate(across(c(variable_level, label), ~ ifelse(stat_name == "N", "n", .x))) |> + ungroup() |> + unique() |> + # sorting + mutate( + ord1 = fct_inorder(stat_variable) |> fct_relevel("SEX", after = 0) |> as.numeric(), + ord2 = ifelse(label == "n", 1, 2) + ) |> + # relabel the variables + mutate(stat_variable = case_when( + stat_variable == "AGE" ~ "Age (YEARS) at First Dose", + stat_variable == "AGEGR1" ~ "Age Group (YEARS) at First Dose", + stat_variable == "SEX" ~ "Sex", + stat_variable == "RACE" ~ "High Level Race", + .default = stat_variable + )) |> + # drop variables not needed + select(ACTARM, stat_variable, label, stat_name, stat, ord1, ord2) |> + # remove duplicates (extra denominators per variable level) + unique() + +# create a demographics table using {tfrmt} +DM_T01 <- tfrmt( + group = stat_variable, + label = label, + param = stat_name, + value = stat, + column = ACTARM, + sorting_cols = c(ord1, ord2), + body_plan = body_plan( + frmt_structure(group_val = ".default", label_val = ".default", frmt("xxx")), + frmt_structure( + group_val = ".default", label_val = ".default", + frmt_combine("{n} ({p}%)", + n = frmt("xxx"), + p = frmt("xx", transform = ~ . * 100) + ) + ) + ), + big_n = big_n_structure(param_val = "bigN", n_frmt = frmt(" (N=xx)")), + col_plan = col_plan( + -starts_with("ord") + ), + col_style_plan = col_style_plan( + col_style_structure(col = c("Placebo", "Xanomeline High Dose", "Xanomeline Low Dose", "Total"), align = "left") + ), + row_grp_plan = row_grp_plan( + row_grp_structure(group_val = ".default", element_block(post_space = " ")) + ) +) |> + print_to_gt(ard_tbl) + +DM_T01 +```