R 中的 apply 家族把常见循环模式封装成函数:指定一组数据和一个操作,由迭代函数逐行、逐列、逐元素或分组执行。
这组函数的差异不只在于“处理什么”,还在于返回什么结构。选择时应同时确认输入对象、迭代方向和预期输出。
快速选择
| 函数 | 主要输入 | 迭代方式 | 典型返回值 |
|---|---|---|---|
apply() |
matrix、array | 按维度 | 向量、矩阵或 array |
lapply() |
list、vector、data frame | 逐元素 | list |
sapply() |
list、vector | 逐元素并尝试简化 | 向量、矩阵或 list |
vapply() |
list、vector | 逐元素并校验类型 | 指定类型的向量或 array |
tapply() |
atomic vector + 分组因子 | 分组聚合 | array 或 list |
可以先用一句话判断:
- 矩阵按行或列:
apply(); - 希望稳定得到 list:
lapply(); - 探索时希望自动简化:
sapply(); - 正式代码要求固定输出类型:
vapply(); - 一个向量按组汇总:
tapply()。
apply():沿矩阵维度计算
apply() 用于 matrix 或 array。MARGIN 指定保留哪些维度:二维矩阵中,1 表示逐行,2 表示逐列。
m <- matrix(1:6, nrow = 2)
m
apply(m, MARGIN = 1, FUN = sum)
apply(m, MARGIN = 2, FUN = mean)
也可以使用简写:
apply(m, 1, sum)
apply(m, 2, mean)
对高维 array,MARGIN 可以包含多个维度:
x <- array(1:24, dim = c(2, 3, 4))
# 保留前两个维度,对第三维求和
apply(x, c(1, 2), sum)
不要随意把 data frame 交给 apply()
apply() 会先把 data frame 转成 matrix。matrix 只能存一种原子类型;只要其中一列是 character,其他列也可能一起被转换成 character。
df <- data.frame(
id = c("A", "B", "C"),
score = c(80, 90, 85)
)
as.matrix(df)
混合类型的 data frame 通常应按列使用 lapply(),或者先明确选择数值列。
apply(df["score"], 2, mean)
lapply():逐元素返回 list
lapply() 对向量或 list 的每个元素执行函数,并始终返回 list。
lapply(1:3, function(x) x^2)
稳定返回 list 是它最重要的特征。无论每次结果是标量、向量还是更复杂的对象,外层结构都不会自动改变。
result <- lapply(
1:3,
function(x) seq_len(x)
)
str(result)
data frame 本质上是等长列组成的 list,因此 lapply() 会逐列处理:
lapply(iris, class)
lapply(iris[1:4], mean)
需要对一批模型、文件或不等长结果迭代时,list 通常也是最安全的容器。
sapply():尝试简化结果
sapply() 先执行与 lapply() 相同的逐元素计算,再尝试把结果简化成更紧凑的结构。
sapply(1:5, sqrt)
sapply(iris[1:4], mean)
当每次返回一个同类型标量时,结果通常是 atomic vector:
sapply(1:4, function(x) x^2)
当每次返回等长向量时,结果可能被简化成 matrix:
sapply(1:3, function(x) c(value = x, square = x^2))
如果各次结果长度不同,则可能仍然返回 list:
sapply(1:3, seq_len)
这种自动简化适合交互探索,但也意味着输出形状取决于实际数据。输入稍有变化,后续代码收到的对象类型就可能不同。
vapply():预先声明返回模板
vapply() 与 sapply() 的用途相近,但要求通过 FUN.VALUE 声明每次调用应返回的类型和长度。
vapply(
1:3,
function(x) x^2,
FUN.VALUE = numeric(1)
)
模板 numeric(1) 表示每次必须返回一个数值。如果某次返回了错误的长度或不兼容类型,vapply() 会立即报错。
vapply(
iris[1:4],
mean,
FUN.VALUE = numeric(1)
)
返回字符标量时使用 character(1):
vapply(
iris,
function(x) class(x)[1],
FUN.VALUE = character(1)
)
返回固定长度向量时,模板也写成相同长度:
vapply(
iris[1:4],
function(x) c(min = min(x), max = max(x)),
FUN.VALUE = c(min = 0, max = 0)
)
正式脚本、函数和包代码更适合使用 vapply(),因为输出契约直接写在调用中。
tapply():按组汇总向量
tapply() 把一个原子向量按 factor 分组,然后在每组内执行函数。
tapply(
iris$Sepal.Length,
iris$Species,
mean
)
它表达的是常见的 split–apply–combine 模式:
- 根据分组变量拆分数据;
- 在每组中执行函数;
- 把结果组合起来。
多个分组变量可以放进 list:
df <- transform(
iris,
WidthGroup = Sepal.Width > median(Sepal.Width)
)
tapply(
df$Sepal.Length,
list(df$Species, df$WidthGroup),
mean
)
缺少某些分组组合时,可以设置默认值:
tapply(
df$Sepal.Length,
list(df$Species, df$WidthGroup),
mean,
default = NA_real_
)
tapply() 很适合快速分组统计;需要同时汇总多列、产生多个统计量或继续管道处理时,data frame 分组工具通常更方便。
额外参数怎样传给函数
apply 家族中写在 FUN 后面的参数会继续传给目标函数。
x <- list(
c(1, 2, NA),
c(3, NA, 5)
)
lapply(x, mean, na.rm = TRUE)
vapply(
x,
mean,
FUN.VALUE = numeric(1),
na.rm = TRUE
)
如果额外参数会随每次迭代变化,就不再是单一固定参数,应使用多输入迭代、显式循环或其他合适结构。
foreach:显式组合每次结果
foreach 是另一套迭代接口。它的特点是通过 .combine 明确说明每次返回值怎样合并,并可以把顺序执行的 %do% 替换为已注册 backend 上的 %dopar%。
library(foreach)
result <- foreach(i = 1:4, .combine = c) %do% {
sqrt(i)
}
result
组合 data frame 行:
result <- foreach(i = 1:3, .combine = rbind) %do% {
data.frame(value = i, square = i^2)
}
.combine 必须与每次迭代的返回结构相符。对于复杂或较大的对象,反复 rbind 可能效率较低;先保留 list、最后一次性合并通常更容易验证。
错误处理
result <- foreach(
i = 1:4,
.errorhandling = "pass"
) %do% {
if (i == 2) stop("test error")
i
}
| 设置 | 行为 |
|---|---|
"stop" |
遇到错误时停止 |
"pass" |
将 error object 放进结果 |
"remove" |
从组合结果中移除失败项 |
"pass" 不是“跳过错误”;它保留错误以便检查。"remove" 虽然让流程继续,但也可能悄悄改变结果数量和样本对应关系。批处理时应同时保存任务 ID、成功状态和错误消息。
切换到并行 backend
library(doParallel)
cluster <- parallel::makeCluster(2)
registerDoParallel(cluster)
result <- foreach(i = 1:4, .combine = c) %dopar% {
sqrt(i)
}
parallel::stopCluster(cluster)
并行 worker 是独立进程。需要的包、函数和数据必须能够在 worker 中获得;随机数、错误处理和输出顺序也要显式设计。任务很小时,启动与传输成本可能比计算本身更大。
foreach 适合已有 .combine 逻辑、需要切换 backend 的批处理。普通顺序迭代若只需稳定的 vector 或 list,lapply()、vapply() 或 purrr 通常更直接;使用 future 的并行策略则参见 R 系列的后续章节。
常见选择错误
用 apply() 处理混合类型 data frame
先发生 matrix 强制转换,随后数值函数可能收到 character。应改用 lapply() 按列处理,或先选择纯数值列。
假设 sapply() 永远返回 vector
它也可能返回 matrix 或 list。后续代码依赖固定类型时,应换成 vapply()。
为了简短而忽略输出结构
迭代代码是否可靠,往往取决于输出约束,而不是函数名有多短。先确定需要 list、atomic vector、matrix 还是分组 array,再选择函数。
把 apply 家族理解成并行计算
这些函数默认仍在当前 R 进程中顺序执行。它们简化的是循环表达,不会自动使用多个 CPU 核心。
与 for 循环的关系
apply 家族不是比 for 更“高级”的语法,而是对常见迭代模式的封装。
out <- vector("list", length(iris))
for (i in seq_along(iris)) {
out[[i]] <- class(iris[[i]])
}
等价的 lapply() 写法是:
out <- lapply(iris, class)
当每次迭代需要更新多个对象、提前退出、跳过步骤或维护复杂状态时,for 往往更直接。模式简单且输出清楚时,apply 家族能减少样板代码。
实用选择
- 探索性代码:
lapply()或sapply(); - 需要保留复杂结果:
lapply(); - 正式代码要求稳定类型:
vapply(); - matrix/array 按维度计算:
apply(); - 单一数值向量做快速分组汇总:
tapply()。
真正需要记住的不是五个函数的语法,而是三件事:输入是什么、沿哪里迭代、输出必须是什么结构。