A core design element of tfrmt
is to offer the ability
to layer tfrmts together. This ability provides an opportunity for
organizations to build template table formats that can be shared,
improved, and reused across multiple projects. Through layering, the
users can customize the templates to create a specific table.
A template is a function that, given a set of inputs (or none),
creates a standard tfrmt
. We will go through their
applications below.
We will create an Adverse Events table template in this vignette to
demonstrate the ideas behind developing a template tfrmt.
Creating a Standard tfrmt
The first step to creating an Adverse Events table template is to
determine the constants in the format. This requires there to be some
standards that can be expected that allows the construction of the basic
tfrmt
:
First, we can provide instructions on cell formatting via the
body_plan
. We know that the cells should be consistently
formatted as follows:
- Total counts should be whole numbers
- Cells within each treatment group column should be a combination of a count and percent of that population. When percent is 100 or zero, value should be blank.
- p-values should be formatted where: >.99 is displayed as “>0.99”, <0.001 is displayed as “<0.001”, and any values between should be displayed to three decimal places. Missing p-values should be replaced with “–”.
Next, because our data is standardized as an analysis results dataset, we can pre-fill the expected column names. This also ensures that the data abides to the standard and names are set consistently:
-
AEBODYSYS
defines the body systems and overall grouping of the data. -
AETERM
defines the labels to display in the table. -
value
defines the values to present in the table. -
treatment
defines the treatment for each value. This defines the column span. -
col
defines the column for each value. -
param
is the column that describes the params of thevalue
column. “AEs” indicates that the number invalue
represents the total counts, “n” represents the count for the specific AE, “pct” is the percent of the treatment population, and finally “pval” is the p-value.
With this information we can construct a template AE
tfrmt
:
ae_tfrmt_template <- tfrmt(
group = AEBODSYS,
label = AETERM,
param = param,
column = c(treatment, col),
value = value,
body_plan = body_plan(
## All entries where the param column is `AEs` (representing total counts)
frmt_structure(
group_val = ".default",
label_val = ".default",
AEs = frmt("[XXX]")
),
## Combine entries where param column is `n` and `pct` to create a cell for
## that population
frmt_structure(
group_val = ".default",
label_val = ".default",
frmt_combine(
"{n} {pct}",
n = frmt("XXX"),
pct = frmt_when(
"==100" ~ "",
"==0" ~ "",
TRUE ~ frmt("(xx.x %)")
)
)
),
## All entries where param column is `pval`, format conditionally.
## When the value is missing, replace the NA with "--".
frmt_structure(
group_val = ".default",
label_val = ".default",
pval = frmt_when(
">0.99" ~ ">0.99",
"<0.001" ~ "<0.001",
TRUE ~ frmt("x.xxx", missing = "--")
)
)
)
)
Functionalising the Template
Now that we have our tfrmt, we can wrap it into a function to form a template. This allows us to have as many layers as we’d like. For example, we can have an organization-level template to be used across all tables, a domain-level template to be used across all tables in a given domain, and a project-level template to be used for study-specific tables.
tfrmt
offers the function layer_tfrmt()
,
which provides the ability for layering tfrmt
together. The
first two arguments are the tfrmt
objects to be layered. By
default the body_plans of the tfrmt are joined together. We will
leverage this function in the creation of our template.
See below for an example of creating a template based on a function
for layering tfrmt
together. In this scenario the base AE
template only has body_plan values for handling total counts, case
counts, and percents, but not p-values. We create another template for
p-values, and then finally layer it with the study specific tfrmt.
ae_base_tfrmt_template <- function(tfrmt_obj){
ae_base <- tfrmt(
group = AEBODSYS,
label = AETERM,
param = param,
column = c(treatment, col),
value = value,
body_plan = body_plan(
## All entries where the param column is `AEs` (representing total counts)
frmt_structure(
group_val = ".default",
label_val = ".default",
AEs = frmt("[XXX]")
),
## Combine entries where param column is `n` and `pct` to create a cell for
## that population
frmt_structure(
group_val = ".default",
label_val = ".default",
frmt_combine(
"{n} {pct}",
n = frmt("XXX"),
pct = frmt_when(
"==100" ~ "",
"==0" ~ "",
TRUE ~ frmt("(xx.x %)")
)
)
)
)
)
layer_tfrmt(x = tfrmt_obj, y = ae_base)
}
ae_pval_tfrmt_template <- function(tfrmt_obj){
ae_pval_template <- tfrmt(
body_plan = body_plan(
## All entries where param column is `pval`, format conditionally.
## When the value is missing, replace the NA with "--".
frmt_structure(
group_val = ".default",
label_val = ".default",
pval = frmt_when(
">0.99" ~ ">0.99",
"<0.001" ~ "<0.001",
TRUE ~ frmt("x.xxx", missing = "--")
)
)
)
)
layer_tfrmt(tfrmt_obj, ae_pval_template)
}
Using these templates and what we learned from the layering vignette, we can apply multiple templates cleanly within a pipe:
study_ae_tfrmt_multi_layer <- ae_base_tfrmt_template() %>%
ae_pval_tfrmt_template() %>%
tfrmt(
title = "Adverse Events for CDISC Pilot Study",
subtitle = "Data subset to AEs with >10% prevalence in the High Dose group",
## Sorting columns of rows
sorting_cols = c(ord1, ord2),
## Nest Preferred terms under SOC
row_grp_plan = row_grp_plan(label_loc = element_row_grp_loc(location = "indented")),
## alisgnment of columns
col_style_plan = col_style_plan(
col_style_structure(align = c(".",","," "), col = vars(starts_with("p_")))
),
## remove order columns from final table
col_plan = col_plan(
span_structure(
treatment = c(
"Xanomeline High Dose (N=84)" = `Xanomeline High Dose`,
"Xanomeline Low Dose (N=84)" = `Xanomeline Low Dose`,
"Placebo (N=86)" = Placebo
),
col = c(
`n (%)` = `n_pct` ,
`[AEs]` = `AEs`
)
),
span_structure(
treatment = c(
"Fisher's Exact p-values" = fisher_pval
),
col = c(
`Placebo vs. Low Dose` = `p_low` ,
`Placebo vs. High Dose` = `p_high`
)
),
- starts_with("ord")
)
)
See how this results in the same table as above:
## filter to keep only AEs with >10% prevalence in the High Dose group
data_ae2 <- data_ae %>%
group_by(AEBODSYS, AETERM) %>%
mutate(pct_high = value[col2=="Xanomeline High Dose" & param=="pct"]) %>%
ungroup %>%
filter(pct_high >10) %>%
select(-pct_high) %>%
rename(
treatment = col2,
col = col1
)
study_ae_tfrmt_multi_layer %>%
print_to_gt(data_ae2) %>%
tab_options(
container.width = 1000
)
Adverse Events for CDISC Pilot Study | ||||||||
Data subset to AEs with >10% prevalence in the High Dose group | ||||||||
Xanomeline High Dose (N=84)
|
Xanomeline Low Dose (N=84)
|
Placebo (N=86)
|
Fisher’s Exact p-values
|
|||||
---|---|---|---|---|---|---|---|---|
n (%) | [AEs] | n (%) | [AEs] | n (%) | [AEs] | Placebo vs. Low Dose | Placebo vs. High Dose | |
ANY BODY SYSTEM | 76 (90.5 %) | [433] | 77 (91.7 %) | [412] | 65 (75.6 %) | [281] | 0.007 | 0.014 |
CARDIAC DISORDERS | 15 (17.9 %) | [ 30] | 13 (15.5 %) | [ 30] | 12 (14.0 %) | [ 26] | 0.831 | 0.534 |
GASTROINTESTINAL DISORDERS | 20 (23.8 %) | [ 36] | 14 (16.7 %) | [ 22] | 17 (19.8 %) | [ 26] | 0.692 | 0.580 |
GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS | 40 (47.6 %) | [124] | 47 (56.0 %) | [118] | 21 (24.4 %) | [ 46] | <0.001 | 0.002 |
APPLICATION SITE PRURITUS | 22 (26.2 %) | [ 35] | 22 (26.2 %) | [ 32] | 6 ( 7.0 %) | [ 10] | <0.001 | <0.001 |
APPLICATION SITE ERYTHEMA | 15 (17.9 %) | [ 23] | 12 (14.3 %) | [ 20] | 3 ( 3.5 %) | [ 3] | 0.015 | 0.002 |
APPLICATION SITE IRRITATION | 9 (10.7 %) | [ 16] | 9 (10.7 %) | [ 18] | 3 ( 3.5 %) | [ 7] | 0.078 | 0.078 |
INFECTIONS AND INFESTATIONS | 13 (15.5 %) | [ 20] | 9 (10.7 %) | [ 16] | 16 (18.6 %) | [ 35] | 0.194 | 0.685 |
NERVOUS SYSTEM DISORDERS | 25 (29.8 %) | [ 41] | 20 (23.8 %) | [ 40] | 8 ( 9.3 %) | [ 11] | 0.013 | <0.001 |
DIZZINESS | 11 (13.1 %) | [ 15] | 8 ( 9.5 %) | [ 13] | 2 ( 2.3 %) | [ 3] | 0.056 | 0.009 |
RESPIRATORY, THORACIC AND MEDIASTINAL DISORDERS | 10 (11.9 %) | [ 22] | 9 (10.7 %) | [ 14] | 8 ( 9.3 %) | [ 12] | 0.803 | 0.626 |
SKIN AND SUBCUTANEOUS TISSUE DISORDERS | 40 (47.6 %) | [104] | 39 (46.4 %) | [111] | 20 (23.3 %) | [ 45] | 0.002 | 0.001 |
PRURITUS | 26 (31.0 %) | [ 38] | 21 (25.0 %) | [ 31] | 8 ( 9.3 %) | [ 11] | 0.008 | <0.001 |
ERYTHEMA | 14 (16.7 %) | [ 22] | 14 (16.7 %) | [ 22] | 8 ( 9.3 %) | [ 12] | 0.175 | 0.175 |
RASH | 9 (10.7 %) | [ 15] | 13 (15.5 %) | [ 18] | 5 ( 5.8 %) | [ 9] | 0.048 | 0.277 |
Alternatives to Full Templates
In addition to defining full tfrmt templates, users can also create reusable frmt templates. The benefit of this is that teams can use these frmts in their tfrmt directly and can be sure their table will comply with the value presentation expected of their organization.
Some common pre-defined frmts may include p-value displays, integers,
n (%)
, among other things.
# defined frmts
int_frmt <- frmt("[XXX]")
pval_frmt <- frmt_when(
">0.99" ~ ">0.99",
"<0.001" ~ "<0.001",
TRUE ~ frmt("x.xxx", missing = "--")
)
n_pct_frmt <- frmt_combine(
"{n} {pct}",
n = frmt("XXX"),
pct = frmt_when(
"==100" ~ "",
"==0" ~ "",
TRUE ~ frmt("(xx.x %)")
)
)
## frmts as functions
int_frmt_func <- function(ints = 2){
str_exp <- paste0("[", paste0(rep("X", ints), collapse = ""), "]")
frmt(str_exp)
}
Now that there are defined frmts, we can use them in our tfrmt to generate our table too.
tfrmt(
group = AEBODSYS,
label = AETERM,
param = param,
column = c(treatment,col),
value = value,
body_plan = body_plan(
frmt_structure(
group_val = ".default",
label_val = ".default",
AEs = int_frmt_func(3)
),
frmt_structure(
group_val = ".default",
label_val = ".default",
n_pct_frmt
),
frmt_structure(
group_val = ".default",
label_val = ".default",
pval = pval_frmt
)
),
## remove order columns from final table
col_plan = col_plan(
span_structure(
treatment = c(
"Xanomeline High Dose (N=84)" = `Xanomeline High Dose`,
"Xanomeline Low Dose (N=84)" = `Xanomeline Low Dose`,
"Placebo (N=86)" = Placebo
),
col = c(
`n (%)` = `n_pct` ,
`[AEs]` = `AEs`
)
),
span_structure(
treatment = c(
"Fisher's Exact p-values" = fisher_pval
),
col = c(
`Placebo vs. Low Dose` = `p_low` ,
`Placebo vs. High Dose` = `p_high`
)
),
- starts_with("ord")
)
) %>%
print_to_gt(data_ae2) %>%
tab_options(
container.width = 1000
)
Xanomeline High Dose (N=84)
|
Xanomeline Low Dose (N=84)
|
Placebo (N=86)
|
Fisher’s Exact p-values
|
|||||
---|---|---|---|---|---|---|---|---|
n (%) | [AEs] | n (%) | [AEs] | n (%) | [AEs] | Placebo vs. Low Dose | Placebo vs. High Dose | |
ANY BODY SYSTEM | 76 (90.5 %) | [433] | 77 (91.7 %) | [412] | 65 (75.6 %) | [281] | 0.007 | 0.014 |
CARDIAC DISORDERS | 15 (17.9 %) | [ 30] | 13 (15.5 %) | [ 30] | 12 (14.0 %) | [ 26] | 0.831 | 0.534 |
GASTROINTESTINAL DISORDERS | 20 (23.8 %) | [ 36] | 14 (16.7 %) | [ 22] | 17 (19.8 %) | [ 26] | 0.692 | 0.580 |
GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS | 40 (47.6 %) | [124] | 47 (56.0 %) | [118] | 21 (24.4 %) | [ 46] | <0.001 | 0.002 |
APPLICATION SITE PRURITUS | 22 (26.2 %) | [ 35] | 22 (26.2 %) | [ 32] | 6 ( 7.0 %) | [ 10] | <0.001 | <0.001 |
APPLICATION SITE ERYTHEMA | 15 (17.9 %) | [ 23] | 12 (14.3 %) | [ 20] | 3 ( 3.5 %) | [ 3] | 0.015 | 0.002 |
APPLICATION SITE IRRITATION | 9 (10.7 %) | [ 16] | 9 (10.7 %) | [ 18] | 3 ( 3.5 %) | [ 7] | 0.078 | 0.078 |
INFECTIONS AND INFESTATIONS | 13 (15.5 %) | [ 20] | 9 (10.7 %) | [ 16] | 16 (18.6 %) | [ 35] | 0.194 | 0.685 |
NERVOUS SYSTEM DISORDERS | 25 (29.8 %) | [ 41] | 20 (23.8 %) | [ 40] | 8 ( 9.3 %) | [ 11] | 0.013 | <0.001 |
DIZZINESS | 11 (13.1 %) | [ 15] | 8 ( 9.5 %) | [ 13] | 2 ( 2.3 %) | [ 3] | 0.056 | 0.009 |
RESPIRATORY, THORACIC AND MEDIASTINAL DISORDERS | 10 (11.9 %) | [ 22] | 9 (10.7 %) | [ 14] | 8 ( 9.3 %) | [ 12] | 0.803 | 0.626 |
SKIN AND SUBCUTANEOUS TISSUE DISORDERS | 40 (47.6 %) | [104] | 39 (46.4 %) | [111] | 20 (23.3 %) | [ 45] | 0.002 | 0.001 |
PRURITUS | 26 (31.0 %) | [ 38] | 21 (25.0 %) | [ 31] | 8 ( 9.3 %) | [ 11] | 0.008 | <0.001 |
ERYTHEMA | 14 (16.7 %) | [ 22] | 14 (16.7 %) | [ 22] | 8 ( 9.3 %) | [ 12] | 0.175 | 0.175 |
RASH | 9 (10.7 %) | [ 15] | 13 (15.5 %) | [ 18] | 5 ( 5.8 %) | [ 9] | 0.048 | 0.277 |
Best Practices
Here we list some of the ideas about best practices when it comes to
defining tfrmt templates for use across organization. This comes from
experience writing and using tfrmt
and has the potential to
change.
A template tfrmt is intended to provide an interface for creating
complex tfrmts more easily. When creating the function, it is helpful to
differentiate the aspects of the display that will remain constant from
those which are likely to change across tables or studies. These changes
can be captured as arguments in the function. These functions should
serve to provide a way to interact with and create tfrmts for standard
tables in such a way that customization for a specific table’s
tfrmt
is simplified.
However, as with normal function development, this should be balanced
with limiting the number of arguments necessary to pass and get a
functional tfrmt. For example, tfrmt_sigdig
accepts a
minimal set of arguments, including a data.frame or tibble, to define a
complex body_plan. Simplifying the way users may enter this complex set
of instructions without having to manually type it is part of its
strength.
In addition to minimizing the arguments to a tfrmt template, it is generally simpler to define and enforce standards around things people can see in a table. For example, the grouping, labels, and value formats are easy to see and understand. Standardizing around meta information that impacts a table but may not as easily be seen, such as value context (params), is more difficult.