13  Split, apply, and combine

13.1 Learning objectives

This session’s overall learning outcome is to:

  1. Explain the split-apply-combine technique and use a combination of functions to use it, while outputting the results into report-friendly tables.

Specific objectives are to:

  1. Describe the “split-apply-combine” method of analyses, then use group_by() together with summarise() to calculate summary statistics, by categorical variables.
  2. Create tables of results in a document using knitr::kable().

13.2 📖 Reading task: Split-apply-combine to summarize data

Time: ~5 minutes

Summarising or applying simple (or complex) statistics to data is a key component of any analysis. Simple summaries or statistics can be done either on all of the data or on groups of it. There are many data analysis tasks that can be approached using the split-apply-combine method, which involves splitting the data into groups, applying some analysis to each group, and then combining the results together.

As we briefly showed in Table 11.1, dplyr has some wrangling verbs that include one to summarise() the data. If you want to do a split-apply-combine analysis to, for example, find the max height by a group, you would use the functions group_by() and then summarise(). Using group_by() splits the data up and summarise() applies an analysis, then immediately combines it back together.

The first position argument to group_by() is, as usual, the data. The next arguments are the columns that contain the values you want to group by. These columns must contain categorical data (e.g. education). group_by() tells R to compute the next operations on the data within each grouping, rather than on all the data.

As with the other functions, summarise() takes the data as the first position argument. The next arguments work similar to the arguments in mutate() with one difference: the output must create a single value (e.g. a mean). Like mutate(), you can add multiple “summaries” by adding new columns separated by ,. You would use summarise() to derive basic descriptive statistics of a certain variable, including min(), max(), mean(), median(), or sd() (standard deviation).

The group_by() function doesn’t do anything by itself and only is used in combination with a summarise(), mutate(), arrange(), or other function. However, the summarise() function can be used on its own.

Sticky/hat up!

When you’re ready to continue, place the sticky/paper hat on your computer to indicate this to the instructor 👒 🎩

13.3 Calculating summary statistics

Let’s try using summarise() without using group_by(). Go to the bottom of your docs/learning.qmd Quarto file and create a new header called ## Calculating summary statistics. Then, create a new code chunk with Ctrl-Alt-I or with the Palette (Ctrl-Shift-P, then type “new chunk”). Let’s start with calculating the maximum value of the BMI variable.

docs/learning.qmd
tidied_post_meal_data |>
  summarise(max_bmi = max(BMI))
# A tibble: 1 × 1
  max_bmi
    <dbl>
1    33.7

We should get the max value of BMI in our data.

Verbally explain this next paragraph about na.rm, but you don’t need to code it.

Sometimes, if there are missing values in your data, you might get NA as a result, which means “missing”. In R, NA values “propagate”, meaning that if there is one value missing, then the max or mean value will also be missing. When this happens, we need to tell max() to exclude any NA values from the calculation by using the argument na.rm = TRUE.

To calculate another summary statistic, you would add another summary column using ,:

docs/learning.qmd
tidied_post_meal_data |>
  summarise(
    max_bmi = max(BMI),
    min_bmi = min(BMI)
  )
# A tibble: 1 × 2
  max_bmi min_bmi
    <dbl>   <dbl>
1    33.7    20.2

This is the basics of using summarise() to calculate summary statistics. The real power of this function comes when you combine it with group_by().

13.4 Summary statistics by a group

While the summarise() function is useful enough on its own, it really shines when combined with group_by(). Create a new header called ## Summarise with group by and below that add a new code chunk with Ctrl-Alt-I or with the Palette (Ctrl-Shift-P, then type “new chunk”). Let’s calculate the mean age and BMI between the two groups.

docs/learning.qmd
tidied_post_meal_data |>
  group_by(Group) |>
  summarise(
    mean_age = mean(Age),
    mean_bmi = mean(BMI)
  )
