【问题标题】:Speed up the loop operation in R加快 R 中的循环操作
【发布时间】:2011-02-23 22:16:40
【问题描述】:

我在 R 中有一个很大的性能问题。我编写了一个迭代 data.frame 对象的函数。它只是向data.frame 添加一个新列并累积一些内容。 (操作简单)。 data.frame 有大约 850K 行。我的电脑仍在工作(现在大约 10 小时),我不知道运行时间。

dayloop2 <- function(temp){
    for (i in 1:nrow(temp)){    
        temp[i,10] <- i
        if (i > 1) {             
            if ((temp[i,6] == temp[i-1,6]) & (temp[i,3] == temp[i-1,3])) { 
                temp[i,10] <- temp[i,9] + temp[i-1,10]                    
            } else {
                temp[i,10] <- temp[i,9]                                    
            }
        } else {
            temp[i,10] <- temp[i,9]
        }
    }
    names(temp)[names(temp) == "V10"] <- "Kumm."
    return(temp)
}

任何想法如何加快此操作?

【问题讨论】:

  • 在测试您的函数时考虑添加类似if(i%%1000) {print(i)}的内容,以大致了解运行时

标签: performance r loops rcpp r-faq


【解决方案1】:

最大的问题和无效的根源是索引 data.frame,我的意思是您使用 temp[,] 的所有这些行。
尽量避免这种情况。我采用了您的功能,更改了索引,并在此处 version_A

dayloop2_A <- function(temp){
    res <- numeric(nrow(temp))
    for (i in 1:nrow(temp)){    
        res[i] <- i
        if (i > 1) {             
            if ((temp[i,6] == temp[i-1,6]) & (temp[i,3] == temp[i-1,3])) { 
                res[i] <- temp[i,9] + res[i-1]                   
            } else {
                res[i] <- temp[i,9]                                    
            }
        } else {
            res[i] <- temp[i,9]
        }
    }
    temp$`Kumm.` <- res
    return(temp)
}

如您所见,我创建了矢量res 来收集结果。最后我将它添加到data.frame,我不需要乱用名字。 那么它有多好呢?

我用nrowdata.frame 运行每个函数,从1,000 到10,000 x 1,000,并用system.time 测量时间

X <- as.data.frame(matrix(sample(1:10, n*9, TRUE), n, 9))
system.time(dayloop2(X))

结果是

您可以看到您的版本与nrow(X) 成指数关系。修改后的版本是线性关系,简单的lm模型预测85万行计算需要6分10秒。

矢量化的力量

正如 Shane 和 Calimo 在他们的回答中所说,矢量化是提高性能的关键。 从您的代码中,您可以移出循环:

  • 调理
  • 初始化结果(temp[i,9]

这导致了这段代码

dayloop2_B <- function(temp){
    cond <- c(FALSE, (temp[-nrow(temp),6] == temp[-1,6]) & (temp[-nrow(temp),3] == temp[-1,3]))
    res <- temp[,9]
    for (i in 1:nrow(temp)) {
        if (cond[i]) res[i] <- temp[i,9] + res[i-1]
    }
    temp$`Kumm.` <- res
    return(temp)
}

比较此函数的结果,这次是 nrow 从 10,000 到 100,000 乘以 10,000。

调谐调谐

另一个调整是在循环索引中将temp[i,9] 更改为res[i](在第 i 次循环迭代中完全相同)。 这又是索引向量和索引data.frame 之间的区别。
第二件事:当您查看循环时,您会发现不需要循环所有i,而只需要循环那些符合条件的。
所以我们开始吧

dayloop2_D <- function(temp){
    cond <- c(FALSE, (temp[-nrow(temp),6] == temp[-1,6]) & (temp[-nrow(temp),3] == temp[-1,3]))
    res <- temp[,9]
    for (i in (1:nrow(temp))[cond]) {
        res[i] <- res[i] + res[i-1]
    }
    temp$`Kumm.` <- res
    return(temp)
}

您获得的性能高度取决于数据结构。准确地说 - 条件中TRUE 值的百分比。 对于我的模拟数据,在一秒以下的 850,000 行中需要计算时间。

我希望你能走得更远,我看到至少有两件事可以做:

  • 写一个C代码来做条件cumsum
  • 如果您知道数据中的最大序列不大,那么您可以将循环更改为矢量化 while,类似于

    while (any(cond)) {
        indx <- c(FALSE, cond[-1] & !cond[-n])
        res[indx] <- res[indx] + res[which(indx)-1]
        cond[indx] <- FALSE
    }
    

用于模拟和数字的代码是available on GitHub

【讨论】:

  • 由于我无法私下询问 Marek,这些图表是如何生成的?
  • @carbontwelve 你问的是数据还是图?地块是用格子包制作的。如果有时间,我会将代码放在网络上的某个位置并通知您。
  • @carbontwelve 哎呀,我错了 :) 这是标准图(来自基础 R)。
  • @Gregor 不幸的是没有。它是累积的,因此您无法对其进行矢量化。简单例子:res = c(1,2,3,4)cond都是TRUE,那么最终结果应该是:13(因为1+2),6(因为第二个现在是3,第三个也是3),106+4)。做简单的总结你得到1357
  • 啊,我应该仔细考虑一下的。谢谢你告诉我这个错误。
