# Publication-ready tables with {gtsummary} and {flextable}

So, I’ve been using R for a while now and I haven’t really found a good solution in terms of producing publication-ready tables that I can customize easily until recently. I thank the developers of {gtsummary} and {flextable} for saving me the trouble of trying to figure out how to customize the back-end of the table generated R into microsoft word document.

The purpose of this post is to just share a few tips on how I would create a publication-ready table using {gtsummary} and {flextable}. There are a few things that I do manually in order to get the look and feel that I want. So let’s dive in.

# Generating a {gtsummary} table

Let’s first load in the packages

library('pacman')
p_load(gtsummary, dplyr)

We’re going to follow along the vignette for {gtsummary} and create the same table in it.

trial %>%
tbl_summary(
by = trt,
type = all_continuous() ~ "continuous2",
statistic = all_continuous() ~ c("{N_nonmiss}",
"{median} ({p25}, {p75})",
"{min}, {max}"),
missing = "no"
) %>%
add_p(pvalue_fun = ~style_pvalue(.x, digits = 2))
## Warning: The .dots argument of group_by() is deprecated as of dplyr 1.0.0.
Characteristic Drug A, N = 98 Drug B, N = 102 p-value1
Age 0.72
N 91 98
Median (IQR) 46 (37, 59) 48 (39, 56)
Range 6, 78 9, 83
Marker Level (ng/mL) 0.085
N 92 98
Median (IQR) 0.84 (0.24, 1.57) 0.52 (0.19, 1.20)
Range 0.00, 3.87 0.00, 3.64
T Stage 0.87
T1 28 (29%) 25 (25%)
T2 25 (26%) 29 (28%)
T3 22 (22%) 21 (21%)
T4 23 (23%) 27 (26%)
I 35 (36%) 33 (32%)
II 32 (33%) 36 (35%)
III 31 (32%) 33 (32%)
Tumor Response 28 (29%) 33 (34%) 0.53
Patient Died 52 (53%) 60 (59%) 0.41
Months to Death/Censor 0.14
N 98 102
Median (IQR) 23.5 (17.4, 24.0) 21.2 (14.6, 24.0)
Range 3.5, 24.0 5.3, 24.0

1 Wilcoxon rank sum test; Pearson's Chi-squared test

How about that. The table looks pretty good! But let’s say I want to add a general note as well as some abbreviations and then footnotes associated with superscripts inside the table. How would I go about doing that?

The way I edit this table is basically using the {flextable} package and adding in the elements I want manaully. Let’s go through that

p_load(flextable)
tab_fl <- trial %>%
select(age, grade, trt) %>%
tbl_summary(
by = trt,
type = all_continuous() ~ "continuous2",
digits = list(age ~ c(0,0,0,0,0,0,0,0,0)),
statistic = list(
all_continuous() ~ c("{N_nonmiss} ({p_nonmiss}%)",
"{median} ({p25}, {p75})",
"{min}, {max}",
"{N_miss} ({p_miss}%)"
),
all_categorical() ~ "{n} / {N} ({p}%)"
),
missing = "no",
label = list(age ~ "Patient Age",
) %>%
add_p(pvalue_fun = ~style_pvalue(.x, digits = 2)) %>%
modify_header(label ~ "**Variable**") %>%
modify_footnote(
list(
all_stat_cols() ~ NA,
p.value ~ NA
)
)  %>%
bold_labels()  %>%
# show_header_names() # to show header names
# manually add table and footnote using {flextable}
# first remove footnote from {gtsummary}
as_flex_table() %>%
# We set a footer to the first column's name but we need to modify it later
footnote(i=1,j=1,part = "header", value = as_paragraph("Note: This data is simulated."), ref_symbols = "") %>%
footnote(i=1,j=1,part = "header", value = as_paragraph("Abbreviations: IQR=Inter Quartile Range."), ref_symbols = "") %>%
compose(i=1,j=1, part='header', value = as_paragraph("Characteristics")) %>%
footnote(i=1,j=5, part="header", value = as_paragraph("Wilcoxon rank sum test; Pearson's Chi-squared test"))

tab_fl
 Characteristics Overall, N = 200 Drug A, N = 98 Drug B, N = 102 p-value1 Patient Age 0.72 N (% not missing) 189 (94%) 91 (93%) 98 (96%) Median (IQR) 47 (38, 57) 46 (37, 59) 48 (39, 56) Range 6, 83 6, 78 9, 83 N missing (% missing) 11 (6%) 7 (7%) 4 (4%) Tumor Grade 0.87 I 68 / 200 (34%) 35 / 98 (36%) 33 / 102 (32%) II 68 / 200 (34%) 32 / 98 (33%) 36 / 102 (35%) III 64 / 200 (32%) 31 / 98 (32%) 33 / 102 (32%) Note: This data is simulated. Abbreviations: IQR=Inter Quartile Range. 1Wilcoxon rank sum test; Pearson's Chi-squared test

There’s a bunch of things going on in the code but you get the idea through the comments! The last several lines are pretty important for the look and feel of the table that you want. I’ve struggled through trying to find the right codes to adjusting the footer but there’s a lot more customization that can be added to the table. Go checkout the github repo.

The last bit of code is saving this into a word document. You need to have the {officer} package to do so.

p_load(officer)

body_add_par("Table 1. Demographic & Clinical Characteristics", style = "table title") %>%
# Add table to doc
body_add_flextable(value = tab_fl) %>%
# Save to location inside computer
print(doc, target = paste0("/directory/flextable_worddoc.docx"))

I hope this was helpful. Generating tables especially for clients can be somewhat tedious to code but will be absolutely worth it once you’ve been coding the tables for a while! Good luck to everyone who needs to do so!

##### Chong H. Kim
###### Health Economics & Outcomes Researcher

My research interests include real-world evidence/observation research, cost-effectiveness analysis, predictive modeling, and spatial statistics.