rguides

tapply()

tapply(X, INDEX, FUN = NULL, ..., default = NA, simplify = TRUE)

tapply() applies a function to subsets of a vector defined by a factor or list of factors. It’s the base R equivalent of dplyr::group_by() %>% summarise() for simple grouped operations.

Syntax

tapply(X, INDEX, FUN = NULL, ..., default = NA, simplify = TRUE)

Parameters

ParameterTypeDefaultDescription
Xvector,The vector to process
INDEXfactor or listGrouping variable(s)
FUNfunctionNULLFunction to apply to each group
...anyAdditional arguments passed to FUN
defaultanyNAValue for empty groups when simplify=TRUE
simplifylogicalTRUEIf TRUE, returns array; if FALSE, returns list

Examples

Basic usage

# Sample data: values grouped by category
values <- c(10, 20, 30, 40, 50)
group <- factor(c("A", "A", "B", "B", "B"))

tapply(values, group, sum)
#  A  B 
# 30 120

With multiple grouping variables

When you pass a list of two factors as INDEX, tapply() computes a two-way table — a matrix with rows for one factor and columns for the other. This is the base R way to produce cross-tabulated summaries like “mean score by gender and department,” where each cell holds the result of applying FUN to the observations that fall into that combination of levels.

# Create data frame
df <- data.frame(
  score = c(85, 92, 78, 88, 95, 72),
  gender = c("M", "M", "F", "F", "M", "F"),
  dept = c("Sales", "Sales", "Sales", "IT", "IT", "IT")
)

# Mean score by gender and department
tapply(df$score, list(df$gender, df$dept), mean)
#       Sales  IT
# F      78.0  80
# M      88.5  95

Using simplify = FALSE

Setting simplify = FALSE forces tapply() to return a named list rather than an array. This is the safer option when the function might return results of different lengths or types across groups, since array simplification would either fail or produce a mangled structure. Lists also make it easy to access individual group results by name with $ or [[.

# Returns a list instead of matrix
result <- tapply(values, group, sum, simplify = FALSE)
result
# $A
# [1] 30
# $B
# [1] 120

# Access individual elements
result[["A"]]
# [1] 30

With custom function

You are not limited to built-in functions like sum or mean. Any function that accepts a vector and returns a scalar works with tapply(). Anonymous functions defined inline — function(x) diff(range(x)) for the spread, or function(v) sum(is.na(v)) for missing-value counts — let you compute exactly the group-level statistic your analysis needs without pre-defining named helpers.

# Get range (max - min) by group
tapply(values, group, function(x) diff(range(x)))
# A  B 
# 10  20

# Count NAs per group (using na.rm in sum)
x <- c(1, 2, NA, 4, NA, 6)
g <- factor(c("A", "A", "A", "B", "B", "B"))
tapply(x, g, function(v) sum(is.na(v)))
# A B 
# 2 1

Common patterns

Computing summary statistics by group

For a quick distributional summary per group, tapply() can call summary() — the built-in function that returns min, quartiles, median, mean, and max. This produces a compact per-group snapshot without writing a custom aggregation function, which is handy during exploratory data analysis when you want to compare distributions across factor levels at a glance.

# Create sample dataset
set.seed(123)
df <- data.frame(
  treatment = sample(c("control", "treatment"), 100, replace = TRUE),
  outcome = rnorm(100)
)

# Multiple statistics
tapply(df$outcome, df$treatment, summary)

Conditional aggregation

Filtering within each group before applying the summary function is a pattern that tapply() handles through the anonymous function body. The expression sum(x[x > 3]) first subsets to values above the threshold, then sums — all within the scope of a single group. This avoids creating intermediate filtered data frames for every group.

# Sum values above threshold per group
values <- c(1, 5, 3, 8, 2, 9)
group <- c("A", "A", "B", "B", "B", "B")
tapply(values, group, function(x) sum(x[x > 3]))
# A  B 
# 5 17

tapply() in practice

tapply(X, INDEX, FUN) applies FUN to subsets of X defined by INDEX. It is the base R function for grouped aggregation: tapply(df$value, df$group, mean) computes the mean value for each group. The result is a named array (or list, if simplify = FALSE) with one element per unique level of INDEX.

When INDEX is a list of two grouping vectors, tapply() computes a two-way table of function values: tapply(sales, list(region, quarter), sum) returns a matrix of total sales by region and quarter. This two-way form is an alternative to xtabs() and aggregate() for cross-tabulated summaries.

tapply() applies its function to each subset independently and collects the results. The function must return a scalar or a vector of the same length for all groups — if different groups return different-length vectors and simplify = TRUE, the result may be unexpected. Set simplify = FALSE to always get a list.

In the tidyverse, dplyr::summarise() after group_by() is more expressive than tapply() for most grouped aggregation tasks, since it handles multiple summary variables, integrates with pipes, and returns a tidy data frame. Use tapply() when you need a matrix result or are working without tidyverse dependencies.

tapply() applies a function to groups of values defined by an index. It returns an array indexed by the unique levels of the index. tapply(x, group, mean) is equivalent to computing a group mean without loading any package. When the function returns vectors of varying length, tapply() returns a list. In the tidyverse, group_by() + summarise() is more readable and handles multiple grouping variables with less boilerplate.

See also

  • diff()
  • round()
  • table() ave(x, group, FUN = mean) returns the group means as a vector aligned with the original data, unlike tapply() which returns a table.tapply() is conceptually similar to SQL GROUP BY. It applies a function to subsets of a vector defined by one or more index factors. When multiple index factors are passed as a list, tapply() returns a multidimensional array with one dimension per index factor. tapply(x, list(a, b), mean) computes means for all combinations of levels in a and b. The result is an array indexed by factor levels, not a data frame — use as.data.frame.table() to convert.