【问题标题】:Why is running "unique" faster on a data frame than a matrix in R?为什么在数据帧上运行“唯一”比在 R 中的矩阵更快?
【发布时间】:2011-12-10 05:00:15
【问题描述】:

我已经开始相信数据框与矩阵相比没有优势,除了符号方便。然而,当我在矩阵和数据帧上运行unique 时,我注意到了这种奇怪:它似乎在数据帧上运行得更快。

a   = matrix(sample(2,10^6,replace = TRUE), ncol = 10)
b   = as.data.frame(a)

system.time({
    u1 = unique(a)
})
 user  system elapsed
1.840   0.000   1.846


system.time({
    u2 = unique(b)
})
 user  system elapsed
0.380   0.000   0.379

随着行数的增加,时序结果的差异更大。所以,这个问题有两个部分。

  1. 为什么矩阵会变慢?转换为数据框,运行unique,然后转换回来似乎更快。

  2. 是否有任何理由不将unique 包裹在myUnique 中,这会在第1 部分中进行转换?


注意 1. 鉴于矩阵是原子的,似乎unique 对于矩阵来说应该更快,而不是更慢。能够迭代固定大小的连续内存块通常应该比运行单独的链表块更快(我假设这就是数据帧的实现方式......)。

注 2。正如 data.table 的性能所证明的那样,在数据帧或矩阵上运行 unique 是一个相对糟糕的主意 - 请参阅 Matthew Dowle 和 cmets 的答案以了解相关时间。我已经将很多对象迁移到数据表中,而这种性能是这样做的另一个原因。因此,尽管应该很好地为用户提供采用数据表的服务,但出于教学/社区的原因,我将暂时保留关于 为什么 这在矩阵对象上需要更长时间的问题。下面的答案解决了时间在哪里,以及其他方法我们怎样才能获得更好的性能(即数据表)。 为什么的答案就在眼前 - 代码可以通过unique.data.frameunique.matrix 找到。 :) 对它在做什么以及为什么缺少这一切的英文解释。

