The apply Family
Reviewed & published by Brayan K
The apply family is a set of R functions that run a function over each element, row, column, or group of your data — replacing explicit loops with concise, readable one-liners.
Part of the free R course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll use sapply() and lapply() over vectors and lists, apply() over rows and columns of a matrix, and tapply() to summarise values by group.
What You'll Learn in This Lesson
- Why the apply family replaces many loops
- How sapply() simplifies results to a vector
- How lapply() always returns a list
- How apply() works over rows (1) and columns (2)
- How tapply() summarises values by group
- How vapply() adds type safety to sapply()
1️⃣ sapply() and lapply()
sapply() applies a function to each element and simplifies the result to a vector when possible. lapply() does the same but always returns a list — predictable when results vary in shape.
# sapply() applies a function to each element and SIMPLIFIES to a vector.
# It replaces a loop with one readable line.
nums <- c(1, 4, 9, 16)
sapply(nums, sqrt) # square root of each -> [1] 1 2 3 4
# It works with your own functions too:
sapply(1:5, function(x) x ^ 2) # -> [1] 1 4 9 16 25
# lapply() is the same but always returns a LIST:
lapply(1:3, function(x) x * 10) # -> list of 10, 20, 302️⃣ apply() over Rows and Columns
apply(X, MARGIN, FUN) runs a function across a matrix or data frame. MARGIN = 1 works on each row; MARGIN = 2 on each column. You can pass a named function or your own.
# apply() runs a function over the rows or columns of a matrix/data frame.
# MARGIN = 1 means rows, MARGIN = 2 means columns.
m <- matrix(c(1, 2, 3, 4, 5, 6), nrow = 2, byrow = TRUE)
# [,1] [,2] [,3]
# [1,] 1 2 3
# [2,] 4 5 6
apply(m, 1, sum) # sum of each ROW -> [1] 6 15
apply(m, 2, max) # max of each COLUMN -> [1] 4 5 6
apply(m, 1, function(r) r[1] * r[3]) # custom: col1 * col3 per row -> [1] 3 243️⃣ tapply() for Grouped Summaries
tapply(values, groups, FUN) splits a vector by a grouping vector and applies a function to each group — the base-R way to get "average by category". vapply() is a safer sapply() where you declare the result type.
# tapply() splits a vector by a grouping factor and applies a function
# to each group — the classic "average by category" move.
scores <- c(80, 90, 70, 60, 100, 85)
team <- c("A", "B", "A", "B", "A", "B")
tapply(scores, team, mean) # mean score per team
# A B
# 83.33333 78.33333
# vapply() is like sapply() but you state the result type for safety:
vapply(1:3, function(x) x + 0.5, numeric(1)) # -> [1] 1.5 2.5 3.5Your turn. Fill in the ___ blank, run it, and compare with the expected output.
# 🎯 YOUR TURN — replace each ___ then run it.
temps_c <- c(0, 20, 37, 100)
# 1) Use sapply() to convert each Celsius value to Fahrenheit (C * 9/5 + 32)
temps_f <- ___(temps_c, function(c) c * 9/5 + 32) # 👉 replace ___ with sapply
print(temps_f)
# 2) Print the average Fahrenheit, rounded to 1 decimal
cat("Average F:", round(mean(temps_f), 1), "\n")
# ✅ Expected output:
# [1] 32.0 68.0 98.6 212.0
# Average F: 102.7Mini-Challenge: Grouped Sales
Write it from the outline, run it, and check it against the example output. tapply() with different functions answers different questions about the same groups.
# 🎯 MINI-CHALLENGE: grouped sales summary.
#
# Given:
# amount <- c(100, 250, 80, 300, 120, 90)
# region <- c("N", "S", "N", "S", "N", "S")
#
# 1. Use tapply() to get the TOTAL amount per region (function = sum).
# 2. Use tapply() to get the COUNT per region (function = length).
# 3. Print both results.
#
# ✅ Example output:
# N S
# 300 640
# N S
# 3 3
# your code hereCommon Errors (and the fix)
- sapply() returns a list, not a vector — results had different lengths so it couldn't simplify. Make the function return a consistent length, or use vapply().
- Wrong MARGIN — getting column results when you wanted rows means you swapped 1 and 2. Remember: 1 = rows, 2 = columns.
- tapply() groups misaligned — the values and groups vectors must be the same length and line up element by element.
- Forgetting na.rm — pass extra args after the function, e.g. apply(m, 2, mean, na.rm = TRUE).
Pro Tips
- 💡 Pass extra arguments: anything after FUN is forwarded — sapply(x, round, digits = 2).
- 💡 Anonymous functions: function(x) ... inline keeps small transforms readable.
- 💡 Modern alternative: the purrr package's map() functions mirror these with a consistent interface.
📋 Quick Reference — apply Family
| Function | Use |
|---|---|
| sapply(x, f) | each element → simplified vector |
| lapply(x, f) | each element → list |
| apply(m, 1, f) | each row of a matrix |
| apply(m, 2, f) | each column of a matrix |
| tapply(v, g, f) | summary per group |
| vapply(x, f, t) | type-safe sapply |
🎉 Lesson Complete!
- ✅ The apply family replaces many loops with one line
- ✅ sapply() simplifies; lapply() returns a list
- ✅ apply() works over rows (1) or columns (2)
- ✅ tapply() summarises values by group
- ✅ vapply() adds result-type safety
Practice quiz
What does sapply(c(1, 4, 9), sqrt) return?
- A list of three values
- A single number 14
- A numeric vector c(1, 2, 3)
- A matrix with one column
Answer: A numeric vector c(1, 2, 3). sapply() simplifies the per-element results to a vector when it can.
How does lapply() differ from sapply()?
- lapply() always returns a list
- lapply() only works on matrices
- lapply() returns a single number
- lapply() requires a factor argument
Answer: lapply() always returns a list. lapply() always returns a list; sapply() simplifies when possible.
In apply(m, MARGIN, FUN), what does MARGIN = 1 do?
- Applies FUN to each column
- Applies FUN to the whole matrix at once
- Transposes the matrix first
- Applies FUN to each row
Answer: Applies FUN to each row. MARGIN = 1 means rows; MARGIN = 2 means columns.
Which call gives the maximum of each COLUMN of matrix m?
- apply(m, 1, max)
- apply(m, 2, max)
- sapply(m, max)
- tapply(m, max)
Answer: apply(m, 2, max). MARGIN = 2 applies the function down each column.
What is tapply(values, groups, FUN) used for?
- Applying FUN to each group of values defined by a factor
- Reading a CSV file
- Transposing a data frame
- Sorting a vector
Answer: Applying FUN to each group of values defined by a factor. tapply() splits values by group and applies FUN to each group.
How is vapply() safer than sapply()?
- It runs in parallel automatically
- It never returns NA
- You declare the expected result type and length
- It ignores the function argument
Answer: You declare the expected result type and length. vapply() checks each result against a template like numeric(1).
What does lapply(1:3, function(x) x * 10) return?
- The vector c(10, 20, 30)
- A list containing 10, 20, 30
- The number 60
- An error
Answer: A list containing 10, 20, 30. lapply() always returns a list, one element per input.
Extra arguments after FUN in sapply(x, round, digits = 2) are...
- Ignored by sapply()
- Used as the result type
- Treated as a second vector to iterate
- Forwarded to FUN on each call
Answer: Forwarded to FUN on each call. Anything after FUN is passed through to the function.
Why might sapply() return a list instead of a vector?
- The input was too long
- The per-element results had differing lengths
- It was given a factor
- round() was used
Answer: The per-element results had differing lengths. If results can't be simplified to equal-length pieces, sapply() returns a list.
For tapply() to align correctly, the values and groups vectors must be...
- Sorted alphabetically
- Both factors
- The same length, matched element by element
- Of length 1
Answer: The same length, matched element by element. Each value pairs with the group at the same position.
Continue this course
- Previous: Reading Data (CSV)
- Next: dplyr Basics — Filter, select, mutate, and arrange data the tidyverse way
- Quick reference: R cheat sheet
Frequently asked questions
What is the apply family and why use it instead of loops?
The apply family — apply(), sapply(), lapply(), tapply(), vapply(), and mapply() — are functions that run another function repeatedly over the elements of a vector, list, or the rows/columns of a matrix. They express 'do this to each piece' in a single readable line, avoiding the boilerplate of a for loop. They make intent clearer and often run faster. Loops aren't wrong, but for transforming collections, the apply family is the idiomatic R approach.
What's the difference between sapply() and lapply()?
Both apply a function to each element of a vector or list. lapply() always returns a list, which is predictable and safe. sapply() ('simplify apply') tries to simplify the result to a vector or matrix when it can — so sapply(1:3, sqrt) gives a tidy numeric vector rather than a list. Use sapply() for convenience when you expect a simple result, and lapply() when you want a guaranteed list or the results are complex.
How does the MARGIN argument in apply() work?
apply(X, MARGIN, FUN) needs to know whether to work over rows or columns of a matrix or data frame. MARGIN = 1 applies FUN to each row, and MARGIN = 2 applies it to each column. A memory aid: 1 looks like a row divider and comes first, 2 like columns. So apply(m, 2, mean) gives the mean of every column, while apply(m, 1, sum) gives the sum of every row.
When should I reach for tapply()?
tapply() is for grouped summaries: you have a vector of values and a matching vector (or factor) of groups, and you want a summary per group — average sales per region, total clicks per day, count per category. tapply(values, groups, FUN) does exactly that. It's the base-R version of the split-apply-combine idea; later you'll see dplyr's group_by() + summarise() do the same thing with a more readable syntax for data frames.