【问题标题】:dplyr on data.table, am I really using data.table?data.table 上的 dplyr,我真的在使用 data.table 吗?
【发布时间】:2015-02-15 04:13:08
【问题描述】:

如果我在 datatable 之上使用 dplyr 语法,我能否在使用 dplyr 语法的同时获得 datatable 的所有速度优势?换句话说,如果我使用 dplyr 语法查询它,我是否误用了数据表?还是我需要使用纯数据表语法来利用它的所有功能。

提前感谢您的任何建议。代码示例:

library(data.table)
library(dplyr)

diamondsDT <- data.table(ggplot2::diamonds)
setkey(diamondsDT, cut) 

diamondsDT %>%
    filter(cut != "Fair") %>%
    group_by(cut) %>%
    summarize(AvgPrice = mean(price),
                 MedianPrice = as.numeric(median(price)),
                 Count = n()) %>%
    arrange(desc(Count))

结果:

#         cut AvgPrice MedianPrice Count
# 1     Ideal 3457.542      1810.0 21551
# 2   Premium 4584.258      3185.0 13791
# 3 Very Good 3981.760      2648.0 12082
# 4      Good 3928.864      3050.5  4906

这是我想出的数据表等效项。不确定它是否符合 DT 良好实践。但是我想知道代码是否真的比幕后的dplyr语法更高效:

diamondsDT [cut != "Fair"
        ] [, .(AvgPrice = mean(price),
                 MedianPrice = as.numeric(median(price)),
                 Count = .N), by=cut
        ] [ order(-Count) ]

【问题讨论】:

  • 为什么不用数据表语法?它既优雅又高效。这个问题很难回答,因为它非常广泛。是的,数据表有dplyr的方法,但数据表也有自己的可比方法
  • 我可以使用数据表语法或课程。但不知何故,我发现 dplyr 语法更优雅。不管我对语法的偏好如何。我真正想知道的是:我是否需要使用纯数据表语法才能获得 100% 的数据表功能优势。
  • 对于最近在data.frames 和相应的data.tables 上使用dplyr 的基准,请参阅here(以及其中的引用)。
  • @Polymerase - 我认为这个问题的答案肯定是“是”
  • @Henrik:我后来意识到我误解了那个页面,因为它们只显示了数据框构造的代码,而不是它们用于 data.table 构造的代码。当我意识到这一点时,我删除了我的评论(希望你没有看到它)。

标签: r data.table dplyr


【解决方案1】:

您现在可以使用 dtplyr,它是 tidyverse 的一部分。它允许您像往常一样使用 dplyr 样式的语句,但利用惰性评估并将您的语句转换为后台的 data.table 代码。翻译的开销是最小的,但你得到了 data.table 的所有(如果不是)大部分好处。更多详细信息请参见官方 git repo here 和 tidyverse page

