一句话概括 apply 函数族
apply 函数族的每个成员做的都是同一件事:拿一个函数,对某个结构中的每个元素运行它,并收集结果。区别在于输入和输出的形状。一览如下:
apply(m, MARGIN, FUN):矩阵的行或列。lapply(x, FUN):列表或向量的每个元素;总是返回列表。sapply(x, FUN):和lapply一样,然后尽可能简化成向量或矩阵。vapply(x, FUN, FUN.VALUE):声明并强制返回类型的sapply。mapply(FUN, x, y, ...):同时并行遍历多个输入,逐个元素处理。tapply(values, groups, FUN):在每个组内应用FUN,每组得到一个结果。
它们都接受你自己写的函数,包括匿名函数,这正是这个函数族强大的地方。
apply():矩阵的行和列
apply 接收一个矩阵、一个 MARGIN(1 表示行,2 表示列)和一个函数:
矩阵是按列填充的,所以两行分别是 1 3 5 和 2 4 6:行和打印为 9 12,列和打印为 3 7 11。对于最常见的两种情况,基础 R 提供了专门的、更快的辅助函数:rowSums、colSums、rowMeans、colMeans。所以 apply 留给没有现成辅助函数的情况,比如 apply(m, 2, max)。
有一个陷阱:对数据框使用 apply 时,它会先悄悄地把数据框转换成矩阵,从而把每一列都强制转换成同一种类型。处理数据框时,把各列当作一个列表,改用 lapply/sapply。
lapply() 总是返回列表,sapply() 会简化
lapply 对每个元素运行函数,返回一个长度相同的列表,无论如何都是这样:
同样的计算,两种形状:lapply 返回一个装着 85 和 80 的列表;sapply 注意到每个结果的长度都是 1,于是简化成一个带名字的数值向量,读起来舒服得多,也更方便继续参与数学运算。
不过这种便利也有锋利的一面:sapply 的返回类型取决于数据。哪怕只有一个元素产生了不同的长度,简化就会失败,你又会悄悄地得到一个列表:
交互使用时这无所谓;但在脚本里,这意味着一整年都正常运行的代码,会在数据形状改变的那天突然出错。这正是 vapply 存在的全部理由。
vapply():类型安全的 sapply
vapply 多了第三个参数 FUN.VALUE:一个模板,声明单个结果必须是什么样子。integer(1) 表示“每次调用恰好返回一个整数”:
你会得到一个带名字的整数向量 5 6 6,更重要的是得到了一个保证:如果 nchar 哪天返回了两个值或一个字符,vapply 会在调用处直接报错停止,而不是让形状错误的结果流到后面的代码里。这个声明同时也起到了文档的作用:
vapply(words, nchar, character(1))
# Error in vapply(words, nchar, character(1)) : values must be type 'character'
经验法则:在控制台用 sapply,在需要无人值守运行的函数和脚本里用 vapply。
mapply() 和 tapply():并行输入与分组
lapply 只遍历一个结构。当每次调用都需要从多个结构的相同位置各取一个元素时,用 mapply。注意函数放在第一个参数:
每个输出元素先把各自的第一个值配对,再配对第二个值,依此类推:10 200 3000 40000。
tapply 是负责分组的成员:它按 groups 把 values 拆开,在每个组内应用函数,每组返回一个结果:
组 a 的均值是 30,组 b 的均值是 40。这是基础 R 对“按类别求平均”的解答,和 group_by + summarize 为数据框回答的是同一类问题。所以如果你已经在用 dplyr,就用它;当你手上只有两个普通向量、想一行搞定时,tapply 最合适。
有两个便利特性适用于整个函数族。写在函数后面的额外参数,每次调用时都会转发给这个函数:
另外,没有现成的函数可用时,可以直接写匿名函数:sapply(x, \(v) max(v) - min(v)) 不需要先定义一个辅助函数,就能算出每个元素的极差。
apply 函数族与 for 循环
你会听说在 R 中 for 循环很慢,而 apply 函数族很快。这基本上是个误解。真正慢的是在循环中不断扩充结果:result <- c(result, new_value) 每轮都会复制整个向量。预先分配好输出的循环,性能完全没问题:
所以要根据可读性来选,而不是性能。apply 版本一行就说清了要做什么(“对每个元素求平方”),还替你处理好了内存分配,这就是它成为简单逐元素操作惯用写法的原因。而当每轮迭代依赖前一轮的结果、需要用 break 提前退出,或者循环体长到写成匿名函数反而更难读时,for 循环就有了用武之地。两种都是正当的 R 写法。
要点回顾
- 整个函数族就是一个思路:对每个元素运行一个函数,收集结果。
apply用于矩阵的行(MARGIN = 1)和列(MARGIN = 2);有rowSums/colMeans时优先用它们。lapply总是返回列表;sapply尽可能简化,这意味着它的返回类型可能随数据而变。vapply锁定返回类型;在不能出意外的代码里用它。mapply把多个输入对齐组合;tapply在组内汇总值。- for 循环本身并不慢,慢的是不断扩充向量。选择读起来最清楚的写法。
下一篇:管道,这个运算符能把这些调用串成可读的、一步一步的处理流程。
常见问题
R 中 lapply 和 sapply 有什么区别?
lapply 总是返回列表,没有例外。sapply 执行同样的计算,然后尝试简化结果:如果每个元素的长度都是 1,就简化成向量;如果长度都相同,就简化成矩阵;如果无法简化,就仍然返回列表。交互使用时 sapply 更顺手;在脚本里 lapply(或 vapply)更安全,因为它的返回类型永远不会变。
R 中的 apply() 是做什么的?
apply(m, MARGIN, FUN) 在矩阵上运行 FUN:MARGIN = 1 表示对每一行应用,MARGIN = 2 表示对每一列应用。apply(m, 1, sum) 得到行和,apply(m, 2, mean) 得到列均值。它是为矩阵和数组准备的;处理列表和向量请改用 lapply/sapply。
R 中 apply 函数族比 for 循环快吗?
通常快不了多少,这是个误解。写得好、预先分配了结果向量的 for 循环,性能与之相当。让 for 循环背上坏名声的,是那些每次只往结果里加一个元素、每轮都复制全部内容的循环。选择 apply 函数族是为了简洁和表达意图,而不是为了速度。
R 中的 vapply 有什么用?
vapply 是带“契约”的 sapply:你要声明每个结果的类型和长度,例如 vapply(x, nchar, integer(1))。如果函数返回了其他东西,R 会立即报错,而不是悄悄地给你一个意料之外的结构。在需要无人值守、长期稳定运行的代码里,优先用它。