# A tibble: 2 × 3
  Group                 mean_age mean_bmi
  <chr>                    <dbl>    <dbl>
1 Control                   40.2     25.8
2 First-degree relative     47.9     25.9
Note

You’ll likely get a message informing you that it is regrouping output. This is a notification and can be ignored. If you don’t want the message displayed, you can add options(dplyr.summarise.inform = FALSE) to the top of your script and run it.

Run this code chunk with Ctrl-Enter to see the output. Cool! Since we don’t need the dataset grouped anymore, it’s good practice to end the grouping with ungroup().

docs/learning.qmd
tidied_post_meal_data |>
  group_by(Group) |>
  summarise(
    mean_age = mean(Age),
    mean_bmi = mean(BMI)
  ) |>
  ungroup()
# A tibble: 2 × 3
  Group                 mean_age mean_bmi
  <chr>                    <dbl>    <dbl>
1 Control                   40.2     25.8
2 First-degree relative     47.9     25.9

Run this code chunk with Ctrl-Enter to see the output. It isn’t different from before except it is no longer grouped. Before continuing, let’s run styler using the Palette (Ctrl-Shift-P, then type “style file”), render the document with Ctrl-Shift-K or with the Palette (Ctrl-Shift-P, then type “render”), then add and commit changes to the Git history using Ctrl-Alt-M or with the Palette (Ctrl-Shift-P, then type “commit”) before finally pushing to GitHub.

13.5 Creating tables of results

A useful combination of doing a split-apply-combine analysis for reporting or showing it as a table for easy viewing and sharing with collaborators. The knitr package has a function called kable() that can be used to create tables from R objects. So let’s test out making one!

We’ll work from the code we just created from above, but we’ll add knitr::kable() to the end of the pipe.

docs/learning.qmd
tidied_post_meal_data |>
  group_by(Group) |>
  summarise(
    mean_age = mean(Age),
    mean_bmi = mean(BMI)
  ) |>
  ungroup() |>
  knitr::kable()
Group mean_age mean_bmi
Control 40.25000 25.75000
First-degree relative 47.86667 25.87333

Then run the code with Ctrl-Enter to see what it looks like. In RStudio, the output doesn’t look too good. But let’s render it with Ctrl-Shift-K or with the Palette (Ctrl-Shift-P, then type “render”) and view the output in the HTML document. It looks much better! But could use some improvements. For instance, the columns names could be more “human friendly”, the table could use a caption, and the numbers could be rounded.

Let’s start with the rounding. We can use the round() function to round the values to a certain number of digits. We’ll add this to the individual new summarise() columns we are making. Let’s round to 1 digit for now.

docs/learning.qmd
tidied_post_meal_data |>
  group_by(Group) |>
  summarise(
    mean_age = round(mean(Age), 1),
    mean_bmi = round(mean(BMI), 1)
  ) |>
  ungroup() |>
  knitr::kable()
Group mean_age mean_bmi
Control 40.2 25.8
First-degree relative 47.9 25.9

Run this code chunk with Ctrl-Enter to see the output. Next, let’s fix the column names. In both mutate() and summarise(), if you want to create columns that have specific characters in the name that R normally doesn’t allow or doesn’t like, you can "" around the name. So in this case, we can put "" around the column names to make them more human readable.

docs/learning.qmd
tidied_post_meal_data |>
  group_by(Group) |>
  summarise(
    "Mean Age (yrs)" = round(mean(Age), 1),
    "Mean BMI (kg/m^2^)" = round(mean(BMI), 1)
  ) |>
  ungroup() |>
  knitr::kable()
Group Mean Age (yrs) Mean BMI (kg/m2)
Control 40.2 25.8
First-degree relative 47.9 25.9

Run this code chunk with Ctrl-Enter to see the output. To add the caption, there are two ways to do it: as an argument to kable() or using the Quarto code chunk options #| tbl-cap: that works similar to the one for figures. We can also reference the table too if we include a label with #| label: tbl-.

