aggregate()
aggregate(formula, data, FUN, ..., subset, na.action = na.omit) aggregate() is a base R function in the stats package that splits data into subsets by one or more grouping variables, applies a summary function to each subset, and returns the results as a data frame. It is the closest base-R analogue to dplyr::group_by() |> summarise() for users who want grouped summaries without loading any extra packages.
The function dispatches to one of four S3 methods depending on the first argument: a formula, a data frame, a numeric vector, or a ts object. The formula and data frame methods are what you reach for almost every time.
Parameters
The formula method takes the argument pattern shown below. The data frame method is similar but takes x and by instead of x and data.
| Parameter | Type | Default | Description |
|---|---|---|---|
x | formula or data frame | required | A formula like y ~ g or cbind(y1, y2) ~ g1 + g2. For the data frame method, the data frame whose numeric columns get summarised. |
data | data frame / list | required (formula method) | Source of variables named in the formula. |
by | list of grouping vectors | required (data frame method) | Each element is coerced to factor. Length must match nrow(x). |
FUN | function, symbol, or string | required | Summary function applied to each subset. Strings are resolved via match.fun(), so FUN = "mean" and FUN = mean are equivalent. |
... | any | — | Extra arguments forwarded to FUN (for example na.rm = TRUE). |
simplify | logical | TRUE | Try to simplify per-subset results into a vector or matrix when possible. |
drop | logical | TRUE | Drop unused factor combinations (behaviour changed in R 3.5.0). |
subset | logical vector | — | Optional row filter; only the formula method accepts it. |
na.action | function | na.omit | Applied across all by variables; default drops whole rows. |
Return value
A data.frame (or an mts/ts object from the time-series method). Grouping columns come first, named from by or auto-labelled Group.1, Group.2, … when unnamed. Aggregated variables follow. Since R 2.11.0, FUN no longer needs to return a scalar: when it returns a vector, each element becomes its own column in the output.
Basic example
The formula interface is usually the cleanest spell. The left side of ~ names the variables to summarise and the right side names the grouping variables.
aggregate(weight ~ feed, data = chickwts, mean)
# feed weight
# 1 casein 323.5833
# 2 horsebean 160.2000
# 3 linseed 218.7500
# 4 meatmeal 276.9091
# 5 soybean 246.4286
# 6 sunflower 328.9167
This is the base-R equivalent of chickwts |> dplyr::group_by(feed) |> dplyr::summarise(weight = mean(weight)) and runs in one line without any attached package.
Summarising multiple variables
Wrap the variables you want summarised in cbind() on the left of the formula and add more grouping variables on the right with +.
aggregate(cbind(Ozone, Temp) ~ Month, data = airquality, mean)
# Month Ozone Temp
# 1 5 23.61538 65.54839
# 2 6 29.44444 79.10000
# 3 7 59.11538 83.90323
# 4 8 59.96154 83.96774
# 5 9 31.44828 76.89655
You can also use the . shortcut to summarise every numeric column at once. aggregate(. ~ Species, data = iris, mean) returns one row per species with means for all four numeric measurements, no list of column names required.
Multiple grouping variables
Pass a list of grouping vectors to the data-frame method, or chain them with + on the right of the formula. Naming the list elements gives the resulting data frame readable column names instead of Group.1 and Group.2.
aggregate(state.x77,
list(Region = state.region,
Cold = state.x77[, "Frost"] > 130),
mean)
# Region Cold Population Income Illiteracy Life Exp Murder
# 1 Northeast FALSE 9713.50 4782.50 1.0400000 71.27000 4.900000
# 2 South FALSE 7465.70 4011.875 1.7375000 69.70625 10.812500
# 3 West FALSE 7192.25 4701.50 1.0375000 71.77500 7.500000
# 4 North Central TRUE 9963.00 4611.083 0.9500000 71.72333 6.583333
Each combination of grouping levels becomes its own row. Note that aggregate() does not sort output rows by group — order follows first appearance in the input, so add arrange() if presentation order matters.
Handling missing values
The default na.action = na.omit discards a whole row when any single grouping or summarised column has an NA. That is rarely what you want for partially missing data. Switch to na.action = na.pass and pass na.rm = TRUE to FUN to get per-column handling.
# Drops every row with any NA in Ozone, Temp, or Month
aggregate(cbind(Ozone, Temp) ~ Month, data = airquality, FUN = mean)
# Keeps rows where each variable is non-NA independently
aggregate(cbind(Ozone, Temp) ~ Month, data = airquality, FUN = mean,
na.action = na.pass, na.rm = TRUE)
The second call gives a longer per-month count, because rows with NA in Ozone still contribute to the mean of Temp and vice versa.
Time-series aggregation
Passing a ts object dispatches to aggregate.ts, which collapses blocks of observations into a single new observation per unit time. nfrequency controls the new frequency and must divide the original frequency(x).
aggregate(presidents, nfrequency = 1, FUN = mean)
# Annual mean approval rating, 1945 to 1974
Unlike the data-frame method, aggregate.ts requires FUN to return a scalar, so multi-stat anonymous functions are off the table here.
Common pitfalls
- The first argument of the formula method was renamed from
formulatoxin R 4.2.0. Old code likeaggregate(formula = y ~ g, ...)now errors. Pass the formula positionally for portability across R versions. - Grouping vectors in
byare silently coerced to factors, so numeric codes lose their numeric type. Convert withfactor()first if you want to control level order or labels. - The default
na.action = na.omitis a frequent surprise. Combinena.action = na.passwithna.rm = TRUEinsideFUNfor per-column NA handling. - Result columns are auto-named
Group.1,Group.2, … when thebylist has no names. Naming the list (list(Region = g, Cold = h)) gives readable output. aggregate()is slow on large data. For more than one million rows, preferdata.table,dplyr::summarise(), orcollapse::fmean()for grouped means.
aggregate() vs tapply() vs dplyr::summarise()
| Function | Returns | Best for |
|---|---|---|
aggregate() | data frame | Multi-variable summaries keyed by grouping variables; formula syntax; pipe-friendly output. |
tapply() | array or list | Single-variable summaries with one or two grouping factors; cross-tabulated matrices. |
dplyr::summarise() | tibble | Tidyverse idiom; multiple summary stats per group; integrates with mutate, filter, and friends. |
data.table syntax | data.table | Fastest on large data; dt[, lapply(.SD, mean), by = g]. |
For most grouped-summary work in base R, aggregate() is the right hammer. Reach for tapply() when you want a matrix result, and switch to dplyr when you are already inside a tidyverse pipeline or need more than one summary statistic per group.