【解决方案2】:

加速 R 代码的一般策略

首先,找出慢速部分的位置。没有必要优化运行缓慢的代码。对于少量代码,简单地思考一下就可以了。如果失败,RProf 和类似的分析工具可能会有所帮助。

找出瓶颈后,请考虑更有效的算法来做你想做的事。如果可能,计算应该只运行一次,所以:

使用更高效的函数可以产生中等或较大的速度增益。例如,paste0 产生的效率增益很小,但 .colSums() 和它的亲戚会产生更明显的增益。 meanparticularly slow

那么你就可以避免一些特别常见的麻烦

  • cbind 会很快让你慢下来。
  • 初始化您的数据结构,然后填写它们,rather than expanding them each time
  • 即使使用预分配,您也可以切换到按引用传递的方法而不是按值传递的方法,但这可能不值得麻烦。
  • 查看R Inferno,了解更多要避免的陷阱。

尝试更好的矢量化,这通常会有所帮助,但并非总是如此。在这方面,像ifelsediff 等固有的矢量化命令将比apply 系列命令提供更多的改进(在编写良好的循环上几乎没有速度提升)。

您也可以尝试向 R 函数提供更多信息。例如,使用vapply rather than sapply,并指定colClasses when reading in text-based data。速度增益将根据您消除多少猜测而变化。

接下来,考虑优化包data.table 包可以在数据处理和读取大量数据 (fread) 中使用它时产生巨大的速度提升。

接下来,尝试通过更有效的调用 R 的方式来提高速度:

  • 编译您的 R 脚本。或者使用 Rajit 包进行实时编译(Dirk 在 this presentation 中有一个示例)。
  • 确保您使用的是优化的 BLAS。这些提供了全面的速度增益。老实说,R 没有在安装时自动使用最高效的库,这很遗憾。希望Revolution R 将他们在这里所做的工作贡献给整个社区。​​li>
  • Radford Neal 进行了一系列优化,其中一些被 R Core 采用,还有许多被分叉到 pqR

最后,如果以上所有方法仍然无法让您达到您需要的速度,您可能需要为慢速代码 sn-p 迁移到 更快的语言。这里Rcppinline 的组合使得只用C++ 代码替换算法中最慢的部分变得特别容易。例如,这里是 my first attempt at doing so,即使是高度优化的 R 解决方案,它也会让你望而却步。

