Skip to main content

Section 3.4 group_by rows

Diagram showing how group_by() assigns rows into groups based on a categorical variable, and then summarize() computes summary statistics separately for each group, returning one row per group.
Figure 3.4.1. Diagram of group_by() and summarize().
We can modify our code above to look at the average wind speed and its spread, keeping the na.rm = TRUE set just in case any missing values are stored in the column:
summary_temp <- weather |>
  summarize(mean = mean(wind_speed, na.rm = TRUE),
            std_dev = sd(wind_speed, na.rm = TRUE))
summary_temp
# A tibble: 1 ร— 2
   mean std_dev
  <dbl>   <dbl>
1  9.43    5.27
Say instead of a single mean wind speed for the whole year, we would like 12 mean wind speeds, one for each of the 12 months separately. In other words, we would like to compute the mean wind speed split by month as shown via generic diagram in Figureย 3.4.1. We can do this by โ€œgroupingโ€ wind speed observations by the values of another variable, in this case by the 12 values of the variable month:
summary_monthly_windspeed <- weather |>
  group_by(month) |>
  summarize(mean = mean(wind_speed, na.rm = TRUE),
            std_dev = sd(wind_speed, na.rm = TRUE))
summary_monthly_windspeed
# A tibble: 12 ร— 3
   month  mean std_dev
   <int> <dbl>   <dbl>
 1     1 10.3     6.01
 2     2 10.9     6.60
 3     3 12.4     6.36
 4     4 10.0     5.02
 5     5  8.88    4.45
 6     6  8.52    4.42
 7     7  7.96    4.36
 8     8  8.83    4.34
 9     9  8.92    4.66
10    10  8.21    4.71
11    11  9.48    4.81
12    12  8.77    5.03
This code is identical to the previous code that created summary_windspeed, but with an extra group_by(month) added before the summarize(). Grouping the weather dataset by month and then applying the summarize() functions yields a data frame that displays the mean and standard deviation wind speed split by the 12 months of the year.
It is important to note that the group_by() function doesnโ€™t change data frames by itself. Rather it changes the meta-data, or data about the data, specifically the grouping structure. Only after applying the summarize() function does the data frame change.
As another example, consider the diamonds data frame included in the ggplot2 package:
diamonds
# A tibble: 53,940 ร— 10
   carat cut       color clarity depth table price     x     y     z
   <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
 1  0.23 Ideal     E     SI2      61.5    55   326  3.95  3.98  2.43
 2  0.21 Premium   E     SI1      59.8    61   326  3.89  3.84  2.31
 3  0.23 Good      E     VS1      56.9    65   327  4.05  4.07  2.31
 4  0.29 Premium   I     VS2      62.4    58   334  4.2   4.23  2.63
 5  0.31 Good      J     SI2      63.3    58   335  4.34  4.35  2.75
 6  0.24 Very Good J     VVS2     62.8    57   336  3.94  3.96  2.48
 7  0.24 Very Good I     VVS1     62.3    57   336  3.95  3.98  2.47
 8  0.26 Very Good H     SI1      61.9    55   337  4.07  4.11  2.53
 9  0.22 Fair      E     VS2      65.1    61   337  3.87  3.78  2.49
10  0.23 Very Good H     VS1      59.4    61   338  4     4.05  2.39
# โ„น 53,930 more rows
Observe that the first line of the output reads # A tibble: 53940 x 10. This is an example of meta-data, in this case the number of observations/rows and variables/columns in diamonds. The actual data itself are the subsequent table of values. Now letโ€™s pipe the diamonds data frame into group_by(cut):
diamonds |>
  group_by(cut)
# A tibble: 53,940 ร— 10
# Groups:   cut [5]
   carat cut       color clarity depth table price     x     y     z
   <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
 1  0.23 Ideal     E     SI2      61.5    55   326  3.95  3.98  2.43
 2  0.21 Premium   E     SI1      59.8    61   326  3.89  3.84  2.31
 3  0.23 Good      E     VS1      56.9    65   327  4.05  4.07  2.31
 4  0.29 Premium   I     VS2      62.4    58   334  4.2   4.23  2.63
 5  0.31 Good      J     SI2      63.3    58   335  4.34  4.35  2.75
 6  0.24 Very Good J     VVS2     62.8    57   336  3.94  3.96  2.48
 7  0.24 Very Good I     VVS1     62.3    57   336  3.95  3.98  2.47
 8  0.26 Very Good H     SI1      61.9    55   337  4.07  4.11  2.53
 9  0.22 Fair      E     VS2      65.1    61   337  3.87  3.78  2.49
10  0.23 Very Good H     VS1      59.4    61   338  4     4.05  2.39
# โ„น 53,930 more rows
Observe that now there is additional meta-data: # Groups: cut [5] indicating that the grouping structure meta-data has been set based on the 5 possible levels of the categorical variable cut: "Fair", "Good", "Very Good", "Premium", and "Ideal". On the other hand, observe that the data has not changed: it is still a table of \(53940 imes 10\) values. Only by combining a group_by() with another data-wrangling operation, in this case summarize(), will the data actually be transformed.
diamonds |>
  group_by(cut) |>
  summarize(avg_price = mean(price))
