BS2004 5-minute demo, publishing-grade tables in R

Author

Eamonn Mallon

Published

January 21, 2026

# ============================================================
# 0. Setup
# ============================================================
# If needed (run once in Console):
# install.packages(c("tidyverse","broom","kableExtra"))

library(tidyverse)
library(broom)
library(kableExtra)

theme_set(theme_minimal(base_size = 12))
options(max.print = 75)

Goal

Turn model output into a clean table suitable for a report, without copy-paste.

1. Fit a simple model (built-in data)

We use the built-in mtcars dataset. Question: does weight and horsepower predict mpg?

data("mtcars")
d <- mtcars |> as_tibble() |> select(mpg, wt, hp)

m <- lm(mpg ~ wt + hp, data = d)
summary(m)

Call:
lm(formula = mpg ~ wt + hp, data = d)

Residuals:
   Min     1Q Median     3Q    Max 
-3.941 -1.600 -0.182  1.050  5.854 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept) 37.22727    1.59879  23.285  < 2e-16 ***
wt          -3.87783    0.63273  -6.129 1.12e-06 ***
hp          -0.03177    0.00903  -3.519  0.00145 ** 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 2.593 on 29 degrees of freedom
Multiple R-squared:  0.8268,    Adjusted R-squared:  0.8148 
F-statistic: 69.21 on 2 and 29 DF,  p-value: 9.109e-12

2. Tidy the model output (broom)

broom::tidy() turns the model output into a data frame.

tidy(m)
# A tibble: 3 × 5
  term        estimate std.error statistic  p.value
  <chr>          <dbl>     <dbl>     <dbl>    <dbl>
1 (Intercept)  37.2      1.60        23.3  2.57e-20
2 wt           -3.88     0.633       -6.13 1.12e- 6
3 hp           -0.0318   0.00903     -3.52 1.45e- 3

3. Make a neat table (kableExtra)

Create a table with rounding, friendly column names, and a caption.

tab <- tidy(m) |>
  mutate(across(where(is.numeric), ~ round(.x, 3))) |>
  rename(
    Term = term,
    Estimate = estimate,
    `Std error` = std.error,
    `t value` = statistic,
    `p value` = p.value
  )

kable(tab, caption = "Multiple regression: mpg as a function of weight and horsepower") |>
  kable_styling(full_width = FALSE)
Multiple regression: mpg as a function of weight and horsepower
Term Estimate Std error t value p value
(Intercept) 37.227 1.599 23.285 0.000
wt -3.878 0.633 -6.129 0.000
hp -0.032 0.009 -3.519 0.001

4. Add confidence intervals (often more useful than p values)

tab_ci <- tidy(m, conf.int = TRUE) |>
  mutate(across(where(is.numeric), ~ round(.x, 3))) |>
  rename(
    Term = term,
    Estimate = estimate,
    `Std error` = std.error,
    `t value` = statistic,
    `p value` = p.value,
    `CI low` = conf.low,
    `CI high` = conf.high
  )

kable(tab_ci, caption = "Same model with 95% confidence intervals") |>
  kable_styling(full_width = FALSE)
Same model with 95% confidence intervals
Term Estimate Std error t value p value CI low CI high
(Intercept) 37.227 1.599 23.285 0.000 33.957 40.497
wt -3.878 0.633 -6.129 0.000 -5.172 -2.584
hp -0.032 0.009 -3.519 0.001 -0.050 -0.013

5. Report model fit in a table (R squared and sample size)

fit <- glance(m) |>
  transmute(
    N = nobs,
    `R squared` = round(r.squared, 3),
    `Adj R squared` = round(adj.r.squared, 3),
    AIC = round(AIC, 1),
    BIC = round(BIC, 1),
    `Residual SE` = round(sigma, 3)
  )

kable(fit, caption = "Model fit summary") |>
  kable_styling(full_width = FALSE)
Model fit summary
N R squared Adj R squared AIC BIC Residual SE
32 0.827 0.815 156.7 162.5 2.593

6. Common report workflow

Typical pattern for any analysis: 1) fit model, 2) tidy() for coefficients, 3) glance() for model summary, 4) kable() for the final table.

Optional: export the table to Word-friendly format

If rendering to Word, the table above will appear in the document. If you need a standalone file export, a simple option is to write a CSV.

write_csv(tab_ci, "model_coefficients_table.csv")

Reproducibility

sessionInfo()
R version 4.5.0 (2025-04-11)
Platform: x86_64-apple-darwin20
Running under: macOS Sequoia 15.6.1

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.5-x86_64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.5-x86_64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: Europe/London
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
 [1] kableExtra_1.4.0 broom_1.0.8      lubridate_1.9.4  forcats_1.0.0   
 [5] stringr_1.5.2    dplyr_1.1.4      purrr_1.1.0      readr_2.1.5     
 [9] tidyr_1.3.1      tibble_3.3.0     ggplot2_4.0.0    tidyverse_2.0.0 

loaded via a namespace (and not attached):
 [1] utf8_1.2.6         generics_0.1.4     xml2_1.4.0         stringi_1.8.7     
 [5] hms_1.1.3          digest_0.6.37      magrittr_2.0.4     evaluate_1.0.4    
 [9] grid_4.5.0         timechange_0.3.0   RColorBrewer_1.1-3 fastmap_1.2.0     
[13] jsonlite_2.0.0     backports_1.5.0    viridisLite_0.4.2  scales_1.4.0      
[17] textshaping_1.0.1  cli_3.6.5          crayon_1.5.3       rlang_1.1.6       
[21] bit64_4.6.0-1      withr_3.0.2        yaml_2.3.10        parallel_4.5.0    
[25] tools_4.5.0        tzdb_0.5.0         vctrs_0.6.5        R6_2.6.1          
[29] lifecycle_1.0.4    bit_4.6.0          htmlwidgets_1.6.4  vroom_1.6.5       
[33] pkgconfig_2.0.3    pillar_1.11.1      gtable_0.3.6       glue_1.8.0        
[37] systemfonts_1.3.1  xfun_0.53          tidyselect_1.2.1   rstudioapi_0.17.1 
[41] knitr_1.50         farver_2.1.2       htmltools_0.5.8.1  rmarkdown_2.29    
[45] svglite_2.2.2      compiler_4.5.0     S7_0.2.0