Introduction
Clinical-trial tables are deceptively difficult to program well. A table that looks simple in a protocol or clinical study report may require careful decisions about analysis populations, variable derivations, missing values, summary statistics, treatment groups, statistical tests, labels, denominators, confidence intervals, footnotes, and final document formatting.
In base R, these decisions are often implemented as a collection of independent summaries followed by substantial formatting code. The gtsummary package takes a different approach: it creates a structured table object and provides a family of functions for adding statistical information, modifying presentation, and converting the result to output formats.
The current gtsummary documentation describes tbl_summary() as the core function for descriptive summaries, while the package also provides functions for regression, survival, survey, cross-tabulation, continuous summaries, and other table types.
summary(). The important clinical-programming workflow is to define the analysis data correctly, create the statistical object, explicitly control the presentation, and then validate the final rendered table.Why gtsummary Matters in Clinical Trials
Clinical programming teams frequently produce tables such as:
- Demographic and baseline characteristics
- Medical history summaries
- Prior and concomitant medications
- Exposure summaries
- Laboratory summaries
- Vital-sign summaries
- Adverse-event summaries
- Disposition summaries
- Efficacy summaries
- Regression-model results
- Survival summaries
These tables often share the same structural requirements. Variables need labels, categorical levels need indentation, continuous variables need appropriate statistics, missing data need explicit handling, treatment columns need consistent headers, and statistical tests need to be documented.
gtsummary provides reusable functions for these recurring tasks. The package documentation currently lists functions including tbl_summary(), tbl_regression(), tbl_uvregression(), tbl_survfit(), tbl_cross(), tbl_continuous(), and a range of add_* and modify_* functions.
The gtsummary Mental Model
A useful way to understand the package is to separate the workflow into four layers.
tbl_summary(), tbl_regression(), or other gtsummary object.gt, flextable, or another supported output and compare it with independently validated expectations.Installing and Loading gtsummary
For a standard R installation, the package can be installed from CRAN.
install.packages("gtsummary")
library(gtsummary)
In a controlled clinical environment, package versions should be pinned and documented. A production submission should not silently change statistical output because a dependency was upgraded between programming runs.
Example Clinical-Trial Data
The package includes a trial example dataset containing 200 patients randomized to Drug A or Drug B. It is convenient for learning because it contains treatment, age, grade, tumor response, and survival-related variables.
library(gtsummary) data(trial) str(trial) head(trial)
The official tutorial uses this dataset to demonstrate baseline summaries and grouped comparisons.
Your First Clinical Table
The simplest gtsummary table is created with tbl_summary().
library(gtsummary)
trial |>
tbl_summary(
include = c(trt, age, grade)
)
Without extensive configuration, gtsummary detects common variable types and chooses appropriate descriptive statistics. For example, continuous variables may be displayed using median and quartiles, while categorical variables may be displayed as counts and percentages. The official documentation emphasizes these sensible defaults while also allowing each component to be customized.
| Variable type | Typical clinical presentation | Example |
|---|---|---|
| Continuous | Median (Q1, Q3) | Age: 58 (47, 66) |
| Categorical | n (%) | Male: 42 (51%) |
| Dichotomous | Positive/negative or selected level | Prior therapy: Yes |
| Missing | Explicitly represented when requested/defaulted | Unknown: 4 |
Building a Typical Table 1
In a randomized trial, the most common use case is a baseline table stratified by treatment.
trial |>
tbl_summary(
by = trt,
include = c(age, grade, response, stage)
)
The by argument defines the grouping variable. Each treatment group becomes a statistical column.
To add between-group p-values:
trial |>
tbl_summary(
by = trt,
include = c(age, grade, response, stage)
) |>
add_p()
add_p() compares values across groups and selects statistical tests according to variable type and the package's configured defaults. The current documentation shows that the function accepts explicit test specifications when the default is not appropriate.
Understanding Default Statistical Tests
The default test depends on the variable and grouping structure. Current gtsummary documentation lists examples including t-tests for continuous variables with two groups, ANOVA for continuous variables with three or more groups, chi-squared tests for categorical variables, and Fisher's exact test in relevant low-count situations.
| Situation | Example test | Clinical consideration |
|---|---|---|
| Continuous, two independent groups | t-test | Check whether the test matches the SAP and assumptions. |
| Continuous, 3+ groups | ANOVA | Consider the prespecified method and distribution. |
| Nonparametric continuous comparison | Wilcoxon/Kruskal-Wallis | Often useful for skewed clinical measurements. |
| Categorical comparison | Chi-squared | Check expected cell counts. |
| Small categorical counts | Fisher's exact | Useful when asymptotic assumptions are questionable. |
Explicitly Choosing Tests
For production clinical programming, it is often preferable to make important statistical choices explicit rather than relying entirely on defaults.
trial |>
tbl_summary(
by = trt,
include = c(age, grade)
) |>
add_p(
test = list(
age ~ "wilcox.test",
grade ~ "fisher.test"
)
)
The package supports formula-based selectors such as all_continuous() and all_categorical(), which can be especially useful when a table contains many variables.
trial |>
tbl_summary(by = trt) |>
add_p(
test = list(
all_continuous() ~ "wilcox.test",
all_categorical() ~ "fisher.test"
)
)
Custom Test Arguments
Some tests require additional arguments. gtsummary supports passing those arguments through test.args.
trial |>
tbl_summary(by = trt) |>
add_p(
test = age ~ "t.test",
test.args = age ~ list(var.equal = TRUE)
)
This pattern is valuable when the SAP explicitly specifies an assumption or parameter that differs from the default test implementation.
Controlling Which Variables Appear
Production tables should generally specify their variables rather than blindly including every column.
trial |>
tbl_summary(
by = trt,
include = c(age, grade, response)
)
Selectors can make larger programs easier to maintain:
trial |>
tbl_summary(
by = trt,
include = where(is.numeric)
)
For regulated work, however, broad selectors should be used carefully. A future dataset change could introduce a new variable and silently change the table. Explicit analysis specifications are usually easier to review and validate.
Variable Labels
Clinical tables should display meaningful labels rather than programmer-oriented variable names.
trial |>
tbl_summary(
by = trt,
label = list(
age ~ "Age (years)",
grade ~ "Tumor Grade",
response ~ "Tumor Response"
)
)
gtsummary can also use label attributes already attached to variables. This is particularly useful when labels are defined upstream in a standardized analysis dataset.
Choosing Summary Statistics
The statistic argument controls how variables are displayed.
trial |>
tbl_summary(
by = trt,
statistic = list(
all_continuous() ~ "{mean} ({sd})",
all_categorical() ~ "{n} ({p}%)"
)
)
For clinical reporting, common continuous choices include mean (SD), median (Q1, Q3), and range.
| Template | Meaning | Example |
|---|---|---|
{mean} ({sd}) | Mean and standard deviation | 57.2 (11.4) |
{median} ({p25}, {p75}) | Median and interquartile range | 55 (48, 64) |
{min}, {max} | Minimum and maximum | 21, 84 |
{n} ({p}%) | Count and percentage | 43 (51%) |
Choosing Median Versus Mean
The correct statistic is a clinical and statistical decision, not a formatting preference.
- Use mean (SD) when the distribution and analysis convention support it.
- Use median (Q1, Q3) for skewed measurements or when specified by the SAP.
- Use n (%) for categorical variables.
- Use minimum and maximum when the range is clinically useful.
Handling Missing Data
Missing values are clinically important because denominators can change across variables.
trial |>
tbl_summary(
by = trt,
missing = "ifany"
)
The table can display missing values explicitly. This makes it easier for a reviewer to understand why the denominator for one variable differs from another.
Suppressing Missing Rows
trial |>
tbl_summary(
by = trt,
missing = "no"
)
Suppressing missing rows may make a table cleaner, but it should not obscure clinically meaningful missingness. The choice should be consistent with the table shell and reporting conventions.
Adding an Overall Column
An overall population column is frequently useful in clinical tables.
trial |> tbl_summary(by = trt) |> add_overall()
This produces a table containing the overall study population in addition to the treatment-group columns.
Adding N
gtsummary also provides add_n() for adding counts to the table.
trial |> tbl_summary(by = trt) |> add_n()
This can be useful when variable-specific denominators differ because of missing observations.
Adding Confidence Intervals
For tables where confidence intervals are appropriate, add_ci() can add confidence-interval information to supported table types.
trial |> tbl_summary(by = trt) |> add_ci()
Confidence intervals should be used when they answer a prespecified scientific question. They should not be added merely because the package makes them easy to generate.
Adding Differences Between Groups
Modern gtsummary workflows can also display between-group differences rather than relying solely on p-values.
trial |> tbl_summary(by = trt) |> add_difference()
This can be more informative than a p-value alone because it presents an effect measure on the original clinical scale where appropriate.
Why Effect Estimates Often Beat P-Values
A p-value answers a narrow question about evidence against a null hypothesis. An estimated difference tells the reader how large the observed contrast is.
| Reporting element | Question answered |
|---|---|
| Group summary | What was observed in each group? |
| Difference | How far apart were the groups? |
| Confidence interval | What range of effects is compatible with the data? |
| P-value | How unusual are the data under the specified null hypothesis? |
Formatting P-Values
Clinical reports usually require consistent p-value formatting.
trial |>
tbl_summary(by = trt) |>
add_p(
pvalue_fun = label_style_pvalue(digits = 2)
)
The package exposes formatting helpers such as label_style_pvalue() so that p-value presentation can be standardized across tables.
Bold Significant P-Values
trial |> tbl_summary(by = trt) |> add_p() |> bold_p()
Although visually useful, bolding a threshold such as 0.05 should be treated as a reporting decision rather than an automatic sign of clinical importance.
Formatting Labels and Levels
gtsummary can distinguish variable labels from categorical levels and provides formatting helpers such as bold_labels() and italicize_levels().
trial |> tbl_summary(by = trt) |> bold_labels() |> italicize_levels()
Custom Headers
Clinical tables frequently require headers such as treatment names, sample sizes, analysis populations, or statistical measures.
trial |>
tbl_summary(by = trt) |>
modify_header(
label ~ "**Characteristic**"
)
The package also supports glue-style fields such as {N}, {level}, and {n} in appropriate table headers.
Spanning Headers
Spanning headers are useful when multiple columns belong to a common treatment or analysis heading.
trial |>
tbl_summary(by = trt) |>
modify_spanning_header(
all_stat_cols() ~ "**Treatment Group**"
)
This separates the logical table structure from the raw variable names and is particularly useful when generating tables that follow a predefined clinical shell.
Captions and Footnotes
Tables often need captions and explanatory footnotes.
trial |>
tbl_summary(by = trt) |>
modify_caption(
"**Table 1. Demographic and Baseline Characteristics**"
)
Footnotes can document summary statistics, statistical tests, abbreviations, and analysis conventions. gtsummary also provides controls for footnote symbols.
Removing or Modifying Footnotes
Production tables may need more precise footnotes than the automatically generated text.
trial |>
tbl_summary(by = trt) |>
modify_footnote_header(
all_stat_cols() ~ "Values are n (%) unless otherwise specified."
)
The exact footnote strategy should follow the reporting standard for the project. Automatically generated statistical-test footnotes are useful during development, but production shells may require controlled wording.
Table Themes
When many tables are generated, repeating formatting commands creates maintenance problems. gtsummary themes allow common settings to be centralized.
theme_gtsummary_compact() trial |> tbl_summary(by = trt)
The package documentation describes themes for package-wide settings such as p-value formatting, confidence-interval separators, language, print engine, and other behavior.
Why Themes Matter in a Clinical Programming Environment
Suppose a study contains 75 tables. If every table independently specifies p-value formatting, statistical labels, footnote symbols, and output preferences, small inconsistencies are likely.
A project-level theme can establish common behavior, while individual tables override only the exceptions.
Creating Regression Tables
gtsummary is not limited to descriptive tables. tbl_regression() takes a fitted regression model and produces a formatted regression table. The current documentation notes that it uses broom::tidy() as its default tidying mechanism and supports several common model classes.
library(survival) model <- lm( age ~ marker + grade, data = trial ) model |> tbl_regression()
Logistic Regression
For binary outcomes, a generalized linear model can be summarized with exponentiated coefficients.
model <- glm(
response ~ age + grade + marker,
data = trial,
family = binomial
)
model |>
tbl_regression(
exponentiate = TRUE
)
For a logistic model, exponentiating coefficients changes the presentation from log-odds coefficients to odds ratios.
Cox Regression
Survival models are especially relevant to clinical-trial reporting.
cox_model <- survival::coxph(
survival::Surv(ttdeath, death) ~ age + grade + marker,
data = trial
)
cox_model |>
tbl_regression(
exponentiate = TRUE
)
For a Cox model, exponentiated coefficients are typically interpreted as hazard ratios.
Confidence Intervals in Regression Tables
tbl_regression() includes confidence intervals by default in its current documentation.
model |>
tbl_regression(
exponentiate = TRUE,
conf.level = 0.95
)
The confidence level should match the prespecified analysis and reporting requirements.
Reference Categories
Categorical predictors require a reference category. The reference is determined by the fitted model and factor coding, not by the table formatter.
trial$grade <- relevel(
factor(trial$grade),
ref = "I"
)
model <- glm(
response ~ grade + age,
data = trial,
family = binomial
)
model |>
tbl_regression(
exponentiate = TRUE
)
Univariable Regression Tables
tbl_uvregression() is designed for a series of one-predictor-at-a-time models. The package documentation describes it as a wrapper around tbl_regression() with nearly identical arguments.
trial |>
tbl_uvregression(
method = glm,
y = response,
include = c(age, grade, marker),
method.args = list(
family = binomial
),
exponentiate = TRUE
)
This is useful for exploratory model summaries and standardized univariable screening tables, provided the analysis plan supports such a presentation.
Adding Global P-Values
For categorical predictors with multiple levels, a row-level coefficient p-value may not represent the global test of the predictor. gtsummary provides add_global_p().
trial |>
tbl_uvregression(
method = glm,
y = response,
include = c(age, grade, marker),
method.args = list(family = binomial),
exponentiate = TRUE
) |>
add_global_p()
Adding Model Sample Size and Events
cox_model |> tbl_regression(exponentiate = TRUE) |> add_n() |> add_nevent()
These additions help the reader understand how much information contributed to the model.
Multiple-Testing Adjustment
gtsummary supports add_q() for multiple-comparison adjustment in supported table workflows.
trial |>
tbl_uvregression(
method = glm,
y = response,
include = c(age, grade, marker),
method.args = list(family = binomial),
exponentiate = TRUE
) |>
add_global_p() |>
add_q()
Adjustment methods and the family of hypotheses being controlled should be defined by the statistical analysis plan. A function call is not a substitute for a multiplicity strategy.
Combining Regression Models
For some clinical reports, investigators want several model specifications presented together. gtsummary supports combining and modifying regression tables, including tools for model statistics and term manipulation.
model1 <- glm( response ~ age + grade, data = trial, family = binomial ) model2 <- glm( response ~ age + grade + marker, data = trial, family = binomial ) tbl1 <- model1 |> tbl_regression(exponentiate = TRUE) tbl2 <- model2 |> tbl_regression(exponentiate = TRUE)
When presenting multiple models, the table shell should make clear which covariates belong to which specification and whether estimates are adjusted or unadjusted.
Survival Tables
gtsummary also includes tbl_survfit() for formatted summaries of survival objects.
fit <- survival::survfit(
survival::Surv(ttdeath, death) ~ trt,
data = trial
)
fit |>
tbl_survfit(
times = c(12, 24, 36)
)
This can be useful for presenting survival probabilities at prespecified time points alongside other clinical results.
Cross Tables
For categorical-by-categorical summaries, tbl_cross() provides a concise cross-tabulation workflow.
trial |>
tbl_cross(
row = trt,
col = grade
) |>
add_p()
Cross tables are useful for response categories, treatment groups, safety classifications, and other categorical clinical endpoints.
Continuous Summary Tables
tbl_continuous() is useful when a continuous measurement needs a more specialized summary structure.
trial |>
tbl_continuous(
variable = marker,
by = trt
)
This can be useful for laboratory and biomarker reporting where the primary table structure differs from a conventional Table 1.
Custom Statistics
Clinical tables often need statistics that are not among the standard defaults. gtsummary supports custom summary statistics and custom table-building workflows.
trial |>
tbl_summary(
by = trt,
statistic = list(
marker ~ "{median} ({p25}, {p75})"
)
)
For genuinely specialized statistics, the package also provides functions such as add_stat() and custom-summary facilities. The current package index documents tbl_custom_summary() for custom summary functions.
When Custom Functions Are Appropriate
Suppose a study requires a geometric mean for a biomarker. The calculation should be implemented explicitly and validated independently.
geo_mean <- function(x) {
x <- x[is.finite(x) & x > 0]
exp(mean(log(x)))
}
trial |>
tbl_summary(
statistic = list(
marker ~ "{geo_mean}"
)
)
The exact syntax for exposing custom statistics depends on the table function and statistic interface being used. In production programming, test custom functions independently before integrating them into a formatted table.
Converting a gtsummary Object to gt
gtsummary objects can be converted to supported output engines. For HTML-oriented reporting, as_gt() is a common choice.
tbl <- trial |> tbl_summary(by = trt) |> add_p() tbl |> as_gt()
This separation between statistical table construction and final rendering is one of the most useful features of the package.
Converting to flextable for Word
Word-based reporting is common in clinical development. The current documentation provides as_flex_table(), which converts a gtsummary object to a flextable object.
tbl <- trial |> tbl_summary(by = trt) |> add_p() tbl |> as_flex_table()
The gtsummary documentation specifically notes that flextable is particularly useful for Word output, where the gt package does not provide the same support.
Saving a Word Table
tbl |>
as_flex_table() |>
flextable::save_as_docx(
path = "table1.docx"
)
For a larger reporting pipeline, consider standardizing page orientation, margins, font sizes, column widths, captions, and footnotes through a reusable rendering function rather than repeating formatting in every table program.
gtsummary and R Markdown / Quarto
gtsummary is well suited to reproducible report generation because the table object can be produced directly inside an R Markdown or Quarto document.
```{r}
library(gtsummary)
trial |>
tbl_summary(by = trt) |>
add_p()
```
The official documentation includes guidance for using gtsummary with R Markdown and Quarto and describes conversion to supported output types.
Why This Is Valuable for Clinical Reporting
Instead of maintaining a separate copy of a table in Word and a separate analysis program, the table becomes a reproducible output of the analysis code.
Inline Reporting
gtsummary also provides inline_text() for retrieving formatted values from table objects and inserting them into narrative text.
tbl <- trial |> tbl_summary(by = trt) inline_text( tbl, variable = age, column = stat_1 )
This can help keep narrative reporting synchronized with table results, reducing the risk that a report says one value while the table displays another. The current package index lists inline reporting support for summary, regression, survival, cross, and other table types.
Building a Table Function
For clinical programming, one of the most useful patterns is to wrap gtsummary in a reusable project function.
make_baseline_table <- function(data, treatment) {
data |>
gtsummary::tbl_summary(
by = {{ treatment }},
include = c(age, sex, race, weight, grade),
missing = "ifany"
) |>
gtsummary::add_overall() |>
gtsummary::add_p() |>
gtsummary::bold_labels() |>
gtsummary::modify_caption(
"**Table 14.1.1 Demographic and Baseline Characteristics**"
)
}
This pattern centralizes table logic. If the project changes the required caption or formatting convention, the programmer updates one function rather than dozens of table programs.
Using Tidy Evaluation Carefully
Reusable functions often accept variable names as arguments. Tidy evaluation allows those arguments to be passed into gtsummary and dplyr pipelines, but this introduces another layer of programming complexity.
make_grouped_summary <- function(data, by_var) {
data |>
gtsummary::tbl_summary(
by = {{ by_var }}
)
}
When building reusable clinical-programming utilities, test the function with multiple datasets, grouping variables, missing-data patterns, and factor structures.
Clinical Table Specifications
A gtsummary program should not be treated as the table specification itself. The statistical programmer should begin from an approved table shell or analysis specification.
| Specification item | Example |
|---|---|
| Population | Safety analysis set |
| Treatment variable | TRT01P |
| Variables | Age, sex, race, baseline disease characteristics |
| Continuous statistic | n, mean, SD, median, Q1, Q3, min, max |
| Categorical statistic | n (%) |
| Missing values | Displayed as separate category |
| Statistical tests | As specified in SAP |
| Footnotes | Controlled study-specific wording |
| Output | RTF, DOCX, HTML, or PDF workflow |
gtsummary Is Not the Analysis Dataset
A common conceptual mistake is to treat a formatted table as the analysis itself.
The table should be the final representation of already-defined analysis data. If the analysis dataset contains the wrong baseline record, wrong treatment assignment, duplicated subject, or incorrect analysis flag, gtsummary will faithfully summarize the wrong information.
ADaM-Oriented Workflow
In a CDISC-oriented environment, a typical workflow might look like:
Example: Safety Table Pattern
Suppose a safety dataset contains one record per subject for a binary safety characteristic and you need counts and percentages by treatment.
safety |>
filter(SAFFL == "Y") |>
tbl_summary(
by = TRT01P,
include = c(SEX, RACE, AGEGR1, PRIORTRT),
statistic = list(
all_categorical() ~ "{n} ({p}%)"
),
missing = "ifany"
) |>
add_overall()
The important part is not the syntax. It is that the population restriction and variable definitions are established before the table is constructed.
Example: Efficacy Table Pattern
For an efficacy analysis, the table might summarize response categories by treatment.
efficacy |>
filter(ITTFL == "Y") |>
tbl_summary(
by = TRT01P,
include = BOR,
statistic = all_categorical() ~ "{n} ({p}%)",
missing = "no"
) |>
add_overall() |>
add_p()
If the response variable is a formal RECIST-derived endpoint, its derivation should occur upstream. The table should not infer response categories from raw tumor measurements merely because those values happen to be available.
Example: Laboratory Table Considerations
Laboratory reporting is usually more complex than a basic Table 1 because the same analyte may require baseline, post-baseline, change-from-baseline, and potentially shift summaries.
For example, a descriptive table might use:
labs |>
filter(PARAMCD == "ALT") |>
tbl_summary(
by = TRT01P,
include = c(AVAL, CHG),
statistic = list(
all_continuous() ~ "{median} ({p25}, {p75})"
)
)
In a production program, the analysis dataset and parameter-level filtering would normally be much more explicit, and multiple visits would often require a dedicated longitudinal or visit-based table structure.
Handling Categorical Variables Correctly
Factor levels matter. A categorical variable should have the intended levels, labels, ordering, and reference category before it reaches the table-generation step.
trial$grade <- factor(
trial$grade,
levels = c("I", "II", "III")
)
trial |>
tbl_summary(
include = grade
)
This is especially important for clinical variables such as disease stage, severity grade, response category, toxicity grade, and treatment-emergent event classifications.
Zero Counts
Clinical tables often need to show categories with zero observations. Whether zero levels appear depends on the factor structure and table configuration.
trial$grade <- factor(
trial$grade,
levels = c("I", "II", "III", "IV")
)
Review the rendered output to confirm that expected categories appear and that zero counts are represented according to the table shell.
Denominators Are Part of the Result
A clinical table is not just a set of numerators and percentages. The denominator is part of the scientific result.
| Variable | Numerator | Denominator | Displayed percentage |
|---|---|---|---|
| Male | 42 | 82 | 51.2% |
| White | 63 | 81 | 77.8% |
| Prior therapy: Yes | 29 | 79 | 36.7% |
If denominators differ, the reason should be understandable from the analysis population and missing-data rules.
Rounding Is a Reporting Decision
Percentages, means, medians, confidence intervals, odds ratios, and hazard ratios all require defined rounding conventions.
trial |>
tbl_summary(
statistic = list(
all_continuous() ~ "{mean} ({sd})"
),
digits = all_continuous() ~ 1
)
Rounding should be controlled centrally where possible. Inconsistent rounding across tables can make a report look unreliable even when the underlying analysis is correct.
Customizing the Output Engine
The statistical table object and the rendered table are conceptually separate.
| Stage | Purpose |
|---|---|
| gtsummary object | Stores variables, statistics, labels, p-values, and table metadata. |
| gt | Strong option for HTML-oriented presentation. |
| flextable | Useful for Word-oriented workflows. |
| kable/kableExtra | Useful in selected R Markdown/LaTeX workflows. |
| huxtable | Another supported output pathway. |
The current package index lists conversion functions including as_gt(), as_flex_table(), as_kable(), as_kable_extra(), and huxtable conversion functions.
Formatting Word Tables With flextable
After conversion, flextable functions can be used for document-specific formatting.
tbl |>
as_flex_table() |>
flextable::autofit() |>
flextable::fontsize(
size = 9,
part = "all"
)
The official as_flex_table() documentation notes that a gtsummary table can be converted and then further styled with flextable functions.
Production Formatting Versus Exploratory Formatting
Exploratory tables can be highly interactive and richly formatted. Submission tables generally need stability, controlled wording, predictable pagination, and traceability.
| Exploratory | Production |
|---|---|
| Rapid iteration | Controlled specification |
| Convenient defaults | Explicit statistical choices |
| Flexible labels | Approved terminology |
| Interactive display | Stable rendered output |
| Ad hoc filtering | Prespecified analysis population |
Validation Strategy
Clinical table validation should occur at more than one level.
Independent QC Example
Suppose the production table reports 43 of 82 patients as male.
qc_n <- safety |> filter(SAFFL == "Y", TRT01P == "Drug A", SEX == "M") |> summarise(n = n()) qc_n
The independently calculated count should agree with the production table. Similar checks should be made for denominators and percentages.
Validate P-Values Independently
For important inferential tables, reproduce the statistical test independently rather than trusting only the table object.
with(
trial,
wilcox.test(
age ~ trt
)
)
The independently calculated result should match the p-value and test specified in the table.
Validate Regression Tables Independently
For regression tables, compare the gtsummary estimate, standard error, confidence interval, and p-value against the fitted model object and an independent extraction.
summary(model) broom::tidy(model, conf.int = TRUE)
gtsummary's regression table is a presentation layer over the model results; it should never be the sole source used to verify the model.
Common gtsummary Mistakes
- Using the wrong analysis population. The table may be mathematically correct but scientifically wrong.
- Relying blindly on default tests. The SAP should determine inferential methods.
- Allowing variable types to be inferred incorrectly. Character, numeric, factor, and labelled variables can behave differently.
- Ignoring missing-value denominators. Different variables may have different available Ns.
- Using formatted tables as the validation source. Independent QC should trace back to analysis data.
- Hard-coding labels inconsistently. Controlled terminology should be centralized.
- Changing output formatting without reviewing pagination. A table can look correct in R and fail in the final document.
- Confusing p-value formatting with statistical analysis. Formatting does not change the inferential method.
- Overusing automatic selectors. Future dataset changes can silently change table content.
- Failing to pin package versions. Reproducibility requires a controlled software environment.
Reusable Table Architecture
A scalable clinical-table project can organize code into layers.
R/ setup.R data_prep.R table_helpers.R table_14_1_1.R table_14_2_1.R table_14_3_1.R qc_table_14_1_1.R output/ tables/ logs/ qc/
Reusable helper functions can standardize captions, headers, footnotes, p-value formatting, and rendering behavior.
A Reusable Clinical Table Helper
clinical_table_theme <- function(tbl) {
tbl |>
gtsummary::bold_labels() |>
gtsummary::modify_header(
label ~ "**Characteristic**"
)
}
make_table1 <- function(data, by_var) {
data |>
gtsummary::tbl_summary(
by = {{ by_var }},
missing = "ifany"
) |>
gtsummary::add_overall() |>
clinical_table_theme()
}
This type of helper can become the foundation of a study-wide table-generation framework.
Handling Study-Specific Footnotes
Study-specific footnotes should be treated as controlled text. For example:
tbl |>
modify_footnote_body(
all_stat_cols() ~
"Percentages are based on the number of patients with non-missing data."
)
In a regulated environment, review footnotes as carefully as the numerical content. A misleading footnote can materially change interpretation even when the numbers are correct.
Table Shell to Code Mapping
A useful programming discipline is to map each shell element to a code component.
| Shell element | Typical gtsummary mechanism |
|---|---|
| Population | Input dataset / filter |
| Treatment columns | by = |
| Variables | include = |
| Variable labels | label = or source labels |
| Statistic | statistic = |
| Digits | digits = |
| P-value | add_p() |
| Overall column | add_overall() |
| Caption | modify_caption() |
| Headers | modify_header() |
| Spanning headers | modify_spanning_header() |
| Footnotes | modify_footnote_* |
| Rendering | as_gt(), as_flex_table(), etc. |
When gtsummary Is a Good Fit
- Repeated descriptive tables share a common structure.
- You want R code to remain readable and close to the statistical specification.
- You need tables for HTML, Word, or reproducible reporting.
- You want reusable formatting and table-building helpers.
- Your team wants standardized clinical table conventions.
When Additional Tools Are Needed
gtsummary is powerful, but it is not a complete replacement for every clinical-reporting technology.
- Highly specialized regulatory table layouts may require additional rendering tools.
- Complex pagination may require document-level formatting.
- Highly customized Word output may require flextable and officer.
- Large legacy reporting systems may have established SAS or other workflows that must remain synchronized.
- Complex nested event tables may require specialized data preparation before gtsummary is appropriate.
gtsummary Versus Hand-Coded dplyr Summaries
| Feature | Hand-coded dplyr | gtsummary |
|---|---|---|
| Basic counts | Excellent | Excellent |
| Publication formatting | Requires extra work | Built in |
| Clinical labels | Manual | Integrated |
| P-values | Manual | Integrated |
| Regression tables | Manual | Integrated |
| Footnotes | Manual | Integrated |
| Reusable themes | Custom framework | Supported |
| Maximum layout control | Very high | High, often with output-engine extensions |
gtsummary Versus gt
These packages are complementary rather than direct competitors.
| Question | gtsummary | gt |
|---|---|---|
| Clinical statistical summaries | Primary strength | Not the primary purpose |
| Regression model tables | Strong | Requires manual preparation |
| Statistical tests | Integrated | Usually supplied separately |
| Fine-grained visual formatting | Good | Excellent |
| Clinical table workflow | Purpose-built | General table rendering |
Use gtsummary for Statistical Structure, Then Format
A strong workflow is:
Advanced: Custom P-Value Functions
For specialized methods, gtsummary supports custom statistical functions. The function interface receives the relevant data and metadata needed to calculate the requested result.
my_test <- function(data, variable, by, ...) {
x <- data[[variable]]
g <- data[[by]]
result <- t.test(x ~ g)
broom::tidy(result)
}
trial |>
tbl_summary(by = trt) |>
add_p(
test = age ~ "my_test"
)
Custom tests should be used sparingly in production because they increase the amount of project-specific statistical code that must be validated.
Advanced: Adjusted Comparisons
Some tables require adjusted comparisons or ANCOVA-style analyses. gtsummary's comparison infrastructure supports adjustment variables in supported workflows.
trial |>
tbl_summary(by = trt) |>
add_p(
adj.vars = c(age, grade)
)
The exact adjusted method should always be aligned with the statistical analysis plan and the supported test method.
Advanced: Multiple Comparison Workflows
When many p-values are presented, consider whether multiplicity is relevant. gtsummary can add adjusted q-values through add_q(), but the choice of adjustment procedure and hypothesis family remains an analysis decision.
tbl |> add_p() |> add_q()
Advanced: Table Objects as Data
A gtsummary object can be converted to a tibble when downstream processing is required.
tbl |> as_tibble()
This can be useful for automated QC, logging, or specialized export workflows.
Automated QC of Table Objects
Because the table is represented as an R object, a programming team can inspect the underlying data before rendering.
tbl |> as_tibble() |> print(n = Inf)
This can help identify unexpected labels, missing rows, incorrect p-values, or unexpected statistics before the table reaches the document-generation step.
Production Checklist
Example End-to-End Table Program
The following illustrates a compact but realistic workflow.
library(dplyr)
library(gtsummary)
table_14_1_1 <- trial |>
filter(!is.na(trt)) |>
tbl_summary(
by = trt,
include = c(age, grade, response),
label = list(
age ~ "Age (years)",
grade ~ "Tumor Grade",
response ~ "Tumor Response"
),
statistic = list(
all_continuous() ~ "{median} ({p25}, {p75})",
all_categorical() ~ "{n} ({p}%)"
),
missing = "ifany"
) |>
add_overall() |>
add_p() |>
modify_header(
label ~ "**Characteristic**"
) |>
modify_caption(
"**Table 14.1.1 Demographic and Baseline Characteristics**"
) |>
bold_labels()
table_14_1_1
What Makes This Production-Ready?
The code is not production-ready simply because it runs. A production implementation also needs:
- A controlled analysis dataset.
- Explicit population definitions.
- Approved variable labels.
- Documented statistical methods.
- Controlled formatting.
- Independent QC.
- Version-controlled source code.
- Reproducible package versions.
- Inspection of the final output.
Common Clinical Reporting Pitfalls
Using a descriptive table to imply an inferential conclusion
A difference between treatment groups in a Table 1 is descriptive unless an inferential method has been explicitly specified and is appropriate for the context.
Relying on default tests without reviewing the SAP
Defaults are convenient, not authoritative.
Changing table statistics without updating footnotes
If a table changes from mean (SD) to median (Q1, Q3), its explanatory text must change too.
Formatting before validating the numbers
Formatting can make an incorrect table look authoritative. Validate the numbers first.
Assuming Word output will look identical to HTML
Different output engines have different layout behavior. Always inspect the actual deliverable.
gtsummary in a Regulatory Programming Strategy
gtsummary can be a strong component of an R-based clinical reporting system, particularly when paired with standardized ADaM inputs, reusable table functions, controlled themes, and independent QC.
It should not, however, be treated as a validation framework by itself. Package functionality, statistical calculations, custom code, and rendered output all require appropriate verification.
Recommended Project Architecture
project/
data/
adam/
R/
functions/
table_helpers.R
formatting.R
qc_helpers.R
tables/
t14_1_1.R
t14_2_1.R
t14_3_1.R
reports/
specs/
table_shells/
analysis_specifications/
qc/
independent_programs/
listings/
output/
tables/
figures/
reports/
renv.lock
An renv-managed environment can help preserve the software environment used to generate a reporting package.
Where gtsummary Fits With Other R Packages
| Package | Typical role |
|---|---|
dplyr | Data preparation and analysis-population filtering |
tidyr | Reshaping and data structure preparation |
broom | Tidying model results |
gtsummary | Clinical statistical tables |
gt | Rich table rendering |
flextable | Word-oriented table formatting |
officer | Office document generation and manipulation |
rmarkdown/quarto | Reproducible report generation |
renv | Package-environment reproducibility |
Key Takeaways
Quick Reference
| Goal | Primary function |
|---|---|
| Baseline / descriptive table | tbl_summary() |
| Add treatment-group p-values | add_p() |
| Add overall population | add_overall() |
| Add N | add_n() |
| Add differences | add_difference() |
| Regression model | tbl_regression() |
| Univariable regression | tbl_uvregression() |
| Survival summary | tbl_survfit() |
| Cross-tabulation | tbl_cross() |
| Continuous summary | tbl_continuous() |
| Add global model p-values | add_global_p() |
| Multiple-testing adjustment | add_q() |
| HTML-style output | as_gt() |
| Word-friendly output | as_flex_table() |
| Inline report values | inline_text() |
References
Sjoberg, D.D. et al. gtsummary: Presentation-Ready Data Summary and Analytic Result Tables. R package documentation, current release 2.6.1. Official reference documentation and vignettes: gtsummary package index.
tbl_summary() tutorial. Official gtsummary documentation describing descriptive summaries, grouping, p-values, labels, statistics, and table modification: tbl_summary tutorial.
tbl_regression() tutorial. Official gtsummary documentation describing regression tables, model tidying, exponentiation, and univariable regression: tbl_regression tutorial.
Comparison tests/methods. Official documentation of tests available to add_p(), including customization and custom tests: gtsummary tests reference.
gtsummary + Quarto/R Markdown. Official reporting and output-engine guidance: gtsummary reporting guide.
as_flex_table(). Official documentation for converting gtsummary objects to flextable for Word-oriented workflows: as_flex_table reference.