【问题讨论】:

    标签: performance r matrix dataframe data.table


    【解决方案1】:
    1. 在此实现中,unique.matrixunique.array 相同

      > identical(unique.array, unique.matrix)

      [1] TRUE

    2. unique.array 必须处理多维数组,这需要额外的处理来“折叠”二维情况下不需要的额外维度(对paste() 的额外调用)。代码的关键部分是:

      collapse <- (ndim > 1L) && (prod(dx[-MARGIN]) > 1L)

      temp <- if (collapse) apply(x, MARGIN, function(x) paste(x, collapse = "\r"))

    3. unique.data.frame 针对 2D 情况进行了优化,unique.matrix 不是。正如您所建议的那样,它可能不在当前的实现中。

    请注意,在所有存在多个维度的情况下(unique.{array,matrix,data.table}),它是比较唯一性的字符串表示形式。对于浮点数,这意味着 15 个十进制数字,所以

    NROW(unique(a &lt;- matrix(rep(c(1, 1+4e-15), 2), nrow = 2)))

    1

    NROW(unique(a &lt;- matrix(rep(c(1, 1+5e-15), 2), nrow = 2)))

    NROW(unique(a &lt;- matrix(rep(c(1, 1+4e-15), 1), nrow = 2)))

    都是2。你确定unique是你想要的吗?

    【讨论】:

    • +1 感谢您的正确回答!很抱歉接受缓慢。我为第 2 点添加了一些代码,以使答案更准确。至于它是否是我想要的 - 我想要唯一的行,这就是获得它的方法。但是,data.table 的版本非常快,所以我将使用它。在我的大多数情况下,数据结构的实现并不是特别有趣,但速度和易用性确实很重要。我将切换到满足我对速度的需求的任何东西。 :)
    【解决方案2】:
    1. 不确定,但我猜因为matrix 是一个连续向量,R 首先将其复制到列向量中(如data.frame),因为paste 需要向量列表。请注意,两者都很慢,因为两者都使用paste

    2. 也许是因为unique.data.table 已经快很多倍了。请通过从 R-Forge 存储库下载升级到 v1.6.7,因为它修复了您在 this question 中提出的 uniquedata.table 不使用pasteunique

    a = matrix(sample(2,10^6,replace = TRUE), ncol = 10)
    b = as.data.frame(a)
    system.time(u1<-unique(a))
       user  system elapsed 
       2.98    0.00    2.99 
    system.time(u2<-unique(b))
       user  system elapsed 
       0.99    0.00    0.99 
    c = as.data.table(b)
    system.time(u3<-unique(c))
       user  system elapsed 
       0.03    0.02    0.05  # 60 times faster than u1, 20 times faster than u2
    identical(as.data.table(u2),u3)
    [1] TRUE
    

    【讨论】:

    • +1 不错!我在 5X 数据集上获得了更大的加速(原始数据集为 60X,如果首先使用 setkey 则大约为 600X;相对于 u1),所以这对我来说更具吸引力。在 R 聊天室中,有人提到矩阵版本在每一行都使用paste。在任何情况下,避免paste 对于像“唯一”这样原始的计算都是一个好主意,data.table() 确实做得很好。 (我认为切换到 data.table 可能是做到这一点的最佳方式,但我会保留这个问题 - 如果有人指出问题,他们会回答原始问题。)
    • @Iterator 好的,我喜欢你的逻辑。
    • unique.data.table 是一个与 unique.matrix 和 unique.data.frame 非常不同的函数:(1)它只检查键的唯一性,而不是整行和(2)它检查用于变量的标识,而不是变量的字符表示(这就是其他人需要所有这些粘贴调用的原因;请参阅文档)。
    • @Allan Engelhardt (1) 在 CRAN 上的 1.6.6 中是正确的,但在 R-Forge 上的 1.6.7 在没有密钥时会检查整行(现在可以工作,这要归功于另一个迭代器问题,见上面的链接)。否则identical() 不会返回上面的TRUE。 (2) 这仅与数字列相关。 paste 否则是一个缓慢的解决方案(即整数、因子和字符列)。 unique.data.table 可以更改为考虑 .Machine$double.eps(现在是 FR#1626,谢谢)
    • unique.data.table 现在在 v1.7.2 中尊重双精度列的容差。与duplicated.data.table 类似的修复,现在两者都适用于无键表和键表。
    【解决方案3】:

    在尝试回答我自己的问题时,尤其是第 1 部分,我们可以通过查看 Rprof 的结果来了解时间都花在了哪里。我再次运行了这个,有 500 万个元素。

    这是第一个唯一操作的结果(对于矩阵):

    > summaryRprof("u1.txt")
    $by.self
                         self.time self.pct total.time total.pct
    "paste"                   5.70    52.58       5.96     54.98
    "apply"                   2.70    24.91      10.68     98.52
    "FUN"                     0.86     7.93       6.82     62.92
    "lapply"                  0.82     7.56       1.00      9.23
    "list"                    0.30     2.77       0.30      2.77
    "!"                       0.14     1.29       0.14      1.29
    "c"                       0.10     0.92       0.10      0.92
    "unlist"                  0.08     0.74       1.08      9.96
    "aperm.default"           0.06     0.55       0.06      0.55
    "is.null"                 0.06     0.55       0.06      0.55
    "duplicated.default"      0.02     0.18       0.02      0.18
    
    $by.total
                         total.time total.pct self.time self.pct
    "unique"                  10.84    100.00      0.00     0.00
    "unique.matrix"           10.84    100.00      0.00     0.00
    "apply"                   10.68     98.52      2.70    24.91
    "FUN"                      6.82     62.92      0.86     7.93
    "paste"                    5.96     54.98      5.70    52.58
    "unlist"                   1.08      9.96      0.08     0.74
    "lapply"                   1.00      9.23      0.82     7.56
    "list"                     0.30      2.77      0.30     2.77
    "!"                        0.14      1.29      0.14     1.29
    "do.call"                  0.14      1.29      0.00     0.00
    "c"                        0.10      0.92      0.10     0.92
    "aperm.default"            0.06      0.55      0.06     0.55
    "is.null"                  0.06      0.55      0.06     0.55
    "aperm"                    0.06      0.55      0.00     0.00
    "duplicated.default"       0.02      0.18      0.02     0.18
    
    $sample.interval
    [1] 0.02
    
    $sampling.time
    [1] 10.84
    

    对于数据框:

    > summaryRprof("u2.txt")
    $by.self
                         self.time self.pct total.time total.pct
    "paste"                   1.72    94.51       1.72     94.51
    "[.data.frame"            0.06     3.30       1.82    100.00
    "duplicated.default"      0.04     2.20       0.04      2.20
    
    $by.total
                            total.time total.pct self.time self.pct
    "[.data.frame"                1.82    100.00      0.06     3.30
    "["                           1.82    100.00      0.00     0.00
    "unique"                      1.82    100.00      0.00     0.00
    "unique.data.frame"           1.82    100.00      0.00     0.00
    "duplicated"                  1.76     96.70      0.00     0.00
    "duplicated.data.frame"       1.76     96.70      0.00     0.00
    "paste"                       1.72     94.51      1.72    94.51
    "do.call"                     1.72     94.51      0.00     0.00
    "duplicated.default"          0.04      2.20      0.04     2.20
    
    $sample.interval
    [1] 0.02
    
    $sampling.time
    [1] 1.82
    

    我们注意到矩阵版本在applypastelapply 上花费了大量时间。相比之下,简单的数据框版本运行duplicated.data.frame,大部分时间都花在paste,大概是聚合结果。

    虽然这解释了时间在哪里,但它没有解释为什么它们有不同的实现,也没有解释从一种对象类型简单地更改为另一种的效果。

    【讨论】:

    • 请注意,行数增加了 5 倍(500K),矩阵版本需要的时间是以前的 6 倍,而 DF 版本需要的时间是以前的 4.7 倍。 8 秒是一个相当大的差异,但这就是我愿意等待这个例子的全部。当我运行一个需要 20 多分钟的操作时,我注意到了这一点——一个更大的矩阵。在早期版本的代码中,同一个对象是一个数据框,所以我知道以前的代码更快。
    • 所以这表明矩阵和data.frame之间的差异没有“逻辑”(阅读:主要)原因,它只是软件的状态:一种方法被正确优化(即writen很可能是用 C 语言编写的),而另一个只是用 R 语言编写的。
    • @TomasT。实际上,在 R 级别的代码中存在差异。我一直在等待其他人查看代码并写下答案,以表彰他们对博览会的认可。当我问这个问题时,我还没有警觉。根据这个答案中的时间安排,代码缓慢的原因是有道理的。如果其他人不这样做,我会等待更长的时间并自己回答这个问题。我认为这是一个很好的 R 教学法来描述如何找出正在发生的事情,然后解释代码在做什么。在实践中,我们应该改用数据表。现在速度更快了。
    • @Iterator 我对一个相关问题感兴趣:为什么unique 在应用于data.tabledata.frame 时要快得多,在我的一个测试中,后者是如此明显,以至于后者甚至无法成功完成,而前者在不到一秒钟的时间内完成。您是否深入探讨了这个主题,能够说出为什么data.frame 的实现相对看起来如此低效?
    猜你喜欢
    • 2018-12-05
    • 1970-01-01
    • 1970-01-01
    • 2011-01-18
    • 1970-01-01
    • 2017-06-07
    • 2013-12-26
    • 2021-01-22
    • 1970-01-01
    相关资源
    最近更新 更多