Group data in R with the group_by() function from dplyr

Group data by one or more columns with group_by from dplyr

The group_by function from dplyr splits a data frame into groups based on the values of one or more columns, so that the next verbs (summarise, mutate, filter, slice or arrange) are applied to each group separately. In this tutorial you will learn how grouping really works, what the "has grouped output by" message means, why you should ungroup your data and when to use the .by argument instead.

Sample data

In this tutorial we will use a subset of the starwars dataset from dplyr: the characters of three species (humans, droids and gungans) with known mass. The resulting data frame contains 26 rows and five columns. The code uses the pipe operator %>% to chain the steps.

library(dplyr)

df <- starwars %>%
  select(name, species, sex, height, mass) %>%
  filter(species %in% c("Human", "Droid", "Gungan"), !is.na(mass))

df
# A tibble: 26 × 5
   name               species sex    height  mass
   <chr>              <chr>   <chr>   <int> <dbl>
 1 Luke Skywalker     Human   male      172    77
 2 C-3PO              Droid   none      167    75
 3 R2-D2              Droid   none       96    32
 4 Darth Vader        Human   male      202   136
 5 Leia Organa        Human   female    150    49
 6 Owen Lars          Human   male      178   120
 7 Beru Whitesun Lars Human   female    165    75
 8 R5-D4              Droid   none       97    32
 9 Biggs Darklighter  Human   male      183    84
10 Obi-Wan Kenobi     Human   male      182    77
# ℹ 16 more rows

What does group_by do?

A common surprise is that group_by doesn’t change the data at all. If you print a grouped data frame you will see exactly the same rows in the same order. The only difference is a new line in the header, # Groups: species [3], which tells you the grouping variable and the number of groups.

df_g <- df %>%
  group_by(species)

df_g
# A tibble: 26 × 5
# Groups:   species [3]
   name               species sex    height  mass
   <chr>              <chr>   <chr>   <int> <dbl>
 1 Luke Skywalker     Human   male      172    77
 2 C-3PO              Droid   none      167    75
 3 R2-D2              Droid   none       96    32
 4 Darth Vader        Human   male      202   136
 5 Leia Organa        Human   female    150    49
 6 Owen Lars          Human   male      178   120
 7 Beru Whitesun Lars Human   female    165    75
 8 R5-D4              Droid   none       97    32
 9 Biggs Darklighter  Human   male      183    84
10 Obi-Wan Kenobi     Human   male      182    77
# ℹ 16 more rows

Internally, the data frame becomes a grouped_df object that stores which rows belong to each group. Nothing happens until a verb uses that information. You can inspect the groups with n_groups, group_vars and group_keys:

class(df_g)
n_groups(df_g)
group_vars(df_g)
group_keys(df_g)
[1] "grouped_df" "tbl_df"     "tbl"        "data.frame"
[1] 3
[1] "species"
# A tibble: 3 × 1
  species
  <chr>
1 Droid
2 Gungan
3 Human

The following table summarizes how each dplyr verb behaves when the data is grouped:

Verb Without groups With group_by
summarise() One row for the whole data One row per group
mutate() Calculations use the whole column Calculations use the rows of each group
filter() Conditions use the whole column Conditions are evaluated within each group
slice_*() Selects rows of the whole data Selects rows of each group
arrange() Sorts the whole data Ignores groups unless .by_group = TRUE

group_by with summarise

The most common use of group_by is together with the summarise function, which collapses each group into a single row. The n function returns the number of rows of each group, so the following code counts the observations per species:

df %>%
  group_by(species) %>%
  summarise(n = n())
# A tibble: 3 × 2
  species     n
  <chr>   <int>
1 Droid       4
2 Gungan      2
3 Human      20

You can calculate as many statistics per group as you want inside the same summarise call. Note that the groups are sorted alphabetically in the output.

df %>%
  group_by(species) %>%
  summarise(n = n(),
            mean_height = mean(height),
            mean_mass = mean(mass),
            max_mass = max(mass))
# A tibble: 3 × 5
  species     n mean_height mean_mass max_mass
  <chr>   <int>       <dbl>     <dbl>    <dbl>
1 Droid       4        140       69.8      140
2 Gungan      2        210       74         82
3 Human      20        180.      81.3      136

If a group contains missing values, functions like mean or sum will return NA for that group. For instance, with the full starwars data, summarise(mean_mass = mean(mass)) returns NA for all three species. Use mean(mass, na.rm = TRUE) to ignore them. Check the post about NA values in R for more details.

You can also group by an expression instead of an existing column. In the example below the characters are split into tall (more than 180 cm) and not tall:

