Chapter 15 Grouping and summarising data

This chapter will explore the summarise and group_by verbs. These two verbs are considered together because they are often used together, and their usage is quite distinct from the other dplyr verbs we’ve encountered:

  • The group_by function adds information into a data object (e.g. a data frame or tibble), which makes subsequent calculations happen on a group-specific basis.

  • The summarise function is a data reduction function calculates single-number summaries of one or more variables, respecting the group structure if present.

15.0.1 Getting ready

We can start a new script by loading and attaching the dplyr package:

library("dplyr")

We’re going to use both the storms and iris data sets in the nasaweather and datasets packages, respectively. The datasets package ships is automatically loaded and attached at start up, so we need to make the nasaweather package available:

library("nasaweather")

Finally, let’s convert both data sets to a tibble so they print to the Console cleanly:

storms_tbl <- tbl_df(storms)
iris_tbl   <- tbl_df(iris)

15.1 Summarising variables with summarise

We use summarise to calculate summaries of variables in an object containing our data. We do this kind of calculation all the time when analysing data. In terms of pseudo-code, usage of summarise looks like this:

summarise(data_set, <expression1>, <expression2>, ...)

The first argument, data_set, must be the name of the data frame or tibble containing our data. We then include a series of one or more additional arguments, each of these is a valid R expression involving at least one variable in data_set. These are given by the pseudo-code placeholder <expression1>, <expression2>, ..., where <expression1> and <expression2> represent the first two expressions, and the ... is acting as placeholder for the remaining expressions. These expressions can be any calculation involving R functions. The only constraint is that they must generate a single value when evaluated.

That last sentence was important. It’s easy to use summarise if can we remember one thing: summarise is designed to work with functions that take a vector as their input and return a single value (i.e. a vector of length one). Any calculation that does this can be used with summarise.

The summarise verb is best understood by example. The R function called mean takes a vector of numbers (several numbers) and calculates their arithmetic mean (one number). We can use mean with summarise to calculate the mean of the Petal.Length and Petal.Width variables in iris_tbl like this:

summarise(iris_tbl, mean(Petal.Length), mean(Petal.Width))
## # A tibble: 1 x 2
##   `mean(Petal.Length)` `mean(Petal.Width)`
##                  <dbl>               <dbl>
## 1                 3.76                1.20

Notice what kind of object summarise returns: it’s a tibble with only one row and two columns. There are two columns because we calculated two means, and there is one row containing these means. Simple. There are a few other things to note about how summarise works:

  • As with all dplyr functions, the expression that performs the required summary calculation is not surrounded by quotes because it is an expression that it “does some calculations”.

  • The order of the expression in the resulting tibble is the same as the order in which they were used as arguments.

  • Even though the dimensions of the output object have changed, summarise returns the same kind of data object as its input. It returns a data frame if our data was originally in a data frame, or a tibble if it was in a tibble.