如果您在这一切之后仍然遇到麻烦,您只需要更多的计算能力。研究并行化 (http://cran.r-project.org/web/views/HighPerformanceComputing.html) 甚至基于 GPU 的解决方案 (gpu-tools)。

其他指南的链接

【讨论】:

    【解决方案3】:

    如果您使用for 循环,您很可能将R 编码为C 或Java 或其他东西。正确矢量化的 R 代码非常快。

    以这两个简单的代码为例,依次生成一个包含 10,000 个整数的列表:

    第一个代码示例是如何使用传统的编码范例编写循环。完成需要 28 秒

    system.time({
        a <- NULL
        for(i in 1:1e5)a[i] <- i
    })
       user  system elapsed 
      28.36    0.07   28.61 
    

    通过预分配内存的简单操作,您可以获得近 100 倍的改进:

    system.time({
        a <- rep(1, 1e5)
        for(i in 1:1e5)a[i] <- i
    })
    
       user  system elapsed 
       0.30    0.00    0.29 
    

    但是使用冒号运算符 : 的基本 R 向量运算,这个运算几乎是瞬时的:

    system.time(a <- 1:1e5)
    
       user  system elapsed 
          0       0       0 
    

    【讨论】:

    • +1 虽然我认为你的第二个例子没有说服力,因为a[i] 没有改变。但是system.time({a &lt;- NULL; for(i in 1:1e5){a[i] &lt;- 2*i} }); system.time({a &lt;- 1:1e5; for(i in 1:1e5){a[i] &lt;- 2*i} }); system.time({a &lt;- NULL; a &lt;- 2*(1:1e5)}) 有类似的结果。
    • @Henry,公平的评论,但正如你所指出的,结果是一样的。我已修改示例以将 a 初始化为 rep(1, 1e5) - 时间相同。
    • 确实,只要有可能,矢量化就是要走的路,但有些循环根本无法以这种方式重新排列
    【解决方案4】:

    这可以通过使用索引或嵌套的ifelse() 语句跳过循环来加快速度。

    idx <- 1:nrow(temp)
    temp[,10] <- idx
    idx1 <- c(FALSE, (temp[-nrow(temp),6] == temp[-1,6]) & (temp[-nrow(temp),3] == temp[-1,3]))
    temp[idx1,10] <- temp[idx1,9] + temp[which(idx1)-1,10] 
    temp[!idx1,10] <- temp[!idx1,9]    
    temp[1,10] <- temp[1,9]
    names(temp)[names(temp) == "V10"] <- "Kumm."
    

    【讨论】:

    • 感谢您的回答。我试图理解你的陈述。第 4 行:“temp[idx1,10]
    • 如果您提供数据样本(使用 dput() 几行),那么我会为您修复它。由于 which()-1 位,索引不相等。但是你应该从这里看到它是如何工作的:不需要任何循环或应用;只需使用矢量化函数。
    • 哇!我刚刚将嵌套的 if..else 函数块和 mapply 更改为嵌套的 ifelse 函数,并获得了 200 倍的加速!
    • 您的一般建议是正确的,但在代码中您错过了一个事实,即 i-th 值取决于 i-1-th 所以它们不能以您的方式设置(使用 @ 987654325@).
    【解决方案5】:

    正如 Ari 在回答末尾提到的那样,Rcppinline 软件包使快速处理变得非常容易。例如,试试这个inline 代码(警告:未测试):

    body <- 'Rcpp::NumericMatrix nm(temp);
             int nrtemp = Rccp::as<int>(nrt);
             for (int i = 0; i < nrtemp; ++i) {
                 temp(i, 9) = i
                 if (i > 1) {
                     if ((temp(i, 5) == temp(i - 1, 5) && temp(i, 2) == temp(i - 1, 2) {
                         temp(i, 9) = temp(i, 8) + temp(i - 1, 9)
                     } else {
                         temp(i, 9) = temp(i, 8)
                     }
                 } else {
                     temp(i, 9) = temp(i, 8)
                 }
             return Rcpp::wrap(nm);
            '
    
    settings <- getPlugin("Rcpp")
    # settings$env$PKG_CXXFLAGS <- paste("-I", getwd(), sep="") if you want to inc files in wd
    dayloop <- cxxfunction(signature(nrt="numeric", temp="numeric"), body-body,
        plugin="Rcpp", settings=settings, cppargs="-I/usr/include")
    
    dayloop2 <- function(temp) {
        # extract a numeric matrix from temp, put it in tmp
        nc <- ncol(temp)
        nm <- dayloop(nc, temp)
        names(temp)[names(temp) == "V10"] <- "Kumm."
        return(temp)
    }
    

    #includeing 的事情也有类似的过程,你只需传递一个参数

    inc <- '#include <header.h>
    

    到 cxx 函数,如 include=inc。真正酷的是它为您完成了所有的链接和编译,因此原型设计非常快。

    免责声明:我不完全确定 tmp 的类应该是数字而不是数字矩阵或其他东西。但我很确定。

    编辑:如果您在此之后仍然需要更快的速度,OpenMP 是一个适合C++ 的并行化工具。我没有尝试从inline 使用它,但它应该可以工作。这个想法是,在n 核心的情况下,循环迭代kk % n 执行。在 Matloff 的 R 编程艺术here,第 16 章,Resorting to C)中找到了合适的介绍。

    【讨论】:

      【解决方案6】:

      我不喜欢重写代码...当然 ifelse 和 lapply 是更好的选择,但有时很难做到这一点。

      我经常使用 data.frames,因为我会使用 df$var[i] 等列表

      这是一个虚构的例子:

      nrow=function(x){ ##required as I use nrow at times.
        if(class(x)=='list') {
          length(x[[names(x)[1]]])
        }else{
          base::nrow(x)
        }
      }
      
      system.time({
        d=data.frame(seq=1:10000,r=rnorm(10000))
        d$foo=d$r
        d$seq=1:5
        mark=NA
        for(i in 1:nrow(d)){
          if(d$seq[i]==1) mark=d$r[i]
          d$foo[i]=mark
        }
      })
      
      system.time({
        d=data.frame(seq=1:10000,r=rnorm(10000))
        d$foo=d$r
        d$seq=1:5
        d=as.list(d) #become a list
        mark=NA
        for(i in 1:nrow(d)){
          if(d$seq[i]==1) mark=d$r[i]
          d$foo[i]=mark
        }
        d=as.data.frame(d) #revert back to data.frame
      })
      

      data.frame 版本:

         user  system elapsed 
         0.53    0.00    0.53
      

      列表版本:

         user  system elapsed 
         0.04    0.00    0.03 
      

      使用向量列表比使用 data.frame 快 17 倍。

      关于为什么内部 data.frames 在这方面如此缓慢的任何 cmets?人们会认为它们像列表一样运作......

      要获得更快的代码,请使用 class(d)='list' 而不是 d=as.list(d)class(d)='data.frame'

      system.time({
        d=data.frame(seq=1:10000,r=rnorm(10000))
        d$foo=d$r
        d$seq=1:5
        class(d)='list'
        mark=NA
        for(i in 1:nrow(d)){
          if(d$seq[i]==1) mark=d$r[i]
          d$foo[i]=mark
        }
        class(d)='data.frame'
      })
      head(d)
      

      【讨论】:

      • 这可能要归功于[&lt;-.data.frame 的开销,当您执行d$foo[i] = mark 时会以某种方式调用它,并且最终可能会在每个向量上创建一个可能整个data.frame 的向量的新副本&lt;- 修改。这将对 SO 提出一个有趣的问题。
      • @Frank 它 (i) 必须确保修改后的对象仍然是有效的 data.frame 并且 (ii) afaik 至少复制一份,可能不止一份。众所周知,数据帧子分配很慢,如果您查看冗长的源代码,这并不奇怪。
      • @Frank, @Roland:df$var[i] 符号是否通过相同的[&lt;-.data.frame 函数?我注意到它确实很长。如果不是,它使用什么功能?
      • @Chris 我相信d$foo[i]=mark 被粗略地翻译成d &lt;- `$&lt;-`(d, 'foo', `[&lt;-`(d$foo, i, mark)),但使用了一些临时变量。
      【解决方案7】:

      这里的答案很棒。未涵盖的一个小方面是问题指出“我的电脑仍在工作(现在大约 10 小时),我不知道运行时”。在开发过程中,我总是将以下代码放入循环中,以了解更改似乎如何影响速度以及监控完成所需的时间。

      dayloop2 <- function(temp){
        for (i in 1:nrow(temp)){
          cat(round(i/nrow(temp)*100,2),"%    \r") # prints the percentage complete in realtime.
          # do stuff
        }
        return(blah)
      }
      

      也适用于 lapply。

      dayloop2 <- function(temp){
        temp <- lapply(1:nrow(temp), function(i) {
          cat(round(i/nrow(temp)*100,2),"%    \r")
          #do stuff
        })
        return(temp)
      }
      

      如果循环中的函数非常快,但循环的数量很大,那么请考虑每隔一段时间打印一次,因为打印到控制台本身会产生开销。例如

      dayloop2 <- function(temp){
        for (i in 1:nrow(temp)){
          if(i %% 100 == 0) cat(round(i/nrow(temp)*100,2),"%    \r") # prints every 100 times through the loop
          # do stuff
        }
        return(temp)
      }
      

      【讨论】:

      • 类似的选项,打印分数 i/n。我总是有类似cat(sprintf("\nNow running... %40s, %s/%s \n", nm[i], i, n)) 这样的东西,因为我通常会循环遍历命名的东西(名称在nm)。
      【解决方案8】:

      在 R 中,您通常可以使用 apply 系列函数(在您的情况下,它可能是 replicate)来加速循环处理。查看提供进度条的plyr 包。

      另一种选择是完全避免循环并用矢量化算法替换它们。我不确定你在做什么,但你可能可以一次将你的函数应用于所有行:

      temp[1:nrow(temp), 10] <- temp[1:nrow(temp), 9] + temp[0:(nrow(temp)-1), 10]
      

      这会快得多,然后您可以根据您的条件过滤行:

      cond.i <- (temp[i, 6] == temp[i-1, 6]) & (temp[i, 3] == temp[i-1, 3])
      temp[cond.i, 10] <- temp[cond.i, 9]
      

      向量化算术需要更多时间和思考问题,但有时您可以节省几个数量级的执行时间。

      【讨论】:

      • 您发现向量函数会比循环或 apply() 快,但 apply() 比循环快并不是真的。在许多情况下, apply() 只是将循环从用户那里抽象出来,但仍然是循环的。请参阅上一个问题:stackoverflow.com/questions/2275896/…
      【解决方案9】:

      看看{purrr}中的accumulate()函数:

      dayloop_accumulate <- function(temp) {
        temp %>%
          as_tibble() %>%
           mutate(cond = c(FALSE, (V6 == lag(V6) & V3 == lag(V3))[-1])) %>%
          mutate(V10 = V9 %>% 
                   purrr::accumulate2(.y = cond[-1], .f = function(.i_1, .i, .y) {
                     if(.y) {
                       .i_1 + .i
                     } else {
                       .i
                     }
                   }) %>% unlist()) %>%
          select(-cond)
      }
      

      【讨论】:

        【解决方案10】:

        使用data.table 处理是一个可行的选择:

        n <- 1000000
        df <- as.data.frame(matrix(sample(1:10, n*9, TRUE), n, 9))
        colnames(df) <- paste("col", 1:9, sep = "")
        
        library(data.table)
        
        dayloop2.dt <- function(df) {
          dt <- data.table(df)
          dt[, Kumm. := {
            res <- .I;
            ifelse (res > 1,             
              ifelse ((col6 == shift(col6, fill = 0)) & (col3 == shift(col3, fill = 0)) , 
                res <- col9 + shift(res)                   
              , # else
                res <- col9                                 
              )
             , # else
              res <- col9
            )
          }
          ,]
          res <- data.frame(dt)
          return (res)
        }
        
        res <- dayloop2.dt(df)
        
        m <- microbenchmark(dayloop2.dt(df), times = 100)
        #Unit: milliseconds
        #       expr      min        lq     mean   median       uq      max neval
        #dayloop2.dt(df) 436.4467 441.02076 578.7126 503.9874 575.9534 966.1042    10
        

        如果忽略条件过滤可能带来的好处,它会非常快。显然,如果您可以对数据子集进行计算,它会有所帮助。

        【讨论】:

        • 你为什么要重复使用data.table的建议?在之前的答案中已经多次提出。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-05-14
        相关资源
        最近更新 更多