docs/learning.qmd
```{r}
#| label: tbl-mean-age-bmi
#| tbl-cap: "Mean values of Age and BMI for each group."
tidied_post_meal_data |>
  group_by(Group) |>
  summarise(
    "Mean Age (yrs)" = round(mean(Age), 1),
    "Mean BMI (kg/m^2^)" = round(mean(BMI), 1)
  ) |>
  ungroup() |>
  knitr::kable()
```
Table 13.1: Mean values of Age and BMI for each group.
Group Mean Age (yrs) Mean BMI (kg/m2)
Control 40.2 25.8
First-degree relative 47.9 25.9

Then, when we use @tbl-mean-age-bmi in the text, it will reference the table like this: Table 13.1, cool eh! Let’s run styler using the Palette (Ctrl-Shift-P, then type “style file”) and then render the document with Ctrl-Shift-K or with the Palette (Ctrl-Shift-P, then type “render”) to see what it looks like!

Before continuing, add and commit these changes into the Git history using Ctrl-Alt-M or with the Palette (Ctrl-Shift-P, then type “commit”), followed by pushing to GitHub.

13.6 🧑‍💻 Exercise: Creating a table using R code

Time: ~15 minutes.

Let’s create a table with more columns and rows, to practice using split-apply-combine with knitr::kable(). In the docs/learning.qmd file, create a new header called ## Bigger table. Then copy the code template below and paste it below the new header. Then, using this template, complete each of the items below.

docs/learning.qmd
```{r}
#| label: tbl-___
#| tbl-cap: "___"
post_meal_data |>
  # Task 2.
  ___(BMI < 30) |>
  # Task 3.
  ___(
    ___ = if_else(Age < 40, "Young", "Old"),
    Group = if_else(Group == "CTR", "Control", "First-degree relative")
  ) |>
  # Task 4.
  ___(___, ____) |>
  summarise(
    "___" = ___(___(auc_cp), ___),
    "___" = ___(___(auc_pg), ___),
    "___" = ___(___(auc_ins), ___)
  ) |>
  # Task 5.
  ___(
    "___" = age_group,
    "___" = Group
  ) |>
  # Task 6.
  ___() |>
  ___::___()
```

See @tbl-___, very nice table! :D
  1. In the code chunk options, set the #| label: to be tbl-summary-table and the #| fig-cap: to be "Median values of AUC measurements by age group and family history group.". Or something similar, it doesn’t have to be exact.

  2. Use filter() to only include rows where BMI is less than 30.

  3. Use mutate() to create a new column called age_group using if_else(). Since you already created this code as well as modifying the Group, we’ve provided it in the template for you, so you don’t need to add it again.

  4. Calculate the median() values of auc_cp, auc_pg, and auc_ins by Group and age_group using group_by() and summarise(). Use the round() function to round the values to 1 digit. Remember, the median() function is used first, inside the round() function (meaning, the round() function is on the outside). Name the new columns "AUC c-Peptide", "AUC Glucose", and "AUC Insulin" (note the "" around the names).

  5. rename() the columns to be more human readable. Rename age_group to "Age group" and Group to "Family history". Remember, renaming follows the new = old format.

  6. Next, ungroup() the data before sending it to knitr::kable() to create the table.

  7. In the text below the code chunk, reference the table with @tbl-summary-table.

  8. Run styler on the document with the Palette (Ctrl-Shift-P, then type “style file”).

  9. Render the document to HTML with Ctrl-Shift-K or with the Palette (Ctrl-Shift-P, then type “render”) to see what the table looks like.

  10. End the exercise by adding and committing to the Git history with Ctrl-Alt-M or with the Palette (Ctrl-Shift-P, then type “commit”), then push to GitHub.