【讨论】:

    【解决方案2】:

    没有直接/简单的答案,因为这两个软件包的理念在某些方面有所不同。所以一些妥协是不可避免的。以下是您可能需要解决/考虑的一些问题。

    涉及i 的操作(== filter()slice() 在 dplyr 中)

    假设 DT 有 10 列。考虑这些 data.table 表达式:

    DT[a > 1, .N]                    ## --- (1)
    DT[a > 1, mean(b), by=.(c, d)]   ## --- (2)
    

    (1) 给出DT 中的行数,其中列a &gt; 1。 (2) 对于i 中与 (1) 相同的表达式,返回由c,d 分组的mean(b)

    常用的dplyr 表达式是:

    DT %>% filter(a > 1) %>% summarise(n())                        ## --- (3) 
    DT %>% filter(a > 1) %>% group_by(c, d) %>% summarise(mean(b)) ## --- (4)
    

    显然,data.table 代码更短。此外,它们还内存效率更高1。为什么?因为在 (3) 和 (4) 中,filter() 首先返回 所有 10 列的行,当在 (3) 中我们只需要行数时,在 (4) 中我们只需要列b, c, d 用于连续操作。为了克服这个问题,我们必须先select()列:

    DT %>% select(a) %>% filter(a > 1) %>% summarise(n()) ## --- (5)
    DT %>% select(a,b,c,d) %>% filter(a > 1) %>% group_by(c,d) %>% summarise(mean(b)) ## --- (6)
    

    必须强调两个包之间的主要哲学差异:

    • data.table 中,我们希望将这些相关的操作放在一起,这样可以查看j-expression(来自同一个函数调用)并意识到(1)中不需要任何列。 i 中的表达式被计算出来,.N 只是给出行数的逻辑向量的总和;整个子集永远不会实现。在 (2) 中,只有列 b,c,d 在子集中具体化,其他列被忽略。

    • 但是在dplyr 中,哲学是让一个函数精确地完成一件事很好。 (至少目前)无法判断filter() 之后的操作是否需要我们过滤的所有列。如果您想有效地执行此类任务,则需要提前考虑。在这种情况下,我个人认为它违反直觉。

    请注意,在 (5) 和 (6) 中,我们仍然对不需要的列 a 进行子集化。但我不确定如何避免这种情况。如果filter() 函数有一个参数来选择要返回的列,我们可以避免这个问题,但是该函数不会只执行一项任务(这也是 dplyr 的设计选择)。

    通过引用子分配

    dplyr 将从不通过引用进行更新。这是两个包之间的另一个巨大(哲学)差异。

    例如,在 data.table 中你可以这样做:

    DT[a %in% some_vals, a := NA]
    

    仅在满足条件的行上更新列a通过引用。目前 dplyr 在内部深度复制整个 data.table 以添加新列。 @BrodieG 已经在他的回答中提到了这一点。

    但在实现FR #617 时,可以用浅拷贝代替深拷贝。也相关:dplyr: FR#614。请注意,您修改的列将始终被复制(因此速度较慢/内存效率较低)。将无法通过引用更新列。

    其他功能

    • 在 data.table 中,您可以在连接时进行聚合,这更容易理解并且内存效率很高,因为中间连接结果永远不会实现。以this post 为例。您不能(目前?)使用 dplyr 的 data.table/data.frame 语法来做到这一点。

    • dplyr 的语法也不支持 data.table 的 rolling joins 功能。

    • 我们最近在 data.table 中实现了重叠连接以连接区间范围 (here's an example),目前这是一个单独的函数 foverlaps(),因此可以与管道运算符 (magrittr / pipeR? -我自己从未尝试过)。

      但最终,我们的目标是将其集成到 [.data.table 中,以便我们可以获取其他功能,例如分组、加入时聚合等。这些功能将具有上述相同的限制。

    • 从 1.9.4 开始,data.table 使用辅助键实现自动索引,用于基于常规 R 语法的基于快速二进制搜索的子集。例如:DT[x == 1]DT[x %in% some_vals] 将在第一次运行时自动创建一个索引,然后将其用于同一列的连续子集,以使用二分搜索快速子集。此功能将继续发展。查看this gist 了解此功能的简要概述。

      filter() 为data.tables 实现的方式来看,它没有利用此功能。

    • dplyr 的一个特性是它还使用相同的语法提供interface to databases,而 data.table 目前没有。

    因此,您必须权衡这些(可能还有其他点),并根据您是否可以接受这些权衡来决定。

    HTH


    (1) 请注意,内存效率直接影响速度(尤其是当数据变大时),因为大多数情况下的瓶颈是将数据从主内存移动到缓存(并尽可能多地使用缓存中的数据 -减少缓存未命中 - 以减少访问主内存)。此处不再赘述。

    【讨论】:

    • 绝对精彩。谢谢你
    • 这是一个很好的答案,但 dplyr 使用与 dplyr 相同的方法实现高效的filter() 加上summarise() 将是可能(如果不太可能)对于 SQL - 即构建一个表达式,然后只按需执行一次。这不太可能在不久的将来实现,因为 dplyr 对我来说已经足够快了,而且实现查询计划器/优化器相对困难。
    • 提高内存效率还有助于另一个重要领域 - 在内存不足之前实际完成任务。在处理大型数据集时,我遇到了 dplyr 和 pandas 的问题,而 data.table 可以优雅地完成工作。
    【解决方案3】:

    回答您的问题:

    • 是的,您正在使用data.table
    • 但不如使用纯 data.table 语法那样高效

    在许多情况下,对于那些想要 dplyr 语法的人来说,这是一个可以接受的折衷方案,尽管它可能会比使用纯数据帧的 dplyr 慢。

    一个重要因素似乎是dplyr 在分组时默认会复制data.table。考虑(使用微基准):

    Unit: microseconds
                                                                   expr       min         lq    median
                                    diamondsDT[, mean(price), by = cut]  3395.753  4039.5700  4543.594
                                              diamondsDT[cut != "Fair"] 12315.943 15460.1055 16383.738
     diamondsDT %>% group_by(cut) %>% summarize(AvgPrice = mean(price))  9210.670 11486.7530 12994.073
                                   diamondsDT %>% filter(cut != "Fair") 13003.878 15897.5310 17032.609
    

    过滤具有相当的速度,但分组却不是。我相信罪魁祸首是dplyr:::grouped_dt中的这一行:

    if (copy) {
        data <- data.table::copy(data)
    }
    

    其中copy 默认为TRUE (并且不能轻易更改为我可以看到的 FALSE)。这可能无法解释 100% 的差异,但 diamonds 大小的一般开销很可能不是全部差异。

    问题在于,为了获得一致的语法,dplyr 分两步进行分组。它首先在与组匹配的原始数据表的副本上设置键,然后才进行分组。 data.table 只是为最大的结果组分配内存,在这种情况下只有一行,因此需要分配多少内存有很大的不同。

    仅供参考,如果有人关心的话,我是通过使用 treeprof (install_github("brodieg/treeprof")) 找到的,这是一个用于 Rprof 输出的实验性(仍然非常 alpha)树查看器:

    请注意,上述内容目前仅适用于 Mac AFAIK。此外,不幸的是,Rprofpackagename::funname 类型的调用记录为匿名,因此它实际上可能是grouped_dt 内的任何和所有datatable:: 调用,但从快速测试来看,datatable::copy 是大的。

    也就是说,您可以很快看到[.data.table 调用的开销并不大,但分组还有一个完全独立的分支。


    EDIT:确认复制:

    > tracemem(diamondsDT)
    [1] "<0x000000002747e348>"    
    > diamondsDT %>% group_by(cut) %>% summarize(AvgPrice = mean(price))
    tracemem[0x000000002747e348 -> 0x000000002a624bc0]: <Anonymous> grouped_dt group_by_.data.table group_by_ group_by <Anonymous> freduce _fseq eval eval withVisible %>% 
    Source: local data table [5 x 2]
    
            cut AvgPrice
    1      Fair 4358.758
    2      Good 3928.864
    3 Very Good 3981.760
    4   Premium 4584.258
    5     Ideal 3457.542
    > diamondsDT[, mean(price), by = cut]
             cut       V1
    1:     Ideal 3457.542
    2:   Premium 4584.258
    3:      Good 3928.864
    4: Very Good 3981.760
    5:      Fair 4358.758
    > untracemem(diamondsDT)
    

    【讨论】:

    • 这太棒了,谢谢。这是否意味着,由于内部数据复制步骤, dplyr::group_by() 将加倍内存需求(与纯数据表语法相比)?这意味着如果我的数据表对象大小为 1GB,并且我使用类似于原始帖子中的 dplyr 链式语法。我需要至少 2GB 的可用内存才能获得结果?
    • 我觉得我在开发版中解决了这个问题?
    • @hadley,我正在使用 CRAN 版本。查看开发人员,您似乎部分解决了该问题,但实际副本仍然存在(尚未测试,仅查看 R/grouped-dt.r 中的 c(20, 30:32) 行。现在可能更快,但是我敢打赌,缓慢的一步就是复制。
    • 我也在等待data.table中的浅拷贝函数;在那之前,我认为安全比快速更好。
    【解决方案4】:

    试试吧。

    library(rbenchmark)
    library(dplyr)
    library(data.table)
    
    benchmark(
    dplyr = diamondsDT %>%
        filter(cut != "Fair") %>%
        group_by(cut) %>%
        summarize(AvgPrice = mean(price),
                     MedianPrice = as.numeric(median(price)),
                     Count = n()) %>%
        arrange(desc(Count)),
    data.table = diamondsDT[cut != "Fair", 
                            list(AvgPrice = mean(price),
                                 MedianPrice = as.numeric(median(price)),
                                 Count = .N), by = cut][order(-Count)])[1:4]
    

    在这个问题上,data.table 似乎比使用 data.table 的 dplyr 快 2.4 倍:

            test replications elapsed relative
    2 data.table          100    2.39    1.000
    1      dplyr          100    5.77    2.414
    

    根据 Polymerase 的评论修改

    【讨论】:

    • 使用microbenchmark 包,我发现在diamonds 的原始(数据框)版本上运行OP 的dplyr 代码的中位时间为0.012 秒,而中位时间为0.012 秒将diamonds 转换为数据表后的时间为 0.024 秒。运行 G. Grothendieck 的 data.table 代码需要 0.013 秒。至少在我的系统上,看起来dplyrdata.table 具有大致相同的性能。但是为什么dplyr在数据框第一次转换成数据表的时候会比较慢呢?
    • 亲爱的 G. Grothendieck,这太棒了。感谢您向我展示此基准实用程序。顺便说一句,您在数据表版本中忘记了 [order(-Count)] 以使 dplyr 的排列 (desc(Count)) 等价。添加后,数据表仍然快 x1.8 左右(而不是 2.9)。
    • @eipi10 你能用这里的数据表版本重新运行你的工作台吗(在最后一步按 desc Count 添加排序): diamondsDT[cut != "Fair", list(AvgPrice = mean(price) , MedianPrice = as.numeric(median(price)), Count = .N), by = cut ] [ order(-Count) ]
    • 还是 0.013 秒。排序操作几乎不需要任何时间,因为它只是对最终表进行重新排序,该表只有四行。
    • 从 dplyr 语法到数据表语法的转换有一些固定的开销,因此可能值得尝试不同的问题大小。另外我可能没有在 dplyr 中实现最有效的数据表代码;补丁总是受欢迎的
    猜你喜欢
    • 1970-01-01
    • 2019-03-06
    • 1970-01-01
    • 2013-11-02
    • 2018-10-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多