Master the art of creating professional, publication-ready tables with flextable and Microsoft Word integration. From basic formatting to advanced conditional styling and automated report generation.
Picture this: You've just finished analyzing your clinical trial data. The statistical models are solid, the results are exciting, and you're ready to submit to that high-impact journal. But then comes the dreaded momentβcreating publication-ready tables. You know the ones: perfectly formatted demographics tables, statistical results with proper significance indicators, and safety summaries that pass regulatory scrutiny. This is where flextable transforms from a helpful package into your secret weapon for professional research communication.
In clinical research, tables aren't just data displaysβthey're the foundation of regulatory submissions, peer review, and clinical decision-making. Consider these real-world scenarios:
Poor table formatting doesn't just look unprofessionalβit can actually impede scientific communication and delay critical medical advances.
Flextable, created by David Gohel (the same genius behind officer), was designed specifically for one purpose: creating tables that look like they belong in prestigious journals and regulatory documents. Unlike other R table packages that focus on quick-and-dirty HTML output, flextable prioritizes professional presentation and seamless integration with Microsoft Wordβstill the gold standard for collaborative research documents.
Pixel-perfect font control, professional spacing, and regulatory-compliant formatting standards
Conditional styling based on statistical significance, effect sizes, or custom business rules
Perfect preservation of formatting when exported to Word documents for collaboration
Built-in support for FDA, EMA, and ICH formatting guidelines
Before flextable: "I need to create a demographics table... let me use kable(), then manually adjust the Word document formatting, then hope the journal's copyeditor doesn't mess it up, then recreate it from scratch when the reviewer asks for changes..."
With flextable: "Let me pipe my summary statistics through flextable() with professional formatting, conditional highlighting for significant results, and export directly to a Word document that looks publication-ready. When reviewers request changes, I'll just modify the R code and regenerate everything instantly."
This isn't just about saving timeβit's about transforming how you think about reproducible, professional research presentation.
We'll build a complete clinical trial analysis report, creating four publication-ready tables that demonstrate every major flextable capability:
By the end, you'll have not just four beautiful tables, but a complete Word document ready for submissionβand the skills to create professional tables for any research project.
Ready to elevate your research presentation? Let's create tables that make reviewers and colleagues say "Wow, this looks professional!" π
By the end of this tutorial, you will:
Complete control over fonts, colors, borders, and cell styling with professional typography support.
Seamless export to Microsoft Word documents with perfect formatting preservation.
Dynamic formatting based on data values with sophisticated conditional styling rules.
Built-in support for regulatory table formats including APA, FDA, and ICH guidelines.
# Core flextable ecosystem
library(flextable) # Main table creation package
library(officer) # Word document integration
library(gdtools) # Font and graphics utilities
# Data manipulation and analysis
library(dplyr) # Data wrangling
library(broom) # Statistical model tidying
library(emmeans) # Marginal means and contrasts
library(rstatix) # Statistical tests and effect sizes
# Specialized formatting
library(scales) # Number and label formatting
library(stringr) # String manipulation for labels
install.packages(c("flextable", "officer", "gdtools")) for complete functionality.
# Generate realistic clinical trial dataset
set.seed(123)
n_per_group <- 60
total_n <- n_per_group * 4
clinical_data <- data.frame(
patient_id = 1:total_n,
treatment = rep(c("Placebo", "Low Dose", "High Dose", "Combination"), each = n_per_group),
age = c(
rnorm(n_per_group, 65, 8), # Placebo
rnorm(n_per_group, 63, 9), # Low Dose
rnorm(n_per_group, 66, 7), # High Dose
rnorm(n_per_group, 64, 8) # Combination
),
gender = sample(c("Male", "Female"), total_n, replace = TRUE, prob = c(0.6, 0.4)),
baseline_sbp = rnorm(total_n, 145, 15),
baseline_dbp = rnorm(total_n, 85, 10),
diabetes = sample(c("Yes", "No"), total_n, replace = TRUE, prob = c(0.3, 0.7)),
smoking = sample(c("Never", "Former", "Current"), total_n, replace = TRUE, prob = c(0.4, 0.4, 0.2))
)
# Create comprehensive demographics table
table1_data <- clinical_data %>%
group_by(treatment) %>%
summarise(
n = n(),
# Age statistics
age_mean = round(mean(age), 1),
age_sd = round(sd(age), 1),
age_summary = paste0(age_mean, " (", age_sd, ")"),
# Gender distribution
male_n = sum(gender == "Male"),
male_pct = round(100 * male_n / n(), 1),
female_n = sum(gender == "Female"),
female_pct = round(100 * female_n / n(), 1),
# Baseline blood pressure
sbp_mean = round(mean(baseline_sbp), 1),
sbp_sd = round(sd(baseline_sbp), 1),
sbp_summary = paste0(sbp_mean, " (", sbp_sd, ")"),
dbp_mean = round(mean(baseline_dbp), 1),
dbp_sd = round(sd(baseline_dbp), 1),
dbp_summary = paste0(dbp_mean, " (", dbp_sd, ")"),
# Comorbidities
diabetes_n = sum(diabetes == "Yes"),
diabetes_pct = round(100 * diabetes_n / n(), 1),
diabetes_summary = paste0(diabetes_n, " (", diabetes_pct, "%)"),
# Smoking status
never_smoker_n = sum(smoking == "Never"),
never_smoker_pct = round(100 * never_smoker_n / n(), 1),
former_smoker_n = sum(smoking == "Former"),
former_smoker_pct = round(100 * former_smoker_n / n(), 1),
current_smoker_n = sum(smoking == "Current"),
current_smoker_pct = round(100 * current_smoker_n / n(), 1)
) %>%
ungroup()
Table 1: Baseline demographics with professional formatting and statistical annotations
# Create publication-ready table with advanced formatting
table1_flex <- table1_data %>%
select(treatment, age_summary, male_n, female_n, sbp_summary,
dbp_summary, diabetes_summary) %>%
flextable() %>%
# Set professional headers
set_header_labels(
treatment = "Treatment Group",
age_summary = "Age, years\nMean (SD)",
male_n = "Male\nn (%)",
female_n = "Female\nn (%)",
sbp_summary = "Systolic BP, mmHg\nMean (SD)",
dbp_summary = "Diastolic BP, mmHg\nMean (SD)",
diabetes_summary = "Diabetes Mellitus\nn (%)"
) %>%
# Apply professional theme
theme_vanilla() %>%
# Typography specifications
fontsize(size = 10, part = "all") %>%
font(fontname = "Times New Roman", part = "all") %>%
# Header formatting
bold(part = "header") %>%
align(align = "center", part = "header") %>%
bg(bg = "#f8f9fa", part = "header") %>%
# Body alignment
align(j = 1, align = "left", part = "body") %>%
align(j = 2:7, align = "center", part = "body") %>%
# Conditional formatting for treatment groups
bg(i = ~ treatment == "Placebo", bg = "#fff3cd") %>%
bg(i = ~ treatment == "Combination", bg = "#d1ecf1") %>%
# Border specifications
border_outer(border = fp_border_default(color = "#000000", width = 2)) %>%
border_inner_h(border = fp_border_default(color = "#cccccc", width = 1)) %>%
border_inner_v(border = fp_border_default(color = "#cccccc", width = 1)) %>%
# Table width optimization
width(width = c(1.8, 1.2, 1, 1, 1.3, 1.3, 1.2)) %>%
# Add footnotes
add_footer_lines("Data presented as mean (standard deviation) or n (%). BP = Blood Pressure.") %>%
fontsize(size = 8, part = "footer") %>%
italic(part = "footer")
# Generate follow-up efficacy data
clinical_data <- clinical_data %>%
mutate(
# Treatment effects with realistic clinical patterns
treatment_effect = case_when(
treatment == "Placebo" ~ 0,
treatment == "Low Dose" ~ -8,
treatment == "High Dose" ~ -15,
treatment == "Combination" ~ -22
),
# Endpoint with treatment effect and individual variation
endpoint_reduction = treatment_effect + rnorm(n(), 0, 12),
endpoint_reduction = pmax(endpoint_reduction, -50) # Clinical floor effect
)
# Fit mixed-effects model for efficacy analysis
library(glmmTMB)
efficacy_model <- glmmTMB(
endpoint_reduction ~ treatment + age + gender + baseline_sbp + (1|patient_id),
data = clinical_data,
family = gaussian()
)
# Extract and format model results
efficacy_results <- broom.mixed::tidy(efficacy_model, conf.int = TRUE) %>%
filter(effect == "fixed", term != "(Intercept)") %>%
mutate(
term = case_when(
term == "treatmentLow Dose" ~ "Low Dose vs Placebo",
term == "treatmentHigh Dose" ~ "High Dose vs Placebo",
term == "treatmentCombination" ~ "Combination vs Placebo",
term == "age" ~ "Age (per year)",
term == "genderMale" ~ "Male vs Female",
term == "baseline_sbp" ~ "Baseline SBP (per mmHg)",
TRUE ~ term
),
# Format estimates and confidence intervals
estimate_formatted = paste0(
sprintf("%.2f", estimate),
" (", sprintf("%.2f", conf.low),
", ", sprintf("%.2f", conf.high), ")"
),
# Format p-values
p_formatted = case_when(
p.value < 0.001 ~ "<0.001",
p.value < 0.01 ~ sprintf("%.3f", p.value),
TRUE ~ sprintf("%.2f", p.value)
),
# Statistical significance indicators
significance = case_when(
p.value < 0.001 ~ "***",
p.value < 0.01 ~ "**",
p.value < 0.05 ~ "*",
TRUE ~ ""
)
)
# Create comprehensive efficacy results table
efficacy_flex <- efficacy_results %>%
select(term, estimate_formatted, p_formatted, significance) %>%
flextable() %>%
# Professional headers
set_header_labels(
term = "Parameter",
estimate_formatted = "Estimate (95% CI)",
p_formatted = "P-value",
significance = "Sig."
) %>%
# Apply clinical trial table theme
theme_booktabs() %>%
# Typography
fontsize(size = 11, part = "all") %>%
font(fontname = "Times New Roman", part = "all") %>%
# Header styling
bold(part = "header") %>%
align(align = "center", part = "header") %>%
bg(bg = "#2c5aa0", part = "header") %>%
color(color = "white", part = "header") %>%
# Body alignment and formatting
align(j = 1, align = "left", part = "body") %>%
align(j = 2:4, align = "center", part = "body") %>%
# Conditional formatting for significance
bg(i = ~ significance == "***", j = 4, bg = "#28a745") %>%
bg(i = ~ significance == "**", j = 4, bg = "#ffc107") %>%
bg(i = ~ significance == "*", j = 4, bg = "#fd7e14") %>%
color(i = ~ significance %in% c("***", "**", "*"), j = 4, color = "white") %>%
bold(i = ~ significance != "", j = 1:4) %>%
# Professional borders
border_outer(border = fp_border_default(color = "#2c5aa0", width = 2)) %>%
hline_top(border = fp_border_default(color = "#2c5aa0", width = 2), part = "header") %>%
hline_bottom(border = fp_border_default(color = "#2c5aa0", width = 2), part = "body") %>%
# Optimized column widths
width(width = c(2.5, 2, 1, 0.8)) %>%
# Comprehensive footnotes
add_footer_lines(c(
"CI = Confidence Interval. Model: Mixed-effects regression adjusting for age, gender, and baseline SBP.",
"Significance: *** p<0.001, ** p<0.01, * p<0.05"
)) %>%
fontsize(size = 9, part = "footer") %>%
italic(part = "footer")
Table 2: Treatment efficacy results with conditional significance formatting
# Perform comprehensive pairwise comparisons
treatment_emmeans <- emmeans(efficacy_model, "treatment")
# All pairwise comparisons with multiplicity adjustment
pairwise_results <- pairs(treatment_emmeans, adjust = "tukey") %>%
broom::tidy(conf.int = TRUE) %>%
mutate(
# Clean comparison labels
contrast_clean = str_replace_all(contrast, " - ", " vs "),
# Format estimates with confidence intervals
estimate_ci = paste0(
sprintf("%.2f", estimate),
" (", sprintf("%.2f", conf.low),
", ", sprintf("%.2f", conf.high), ")"
),
# Effect size interpretation
effect_size = case_when(
abs(estimate) < 5 ~ "Small",
abs(estimate) < 15 ~ "Moderate",
abs(estimate) < 25 ~ "Large",
TRUE ~ "Very Large"
),
# Clinical significance
clinical_significance = case_when(
abs(estimate) >= 10 & adj.p.value < 0.05 ~ "Clinically Significant",
abs(estimate) >= 10 & adj.p.value >= 0.05 ~ "Clinically Relevant",
abs(estimate) < 10 & adj.p.value < 0.05 ~ "Statistically Significant",
TRUE ~ "Not Significant"
),
# Format adjusted p-values
p_adj_formatted = case_when(
adj.p.value < 0.001 ~ "<0.001",
adj.p.value < 0.01 ~ sprintf("%.3f", adj.p.value),
TRUE ~ sprintf("%.3f", adj.p.value)
)
)
# Create advanced pairwise comparisons table
pairwise_flex <- pairwise_results %>%
select(contrast_clean, estimate_ci, p_adj_formatted, effect_size, clinical_significance) %>%
flextable() %>%
# Professional headers with line breaks
set_header_labels(
contrast_clean = "Treatment\nComparison",
estimate_ci = "Difference in Reduction\n(95% CI)",
p_adj_formatted = "Adjusted\nP-value",
effect_size = "Effect\nSize",
clinical_significance = "Clinical\nInterpretation"
) %>%
# Apply sophisticated theme
theme_zebra() %>%
# Typography specifications
fontsize(size = 10, part = "all") %>%
font(fontname = "Arial", part = "all") %>%
# Header styling
bold(part = "header") %>%
align(align = "center", part = "header") %>%
bg(bg = "#34495e", part = "header") %>%
color(color = "white", part = "header") %>%
# Body alignment
align(j = 1, align = "left", part = "body") %>%
align(j = 2:5, align = "center", part = "body") %>%
# Conditional formatting for clinical significance
bg(i = ~ clinical_significance == "Clinically Significant", bg = "#d4edda") %>%
bg(i = ~ clinical_significance == "Clinically Relevant", bg = "#fff3cd") %>%
bg(i = ~ clinical_significance == "Statistically Significant", bg = "#cce5ff") %>%
bg(i = ~ clinical_significance == "Not Significant", bg = "#f8d7da") %>%
# Effect size formatting
bold(i = ~ effect_size %in% c("Large", "Very Large"), j = 4) %>%
color(i = ~ effect_size == "Very Large", j = 4, color = "#dc3545") %>%
color(i = ~ effect_size == "Large", j = 4, color = "#fd7e14") %>%
# Professional borders
border_outer(border = fp_border_default(color = "#34495e", width = 2)) %>%
hline(i = c(3, 6), border = fp_border_default(color = "#95a5a6", width = 1)) %>%
# Optimized column widths
width(width = c(2.2, 2.2, 1.2, 1, 1.8)) %>%
# Detailed footnotes
add_footer_lines(c(
"All pairwise comparisons adjusted using Tukey's method for multiple comparisons.",
"Clinical significance defined as |difference| β₯10 mmHg reduction with p<0.05.",
"Effect sizes: Small <5, Moderate 5-15, Large 15-25, Very Large >25 mmHg."
)) %>%
fontsize(size = 8, part = "footer") %>%
italic(part = "footer")
Table 3: Comprehensive pairwise treatment comparisons with clinical interpretation
# Create comprehensive Word document with multiple tables
library(officer)
# Initialize Word document with professional template
doc <- read_docx() %>%
# Document title and metadata
body_add_par("Clinical Trial Statistical Analysis Report",
style = "heading 1") %>%
body_add_par(paste("Generated on:", Sys.Date()),
style = "Normal") %>%
body_add_break() %>%
# Executive summary
body_add_par("Executive Summary", style = "heading 2") %>%
body_add_par(paste(
"This report presents comprehensive statistical analyses of a",
"randomized controlled trial comparing four treatment arms:",
"placebo, low dose, high dose, and combination therapy.",
"The primary endpoint was reduction in systolic blood pressure",
"from baseline to week 12."
), style = "Normal") %>%
body_add_break() %>%
# Table 1: Demographics
body_add_par("Table 1: Baseline Demographics and Characteristics",
style = "heading 2") %>%
body_add_flextable(table1_flex) %>%
body_add_break() %>%
# Table 2: Efficacy Results
body_add_par("Table 2: Primary Efficacy Analysis",
style = "heading 2") %>%
body_add_flextable(efficacy_flex) %>%
body_add_break() %>%
# Table 3: Pairwise Comparisons
body_add_par("Table 3: Pairwise Treatment Comparisons",
style = "heading 2") %>%
body_add_flextable(pairwise_flex) %>%
body_add_break() %>%
# Statistical methods section
body_add_par("Statistical Methods", style = "heading 2") %>%
body_add_par(paste(
"Primary efficacy analysis was conducted using a mixed-effects",
"linear regression model with treatment as fixed effect and",
"patient as random effect. Covariates included age, gender,",
"and baseline systolic blood pressure. Pairwise comparisons",
"between treatment groups were performed using estimated",
"marginal means with Tukey adjustment for multiple comparisons.",
"All analyses were performed using R statistical software",
"with significance level set at Ξ± = 0.05."
), style = "Normal")
# Save the comprehensive report
print(doc, target = "flextable_examples/comprehensive_clinical_report.docx")
# Generate safety data for demonstration
set.seed(456) # Different seed for safety data
safety_data <- clinical_data %>%
rowwise() %>%
mutate(
# Simulate adverse events
any_ae = sample(c("Yes", "No"), 1, prob = c(0.4, 0.6)),
# Serious adverse events (only if any AE)
serious_ae = ifelse(any_ae == "Yes",
sample(c("Yes", "No"), 1, prob = c(0.1, 0.9)),
"No"),
# Treatment-related AEs (varies by treatment)
tr_ae_prob = case_when(
treatment == "Placebo" ~ 0.2,
treatment == "Low Dose" ~ 0.3,
treatment == "High Dose" ~ 0.5,
treatment == "Combination" ~ 0.6
),
treatment_related_ae = ifelse(any_ae == "Yes",
sample(c("Yes", "No"), 1, prob = c(tr_ae_prob, 1 - tr_ae_prob)),
"No"),
# Discontinuations due to AEs
discontinuation = sample(c("Yes", "No"), 1, prob = c(0.05, 0.95))
) %>%
ungroup() %>%
select(-tr_ae_prob) # Remove helper column
# Create safety summary table
safety_summary <- safety_data %>%
group_by(treatment) %>%
summarise(
n = n(),
# Any adverse event
any_ae_n = sum(any_ae == "Yes"),
any_ae_pct = round(100 * any_ae_n / n(), 1),
any_ae_summary = paste0(any_ae_n, " (", any_ae_pct, "%)"),
# Serious adverse events
serious_ae_n = sum(serious_ae == "Yes"),
serious_ae_pct = round(100 * serious_ae_n / n(), 1),
serious_ae_summary = paste0(serious_ae_n, " (", serious_ae_pct, "%)"),
# Treatment-related AEs
tr_ae_n = sum(treatment_related_ae == "Yes"),
tr_ae_pct = round(100 * tr_ae_n / n(), 1),
tr_ae_summary = paste0(tr_ae_n, " (", tr_ae_pct, "%)"),
# Discontinuations
disc_n = sum(discontinuation == "Yes"),
disc_pct = round(100 * disc_n / n(), 1),
disc_summary = paste0(disc_n, " (", disc_pct, "%)")
) %>%
ungroup()
# Create professional safety table
safety_flex <- safety_summary %>%
select(treatment, any_ae_summary, serious_ae_summary,
tr_ae_summary, disc_summary) %>%
flextable() %>%
# Professional headers
set_header_labels(
treatment = "Treatment\nGroup",
any_ae_summary = "Any Adverse\nEvent\nn (%)",
serious_ae_summary = "Serious Adverse\nEvent\nn (%)",
tr_ae_summary = "Treatment-Related\nAdverse Event\nn (%)",
disc_summary = "Discontinuation\ndue to AE\nn (%)"
) %>%
# Apply regulatory theme
theme_box() %>%
# Typography
fontsize(size = 9, part = "all") %>%
font(fontname = "Calibri", part = "all") %>%
# Header styling
bold(part = "header") %>%
align(align = "center", part = "header") %>%
bg(bg = "#8b0000", part = "header") %>%
color(color = "white", part = "header") %>%
# Body formatting
align(j = 1, align = "left", part = "body") %>%
align(j = 2:5, align = "center", part = "body") %>%
# Conditional formatting for safety signals
bg(i = ~ treatment == "High Dose", bg = "#ffe6e6") %>%
bg(i = ~ treatment == "Combination", bg = "#ffcccc") %>%
# Professional borders
border_outer(border = fp_border_default(color = "#8b0000", width = 2)) %>%
# Column width optimization
width(width = c(1.5, 1.3, 1.3, 1.5, 1.3)) %>%
# Safety footnotes
add_footer_lines(c(
"AE = Adverse Event. Safety population includes all randomized patients who received β₯1 dose.",
"Treatment-related AEs determined by investigator assessment."
)) %>%
fontsize(size = 8, part = "footer") %>%
italic(part = "footer")
Table 4: Safety and tolerability summary with regulatory formatting
1. Typography Hierarchy:
2. Color Strategy:
3. Layout Guidelines:
# Template for professional table creation
create_professional_table <- function(data, title = NULL) {
data %>%
flextable() %>%
# Apply consistent theme
theme_vanilla() %>%
# Standard typography
fontsize(size = 10, part = "all") %>%
font(fontname = "Times New Roman", part = "all") %>%
# Header formatting
bold(part = "header") %>%
align(align = "center", part = "header") %>%
bg(bg = "#2c5aa0", part = "header") %>%
color(color = "white", part = "header") %>%
# Professional borders
border_outer(border = fp_border_default(color = "#2c5aa0", width = 2)) %>%
border_inner_h(border = fp_border_default(color = "#cccccc", width = 1)) %>%
# Auto-fit width
autofit() %>%
# Add title if provided
{if (!is.null(title)) add_header_lines(., title) else .} %>%
{if (!is.null(title)) bold(., part = "header") else .} %>%
{if (!is.null(title)) fontsize(., size = 12, part = "header") else .}
}