【问题标题】:r: for loop operation with nested indices runs super slowr:带有嵌套索引的for循环操作运行速度非常慢
【发布时间】:2012-01-09 23:53:01
【问题描述】:

我想为数据框的每一行运行一个操作,更改一列。我是一个 apply/ddply/sqldf 人,但是当它们有意义时我会使用循环,我认为这是其中之一。这种情况很棘手,因为要更改的列取决于逐行更改的信息;根据一个单元格中的信息,我应该只更改该行中其他十个单元格中的一个。有 75 列和 20000 行,操作需要 10 分钟,而我的脚本中的每个其他操作需要 0-5 秒,最多 10 秒。我已将我的问题简化为下面非常简单的测试用例。

n <- 20000
t.df <- data.frame(matrix(1:5000, ncol=10, nrow=n) )
system.time(
 for (i in 1:nrow(t.df)) {
 t.df[i,(t.df[i,1]%%10 + 1)] <- 99
 }
)

十列需要 70 秒,当 ncol=50 时需要 360 秒。太疯狂了。循环是错误的方法吗?有没有更好、更有效的方法来做到这一点?

我已经尝试将嵌套项 (t.df[i,1]%%10 + 1) 初始化为 for 循环外的列表。它节省了大约 30 秒(总共 10 分钟),但使上面的示例代码更加复杂。所以它有帮助,但不是解决方案。

我目前最好的想法是在准备这个测试用例时产生的。对我来说,只有 10 列是相关的(75-11 列是不相关的)。由于运行时间很大程度上取决于列数,因此我可以在排除不相关列的数据框上运行上述操作。这将使我减少一分钟多一点。但是“带有嵌套索引的 for 循环”甚至是思考我的问题的最佳方式吗?

【问题讨论】:

  • +1 用于简化的测试用例、清晰描述的问题和可重现的示例。

标签: performance r for-loop nested vectorization


【解决方案1】:

当您确实需要混合列类型(因此您不能使用matrix)时,另一个选项是:= in data.table。来自?":=" 的示例:

require(data.table)
m = matrix(1,nrow=100000,ncol=100)
DF = as.data.frame(m)
DT = as.data.table(m)    
system.time(for (i in 1:1000) DF[i,1] <- i)
    # 591 seconds 
system.time(for (i in 1:1000) DT[i,V1:=i])
    # 1.16 seconds  ( 509 times faster )

