Written and maintained by CASRAI Editorial Board
Last updated
Grouping in R means splitting a data frame by the values of one or more variables and computing something for each piece, such as a mean per treatment arm or a count per site. R gives you three standard routes: group_by() with summarise() from dplyr, aggregate() from the stats package, and tapply() from base R. They return different object types and treat missing and empty groups differently, which is the part that usually causes surprises.
What grouping is used for in research
Almost every results table starts with a grouped summary: an outcome by condition, a measurement by batch, a response by site or cohort. Grouping in R lets you produce those summaries from the raw data frame in a reproducible script instead of by hand. If you only need a summary of the whole data set, see summary statistics in R; if you need the group-wise spread, see standard deviation in R.
Install or access (official sources only)
aggregate() belongs to the stats package and tapply() to the base package, so both are available in any R installation with nothing to install. group_by(), summarise() and ungroup() belong to dplyr, which is documented at dplyr.tidyverse.org and distributed through CRAN.
install.packages("dplyr")
library(dplyr)
Step-by-step: summarise by group
1. dplyr: group_by() then summarise()
- Pass the data frame to
group_by()and name the grouping variable(s). Use the pipe if you are comfortable with it (see the pipe in R). - Pass the result to
summarise()and name each new column you want. - Check whether the result is still grouped (see Output below) and call
ungroup()if it is.
library(dplyr)
mtcars |>
group_by(cyl) |>
summarise(mean_mpg = mean(mpg))
Grouping by more than one variable works by naming them all. By default group_by() replaces any grouping already on the data; set .add = TRUE to append instead. The dplyr reference documents this with mtcars |> group_by(cyl) followed by group_by(vs, am, .add = TRUE), after which group_vars() returns “cyl”, “vs”, “am”.
mtcars |>
group_by(cyl, am) |>
summarise(mean_mpg = mean(mpg))
2. dplyr: grouping for a single operation with .by
summarise() has a .by argument, described in the reference as “a selection of columns to group by for just this operation, functioning as an alternative to group_by()”. The data frame is not left grouped afterwards.
mtcars |>
summarise(mean_mpg = mean(mpg), .by = cyl)
3. Base R: aggregate()
The formula method takes the outcome on the left and the grouping variables on the right. Several outcomes can be combined with cbind().
aggregate(mpg ~ cyl, data = mtcars, FUN = mean)
aggregate(cbind(mpg, hp) ~ cyl + am, data = mtcars, FUN = mean)
The data frame method takes by, “a list of grouping elements, each as long as the variables in the data frame x”.
aggregate(mtcars$mpg, by = list(cyl = mtcars$cyl), FUN = mean)
4. Base R: tapply()
tapply() takes a vector, an INDEX that is a list of factors of the same length (non-factors are coerced with as.factor), and a function.
tapply(mtcars$mpg, mtcars$cyl, mean)
Output and how to read it
| Function | What comes back | Reading it |
|---|---|---|
summarise() on grouped data |
A tibble, one row per group, with the grouping columns and your named summaries | Check whether the result is still grouped; see the .groups argument below |
aggregate() |
A data frame with the grouping variables first, followed by the aggregated columns | Ready to pass to plotting or modelling code as an ordinary data frame |
tapply() |
With simplify = TRUE and a single atomic result per group, an array (possibly one-dimensional); with simplify = FALSE, a list with a dim attribute |
Group labels are the array names, not a column; empty cells are NA unless you set default |
The thing worth knowing about summarise() is what it does to the grouping of its output. The .groups argument (marked experimental in the reference) accepts four values:
"drop_last": drops the last grouping level. This is the default for a grouped data frame, and was the only behaviour before dplyr 1.0.0."drop": drops all grouping levels."keep": same grouping as the input data."rowwise": each row becomes its own group.
If you group by two variables and do not set .groups, the result is still grouped by the first one, so a later mutate() or summarise() runs within those groups.
Troubleshooting
The output is still grouped, and dplyr printed a message about regrouping
When .groups is not specified, dplyr reports that summarise() “has regrouped the output” and suggests summarise(.groups = "drop_last") to silence the message. The message is not shown if the result is ungrouped, if the option dplyr.summarise.inform is FALSE, or if summarise() is called from a package function. Fix the underlying cause by choosing the grouping you want: summarise(..., .groups = "drop") or a trailing ungroup(). ungroup() also accepts variable names, which removes only those variables from the grouping.
My new group_by() seems to have forgotten the earlier grouping
That is the default: .add = FALSE means a new group_by() replaces existing groups. Use .add = TRUE to add to them.
Rows are missing from aggregate() output
For the formula method, the default na.action is na.omit, so only complete cases for the variables in the formula are used. A group whose rows all have a missing value in the outcome will therefore not appear. tapply() behaves differently: groups come out as cells, and empty cells are NA (or the value of default).
Empty factor levels
For group_by(), the .drop argument controls whether groups from unused factor levels are dropped. The default is TRUE, unless the data was previously grouped with .drop = FALSE.
Free alternatives: three ways to group in R
| Approach | Package | Group specification | Returns |
|---|---|---|---|
group_by() + summarise() |
dplyr | Column names; or .by for one operation |
Tibble (grouping status set by .groups) |
aggregate() |
stats (base R) | Formula such as y ~ x, or a by list |
Data frame, grouping columns first |
tapply() |
base R | INDEX: a list of factors the same length as X |
Array (or list if simplify = FALSE) |
Related reading: ranking the grouped result is covered in order by in R, a per-row group mean in ave() in R, counts across two variables in crosstabs in R, and the underlying object in data frames in R.
FAQ
What is the difference between group_by() and aggregate()?
group_by() marks the data as grouped and the next verb works per group; aggregate() does the split, apply and combine in one call and returns a plain data frame with the grouping columns first.
How do I group by more than one column in R?
Name every variable in group_by(), use cbind(y1, y2) ~ g1 + g2 style formulas in aggregate(), or pass a list of factors as INDEX to tapply().
Why is my data still grouped after summarise()?
With the default .groups for a grouped data frame, only the last grouping level is dropped. Use .groups = "drop" or ungroup().
Does tapply() return a data frame?
No. With a single atomic result per group it returns an array, possibly one-dimensional, so convert it if you need a data frame.
Sources (official docs, checked 2026-10-29)
- dplyr reference, group_by() and summarise().
- R manual, aggregate() (stats).
- R manual, tapply() (base).







