Publication Tables

The Data Visualization page covers when tables beat graphs (roughly: 20 numbers or fewer, or many localized comparisons) and how to design them (sort by values, not alphabetically; shade; round a lot). This page shows how to actually build such tables in R, reproducibly.

Code
library(dplyr)
library(gapminder)

gap2007 <- gapminder %>% filter(year == 2007)

Descriptive tables with gt

gt is to tables what ggplot2 is to graphs: a grammar. You pipe a data frame in and add layers of formatting.

Code
library(gt)

gap2007 %>%
  group_by(continent) %>%
  summarise(
    Countries = n(),
    `Life Expectancy` = mean(lifeExp),
    `GDP per Capita` = mean(gdpPercap),
    Population = sum(pop)
  ) %>%
  arrange(desc(`Life Expectancy`)) %>%     # sort by values, not alphabet
  gt() %>%
  fmt_number(columns = `Life Expectancy`, decimals = 1) %>%
  fmt_currency(columns = `GDP per Capita`, decimals = 0) %>%
  fmt_number(columns = Population, suffixing = TRUE) %>%  # 1.2B not 1200000000
  data_color(columns = `Life Expectancy`, palette = "viridis") %>%
  tab_header(title = "The World in 2007, by Continent") %>%
  tab_source_note("Data: Gapminder")
The World in 2007, by Continent
continent Countries Life Expectancy GDP per Capita Population
Oceania 2 80.7 $29,810 24.55M
Europe 30 77.6 $25,054 586.10M
Americas 25 73.6 $11,003 898.87M
Asia 33 70.7 $12,473 3.81B
Africa 52 54.8 $3,089 929.54M
Data: Gapminder

Note how the design rules from the visualization page appear as code: arrange() sorts by the substantively interesting column, fmt_number(decimals = 1) rounds a lot, data_color() shades cells by value so patterns are visible.

Regression tables with modelsummary

Hand-copying coefficients into Word tables is the leading cause of avoidable errors in submitted papers. modelsummary builds the table straight from the model objects.

Code
library(modelsummary)

m1 <- lm(lifeExp ~ log(gdpPercap), data = gap2007)
m2 <- lm(lifeExp ~ log(gdpPercap) + continent, data = gap2007)

modelsummary(
  list("Bivariate" = m1, "+ Continent" = m2),
  coef_rename = c(
    "log(gdpPercap)" = "Log GDP per Capita",
    "continentAmericas" = "Americas",
    "continentAsia" = "Asia",
    "continentEurope" = "Europe",
    "continentOceania" = "Oceania"
  ),
  gof_map = c("nobs", "r.squared"),
  stars = TRUE,
  notes = "Reference category: Africa. OLS estimates, standard errors in parentheses."
)
Bivariate + Continent
+ p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001
Reference category: Africa. OLS estimates, standard errors in parentheses.
(Intercept) 4.950 20.138***
(3.858) (4.033)
Log GDP per Capita 7.203*** 4.631***
(0.442) (0.527)
Americas 11.694***
(1.655)
Asia 10.114***
(1.476)
Europe 11.268***
(1.894)
Oceania 12.929**
(4.521)
Num.Obs. 142 142
R2 0.654 0.767

modelsummary() outputs to HTML, LaTeX, and Word depending on your output format — one code chunk serves the website and the paper. Its sibling datasummary_skim() gives a one-line descriptive overview of a whole dataset:

Code
datasummary_skim(gap2007 %>% select(lifeExp, gdpPercap, pop))
Unique Missing Pct. Mean SD Min Median Max Histogram
lifeExp 142 0 67.0 12.1 39.6 71.9 82.6
gdpPercap 142 0 11680.1 12859.9 277.6 6124.4 49357.2
pop 142 0 44021219.6 147621397.9 199579.0 10517531.0 1318683096.0

Coefficient plot or regression table?

You have both tools now. A useful rule:

  • Table: when readers need to look up exact values (reviewers do), or when you report many models/specifications.
  • Coefficient plot: when readers should compare magnitudes and see which intervals cross zero — presentations, blog posts, and the main text of a paper.

Many papers now do both: plot in the text, full table in the appendix. Since both come from the same model object, this costs you two chunks, not an afternoon.

Further tools

  • gtsummary — publication-ready descriptive and regression tables with sensible medical/social-science defaults.
  • kableExtra — lighter-weight alternative to gt, good for PDF output.
  • gt::gtsave() exports any gt table to .png, .html, .rtf, or .tex.