From 3a3565da6244cbe840e71b69fbbaabe8b6064076 Mon Sep 17 00:00:00 2001 From: Alanah Jonas <156101954+alanahjonas95@users.noreply.github.com> Date: Wed, 7 Jan 2026 11:54:35 +0000 Subject: [PATCH 01/14] add cards tfrmt example code to demographics example --- tlg/demographic.qmd | 100 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/tlg/demographic.qmd b/tlg/demographic.qmd index 4af3b9cc..c21ee988 100644 --- a/tlg/demographic.qmd +++ b/tlg/demographic.qmd @@ -143,3 +143,103 @@ 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 dups (extra denoms 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 + +``` \ No newline at end of file From c34dc47b2a21378b7f2d3e276a40380c8f27bfb3 Mon Sep 17 00:00:00 2001 From: Alanah Jonas <156101954+alanahjonas95@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:25:08 +0000 Subject: [PATCH 02/14] add ae example --- tlg/adverse_events.qmd | 159 ++++++++++++++++++++++++++++++++++++++++- tlg/demographic.qmd | 4 +- 2 files changed, 158 insertions(+), 5 deletions(-) diff --git a/tlg/adverse_events.qmd b/tlg/adverse_events.qmd index 830d2a40..c5cd7256 100644 --- a/tlg/adverse_events.qmd +++ b/tlg/adverse_events.qmd @@ -16,12 +16,16 @@ knitr::knit_hooks$set(purl = invisible_hook_purl) 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. +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 +43,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 +59,7 @@ adae <- adae %>% split_fun <- drop_split_levels ``` -## Adverse Events table +### Adverse Events table Now we create the Adverse Events table. @@ -97,3 +101,152 @@ 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 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}. + +### 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 +``` \ No newline at end of file diff --git a/tlg/demographic.qmd b/tlg/demographic.qmd index c21ee988..9d260f58 100644 --- a/tlg/demographic.qmd +++ b/tlg/demographic.qmd @@ -16,8 +16,8 @@ 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. ## Data preprocessing From d4b8e6fead16d092c5ed30036c7f3d3356f99069 Mon Sep 17 00:00:00 2001 From: Alanah Jonas <156101954+alanahjonas95@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:30:31 +0000 Subject: [PATCH 03/14] add name to description --- DESCRIPTION | 1 + 1 file changed, 1 insertion(+) diff --git a/DESCRIPTION b/DESCRIPTION index 72e6f0b3..deb53a0a 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 From c82677334caa24a815375acdab8fb5d099c41369 Mon Sep 17 00:00:00 2001 From: Alanah Jonas <156101954+alanahjonas95@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:32:24 +0000 Subject: [PATCH 04/14] add packages --- DESCRIPTION | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DESCRIPTION b/DESCRIPTION index deb53a0a..b679033e 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -27,6 +27,7 @@ Imports: cards, dplyr, filters, + forcats, gtsummary, lubridate, labelled, @@ -51,6 +52,7 @@ Imports: teal.modules.clinical, teal.modules.general, tern, + tfrmt, tidyr, xportr, logr, From 879f3d3a396a1526bbe95acbb232d2c175e2c75d Mon Sep 17 00:00:00 2001 From: Alanah Jonas <156101954+alanahjonas95@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:47:35 +0000 Subject: [PATCH 05/14] run styler --- tlg/adverse_events.qmd | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/tlg/adverse_events.qmd b/tlg/adverse_events.qmd index c5cd7256..93941686 100644 --- a/tlg/adverse_events.qmd +++ b/tlg/adverse_events.qmd @@ -130,10 +130,10 @@ Subset to safety population. ```{r} # Filter to include only subjects marked as part of the safety population adsl <- pharmaverseadam::adsl |> - filter(SAFFL == "Y") + filter(SAFFL == "Y") # Load adverse event data adae <- pharmaverseadam::adae |> - filter(SAFFL == "Y" & TRTEMFL == "Y") + filter(SAFFL == "Y" & TRTEMFL == "Y") ``` ------------------------------------------------------------------------ @@ -166,7 +166,7 @@ adsl2 <- adsl |> ae2_ard <- ard_stack_hierarchical( data = adae2, - by = TRT01A, # Note: by variables must be present in the denominator dataset + by = TRT01A, # Note: by variables must be present in the denominator dataset variables = c(AEBODSYS, AETERM), denominator = adsl2, statistic = ~ c("n", "p"), @@ -174,8 +174,7 @@ ae2_ard <- ard_stack_hierarchical( over_variables = TRUE, overall = TRUE ) |> -filter(group2=="TRT01A" | variable=="TRT01A") #filter to stats we need - + filter(group2 == "TRT01A" | variable == "TRT01A") # filter to stats we need ``` @@ -185,14 +184,14 @@ filter(group2=="TRT01A" | variable=="TRT01A") #filter to stats we need Further refine the ARD for use in tfrmt ```{r} -ae3_ard <- bind_ard(ae_ard, ae2_ard) |> +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") |> + 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)) + 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 |> @@ -213,7 +212,7 @@ ordering_aeterm <- ae3_ard |> 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) + select(AEBODSYS, AETERM, ord1, ord2, stat, stat_name, TRT01A) ``` ------------------------------------------------------------------------ @@ -238,9 +237,11 @@ AE_T01 <- tfrmt_n_pct( 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), + 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 = " ") )), @@ -249,4 +250,4 @@ AE_T01 <- tfrmt_n_pct( print_to_gt(ae4_ard) AE_T01 -``` \ No newline at end of file +``` From 8f79d4b645680aed54a6f88777336e3341c54284 Mon Sep 17 00:00:00 2001 From: Alanah Jonas <156101954+alanahjonas95@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:51:00 +0000 Subject: [PATCH 06/14] run styler --- tlg/demographic.qmd | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/tlg/demographic.qmd b/tlg/demographic.qmd index 9d260f58..952b2b5c 100644 --- a/tlg/demographic.qmd +++ b/tlg/demographic.qmd @@ -164,8 +164,10 @@ library(tfrmt) ard <- ard_stack( adsl, - ard_continuous(variables = AGE, - statistic = ~ continuous_summary_fns(c("N", "mean", "sd", "min", "max"))), + 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, @@ -175,23 +177,19 @@ ard <- # tidy the ARD for use in {tfrmt} ard_tbl <- ard |> - #reshape the data + # reshape the data shuffle_card(fill_overall = "Total") |> - # transform group-level freqs/pcts into a singular "bigN" row - prep_big_n(vars = "ACTARM") |> - + prep_big_n(vars = "ACTARM") |> # consolidate vars into a single variable column - prep_combine_vars(vars = c("AGE","AGEGR1","SEX","RACE")) |> - + 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 + 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) @@ -232,8 +230,8 @@ DM_T01 <- tfrmt( -starts_with("ord") ), col_style_plan = col_style_plan( - col_style_structure(col = c("Placebo", "Xanomeline High Dose", "Xanomeline Low Dose", "Total"), align = "left") - ), + 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 = " ")) ) @@ -241,5 +239,4 @@ DM_T01 <- tfrmt( print_to_gt(ard_tbl) DM_T01 - -``` \ No newline at end of file +``` From 422d2c314ad5880e8de7295b8baf6a5a9c15317b Mon Sep 17 00:00:00 2001 From: Alanah Jonas <156101954+alanahjonas95@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:04:43 +0000 Subject: [PATCH 07/14] update wordlist and fix spelling --- inst/WORDLIST | 9 +++++++++ tlg/adverse_events.qmd | 4 ++-- tlg/demographic.qmd | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/inst/WORDLIST b/inst/WORDLIST index a57298a7..8c432934 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -164,6 +164,7 @@ bds BDS BILI BILIBL +bigN BLAS blq BLQ @@ -351,11 +352,14 @@ fansi FAS FASFL fastmap +fct FDA's fileext flextable flx fmt +fns +forcats formatters FORMN FRLTU @@ -388,6 +392,7 @@ iconv ifelse iframe init +inorder inputId installedpackages ITT @@ -494,6 +499,7 @@ onco ONCO ontrtfl ONTRTFL +ord os othgrpvars outfile @@ -532,6 +538,7 @@ PCTEST PCTESTCD PCTPT PCTPTNUM +pcts pfs PFS pharma @@ -604,6 +611,7 @@ refdates refdt REGIONCAT REGIONx +relevel renderPlot renderUI renv @@ -713,6 +721,7 @@ tempfile TEMPLOC TESTCD tf +tfrmt tformat tgdt tgt diff --git a/tlg/adverse_events.qmd b/tlg/adverse_events.qmd index 93941686..64e1ade3 100644 --- a/tlg/adverse_events.qmd +++ b/tlg/adverse_events.qmd @@ -193,14 +193,14 @@ ae3_ard <- bind_ard(ae_ard, ae2_ard) |> prep_hierarchical_fill(vars = c("AEBODSYS", "AETERM"), fill = "ANY EVENT") |> mutate(TRT01A = ifelse(TRT01A == "Overall TRT01A", "Total", TRT01A)) -# create ordering columns, sort by aebodsys +# 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 +# sort by AETERM after AEBODSYS order ordering_aeterm <- ae3_ard |> filter(TRT01A == "Total", stat_name == "n") |> group_by(AEBODSYS) |> diff --git a/tlg/demographic.qmd b/tlg/demographic.qmd index 952b2b5c..6d7107ca 100644 --- a/tlg/demographic.qmd +++ b/tlg/demographic.qmd @@ -204,7 +204,7 @@ ard_tbl <- )) |> # drop variables not needed select(ACTARM, stat_variable, label, stat_name, stat, ord1, ord2) |> - # remove dups (extra denoms per variable level) + # remove duplicates (extra denominators per variable level) unique() # create a demographics table using {tfrmt} From ff2f2fda0cbe94cd97529f08d9a91852236f9820 Mon Sep 17 00:00:00 2001 From: Alanah Jonas <156101954+alanahjonas95@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:09:02 +0000 Subject: [PATCH 08/14] update word list --- inst/WORDLIST | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/inst/WORDLIST b/inst/WORDLIST index 8c432934..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 @@ -362,7 +364,9 @@ fns forcats formatters FORMN +freqs FRLTU +frmt FRVDT fs Garolini From 6a41c55dcb64a2c3ee94117f03dd5503d9b22ea8 Mon Sep 17 00:00:00 2001 From: Alanah Jonas <156101954+alanahjonas95@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:18:49 +0000 Subject: [PATCH 09/14] Update tlg/adverse_events.qmd Co-authored-by: Jeff Dickinson --- tlg/adverse_events.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tlg/adverse_events.qmd b/tlg/adverse_events.qmd index 64e1ade3..0e8258c8 100644 --- a/tlg/adverse_events.qmd +++ b/tlg/adverse_events.qmd @@ -104,7 +104,7 @@ 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. +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. From 3299de81ca89f5d566e22889ec52c63f8513a676 Mon Sep 17 00:00:00 2001 From: Alanah Jonas <156101954+alanahjonas95@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:18:56 +0000 Subject: [PATCH 10/14] Update tlg/adverse_events.qmd Co-authored-by: Jeff Dickinson --- tlg/adverse_events.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tlg/adverse_events.qmd b/tlg/adverse_events.qmd index 0e8258c8..b8bc5aae 100644 --- a/tlg/adverse_events.qmd +++ b/tlg/adverse_events.qmd @@ -110,7 +110,7 @@ In the example below, we will use the [{tfrmt}](https://github.com/GSK-Biostatis - 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}. +Then, we use the ARD to build the adverse events table with {tfrmt}. ### Load libraries From 0d4f23f2287263ae1116525084258f1a768b330f Mon Sep 17 00:00:00 2001 From: Alanah Jonas <156101954+alanahjonas95@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:19:03 +0000 Subject: [PATCH 11/14] Update tlg/demographic.qmd Co-authored-by: Jeff Dickinson --- tlg/demographic.qmd | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tlg/demographic.qmd b/tlg/demographic.qmd index 6d7107ca..b8993da9 100644 --- a/tlg/demographic.qmd +++ b/tlg/demographic.qmd @@ -19,6 +19,12 @@ This guide will show you how pharmaverse packages, along with some from tidyvers 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 From daec71d81c35b86b51a405d6c711587dc217e5d1 Mon Sep 17 00:00:00 2001 From: Alanah Jonas <156101954+alanahjonas95@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:19:10 +0000 Subject: [PATCH 12/14] Update tlg/adverse_events.qmd Co-authored-by: Jeff Dickinson --- tlg/adverse_events.qmd | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tlg/adverse_events.qmd b/tlg/adverse_events.qmd index b8bc5aae..ac656588 100644 --- a/tlg/adverse_events.qmd +++ b/tlg/adverse_events.qmd @@ -15,6 +15,12 @@ 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. From 5c082c747ce4fb78d83e43b88418401d5311e7ee Mon Sep 17 00:00:00 2001 From: Alanah Jonas <156101954+alanahjonas95@users.noreply.github.com> Date: Fri, 16 Jan 2026 09:50:26 +0000 Subject: [PATCH 13/14] render qmd to create .r files --- tlg/adverse_events.R | 110 +++++++++++++++++++++++++++++++++++++++++++ tlg/demographic.R | 86 +++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+) diff --git a/tlg/adverse_events.R b/tlg/adverse_events.R index b5c71c8f..77b08834 100644 --- a/tlg/adverse_events.R +++ b/tlg/adverse_events.R @@ -57,3 +57,113 @@ 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/demographic.R b/tlg/demographic.R index ea89ee17..3851e538 100644 --- a/tlg/demographic.R +++ b/tlg/demographic.R @@ -81,3 +81,89 @@ 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 + From a9858e2adb6fe70bdb74c3118022ebe079ea57ec Mon Sep 17 00:00:00 2001 From: Alanah Jonas <156101954+alanahjonas95@users.noreply.github.com> Date: Fri, 16 Jan 2026 10:03:11 +0000 Subject: [PATCH 14/14] run styler --- tlg/adverse_events.R | 1 - tlg/demographic.R | 1 - 2 files changed, 2 deletions(-) diff --git a/tlg/adverse_events.R b/tlg/adverse_events.R index 77b08834..43bc0bd1 100644 --- a/tlg/adverse_events.R +++ b/tlg/adverse_events.R @@ -166,4 +166,3 @@ AE_T01 <- tfrmt_n_pct( print_to_gt(ae4_ard) AE_T01 - diff --git a/tlg/demographic.R b/tlg/demographic.R index 3851e538..3163bcad 100644 --- a/tlg/demographic.R +++ b/tlg/demographic.R @@ -166,4 +166,3 @@ DM_T01 <- tfrmt( print_to_gt(ard_tbl) DM_T01 -