df %>%
  group_by(tall = height > 180) %>%
  summarise(n = n(), mean_mass = mean(mass))
# A tibble: 2 × 3
  tall      n mean_mass
  <lgl> <int>     <dbl>
1 FALSE    12      70.8
2 TRUE     14      85.9

Group by multiple columns

To group by more than one variable, pass all of them to group_by separated by commas. Each combination of values that appears in the data becomes a group:

res <- df %>%
  group_by(species, sex) %>%
  summarise(n = n(), mean_mass = mean(mass))

res
`summarise()` has grouped output by 'species'. You can override using the `.groups` argument.
# A tibble: 4 × 4
# Groups:   species [3]
  species sex        n mean_mass
  <chr>   <chr>  <int>     <dbl>
1 Droid   none       4      69.8
2 Gungan  male       2      74
3 Human   female     3      56.3
4 Human   male      17      85.7

What does “summarise() has grouped output by” mean?

The message is not an error nor a warning. Each summarise call removes the last grouping variable (sex in this case) and keeps the rest. The message just tells you that the result is still grouped by species, as the # Groups: species [3] line shows.

This matters because any verb you apply next will work within species. For instance, if you try to calculate the percentage of characters of each row, the denominator sum(n) is the total of each species, not the total of the data:

res %>%
  mutate(pct = n / sum(n) * 100)
# A tibble: 4 × 5
# Groups:   species [3]
  species sex        n mean_mass   pct
  <chr>   <chr>  <int>     <dbl> <dbl>
1 Droid   none       4      69.8   100
2 Gungan  male       2      74     100
3 Human   female     3      56.3    15
4 Human   male      17      85.7    85

This output is correct if you wanted the percentage of each sex within each species (15% of the humans are female). If you wanted the percentage over the 26 characters, set .groups = "drop" to remove all the groups after summarising:

df %>%
  group_by(species, sex) %>%
  summarise(n = n(), .groups = "drop") %>%
  mutate(pct = n / sum(n) * 100)
# A tibble: 4 × 4
  species sex        n   pct
  <chr>   <chr>  <int> <dbl>
1 Droid   none       4 15.4
2 Gungan  male       2  7.69
3 Human   female     3 11.5
4 Human   male      17 65.4