Click for the solution. Only click if you are struggling or are out of time.
# Code might look like this:
#| label: tbl-summary-table
#| tbl-cap: "Median values of AUC measurements by age group and family history group."
post_meal_data |>
  # Task 2.
  filter(BMI < 30) |>
  # Task 3.
  mutate(
    age_group = if_else(Age < 40, "Young", "Old"),
    Group = if_else(Group == "CTR", "Control", "First-degree relative")
  ) |>
  # Task 4.
  group_by(Group, age_group) |>
  summarise(
    "AUC c-Peptide" = round(median(auc_cp), 1),
    "AUC Glucose" = round(median(auc_pg), 1),
    "AUC Insulin" = round(median(auc_ins), 1)
  ) |>
  # Task 5.
  rename(
    "Age group" = age_group,
    "Family history" = Group
  ) |>
  # Task 6.
  ungroup() |>
  knitr::kable()

# See @tbl-summary-table, very nice table! :D
Sticky/hat up!

When you’re ready to continue, place the sticky/paper hat on your computer to indicate this to the instructor 👒 🎩

13.7 Summary

  • Use the “split-apply-combine” technique with the functions summarise() and group_by() from the dplyr package to calculate summaries of your data.
  • Make tables by using knitr::kable()
  • Use the #| label: fig-LABELNAME and #| fig-cap: "CAPTION" options to label and caption tables, and reference the table in the text with @tbl-LABELNAME.

This lists some, but not all, of the code used in the section. Some code is incorporated into Markdown content, so is harder to automatically list here in a code chunk. The code below also includes the code from the exercises.

tidied_post_meal_data |>
  summarise(max_bmi = max(BMI))
tidied_post_meal_data |>
  summarise(
    max_bmi = max(BMI),
    min_bmi = min(BMI)
  )
tidied_post_meal_data |>
  group_by(Group) |>
  summarise(
    mean_age = mean(Age),
    mean_bmi = mean(BMI)
  )
tidied_post_meal_data |>
  group_by(Group) |>
  summarise(
    mean_age = mean(Age),
    mean_bmi = mean(BMI)
  ) |>
  ungroup()
tidied_post_meal_data |>
  group_by(Group) |>
  summarise(
    mean_age = mean(Age),
    mean_bmi = mean(BMI)
  ) |>
  ungroup() |>
  knitr::kable()
tidied_post_meal_data |>
  group_by(Group) |>
  summarise(
    mean_age = round(mean(Age), 1),
    mean_bmi = round(mean(BMI), 1)
  ) |>
  ungroup() |>
  knitr::kable()
tidied_post_meal_data |>
  group_by(Group) |>
  summarise(
    "Mean Age (yrs)" = round(mean(Age), 1),
    "Mean BMI (kg/m^2^)" = round(mean(BMI), 1)
  ) |>
  ungroup() |>
  knitr::kable()
tidied_post_meal_data |>
  group_by(Group) |>
  summarise(
    "Mean Age (yrs)" = round(mean(Age), 1),
    "Mean BMI (kg/m^2^)" = round(mean(BMI), 1)
  ) |>
  ungroup() |>
  knitr::kable()
# Code might look like this:
#| label: tbl-summary-table
#| tbl-cap: "Median values of AUC measurements by age group and family history group."
post_meal_data |>
  # Task 2.
  filter(BMI < 30) |>
  # Task 3.
  mutate(
    age_group = if_else(Age < 40, "Young", "Old"),
    Group = if_else(Group == "CTR", "Control", "First-degree relative")
  ) |>
  # Task 4.
  group_by(Group, age_group) |>
  summarise(
    "AUC c-Peptide" = round(median(auc_cp), 1),
    "AUC Glucose" = round(median(auc_pg), 1),
    "AUC Insulin" = round(median(auc_ins), 1)
  ) |>
  # Task 5.
  rename(
    "Age group" = age_group,
    "Family history" = Group
  ) |>
  # Task 6.
  ungroup() |>
  knitr::kable()

# See @tbl-summary-table, very nice table! :D