Notice that summarise used the expressions to name the variables. Variable names like mean(Petal.Length) and mean(Petal.Width) are not very helpful. They’re quite long for one. More problematically, they contain special reserved characters like (, which makes referring to columns in the resulting tibble more difficult than it needs to be:

# make a summary tibble an assign it a name
iris_means <- summarise(iris_tbl, mean(Petal.Length), mean(Petal.Width))
# extract the mean petal length
iris_means$`mean(Petal.Length)`
## [1] 3.758

We have to place ‘back ticks’ (as above) or ordinary quotes around the name to extract the new column when it includes special characters.

It’s better to avoid using the default names. The summarise function can name the new variables at the same time as they are created. Predictably, we do this by naming the arguments using =, placing the name we require on the left hand side. For example:

summarise(iris_tbl, Mean_PL = mean(Petal.Length), Mean_PW = mean(Petal.Width))
## # A tibble: 1 x 2
##   Mean_PL Mean_PW
##     <dbl>   <dbl>
## 1    3.76    1.20

There are very many base R functions that can be used with summarise. A few useful ones for calculating summaries of numeric variables are:

  • min and max calculate the minimum and maximum values of a vector.

  • mean and median calculate the mean and median of a numeric vector.

  • sd and var calculate the standard deviation and variance of a numeric vector.

We can combine more than one function in a summarise expression as long as it returns a single number. This means we can do arbitrarily complicated calculations in a single step. For example, if we need to know the ratio of the mean and median values of petal length and petal width in iris_tbl, we use:

summarise(iris_tbl,
          Mn_Md_PL = mean(Petal.Length) / median(Petal.Length),
          Mn_Md_PW = mean(Petal.Width)  / median(Petal.Width))
## # A tibble: 1 x 2
##   Mn_Md_PL Mn_Md_PW
##      <dbl>    <dbl>
## 1    0.864    0.923

Notice that we placed each argument on a separate line in this example. This is just a style issue—we don’t have to do this, but since R doesn’t care about white space, we can use new lines and spaces to keep everything a bit more more human-readable. It pays to organise summarise calculations like this as they become longer. It allows us to see the logic of the calculations more easily, and helps us spot potential errors when they occur.

15.1.1 Helper functions

There are a small number dplyr helper functions that can be used with summarise. These generally provide summaries that aren’t available directly using base R functions. For example, the n_distinct function is used to calculate the number of distinct values in a variable:

summarise(iris_tbl, Num.PL.Vals = n_distinct(Petal.Length))
## # A tibble: 1 x 1
##   Num.PL.Vals
##         <int>
## 1          43

This tells us that there are 43 unique values of Petal.Length. We won’t explore any others here. The handy cheat sheat is worth looking over to see what additional options are available.

15.2 Grouped operations using group_by

Performing a calculation with one or more variables over the whole data set is useful, but very often we also need to carry out an operation on different subsets of our data. For example, it’s probably more useful to know how the mean sepal and petal traits vary among the different species in the iris_tbl data set, rather than knowing the overall mean of these traits. We could calculate separate means by using filter to create different subsets of iris_tbl, and then using summary on each of these to calculate the relevant means. This would get the job done, but it’s not very efficient and very soon becomes tiresome when we have to work with many groups.

The group_by function provides a more elegant solution to this kind of problem. It doesn’t do all that much on its own though. All the group_by function does is add a bit of grouping information to a tibble or data frame. In effect, it defines subsets of data on the basis of one or more grouping variables. The magic happens when the grouped object is used with a dplyr verb like summarise or mutate. Once a data frame or tibble has been tagged with grouping information, operations that involve these (and other) verbs are carried out on separate subsets of the data, where the subsets correspond to the different values of the grouping variable(s).

Basic usage of group_by looks like this:

group_by(data_set, vname1, vname2, ...)

The first argument, data_set (“data object”), must be the name of the object containing our data. We then have to include one or more additional arguments, where each of these is the name of a variable in data_set. I have expressed this as vname1, vname2, ..., where vname1 and vname2 are names of the first two variables, and the ... is acting as placeholder for the remaining variables.

As usual, it’s much easier to understand how group_by works once we’ve seen it in action. We’ll illustrate group_by by using it alongside summarise with the storms_tbl data set. We’re aiming to calculate the mean wind speed for every type of storm. The first step is to use group_by to add grouping information to storms_tbl:

group_by(storms_tbl, type)
## # A tibble: 2,747 x 11
## # Groups:   type [4]
##    name    year month   day  hour   lat  long pressure  wind type   seasday
##    <chr>  <int> <int> <int> <int> <dbl> <dbl>    <int> <int> <chr>    <int>
##  1 Allis…  1995     6     3     0  17.4 -84.3     1005    30 Tropi…       3
##  2 Allis…  1995     6     3     6  18.3 -84.9     1004    30 Tropi…       3
##  3 Allis…  1995     6     3    12  19.3 -85.7     1003    35 Tropi…       3
##  4 Allis…  1995     6     3    18  20.6 -85.8     1001    40 Tropi…       3
##  5 Allis…  1995     6     4     0  22   -86        997    50 Tropi…       4
##  6 Allis…  1995     6     4     6  23.3 -86.3      995    60 Tropi…       4
##  7 Allis…  1995     6     4    12  24.7 -86.2      987    65 Hurri…       4
##  8 Allis…  1995     6     4    18  26.2 -86.2      988    65 Hurri…       4
##  9 Allis…  1995     6     5     0  27.6 -86.1      988    65 Hurri…       5
## 10 Allis…  1995     6     5     6  28.5 -85.6      990    60 Tropi…       5
## # … with 2,737 more rows

Compare this to the output produced when we print the original storms_tbl data set:

storms_tbl
## # A tibble: 2,747 x 11
##    name    year month   day  hour   lat  long pressure  wind type   seasday
##    <chr>  <int> <int> <int> <int> <dbl> <dbl>    <int> <int> <chr>    <int>
##  1 Allis…  1995     6     3     0  17.4 -84.3     1005    30 Tropi…       3
##  2 Allis…  1995     6     3     6  18.3 -84.9     1004    30 Tropi…       3
##  3 Allis…  1995     6     3    12  19.3 -85.7     1003    35 Tropi…       3
##  4 Allis…  1995     6     3    18  20.6 -85.8     1001    40 Tropi…       3
##  5 Allis…  1995     6     4     0  22   -86        997    50 Tropi…       4
##  6 Allis…  1995     6     4     6  23.3 -86.3      995    60 Tropi…       4
##  7 Allis…  1995     6     4    12  24.7 -86.2      987    65 Hurri…       4
##  8 Allis…  1995     6     4    18  26.2 -86.2      988    65 Hurri…       4
##  9 Allis…  1995     6     5     0  27.6 -86.1      988    65 Hurri…       5
## 10 Allis…  1995     6     5     6  28.5 -85.6      990    60 Tropi…       5
## # … with 2,737 more rows

There is almost no change in the printed information—group_by really doesn’t do much on its own. The main change is that the tibble resulting from the group_by operation has a little bit of additional information printed at the top: Groups: type [4]. The Groups: type part of this tells us that the tibble is grouped by the type variable and nothing else. The [4] part tells us that there are 4 different groups.

The only thing group_by did was add this grouping information to a copy of storms_tbl. The original storms_tbl object was not altered in any way. If we actually want to do anything useful useful with the result we need to assign it a name so that we can work with it:

storms_grouped <- group_by(storms_tbl, type)

Now we have a grouped tibble called storms_grouped, where the groups are defined by the values of type. Any operations on this tibble will now be performed on a “by group” basis. To see this in action, we use summarise to calculate the mean wind speed:

summarise(storms_grouped, mean.wind = mean(wind))
## # A tibble: 4 x 2
##   type                mean.wind
##   <chr>                   <dbl>
## 1 Extratropical            40.1
## 2 Hurricane                84.7
## 3 Tropical Depression      27.4
## 4 Tropical Storm           47.3

When we used summarise on an ungrouped tibble the result was a tibble with one row: the overall global mean. Now the resulting tibble has four rows, one for each value of type: The type variable in the new tibble tells us what these values are; the mean.wind variable shows the mean wind speed for each value.

15.2.1 More than one grouping variable

What if we need to calculate summaries using more than one grouping variable? The workflow is unchanged. Let’s assume we want to know the mean wind speed and atmospheric pressure associated with each storm type in each year. We first make a grouped copy of the data set with the appropriate grouping variables:

# group the storms_tbl data by storm year + assign the result a name
storms_grouped <- group_by(storms_tbl, type, year)
# 
storms_grouped
## # A tibble: 2,747 x 11
## # Groups:   type, year [24]
##    name    year month   day  hour   lat  long pressure  wind type   seasday
##    <chr>  <int> <int> <int> <int> <dbl> <dbl>    <int> <int> <chr>    <int>
##  1 Allis…  1995     6     3     0  17.4 -84.3     1005    30 Tropi…       3
##  2 Allis…  1995     6     3     6  18.3 -84.9     1004    30 Tropi…       3
##  3 Allis…  1995     6     3    12  19.3 -85.7     1003    35 Tropi…       3
##  4 Allis…  1995     6     3    18  20.6 -85.8     1001    40 Tropi…       3
##  5 Allis…  1995     6     4     0  22   -86        997    50 Tropi…       4
##  6 Allis…  1995     6     4     6  23.3 -86.3      995    60 Tropi…       4
##  7 Allis…  1995     6     4    12  24.7 -86.2      987    65 Hurri…       4
##  8 Allis…  1995     6     4    18  26.2 -86.2      988    65 Hurri…       4
##  9 Allis…  1995     6     5     0  27.6 -86.1      988    65 Hurri…       5
## 10 Allis…  1995     6     5     6  28.5 -85.6      990    60 Tropi…       5
## # … with 2,737 more rows

We grouped the storms_tbl data by type and year and assigned the grouped tibble the name storms_grouped. When we print this to the Console we see Groups: type, year [24] near the top, which tells us that the tibble is grouped by two variables with 24 unique combinations of values. We then calculate the mean wind speed and pressure of each storm type in each year:

summarise(storms_grouped, 
          mean_wind     = mean(wind), 
          mean_pressure = mean(pressure))
## # A tibble: 24 x 4
## # Groups:   type [?]
##    type           year mean_wind mean_pressure
##    <chr>         <int>     <dbl>         <dbl>
##  1 Extratropical  1995      38.7          995.
##  2 Extratropical  1996      40.4          991.
##  3 Extratropical  1997      38.9         1000.
##  4 Extratropical  1998      42.9          991.
##  5 Extratropical  1999      38.9          992.
##  6 Extratropical  2000      39.7          997.
##  7 Hurricane      1995      82.0          970.
##  8 Hurricane      1996      85.5          969.
##  9 Hurricane      1997      80.4          976.
## 10 Hurricane      1998      87.0          972.
## # … with 14 more rows

This calculates mean wind speed and atmospheric pressure for different combination of type and year. The first line shows us that the mean wind speed and pressure associated with extra-tropical storms in 1995 was 38.7 mph and 995 millibars, the second line shows us that the mean wind speed and pressure associated with extra-tropical storms in 1995 was 40.4 mph and 991 millibars, and so on. There are 24 rows in total because there were 24 unique combinations of type and year in the original storms_tbl.

15.2.2 Using group_by with other verbs

The summarise function is the only dplyr verb we’ll use with grouped tibbles in this book. However, all the main verbs alter their behaviour to respect group identity when used with tibbles with grouping information. When mutate or transmute are used with a grouped object they still add new variables, but now the calculations occur “by group”. Here’s an example using transmute:

# group the storms data by storm name + assign the result a name
storms_grouped <- group_by(storms_tbl, name)
# create a data set 'mean centred' wind speed variable
transmute(storms_grouped, wind_centred = wind - mean(wind))
## # A tibble: 2,747 x 2
## # Groups:   name [79]
##    name    wind_centred
##    <chr>          <dbl>
##  1 Allison       -14.4 
##  2 Allison       -14.4 
##  3 Allison        -9.39
##  4 Allison        -4.39
##  5 Allison         5.61
##  6 Allison        15.6 
##  7 Allison        20.6 
##  8 Allison        20.6 
##  9 Allison        20.6 
## 10 Allison        15.6 
## # … with 2,737 more rows

In this example we calculated the “group mean-centered” version of the wind variable. The new wind_centred variable contains the difference between the wind speed and the mean of whichever storm type is associated with the observation.

15.3 Removing grouping information

On occasion it’s necessary to remove grouping information from a data object. This is most often done when working with “pipes” (the topic of the next chapter) when we need to revert back to operating on the whole data set. The ungroup function removes grouping information:

ungroup(storms_grouped)
## # A tibble: 2,747 x 11
##    name    year month   day  hour   lat  long pressure  wind type   seasday
##    <chr>  <int> <int> <int> <int> <dbl> <dbl>    <int> <int> <chr>    <int>
##  1 Allis…  1995     6     3     0  17.4 -84.3     1005    30 Tropi…       3
##  2 Allis…  1995     6     3     6  18.3 -84.9     1004    30 Tropi…       3
##  3 Allis…  1995     6     3    12  19.3 -85.7     1003    35 Tropi…       3
##  4 Allis…  1995     6     3    18  20.6 -85.8     1001    40 Tropi…       3
##  5 Allis…  1995     6     4     0  22   -86        997    50 Tropi…       4
##  6 Allis…  1995     6     4     6  23.3 -86.3      995    60 Tropi…       4
##  7 Allis…  1995     6     4    12  24.7 -86.2      987    65 Hurri…       4
##  8 Allis…  1995     6     4    18  26.2 -86.2      988    65 Hurri…       4
##  9 Allis…  1995     6     5     0  27.6 -86.1      988    65 Hurri…       5
## 10 Allis…  1995     6     5     6  28.5 -85.6      990    60 Tropi…       5
## # … with 2,737 more rows

Looking at the top right of the printed summary, we can see that the Group: part is now gone—the ungroup function effectively created a copy of storms_grouped that is identical to the original storms_tbl tibble.