The possible values of .groups are:

  • "drop_last": drops the last grouping variable. This is the default behaviour and the one that prints the message.
  • "drop": drops all the groups. The result is an ungrouped tibble. This is the safest choice in most cases.
  • "keep": keeps the same groups as the input (# Groups: species, sex [4]).
  • "rowwise": each row becomes its own group.

Setting .groups explicitly, even to "drop_last", also silences the message.

group_by with mutate

When the data is grouped, the mutate function keeps all the rows but the calculations use only the values of the group of each row. This is useful to compare each observation with its group. In the following example mean(mass) is the mean of each species, so diff_mean is the difference between the mass of each character and the average mass of its species:

df %>%
  group_by(species) %>%
  mutate(mean_mass = mean(mass),
         diff_mean = mass - mean_mass) %>%
  select(name, species, mass, mean_mass, diff_mean)
# A tibble: 26 × 5
# Groups:   species [3]
   name               species  mass mean_mass diff_mean
   <chr>              <chr>   <dbl>     <dbl>     <dbl>
 1 Luke Skywalker     Human      77      81.3     -4.31
 2 C-3PO              Droid      75      69.8      5.25
 3 R2-D2              Droid      32      69.8    -37.8
 4 Darth Vader        Human     136      81.3     54.7
 5 Leia Organa        Human      49      81.3    -32.3
 6 Owen Lars          Human     120      81.3     38.7
 7 Beru Whitesun Lars Human      75      81.3     -6.31
 8 R5-D4              Droid      32      69.8    -37.8
 9 Biggs Darklighter  Human      84      81.3      2.69
10 Obi-Wan Kenobi     Human      77      81.3     -4.31
# ℹ 16 more rows

Notice that mean_mass takes the value 81.3 for all the humans and 69.8 for all the droids. Without group_by, the column would contain the overall mean (79.0) in every row.

In the same way, you can compute the percentage of the group total or the rank within each group. Below, pct_mass is the percentage that each character represents over the total mass of its species and rank_height is the position of each character in its species when sorted from tallest to shortest:

df %>%
  group_by(species) %>%
  mutate(pct_mass = mass / sum(mass) * 100,
         rank_height = min_rank(desc(height))) %>%
  select(name, species, height, mass, pct_mass, rank_height)
# A tibble: 26 × 6
# Groups:   species [3]
   name               species height  mass pct_mass rank_height
   <chr>              <chr>    <int> <dbl>    <dbl>       <int>
 1 Luke Skywalker     Human      172    77     4.73          16
 2 C-3PO              Droid      167    75    26.9            2
 3 R2-D2              Droid       96    32    11.5            4
 4 Darth Vader        Human      202   136     8.36           1
 5 Leia Organa        Human      150    49     3.01          20
 6 Owen Lars          Human      178   120     7.38          13
 7 Beru Whitesun Lars Human      165    75     4.61          19
 8 R5-D4              Droid       97    32    11.5            3
 9 Biggs Darklighter  Human      183    84     5.17           8
10 Obi-Wan Kenobi     Human      182    77     4.73          11
# ℹ 16 more rows

Darth Vader is the tallest human (rank 1) and C-3PO is the second tallest droid. Unlike summarise, mutate doesn’t remove any grouping variable, so the result is still grouped by species.

group_by with filter

The filter function evaluates the conditions within each group when the data is grouped. This makes it possible to keep, for instance, the characters that are heavier than the average of their own species:

df %>%
  group_by(species) %>%
  filter(mass > mean(mass))
# A tibble: 9 × 5
# Groups:   species [3]
  name              species sex   height  mass
  <chr>             <chr>   <chr>  <int> <dbl>
1 C-3PO             Droid   none     167    75
2 Darth Vader       Human   male     202   136
3 Owen Lars         Human   male     178   120
4 Biggs Darklighter Human   male     183    84
5 Anakin Skywalker  Human   male     188    84
6 IG-88             Droid   none     200   140
7 Qui-Gon Jinn      Human   male     193    89
8 Roos Tarpals      Gungan  male     224    82
9 Mace Windu        Human   male     188    84

C-3PO weighs 75 kg, which is less than the overall mean, but it is kept because it is heavier than the average droid (69.8 kg).

You can also filter whole groups. Using n() inside filter you can keep only the groups with more than a given number of rows:

df %>%
  group_by(species) %>%
  filter(n() > 2) %>%
  summarise(n = n())
# A tibble: 2 × 2
  species     n
  <chr>   <int>
1 Droid       4
2 Human      20

Top rows per group with slice

To get the top N rows of each group the easiest option is slice_max (or slice_min for the lowest values). The following code returns the two heaviest characters of each species:

df %>%
  group_by(species) %>%
  slice_max(mass, n = 2)
# A tibble: 6 × 5
# Groups:   species [3]
  name          species sex   height  mass
  <chr>         <chr>   <chr>  <int> <dbl>
1 IG-88         Droid   none     200   140
2 C-3PO         Droid   none     167    75
3 Roos Tarpals  Gungan  male     224    82
4 Jar Jar Binks Gungan  male     196    66
5 Darth Vader   Human   male     202   136
6 Owen Lars     Human   male     178   120

Keep in mind that slice_max returns all the tied rows by default, so you can get more than n rows per group. Set with_ties = FALSE if you need exactly n rows. Other slice functions also work per group: slice_head(n = 1) returns the first row of each group and slice_sample draws a random sample of each group. Read more about selecting rows by position.

df %>%
  group_by(species) %>%
  slice_head(n = 1)
# A tibble: 3 × 5
# Groups:   species [3]
  name           species sex   height  mass
  <chr>          <chr>   <chr>  <int> <dbl>
1 C-3PO          Droid   none     167    75
2 Jar Jar Binks  Gungan  male     196    66
3 Luke Skywalker Human   male     172    77

group_by with arrange

The arrange function is the exception: it ignores the groups by default and sorts the whole data frame. If you want the rows sorted within each group (and the groups next to each other) set .by_group = TRUE:

df %>%
  group_by(species) %>%
  arrange(desc(height), .by_group = TRUE)
# A tibble: 26 × 5
# Groups:   species [3]
   name             species sex   height  mass
   <chr>            <chr>   <chr>  <int> <dbl>
 1 IG-88            Droid   none     200   140
 2 C-3PO            Droid   none     167    75
 3 R5-D4            Droid   none      97    32
 4 R2-D2            Droid   none      96    32
 5 Roos Tarpals     Gungan  male     224    82
 6 Jar Jar Binks    Gungan  male     196    66
 7 Darth Vader      Human   male     202   136
 8 Qui-Gon Jinn     Human   male     193    89
 9 Dooku            Human   male     193    80
10 Anakin Skywalker Human   male     188    84
# ℹ 16 more rows

Without .by_group = TRUE the first rows would be Roos Tarpals, Darth Vader and IG-88, the three tallest characters regardless of their species.

ungroup: remove the groups

The groups are kept until you remove them. mutate, filter, slice and arrange return grouped data and summarise only drops one level. That is why forgetting to ungroup is one of the most common bugs with dplyr: you save a grouped data frame and, later, an operation that should use the whole data is applied by group without any warning.

In the following example the data was grouped to compute mean_mass. Later, you want the heaviest character of the data, but as the data is still grouped you get the heaviest character of each species:

df_g <- df %>%
  group_by(species) %>%
  mutate(mean_mass = mean(mass))

df_g %>% slice_max(mass, n = 1)
# A tibble: 3 × 6
# Groups:   species [3]
  name         species sex   height  mass mean_mass
  <chr>        <chr>   <chr>  <int> <dbl>     <dbl>
1 IG-88        Droid   none     200   140      69.8
2 Roos Tarpals Gungan  male     224    82      74
3 Darth Vader  Human   male     202   136      81.3

Calling ungroup removes all the groups and the result is the expected one:

df_g %>%
  ungroup() %>%
  slice_max(mass, n = 1)
# A tibble: 1 × 6
  name  species sex   height  mass mean_mass
  <chr> <chr>   <chr>  <int> <dbl>     <dbl>
1 IG-88 Droid   none     200   140      69.8

A good habit is to end every grouped pipeline with ungroup() (or with .groups = "drop" in summarise) before saving the result. If you are not sure whether a data frame is grouped, print it and look for the # Groups: line or run group_vars(df).

The .by argument (dplyr 1.1.0 or newer)

Since dplyr 1.1.0, summarise, mutate, filter and slice accept a .by argument (in slice_max, slice_head and the other slice_* helpers it is called by). It groups the data only for that operation and always returns an ungrouped data frame, so you don’t need to ungroup and you won’t see the .groups message.

df %>%
  summarise(n = n(), mean_mass = mean(mass), .by = species)
# A tibble: 3 × 3
  species     n mean_mass
  <chr>   <int>     <dbl>
1 Human      20      81.3
2 Droid       4      69.8
3 Gungan      2      74

The results are the same as with group_by, but note that the groups keep the order in which they first appear in the data instead of being sorted alphabetically. To group by several columns use c():

df %>%
  summarise(n = n(), mean_mass = mean(mass), .by = c(species, sex))
# A tibble: 4 × 4
  species sex        n mean_mass
  <chr>   <chr>  <int>     <dbl>
1 Human   male      17      85.7
2 Droid   none       4      69.8
3 Human   female     3      56.3
4 Gungan  male       2      74

You can’t combine both approaches. If the data is already grouped and you pass .by, you will get the following error:

Error in `summarise()`:
! Can't supply `.by` when `.data` is a grouped data frame.

So, which one should you use? Use .by for a single grouped operation, as it is shorter and can’t leave groups behind. Use group_by when several consecutive verbs share the same groups or when you need arrange(.by_group = TRUE) or functions such as group_keys.

Count rows by group with count

Counting observations per group is so common that dplyr provides a shortcut. count(species) is equivalent to group_by(species) %>% summarise(n = n()), but it also returns an ungrouped data frame:

df %>%
  count(species)
# A tibble: 3 × 2
  species     n
  <chr>   <int>
1 Droid       4
2 Gungan      2
3 Human      20

You can count by several variables, sort the output from the largest to the smallest count with sort = TRUE and change the name of the new column with the name argument:

df %>%
  count(species, sex, sort = TRUE)
# A tibble: 4 × 3
  species sex        n
  <chr>   <chr>  <int>
1 Human   male      17
2 Droid   none       4
3 Human   female     3
4 Gungan  male       2

If you want to keep all the rows and add the size of the group as a new column use add_count(species) instead.

Base R equivalents

If you don’t want to use dplyr you can obtain grouped summaries in base R. The aggregate function returns a data frame and the tapply function returns a named vector:

aggregate(mass ~ species, data = df, FUN = mean)

tapply(df$mass, df$species, mean)
  species  mass
1   Droid 69.75
2  Gungan 74.00
3   Human 81.31

 Droid Gungan  Human
 69.75  74.00  81.31

For counts by group, the table function is the base R alternative to count. However, dplyr is more convenient when you need grouped mutate or filter operations, which require more code in base R.

If you load the plyr package after dplyr, plyr::summarise masks dplyr::summarise and the groups are ignored: you will get a single row for the whole data or an error saying that n() must only be used inside data-masking verbs. Load plyr before dplyr or call dplyr::summarise explicitly.