# A tibble: 5 ร— 2
  cut       avg_price
  <ord>         <dbl>
1 Fair          4359.
2 Good          3929.
3 Very Good     3982.
4 Premium       4584.
5 Ideal         3458.
If you would like to remove this grouping structure meta-data, we can pipe the resulting data frame into the ungroup() function:
diamonds |>
  group_by(cut) |>
  ungroup()
# A tibble: 53,940 ร— 10
   carat cut       color clarity depth table price     x     y     z
   <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
 1  0.23 Ideal     E     SI2      61.5    55   326  3.95  3.98  2.43
 2  0.21 Premium   E     SI1      59.8    61   326  3.89  3.84  2.31
 3  0.23 Good      E     VS1      56.9    65   327  4.05  4.07  2.31
 4  0.29 Premium   I     VS2      62.4    58   334  4.2   4.23  2.63
 5  0.31 Good      J     SI2      63.3    58   335  4.34  4.35  2.75
 6  0.24 Very Good J     VVS2     62.8    57   336  3.94  3.96  2.48
 7  0.24 Very Good I     VVS1     62.3    57   336  3.95  3.98  2.47
 8  0.26 Very Good H     SI1      61.9    55   337  4.07  4.11  2.53
 9  0.22 Fair      E     VS2      65.1    61   337  3.87  3.78  2.49
10  0.23 Very Good H     VS1      59.4    61   338  4     4.05  2.39
# โ„น 53,930 more rows
Observe how the # Groups: cut [5] meta-data is no longer present.
Letโ€™s now revisit the n() counting summary function we briefly introduced previously. Recall that the n() function counts rows. This is opposed to the sum() summary function that returns the sum of a numerical variable. For example, suppose weโ€™d like to count how many flights departed each of the three airports in New York City:
by_origin <- flights |>
  group_by(origin) |>
  summarize(count = n())
by_origin
# A tibble: 3 ร— 2
  origin  count
  <chr>   <int>
1 EWR    138578
2 JFK    133048
3 LGA    163726
We see that LaGuardia ("LGA") had the most flights departing in 2023 followed by Newark ("EWR") and lastly by "JFK". Note there is a subtle but important difference between sum() and n(); while sum() returns the sum of a numerical variable, n() returns a count of the number of rows/observations.

Subsection 3.4.1 Grouping by more than one variable

You are not limited to grouping by one variable. Say you want to know the number of flights leaving each of the three New York City airports for each month. We can also group by a second variable month using group_by(origin, month):
by_origin_monthly <- flights |>
  group_by(origin, month) |>
  summarize(count = n())
`summarise()` has grouped output by 'origin'. You can override using the
`.groups` argument.
Note that an additional message appears here specifying the grouping done. The .groups argument to summarize() has four options: drop_last, drop, keep, and rowwise:
In most circumstances, the default is drop_last which drops the last grouping variable. The message is informing us that the default behavior is to drop the last grouping variable, which in this case is month.
by_origin_monthly
# A tibble: 36 ร— 3
# Groups:   origin [3]
   origin month count
   <chr>  <int> <int>
 1 EWR        1 11623
 2 EWR        2 10991
 3 EWR        3 12593
 4 EWR        4 12022
 5 EWR        5 12371
 6 EWR        6 11339
 7 EWR        7 11646
 8 EWR        8 11561
 9 EWR        9 11373
10 EWR       10 11805
# โ„น 26 more rows
Observe that there are 36 rows to by_origin_monthly because there are 12 months for 3 airports (EWR, JFK, and LGA). Why do we group_by(origin, month) and not group_by(origin) and then group_by(month)? Letโ€™s investigate:
by_origin_monthly_incorrect <- flights |>
  group_by(origin) |>
  group_by(month) |>
  summarize(count = n())
by_origin_monthly_incorrect
# A tibble: 12 ร— 2
   month count
   <int> <int>
 1     1 36020
 2     2 34761
 3     3 39514
 4     4 37476
 5     5 38710
 6     6 35921
 7     7 36211
 8     8 36765
 9     9 35505
10    10 36586
11    11 34521
12    12 33362
What happened here is that the second group_by(month) overwrote the grouping structure meta-data of the earlier group_by(origin), so that in the end we are only grouping by month. The lesson here is if you want to group_by() two or more variables, you should include all the variables at the same time in the same group_by() adding a comma between the variable names.

Checkpoint 3.4.2. Learning Check 3.5.

Recall from Chapterย 2 when we looked at wind speeds by months in NYC. What does the standard deviation column in the summary_monthly_windspeed data frame tell us about wind speeds in NYC throughout the year?

Checkpoint 3.4.3. Learning Check 3.6.

What code would be required to get the mean and standard deviation wind speed for each day in 2023 for NYC?

Checkpoint 3.4.4. Learning Check 3.7.

Recreate by_monthly_origin, but instead of grouping via group_by(origin, month), group variables in a different order group_by(month, origin). What differs in the resulting dataset?

Checkpoint 3.4.5. Learning Check 3.8.

How could we identify how many flights left each of the three airports for each carrier?

Checkpoint 3.4.6. Learning Check 3.9.

How does the filter() operation differ from a group_by() followed by a summarize()?