"apply"函数有哪些优点?何时使用它们比使用"for"循环更好,何时不是?

8

可能是重复问题:
R的apply家族是否只是语法糖

就像标题所说的那样。也许是一个愚蠢的问题,但我的理解是,在使用“apply”函数时,迭代是在编译代码中而不是在R解析器中执行的。这似乎意味着,例如lapply仅在有很多迭代且每个操作相对简单时才比“for”循环更快。例如,如果lapply中包装的单个函数调用需要10秒钟,并且只有12次迭代,我想象使用“for”和“lapply”之间几乎没有任何区别。

现在我想了想,如果“lapply”内部的函数必须被解析,除非您正在执行某些已编译函数的操作(例如求和或乘法等),否则为什么使用“lapply”而不是“for”会有任何性能优势呢?

提前感谢!

Josh


1
请查看此线程:https://dev59.com/73E95IYBdhLWcg3wheNR - Gavin Simpson
1
这一个:https://dev59.com/YW035IYBdhLWcg3wPdkS - Aaron left Stack Overflow
1
这个问题在2008年5月的R帮助台文章中也有很好的解答:http://promberger.info/files/rnews-vectorvsloops2008.pdf - DrewConway
这个链接也是:https://dev59.com/Mk7Sa4cB1Zd3GeqP57Ut - SRKX
2个回答

14

为什么有些人更喜欢使用apply类函数而不是for循环?或者反之亦然,这其中有几个原因。

首先,如果正确执行,for()以及apply(), sapply()的执行速度通常相当。相比之下,lapply()在R内部的编译代码中完成了更多的操作,所以它可以比其他函数更快。这种速度优势似乎最明显的情况是当“循环”数据本身占用了较大的计算时间时;在一般的日常应用中,你可能难以从本质上更快的lapply()中获得多少收益。总的来说,这些函数都会调用R函数,因此需要解释并运行。

for()循环通常更容易实现,尤其是如果您来自于一个循环程序背景。使用循环可能比将迭代计算强制转化为apply家族函数更自然。然而,为了正确使用for()循环,您需要进行一些额外的工作来设置存储和管理将循环的输出重新组合的过程。apply函数可以自动地为您完成这些操作,例如:

IN <- runif(10)
OUT <- logical(length = length(IN))
for(i in IN) {
    OUT[i] <- IN > 0.5
}

这只是一个愚蠢的例子,因为>是一个向量化运算符,但我想表达的是,你必须管理输出。主要问题在于,对于for()循环,您始终需要分配足够的存储空间来容纳输出,然后才能开始循环。如果您不知道需要多少存储空间,那么就分配一个合理的块状存储空间,然后在循环中检查是否已用尽该存储空间,并添加另一个大块的存储空间。

在我看来,使用apply函数族的主要原因是使代码更优雅、更易读。我们可以让R处理输出存储和设置循环(如上所示),并简洁地要求R在数据子集上运行函数。对我来说,速度通常不是决定因素。我使用最适合情况的函数,并且会生成简单易懂的代码,因为如果我无法记住代码一天、一周或更长时间,我浪费的时间可能比我节省的时间还要多!

apply函数族适用于标量或向量操作。一个for()循环通常适用于使用相同索引 i 执行多个迭代操作。例如,我编写的代码使用for()循环在对象上进行 k 重或自举交叉验证。我可能永远不会考虑使用apply函数族中的一个来完成这个任务,因为每个CV迭代需要多个操作,需要访问当前帧中的大量对象,并填充几个输出对象以容纳迭代的输出。

至于最后一点,即lapply()为什么可能比for()apply()更快,您需要意识到“循环”可以在解释的R代码或编译的代码中执行。是的,两者仍然会调用需要解释的R函数,但是如果您正在从编译的C代码(例如lapply())直接进行循环和调用,则性能提升可能就来自于此,而apply()则相当于实际的R代码中的for()循环。请查看apply()的源代码,看看它是一个for()循环的包装器,然后查看lapply()的代码,如下所示:

> lapply
function (X, FUN, ...) 
{
    FUN <- match.fun(FUN)
    if (!is.vector(X) || is.object(X)) 
        X <- as.list(X)
    .Internal(lapply(X, FUN))
}
<environment: namespace:base>

你应该能够看出为什么lapply()for()与其他apply系列函数的速度会有所不同。.Internal()是R调用编译过的C代码的一种方式。除了对FUN进行操作和检查外,整个计算都是在C中完成的,同时调用R函数FUN。将其与apply()的源代码进行比较。


不错!你成功地添加了一些超出相关问题的新信息。谢谢,我学到了一些新东西。 - Aaron left Stack Overflow
太好了,谢谢。我想澄清一件事情,关于可能的性能提升。在lapply()调用内执行的代码每次迭代仍然需要相同的时间,对吧?所以,如果每次迭代本身需要几秒钟或更长时间,那么与apply()或for()相比的性能增益将是微不足道的,我的理解正确吗? - Josh

3

来自Burns的R地狱(pdf),第25页:

当每次迭代是一个非平凡的任务时,请使用显式的for循环。但是,如果一个简单的循环可以更清晰、更简洁地表达,那么可以使用apply函数。有至少一个例外...如果结果将是一个列表,其中一些组件可能是NULL,那么使用for循环会有麻烦(大麻烦),而lapply会给出预期的答案。


1
完全不同意。如果迭代是一项非平凡的任务,最好将该代码块移入一个适当的函数,然后使用*apply()调用它。 - geoffjentry

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接