Creating tables in R Markdown

Tutorial learning objectives

Note

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:

library(dplyr)
library(ggplot2)
library(readr)
library(knitr)
library(palmerpenguins)
library(kableExtra)
library(biol202)

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.898

This 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

NOTE: This table will look different when knitted to PDF (rather than HTML)

Note

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 kbl function 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_styling function 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.

Descriptive statistics for circadian rhythm shift, by treatment
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.

Note

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.2

Now 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")
Descriptive statistics for bill length among male penguins.
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!

Note

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.