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.
<- here::here("blog/2024/04/29/")
dir_post <- nanoparquet::read_parquet(file.path(dir_post, "data", "cleaned.parquet")) |>
data 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.
<- data |>
tab_count 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.
<- tab_count |>
tt_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 asmultirow
andmulticolumn
in \(\LaTeX\).style_tt()
styles the rows as bold, italic, etc.format_tt()
formats the cells as numeric, percentage, etc.replace
argument replaces theNA
cells 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 |
+ gender)
age_c <- list(
models "(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,
<- c(
cm "type_personPassenger" = "Passenger",
"type_personPedestrian" = "Pedestrian",
"positive_alcoholTRUE" = "Positive Alcohol"
)
<- tibble(
gm 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)
)
<- modelsummary(models,
tt_reg 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_map
renames the coefficientsgof_map
renames the goodness-of-fit statistics- Since the
modelsummary
function returns atinytable
object, you can use thetinytable
functions 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
kableExtra
andgt
>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
kableExtra
andgt
. I think this is becausetinytable
is a small package that only uses baseR
Have a happy R life 🥂!