【讨论】:

    【解决方案2】:

    使用rowcol 对我来说似乎不那么复杂:

    t.df[col(t.df) == (row(t.df) %% 10) + 1]  <- 99
    

    我认为 Tommy's 仍然更快,但使用 rowcol 可能更容易理解。

    【讨论】:

    • 甜蜜!我觉得我需要一段时间才能理解这一点,但我渴望这样思考。
    • 过去一个小时我一直在玩所有的答案。这是我最喜欢的一个:它与其他方法相比在某个数量级上,并且它似乎最适用于数据帧和其他数据类型(如字符串): t.df
    • 仅供参考,是的,没有汤米的那么快。使用矩阵需要大约 4 倍的时间。事实上,如果你不先把它变成一个矩阵,它甚至不如 JD Long 的矩阵类型的简单变体那么快。如果 t.df 是一个矩阵,它比 JD Long 的解决方案更快。
    【解决方案3】:

    更新:在基准测试练习中添加了 Tommy 解决方案的矩阵版本。

    您可以对其进行矢量化处理。这是我的解决方案以及与循环的比较

    n <- 20000
    t.df <- (matrix(1:5000, ncol=10, nrow=n))
    
    f_ramnath <- function(x){
      idx <- x[,1] %% 10 + 1
      x[cbind(1:NROW(x), idx)] <- 99  
      return(x)
    }
    
    f_long <- function(t.df){
      for (i in 1:nrow(t.df)) {
        t.df[i,(t.df[i,1]%%10 + 1)] <- 99
      }
      return(t.df)
    }
    
    f_joran <- function(t.df){
      t.df[col(t.df) == (row(t.df) %% 10) + 1]  <- 99
      return(t.df)
    }
    
    f_tommy <- function(t.df){
      t2.df <- t.df
      # Create a logical matrix with TRUE wherever the replacement should happen
      m <- array(FALSE, dim=dim(t2.df))
      m[cbind(seq_len(nrow(t2.df)), t2.df[,1]%%10L + 1L)] <- TRUE
      t2.df[m] <- 99
      return(t2.df)
    }
    
    f_tommy_mat <- function(m){
      m[cbind(seq_len(nrow(m)), m[,1]%%10L + 1L)] <- 99
    }
    

    要比较不同方法的性能,我们可以使用rbenchmark

    library(rbenchmark)
    benchmark(f_long(t.df), f_ramnath(t.df), f_joran(t.df), f_tommy(t.df), 
      f_tommy_mat(t.df), replications = 20,  order = 'relative',
      columns = c('test', 'elapsed', 'relative')
    
                   test elapsed  relative
    5 f_tommy_mat(t.df)   0.135  1.000000
    2   f_ramnath(t.df)   0.172  1.274074
    4     f_tommy(t.df)   0.311  2.303704
    3     f_joran(t.df)   0.705  5.222222
    1      f_long(t.df)   2.411 17.859259
    

    【讨论】:

    • 这是一个很棒的工具,我已经用它替换了system.time。它会进行多次复制,这样可以提供更好的比较,而且还可以很好地总结比较结果。
    • @Ramnath - 我有点反对f_tommy 版本:我的第一个版本与您的非常相似(并且较早发布:-)并假设了matrix 参数。 f_tommy 版本直接在data.frame 上工作,从而解决了一个不同的问题(实际提出的问题)。它还会制作一个额外的副本,以免覆盖原件。其他解决方案都没有这样做,因此比较不利。
    • @Tommy。你是绝对正确的。这不是苹果对苹果的比较。我已经用你原来的矩阵解决方案更新了基准测试。
    【解决方案4】:

    @JD Long 说得对,如果t.df 可以表示为矩阵,事情会快很多。

    ...然后您实际上可以对整个事物进行矢量化处理,使其闪电般快速:

    n <- 20000
    t.df <- data.frame(matrix(1:5000, ncol=10, nrow=n) )
    system.time({
      m <- as.matrix(t.df)
      m[cbind(seq_len(nrow(m)), m[,1]%%10L + 1L)] <- 99
      t2.df <- as.data.frame(m)
    }) # 0.00 secs
    

    不幸的是,我在这里使用的矩阵索引似乎不适用于data.frame

    编辑 我创建一个逻辑矩阵来索引的变体适用于data.frame,并且几乎一样快:

    n <- 20000
    t.df <- data.frame(matrix(1:5000, ncol=10, nrow=n) )
    system.time({
      t2.df <- t.df
    
      # Create a logical matrix with TRUE wherever the replacement should happen
      m <- array(FALSE, dim=dim(t2.df))
      m[cbind(seq_len(nrow(t2.df)), t2.df[,1]%%10L + 1L)] <- TRUE
    
      t2.df[m] <- 99
    }) # 0.01 secs
    

    【讨论】:

    • 您可以随时使用rowcol,就像我的回答一样!不过,你的速度仍然更快。
    • 是的,亲爱的。我没有意识到使用数据框会做出什么妥协。
    【解决方案5】:

    似乎真正的瓶颈在于以 data.frame 的形式获取数据。我假设在您的实际问题中,您有一个令人信服的理由使用 data.frame。有什么方法可以将数据转换为可以保留在矩阵中的方式?

    顺便说一句,很好的问题和一个很好的例子。

    以下是矩阵上的循环比 data.frames 上快多少的说明:

    > n <- 20000
    > t.df <- (matrix(1:5000, ncol=10, nrow=n) )
    > system.time(
    +   for (i in 1:nrow(t.df)) {
    +     t.df[i,(t.df[i,1]%%10 + 1)] <- 99
    +   }
    + )
       user  system elapsed 
      0.084   0.001   0.084 
    > 
    > n <- 20000
    > t.df <- data.frame(matrix(1:5000, ncol=10, nrow=n) )
    > system.time(
    +   for (i in 1:nrow(t.df)) {
    +     t.df[i,(t.df[i,1]%%10 + 1)] <- 99
    +   }
    +   )
       user  system elapsed 
     31.543  57.664  89.224 
    

    【讨论】:

    • 神圣的地狱。这让我从 15 分钟缩短到 0.15 秒。惊人。循环与应用是错误的问题;它的数据框与矩阵。谢谢!
    • 有一次我在芝加哥抓到 Josh Ulrich,把他拉到办公室检查我的一些代码。我确信他会向我展示所有这些花哨的功夫,以使我的代码更快。他耸了耸肩,用他平静的方式说:“尝试多使用矩阵,少使用 data.frames”,然后我们就去喝咖啡了。最好的。代码审查。埃瓦尔。 :)
    猜你喜欢
    • 2016-11-23
    • 2018-08-02
    • 2015-03-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-07
    • 2021-08-19
    • 1970-01-01
    相关资源
    最近更新 更多