install.packages("rtables")
library(rtables)
Creating Submission-Ready Tables in R with rtables
Introduction
We’ve already explored gt
and gtsummary
for quick summaries and polished tables. Now, let’s look at rtables
— a powerful package from the pharmaverse ecosystem, built for creating complex, customizable tables used in clinical trial and statistical reporting. It’s especially handy when you need full control over table layout, nested groups, and precise summaries.
Let us begin with installing and loading the required package
About the dataset
Variable | Description |
---|---|
Patient_ID | Unique subject identifier |
Treatment | Treatment arm assigned to the subject |
Center | Clinical center/site identifier |
Day | Day of assessment (e.g., 1, 7, 14 for longitudinal follow-up) |
VAS | Visual Analogue Scale score (e.g., pain score 0–100) |
Common steps for any rtables
creation
basic_table()
Initializes the layout of a table. It sets the foundation on which all other specifications are built.split_cols_by()
andsplit_rows_by()
Define how the dataset should be split across columns and rows, respectively. For example, you might split columns by treatment arm and rows by a categorical variable such as severity or system organ class.analyze()
Specifies what analysis or summary statistics should be computed for each group defined by the splits. Common summaries include means, counts, proportions, or custom functions.build_table()
Executes the layout and applies it to a dataset. This step returns the final table object that can be printed or exported.
# STEP 1: Pre-processing – Ensure Treatment is a factor (required by rtables for column splitting)
$Treatment <- as.factor(vas_df$Treatment)
vas_df$Day <- as.factor(vas_df$Day)
vas_df
# STEP 2: Capture column levels used by rtables (to ensure alignment)
<- levels(vas_df$Treatment)
col_levels
# STEP 3: Create column counts based on unique Patient_IDs per Treatment group
# To display unique patients per group, not row count.
<- vas_df %>%
PID_byTreatment group_by(Treatment) %>%
summarise(n = n_distinct(Patient_ID)) %>%
deframe() # Converts to named vector for col_counts argument
# STEP 4: Reorder the named vector
# Always match the order of the names in col_counts to the factor levels used by split_cols_by() to prevent alignment issues.
<- PID_byTreatment[col_levels] # Ensures proper alignment in table header
PID_byTreatment
# STEP 5: Create custom summary function
<- function(x) {
vas_stats list(
"Mean" = round(mean(x, na.rm = TRUE), 2),
"SD" = round(sd(x, na.rm = TRUE), 2),
"Min" = round(min(x, na.rm = TRUE), 2),
"Max" = round(max(x, na.rm = TRUE), 2)
)
}
# STEP 6: Define the table layout
<- basic_table(title = "VAS Summary by Treatment and Day") %>%
lyt split_cols_by("Treatment") %>%
split_rows_by("Day", split_label = "Study Day", child_labels = "visible") %>%
analyze("VAS", afun = vas_stats, var_labels = "", show_labels = "visible") %>%
append_topleft("Days")
# STEP 7: Build the table using the dataset and the column counts
<- build_table(lyt, vas_df, col_counts = PID_byTreatment)
rtbl
# Display the resulting table
rtbl
VAS Summary by Treatment and Day
————————————————————————————————————
Drug A Drug B Placebo
Days (N=11) (N=11) (N=8)
————————————————————————————————————
1
Mean 46.8 46.97 54.88
SD 28.01 28.6 25.35
Min 17.5 14.2 18.4
Max 94.8 98.5 98.4
7
Mean 55.15 51.37 57.86
SD 21.28 30.07 32.64
Min 13.1 7.7 15.4
Max 84.7 89.3 98
14
Mean 38 52.68 35.34
SD 27.38 26.77 27.95
Min 6.1 11.1 1
Max 95.4 91.4 82.2
Conclusion: Choosing between rtables
and gtsummary
Both rtables
and gtsummary
serve valuable purposes, but they are optimized for different end goals:
Use rtables
when:
- You’re working on clinical trial submissions (e.g., FDA/EMA).
- You need summary tables broken down by visit, treatment arm, or subgroup.
- Your tables require nested grouping, such as Treatment → Visit → Adverse Event.
- You need precise control over N counts, column structure, and formatting.
- You’re preparing production-grade tables for regulatory review or internal clinical teams.
Use gtsummary
when:
- You need quick exploratory summaries during data analysis.
- You’re preparing tables for manuscripts, slides, or posters.
- You want to include simple statistical comparisons (e.g., t-tests, chi-square).
- You value tidyverse compatibility and integration with
gt
orflextable
for polished outputs.
💡 Analogy:
Think ofgtsummary
as a presentation tool for data, andrtables
as a regulatory tool built for submission-critical reporting.