You can achieve something like this with the datasummary
function from the modelsummary
package (disclaimer: I am the maintainer). You will find a detailed description and many examples on the packages's website.
Load the library and define a custom function to create a mean +/- sd. We use backticks because the name of our function includes spaces. Note that because of the unicode character, this may not work on Windows.
library(modelsummary)
`Mean ± SD` <- function(x) {
sprintf("%.0f ± %.0f", round(mean(x)), round(sd(x)))
}
Clean up variable labels:
dat <- mtcars
dat$am = ifelse(dat$am == 0, "automatic", "manual")
dat$vs = ifelse(dat$vs == 0, "v-shaped", "straight")
Finally, we use datasummary
to create the table. A few things to note:
- Rows go on the left side of the formula
- Columns go on the right side of the formula
- Statistics and variables joined by a
+
will be displayed one after the other.
- Statistics and variables joined by a
*
will be "nested" inside one another.
- Parentheses can be used to nest several variables/statistics
1
is a shortcut for "all".
You will find detailed instructions and examples on the package website. This table can be exported to HTML, LaTeX, Word, and more using the output
argument:
datasummary(
mpg * (Min + Max + `Mean ± SD`) +
hp * (Min + Max + `Mean ± SD`) +
wt * (Min + Max + `Mean ± SD`) ~
am * vs + 1,
data = dat)

An alternative approach is to reshape the data beforehand. With this, you don't need to specify each variable individually in the formula:
library(tidyverse)
dat <- mtcars %>%
select(mpg, wt, hp, am, vs) |>
mutate(am = ifelse(am == 0, "automatic", "manual"),
vs = ifelse(vs == 0, "v-shaped", "straight")) |>
pivot_longer(cols = c("mpg", "wt", "hp"),
names_to = "variables")
datasummary(
variables * (Min + Max + `Mean ± SD`) ~ value * am * vs + Heading(`All`) * value,
data = dat)