rguides

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.

ParameterTypeDefaultDescription
xformula or data framerequiredA formula like y ~ g or cbind(y1, y2) ~ g1 + g2. For the data frame method, the data frame whose numeric columns get summarised.
datadata frame / listrequired (formula method)Source of variables named in the formula.
bylist of grouping vectorsrequired (data frame method)Each element is coerced to factor. Length must match nrow(x).
FUNfunction, symbol, or stringrequiredSummary function applied to each subset. Strings are resolved via match.fun(), so FUN = "mean" and FUN = mean are equivalent.
...anyExtra arguments forwarded to FUN (for example na.rm = TRUE).
simplifylogicalTRUETry to simplify per-subset results into a vector or matrix when possible.
droplogicalTRUEDrop unused factor combinations (behaviour changed in R 3.5.0).
subsetlogical vectorOptional row filter; only the formula method accepts it.
na.actionfunctionna.omitApplied 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 formula to x in R 4.2.0. Old code like aggregate(formula = y ~ g, ...) now errors. Pass the formula positionally for portability across R versions.
  • Grouping vectors in by are silently coerced to factors, so numeric codes lose their numeric type. Convert with factor() first if you want to control level order or labels.
  • The default na.action = na.omit is a frequent surprise. Combine na.action = na.pass with na.rm = TRUE inside FUN for per-column NA handling.
  • Result columns are auto-named Group.1, Group.2, … when the by list 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, prefer data.table, dplyr::summarise(), or collapse::fmean() for grouped means.

aggregate() vs tapply() vs dplyr::summarise()

FunctionReturnsBest for
aggregate()data frameMulti-variable summaries keyed by grouping variables; formula syntax; pipe-friendly output.
tapply()array or listSingle-variable summaries with one or two grouping factors; cross-tabulated matrices.
dplyr::summarise()tibbleTidyverse idiom; multiple summary stats per group; integrates with mutate, filter, and friends.
data.table syntaxdata.tableFastest 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.

See also