library(dplyr)
library(ggplot2)
library(readr)
library(knitr)
library(palmerpenguins)
library(kableExtra)
library(biol202)Creating tables in R Markdown
Tutorial learning objectives
- Learn how to produce nice tables in R Markdown for PDF output
IMPORTANT: This tutorial aims to help you produce nice tables when knitting to PDF. However, because this tutorial is presented in HTML format, some of the output does not look the same as it will in PDF format. These instances are noted.
EXTRA: there is a new package called flextable that, if you’re keen, you are welcome to check out! We don’t cover it here.
Load packages and import data
Load the following packages, including palmerpenguins and kableExtra:
We’ll use the circadian dataset, which is also used in the “comparing more than 2 means tutorial”. These data are associated with Example 15.1 in the text.
The circadian data describe melatonin production in 22 people randomly assigned to one of three light treatments.
data(circadian)Formatting a wide table nicely
Sometimes a table you want to include in a report is wide — it has many columns — which can make it awkward to read, or a poor fit for a printed page. This often happens with a table of grouped descriptive statistics, of the kind we learned to build in the “comparing more than 2 means” tutorial: each descriptive statistic (count, mean, SD, etc.) becomes its own column, and if you have several of those alongside several groups, the table can end up quite wide. Here we’ll learn a general technique for handling this: transposing the table (swapping its rows and columns) and formatting it nicely with the kbl function from the kableExtra package.
Let’s build a table of descriptive statistics for the circadian dataset’s “shift” variable, grouped by “treatment”:
circadian.stats <- circadian %>%
group_by(treatment) %>%
summarise(
Count = sum(!is.na(shift)),
Count_NA = sum(is.na(shift)),
Mean = mean(shift, na.rm = TRUE),
SD = sd(shift, na.rm = TRUE),
SEM = SD/sqrt(Count),
Low_95_CL = t.test(shift, conf.level = 0.95)$conf.int[1],
Up_95_CL = t.test(shift, conf.level = 0.95)$conf.int[2]
)
circadian.stats
#> # A tibble: 3 × 8
#> treatment Count Count_NA Mean SD SEM Low_95_CL Up_95_CL
#> <fct> <int> <int> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 control 8 0 -0.309 0.618 0.218 -0.825 0.208
#> 2 knee 7 0 -0.336 0.791 0.299 -1.07 0.396
#> 3 eyes 7 0 -1.55 0.706 0.267 -2.20 -0.898This table has 8 columns (treatment plus 7 descriptive statistics) for just 3 treatment groups. Imagine how wide it would get with more groups, or more statistics!
The fix is to “transpose” (rotate) the table using the base t function, so that its many columns become rows instead, and its few rows become columns:
circadian.stats.transposed <- t(circadian.stats)Now we’ll use the kbl function with various arguments to ensure the output looks good and includes a table caption:
NOTE: This table will look different when knitted to PDF (rather than HTML)
IMPORTANT: Notice that, unlike figure captions, which must be provided in the chunk header, the caption for a table is provided as an argument to the kbl function within the actual code!
kbl(circadian.stats.transposed, caption = "Descriptive statistics for circadian rhythm shift, by treatment",
booktabs = TRUE) %>%
kable_styling(latex_options = "hold_position")
In the preceding code we:
- use the
kblfunction and provide 3 arguments:- provide the name of the table-like object we wish to format for output
- provide a table caption
- provide an argument “booktabs = TRUE” (this provides nice formatting)
Then we include pipes (“%>%), and follow with:
- the
kable_stylingfunction with a few arguments:- “latex_options = ‘hold_position’” which forces the table to appear where the code chunk comes
For lots of great examples of how the kableExtra package can be used, see this vignette for PDF output, and this one for HTML output.
| treatment | control | knee | eyes |
| Count | 8 | 7 | 7 |
| Count_NA | 0 | 0 | 0 |
| Mean | -0.3087500 | -0.3357143 | -1.5514286 |
| SD | 0.6175629 | 0.7908193 | 0.7063151 |
| SEM | 0.2183415 | 0.2989016 | 0.2669620 |
| Low_95_CL | -0.8250455 | -1.0671002 | -2.2046610 |
| Up_95_CL | 0.2075455 | 0.3956716 | -0.8981961 |
Notice one trade-off of transposing: the treatment, Count, and Count_NA rows are whole numbers, but the numeric rows below them (Mean, SD, etc.) now show many decimal places, and there’s no way to tell kbl to round only some of the rows — transposing converts the whole table to plain text first, so kbl’s digits argument no longer has any numeric columns to act on. If you need precise control over decimal places, either round the values yourself inside summarise before transposing, or use the non-transposed approach in the next section, which keeps that control.
TIP: If you have many groups and/or many descriptive statistics, the transposed table can still end up wide. In that case, add the “scale_down” option to kable_styling, which shrinks the whole table (including the font) to fit the page width:
kbl(circadian.stats.transposed, caption = "Descriptive statistics for circadian rhythm shift, by treatment",
booktabs = TRUE) %>%
kable_styling(latex_options = c("scale_down", "hold_position"))
For a table with a very large number of columns, this can make the font too small to read comfortably — in that case, consider splitting the table into two, or reporting only the most essential columns.
A nicely formatted table of descriptive statistics
Here is the code (which you’ve already learned) to create a good table of descriptive statistics for a numeric response variable grouped by categories in a categorical explanatory variable.
We’ll use the “penguins” dataset again, and calculate descriptive statistics for bill lengths of male penguins, grouped by species:
penguins.stats <- penguins %>%
filter(sex == "male") %>%
group_by(species) %>%
summarise(
Count = sum(!is.na(bill_length_mm)),
Count_NA = sum(is.na(bill_length_mm)),
Mean = mean(bill_length_mm, na.rm = TRUE),
SD = sd(bill_length_mm, na.rm = TRUE),
SEM = SD/sqrt(Count),
Low_95_CL = t.test(bill_length_mm, conf.level = 0.95)$conf.int[1],
Up_95_CL = t.test(bill_length_mm, conf.level = 0.95)$conf.int[2]
)Here’s what the raw table looks like:
penguins.stats
#> # A tibble: 3 × 8
#> species Count Count_NA Mean SD SEM Low_95_CL Up_95_CL
#> <fct> <int> <int> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 Adelie 73 0 40.4 2.28 0.267 39.9 40.9
#> 2 Chinstrap 34 0 51.1 1.56 0.268 50.5 51.6
#> 3 Gentoo 61 0 49.5 2.72 0.348 48.8 50.2Now let’s format it for PDF output:
kbl(penguins.stats, caption = "Descriptive statistics for bill length among male penguins.",
booktabs = TRUE, digits = c(0, 0, 0, 2, 3, 3, 3, 3)) %>%
kable_styling(latex_options = c("scale_down", "hold_position"), position = "center")| species | Count | Count_NA | Mean | SD | SEM | Low_95_CL | Up_95_CL |
|---|---|---|---|---|---|---|---|
| Adelie | 73 | 0 | 40.39 | 2.277 | 0.267 | 39.859 | 40.922 |
| Chinstrap | 34 | 0 | 51.09 | 1.565 | 0.268 | 50.548 | 51.640 |
| Gentoo | 61 | 0 | 49.47 | 2.721 | 0.348 | 48.777 | 50.171 |
The key difference from the transposed table in the previous section is that here we specify the number of decimal places we want each descriptive statistic to be reported to, since the table is not transposed and so still has genuine numeric columns for kbl to round.
Specifically, the “digits” argument accepts a vector of numbers, whose length is equal to the number of columns being reported in the table, and these numbers indicate the number of decimal places to include for that specific variable.
At this point it would be a good idea to revisit the Biology department’s guidelines for reporting descriptive statistics.
For example, we can see that the first three numbers in the “digits” argument are zeroes, and these correspond to the first three columns of the table: “species”, “Count”, “Count_NA”. These are columns whose values don’t require decimal places.
For the “Mean” column we report the values to 1 more decimal place than was used in the measurement (which you find out by looking at the raw data in the “penguins” object), so here, 2 decimal places.
For measures of spread (like the standard deviation) and measures of uncertainty (including SEM and confidence limits), report the numbers to 2 more decimal places than was used in the measurement, so here, 3 decimal places.
You now know how to produce nicely formatted tables in your knitted PDF output!
IMPORTANT: Be sure to try knitting to PDF as soon as you’ve used any of the kable or kableExtra package functions in a code chunk, as this will help you trouble-shoot if you encounter problems.