library(dplyr)
library(tidyr)
library(tinytable)kableExtra or gt? No, tinytable!
In recent years, kableExtra and gt have been popular packages for creating tables in R. I have used kableExtra for tables in a (LaTeX compiled) paper, and gt for tables in a (HTML compiled) slides, and I taught it in my R workshop. However, I found a new package called tinytable that is more flexible and easier to use than the other two packages. In this post, I will introduce how to use tinytable to create tables for LaTeX documents as an updated section of my workshop.
What is tinytable?
The tinytable is a small (zero-dependencies, only uses baseR!) but powerful package that provides a simple and flexible way to create tables in R. This package is developed by Vincent Arel-Bundock, the maintainer of the modelsummary package, and is designed to work seamlessly with modelsummary.
An Example of Creating Tables with tinytable
I use the Madrid traffic accident dataset, which is from my workshop. You can find the downloading and cleaning codes in my blog repository.
dir_post <- here::here("blog/2024/04/29/")
data <- nanoparquet::read_parquet(file.path(dir_post, "data", "cleaned.parquet")) |>
mutate(is_died = injury8 == "Died within 24 hours",
is_hospitalized = injury8 %in% c("Hospitalization after 24 hours",
"Hospitalization within 24 hours",
"Died within 24 hours"))I will create a table that shows the number of accidents.
tab_count <- data |>
filter(!is.na(weather), !is.na(gender)) |>
summarize(n = n(), .by = c(year, gender, weather)) |>
pivot_wider(names_from = c(gender, year), values_from = n) |>
arrange(weather) |>
select(weather, starts_with("Men"), starts_with("Women"))
tab_count# A tibble: 6 × 11
weather Men_2019 Men_2020 Men_2021 Men_2022 Men_2023 Women_2019 Women_2020
<fct> <int> <int> <int> <int> <int> <int> <int>
1 sunny 24399 14969 19208 20679 22451 11971 6958
2 cloud 1159 1190 1325 2082 2011 555 554
3 soft rain 2126 1198 1281 1930 1224 1068 542
4 hard rain 386 202 386 527 317 222 96
5 snow 2 2 124 5 NA NA NA
6 hail 11 5 6 4 3 3 3
# ℹ 3 more variables: Women_2021 <int>, Women_2022 <int>, Women_2023 <int>
You can create a table by using the tt() function.
tt_count <- tab_count |>
`colnames<-`(c("", rep(2019:2023, 2))) |>
tt() |>
group_tt(i = list("Good Weather" = 1, "Bad Weather" = 3),
j = list("Men" = 2:6, "Women" = 7:11)) |>
style_tt(i = c(1, 4), bold = TRUE) |>
format_tt(replace = "-")
tt_count |>
theme_tt("tabular") |>
save_tt(file.path(dir_post, "tex", "table_count.tex"),
overwrite = TRUE)To format the table as an table in a paper,
group_tt()groups the rows and columns asmultirowandmulticolumnin \(\LaTeX\).style_tt()styles the rows as bold, italic, etc.format_tt()formats the cells as numeric, percentage, etc.replaceargument replaces theNAcells with the specified character.- You cannot change the column names in the
tt()function, so you need to usecolnames<-(), related to the issue #194 - To save the table as a plain table (without
\begin{table}and\end{table}), usetheme_tt("tabular").
A trick to insert a LaTeX table in a Quarto document
In the previous section, I inserted a \(\LaTeX\) table by SVG format. Actually, I converted a tinytable object to a svg figure by the following two steps:
1. Save the tinytable object as a PDF file.
tt_count |>
save_tt(file.path(dir_post, "img", "table_count.pdf"),
overwrite = TRUE)tinytable::save_tt() is a powerful funtion that can save the tinytable object as a PDF file. If the file extension is .pdf, the function compile it as a single PDF file by tinytex package.
2. Convert the PDF file to a SVG file.
```{bash}
#!/bin/bash
pdf2svg img/table_count.pdf img/table_count.svg
```pdf2svg is a command line tool that converts a PDF file to a SVG file. And importantly, knitr can run the bash script in a code chunk.
Modelsummary
library(modelsummary)
library(fixest)Since tinytable is designed to work seamlessly with modelsummary, you can create a table of regression results by using modelsummary and tinytable.
setFixest_fml(..ctrl = ~ type_person + positive_alcohol + positive_drug |
age_c + gender)
models <- list(
"(1)" = feglm(xpd(is_hospitalized ~ ..ctrl),
family = binomial(logit), data = data),
"(2)" = feglm(xpd(is_hospitalized ~ ..ctrl + type_vehicle),
family = binomial(logit), data = data),
"(3)" = feglm(xpd(is_hospitalized ~ ..ctrl + type_vehicle + weather),
family = binomial(logit), data = data),
"(4)" = feglm(xpd(is_died ~ ..ctrl),
family = binomial(logit), data = data),
"(5)" = feglm(xpd(is_died ~ ..ctrl + type_vehicle),
family = binomial(logit), data = data),
"(6)" = feglm(xpd(is_died ~ ..ctrl + type_vehicle + weather),
family = binomial(logit), data = data)
)
modelsummary(models)| (1) | (2) | (3) | (4) | (5) | (6) | |
|---|---|---|---|---|---|---|
| type_personPassenger | 0.026 | 0.501 | 0.475 | -1.697 | -1.445 | -1.440 |
| (0.100) | (0.069) | (0.068) | (0.616) | (0.631) | (0.631) | |
| type_personPedestrian | 2.117 | 2.381 | 2.298 | 2.190 | 2.326 | 2.327 |
| (0.114) | (0.061) | (0.058) | (0.260) | (0.248) | (0.246) | |
| positive_alcoholTRUE | 0.007 | 0.355 | 0.381 | -13.685 | -13.421 | -13.460 |
| (0.072) | (0.081) | (0.081) | (0.059) | (0.055) | (0.054) | |
| Num.Obs. | 197477 | 197369 | 176133 | 118462 | 116344 | 112659 |
| R2 | 0.056 | 0.170 | 0.163 | 0.103 | 0.142 | 0.145 |
| R2 Adj. | 0.056 | 0.169 | 0.162 | 0.087 | 0.116 | 0.116 |
| R2 Within | 0.048 | 0.055 | 0.053 | 0.070 | 0.070 | 0.071 |
| R2 Within Adj. | 0.048 | 0.055 | 0.052 | 0.067 | 0.067 | 0.068 |
| AIC | 82581.4 | 72694.8 | 70519.4 | 2029.6 | 1960.3 | 1940.0 |
| BIC | 82795.5 | 73204.5 | 71063.6 | 2213.6 | 2240.5 | 2248.2 |
| RMSE | 0.23 | 0.22 | 0.24 | 0.03 | 0.04 | 0.04 |
| Std.Errors | by: age_c | by: age_c | by: age_c | by: age_c | by: age_c | by: age_c |
| FE: age_c | X | X | X | X | X | X |
| FE: gender | X | X | X | X | X | X |
| FE: type_vehicle | X | X | X | X | ||
| FE: weather | X | X |
To polish the table for a paper,
cm <- c(
"type_personPassenger" = "Passenger",
"type_personPedestrian" = "Pedestrian",
"positive_alcoholTRUE" = "Positive Alcohol"
)
gm <- tibble(
raw = c("nobs", "FE: age_c", "FE: gender",
"FE: type_vehicle", "FE: weather"),
clean = c("Observations", "FE: Age Group", "FE: Gender",
"FE: Type of Vehicle", "FE: Weather"),
fmt = c(0, 0, 0, 0, 0)
)
tt_reg <- modelsummary(models,
stars = c("+" = .1, "*" = .05, "**" = .01),
coef_map = cm,
gof_map = gm) |>
group_tt(j = list("Hospitalization" = 2:4,
"Died within 24 hours" = 5:7))
tt_reg |>
theme_tt("tabular") |>
save_tt(file.path(dir_post, "tex", "table_reg.tex"),
overwrite = TRUE)coef_maprenames the coefficientsgof_maprenames the goodness-of-fit statistics- Since the
modelsummaryfunction returns atinytableobject, you can use thetinytablefunctions to polish the table
Last Comments
In this post, I introduced the tinytable package, which is a gamechanger of creating tables in R. In the wrangling process, I firmly believe that tinytable is more flexible and powerful than kableExtra and gt for the following reasons:
- Covers almost all the features of
kableExtraandgt>Extras. You can usemultirow&multicolumn, highlight cells, format cells, and mathematical expressions - Allows to export tables not only in HTML and LaTeX but also in PDF (with
tinytex) and Typst - Compiles faster than
kableExtraandgt. I think this is becausetinytableis a small package that only uses baseR
Have a happy R life 🥂!