【问题标题】:Is R's apply family more than syntactic sugar?R 的应用系列不仅仅是语法糖吗?
【发布时间】:2011-01-17 13:37:57
【问题描述】:

...关于执行时间和/或内存。

如果这不是真的,用代码 sn-p 证明它。请注意,向量化的加速不计算在内。加速必须来自apply (tapply, sapply, ...) 本身。

【问题讨论】:

    标签: r apply


    【解决方案1】:

    ...正如我刚刚在其他地方所写的,vapply 是你的朋友! ...这就像 sapply,但您还指定了返回值类型,这使它更快。

    foo <- function(x) x+1
    y <- numeric(1e6)
    
    system.time({z <- numeric(1e6); for(i in y) z[i] <- foo(i)})
    #   user  system elapsed 
    #   3.54    0.00    3.53 
    system.time(z <- lapply(y, foo))
    #   user  system elapsed 
    #   2.89    0.00    2.91 
    system.time(z <- vapply(y, foo, numeric(1)))
    #   user  system elapsed 
    #   1.35    0.00    1.36 
    

    一月。 2020年1月1日更新:

    system.time({z1 <- numeric(1e6); for(i in seq_along(y)) z1[i] <- foo(y[i])})
    #   user  system elapsed 
    #   0.52    0.00    0.53 
    system.time(z <- lapply(y, foo))
    #   user  system elapsed 
    #   0.72    0.00    0.72 
    system.time(z3 <- vapply(y, foo, numeric(1)))
    #   user  system elapsed 
    #    0.7     0.0     0.7 
    identical(z1, z3)
    # [1] TRUE
    

    【讨论】:

    • 最初的发现似乎不再正确。 for 循环在我的 Windows 10 2 核计算机上更快。我用 5e6 元素做到了这一点 - 循环是 2.9 秒,而 vapply 是 3.1 秒。
    【解决方案2】:

    我在其他地方写过,像 Shane 的例子并没有真正强调各种循环语法之间的性能差异,因为时间都花在了函数中,而不是真正强调循环。此外,代码不公平地将没有内存的 for 循环与返回值的应用族函数进行比较。这里有一个稍微不同的例子来强调这一点。

    foo <- function(x) {
       x <- x+1
     }
    y <- numeric(1e6)
    system.time({z <- numeric(1e6); for(i in y) z[i] <- foo(i)})
    #   user  system elapsed 
    #  4.967   0.049   7.293 
    system.time(z <- sapply(y, foo))
    #   user  system elapsed 
    #  5.256   0.134   7.965 
    system.time(z <- lapply(y, foo))
    #   user  system elapsed 
    #  2.179   0.126   3.301 
    

    如果您打算保存结果,则应用族函数可能比语法糖

    (z 的简单 unlist 只有 0.2 秒,因此 lapply 快得多。在 for 循环中初始化 z 非常快,因为我给出了 6 次运行中最后 5 次的平均值,因此将其移到系统之外.time 几乎不会影响事物)

    还有一点需要注意的是,使用 apply 系列函数还有另一个原因,与它们的性能、清晰度或缺乏副作用无关。 for 循环通常会促进尽可能多地放入循环中。这是因为每个循环都需要设置变量来存储信息(以及其他可能的操作)。 Apply 语句往往偏向于另一种方式。通常,您希望对数据执行多项操作,其中一些可以矢量化,但有些可能无法矢量化。在 R 中,与其他语言不同,最好将这些操作分开并运行那些在 apply 语句(或函数的向量化版本)中未向量化的操作,以及那些被向量化为真正向量操作的操作。这通常会极大地提高性能。

    以 Joris Meys 为例,他用一个方便的 R 函数替换了传统的 for 循环,我们可以使用它来展示以更 R 友好的方式编写代码的效率,以实现类似的加速,而无需专门的函数。

    set.seed(1)  #for reproducability of the results
    
    # The data - copied from Joris Meys answer
    X <- rnorm(100000)
    Y <- as.factor(sample(letters[1:5],100000,replace=T))
    Z <- as.factor(sample(letters[1:10],100000,replace=T))
    
    # an R way to generate tapply functionality that is fast and 
    # shows more general principles about fast R coding
    YZ <- interaction(Y, Z)
    XS <- split(X, YZ)
    m <- vapply(XS, mean, numeric(1))
    m <- matrix(m, nrow = length(levels(Y)))
    rownames(m) <- levels(Y)
    colnames(m) <- levels(Z)
    m
    

    这最终比for 循环快得多,并且比内置优化的tapply 函数慢一点。这并不是因为vapplyfor 快得多,而是因为它在循环的每次迭代中只执行一个操作。在这段代码中,其他所有内容都是矢量化的。在 Joris Meys 传统的for 循环中,每次迭代都会发生许多(7?)操作,并且有相当多的设置只是为了执行它。另请注意,这比 for 版本更紧凑。

    【讨论】:

    • 但 Shane 的示例是现实的,因为大部分时间 通常花在函数中,而不是循环中。
    • 为自己说话... :)... 也许 Shane 的观点在某种意义上是现实的,但在同样的意义上,分析完全没用。人们在必须进行大量迭代时会关心迭代机制的速度,否则他们的问题无论如何都在别处。任何功能都是如此。如果我写一个需要 0.001 秒的罪,而其他人写一个需要 0.002 的罪,谁在乎?好吧,只要你必须做一堆你关心的。
    • 在 12 核 3Ghz 英特尔至强,64 位上,我得到的数字与你完全不同 - for 循环显着改善:对于你的三个测试,我得到 2.798 0.003 2.803; 4.908 0.020 4.934; 1.498 0.025 1.528,并且 vapply 更好:@ 987654331@
    • 它确实因操作系统和 R 版本而异......绝对意义上的 CPU。我刚刚在 Mac 上运行了 2.15.2,得到了 sapply 50% 比 forlapply 快两倍。
    • 在您的示例中,您的意思是将y 设置为1:1e6,而不是numeric(1e6)(零向量)。试图一遍又一遍地将foo(0) 分配给z[0] 并不能很好地说明典型的for 循环用法。否则,该消息就会出现。
    【解决方案3】:

    在向量子集上应用函数时,tapply 可能比 for 循环快得多。示例:

    df <- data.frame(id = rep(letters[1:10], 100000),
                     value = rnorm(1000000))
    
    f1 <- function(x)
      tapply(x$value, x$id, sum)
    
    f2 <- function(x){
      res <- 0
      for(i in seq_along(l <- unique(x$id)))
        res[i] <- sum(x$value[x$id == l[i]])
      names(res) <- l
      res
    }            
    
    library(microbenchmark)
    
    > microbenchmark(f1(df), f2(df), times=100)
    Unit: milliseconds
       expr      min       lq   median       uq      max neval
     f1(df) 28.02612 28.28589 28.46822 29.20458 32.54656   100
     f2(df) 38.02241 41.42277 41.80008 42.05954 45.94273   100
    

    apply,然而,在大多数情况下并没有提供任何速度提升,在某些情况下甚至会慢很多:

    mat <- matrix(rnorm(1000000), nrow=1000)
    
    f3 <- function(x)
      apply(x, 2, sum)
    
    f4 <- function(x){
      res <- 0
      for(i in 1:ncol(x))
        res[i] <- sum(x[,i])
      res
    }
    
    > microbenchmark(f3(mat), f4(mat), times=100)
    Unit: milliseconds
        expr      min       lq   median       uq      max neval
     f3(mat) 14.87594 15.44183 15.87897 17.93040 19.14975   100
     f4(mat) 12.01614 12.19718 12.40003 15.00919 40.59100   100
    

    但是对于这些情况,我们有colSumsrowSums

    f5 <- function(x)
      colSums(x) 
    
    > microbenchmark(f5(mat), times=100)
    Unit: milliseconds
        expr      min       lq   median       uq      max neval
     f5(mat) 1.362388 1.405203 1.413702 1.434388 1.992909   100
    

    【讨论】:

    • 重要的是要注意(对于小段代码)microbenchmark 它比system.time 精确得多。如果您尝试比较system.time(f3(mat))system.time(f4(mat)),几乎每次都会得到不同的结果。有时只有适当的基准测试才能显示最快的功能。
    【解决方案4】:

    R 中的 apply 函数没有提供比其他循环函数更好的性能(例如 for)。 lapply 是一个例外,它可以更快一点,因为它在 C 代码中比在 R 中做更多的工作(参见 this question for an example of this)。

    但总的来说,规则是您应该使用 apply 函数来清晰,而不是性能

    我要补充一点,apply 函数有 no side effects,这是使用 R 进行函数式编程时的一个重要区别。这可以通过使用覆盖assign&lt;&lt;-,但这可能非常危险。副作用也会使程序更难理解,因为变量的状态取决于历史。

    编辑:

    只是通过一个递归计算斐波那契数列的简单示例来强调这一点;这可以运行多次以获得准确的测量值,但关键是没有一种方法具有显着不同的性能:

    > fibo <- function(n) {
    +   if ( n < 2 ) n
    +   else fibo(n-1) + fibo(n-2)
    + }
    > system.time(for(i in 0:26) fibo(i))
       user  system elapsed 
       7.48    0.00    7.52 
    > system.time(sapply(0:26, fibo))
       user  system elapsed 
       7.50    0.00    7.54 
    > system.time(lapply(0:26, fibo))
       user  system elapsed 
       7.48    0.04    7.54 
    > library(plyr)
    > system.time(ldply(0:26, fibo))
       user  system elapsed 
       7.52    0.00    7.58 
    

    编辑 2:

    关于 R 的并行包的使用(例如 rpvm、rmpi、snow),它们通常提供apply 系列功能(即使foreach 包本质上是等效的,尽管名称如此)。这是snowsapply函数的简单示例:

    library(snow)
    cl <- makeSOCKcluster(c("localhost","localhost"))
    parSapply(cl, 1:20, get("+"), 3)
    

    本例使用socket集群,无需安装额外软件;否则您将需要 PVM 或 MPI 之类的东西(请参阅Tierney's clustering page)。 snow 具有以下应用功能:

    parLapply(cl, x, fun, ...)
    parSapply(cl, X, FUN, ..., simplify = TRUE, USE.NAMES = TRUE)
    parApply(cl, X, MARGIN, FUN, ...)
    parRapply(cl, x, fun, ...)
    parCapply(cl, x, fun, ...)
    

    apply 函数应该用于并行执行是有道理的,因为它们没有side effects。当您在 for 循环中更改变量值时,它会被全局设置。另一方面,所有apply 函数都可以安全地并行使用,因为更改是函数调用的局部变量(除非您尝试使用assign&lt;&lt;-,在这种情况下您可能会引入副作用)。不用说,注意局部变量和全局变量至关重要,尤其是在处理并行执行时。

    编辑:

    这里有一个简单的例子来说明for*apply 在副作用方面的区别:

    > df <- 1:10
    > # *apply example
    > lapply(2:3, function(i) df <- df * i)
    > df
     [1]  1  2  3  4  5  6  7  8  9 10
    > # for loop example
    > for(i in 2:3) df <- df * i
    > df
     [1]  6 12 18 24 30 36 42 48 54 60
    

    注意父环境中的df 是如何被for 而不是*apply 改变的。

    【讨论】:

    • 大多数 R 的多核包也通过 apply 系列函数实现并行化。因此,结构化程序以便它们使用 apply 可以以非常小的边际成本进行并行化。
    • Sharpie - 谢谢你!任何想法显示(在 Windows XP 上)的示例?
    • 我建议查看snowfall 包并尝试他们的小插图中的示例。 snowfall 构建在 snow 包之上,并进一步抽象了并行化的细节,使得执行并行化的 apply 函数变得非常简单。
    • @Sharpie 但请注意,foreach 已经可用并且似乎在 SO 上受到了很多询问。
    • @Shane,在您的答案的最顶部,您链接到另一个问题作为lapplyfor 循环“快一点”的例子。但是,在那里,我没有看到任何建议。您只提到lapplysapply 快,这是众所周知的事实,因为其他原因(sapply 试图简化输出,因此必须进行大量数据大小检查和潜在转换)。与for 无关。我错过了什么吗?
    【解决方案5】:

    有时加速可能会很大,例如当您必须嵌套 for 循环以根据多个因素的分组获得平均值时。在这里,您有两种方法可以得到完全相同的结果:

    set.seed(1)  #for reproducability of the results
    
    # The data
    X <- rnorm(100000)
    Y <- as.factor(sample(letters[1:5],100000,replace=T))
    Z <- as.factor(sample(letters[1:10],100000,replace=T))
    
    # the function forloop that averages X over every combination of Y and Z
    forloop <- function(x,y,z){
    # These ones are for optimization, so the functions 
    #levels() and length() don't have to be called more than once.
      ylev <- levels(y)
      zlev <- levels(z)
      n <- length(ylev)
      p <- length(zlev)
    
      out <- matrix(NA,ncol=p,nrow=n)
      for(i in 1:n){
          for(j in 1:p){
              out[i,j] <- (mean(x[y==ylev[i] & z==zlev[j]]))
          }
      }
      rownames(out) <- ylev
      colnames(out) <- zlev
      return(out)
    }
    
    # Used on the generated data
    forloop(X,Y,Z)
    
    # The same using tapply
    tapply(X,list(Y,Z),mean)
    

    两者都给出完全相同的结果,都是一个 5 x 10 矩阵,具有平均值和命名的行和列。但是:

    > system.time(forloop(X,Y,Z))
       user  system elapsed 
       0.94    0.02    0.95 
    
    > system.time(tapply(X,list(Y,Z),mean))
       user  system elapsed 
       0.06    0.00    0.06 
    

    给你。我赢了什么? ;-)

    【讨论】:

    • 啊,太可爱了 :-) 我真的想知道是否有人会遇到我相当晚的答案。
    • 我总是按“活跃”排序。 :) 不确定如何概括您的答案;有时*apply 更快。但我认为更重要的一点是副作用(用一个例子更新了我的答案)。
    • 我认为当您想将函数应用于不同的子集时,apply 尤其快。如果嵌套循环有一个智能应用解决方案,我想应用解决方案也会更快。在大多数情况下,我猜 apply 并没有获得太多的速度,但我绝对同意副作用。
    • 这有点离题,但对于这个具体的例子,data.table 更快,我认为“更容易”。 library(data.table)dt&lt;-data.table(X,Y,Z,key=c("Y,Z"))system.time(dt[,list(X_mean=mean(X)),by=c("Y,Z")])
    • 这种比较是荒谬的。 tapply 是针对特定任务的专用函数,这就是它比 for 循环更快的原因。它不能做 for 循环可以做的事情(而常规 apply 可以)。你在比较苹果和橙子。
    猜你喜欢
    • 2015-06-25
    • 2012-01-12
    • 1970-01-01
    • 2013-11-28
    • 2011-10-26
    • 2020-01-10
    • 1970-01-01
    • 2011-08-14
    相关资源
    最近更新 更多