dplyr在data.table上,我真的在使用data.table吗?


91

如果我使用dplyr语法上的顶部数据表中,同时仍然使用dplyr的语法我能得到的所有数据表的速度益处?换句话说,如果我使用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) ]

7
为什么不使用数据表语法?既优雅又高效。这个问题非常广泛,因此并不能真正回答。是的,存在dplyr用于数据表的方法,但是数据表也有其自己的可比较方法
Rich Scriven 2014年

7
我可以使用数据表语法或课程。但是以某种方式,我发现dplyr语法更加优雅。无论我偏爱哪种语法。我真正想知道的是:我是否需要使用纯数据表语法才能获得100%的数据表功能优势。
聚合酶

3
有关s和相应sdplyr上使用的最新基准,请参见此处(以及其中的参考文献)。data.framedata.table
亨里克

2
@Polymerase-我认为该问题的答案肯定是“是”
Rich Scriven

1
@Henrik:后来我意识到我对这个页面有误解,因为它们只显示用于数据框构造的代码,而不显示用于data.table构造的代码。当我意识到它时,我删除了我的评论(希望您没有看到它)。
IRTFM

Answers:


77

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

涉及i(==filter()slice()dplyr中)的运算

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

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

(1)给出DTwhere列中的行数a > 1。(2)返回与(1)中相同表达式的mean(b)分组依据。c,di

常用的dplyr表达式是:

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

显然,数据表代码更短。此外,它们还具有更高的存储效率1。为什么?因为在(3)和(4)中,首先filter()返回所有10列的行,所以在(3)中,我们只需要行数,而在(4)中,我们只需要列即可b, c, d进行连续操作。为了克服这个问题,我们必须在select()列apriori中:

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中,您可以在连接时进行聚合,这很容易理解,并且由于中间连接的结果从未实现,因此可以提高内存效率。查看此帖子以获取示例。您目前无法使用dplyr的data.table / data.frame语法执行此操作。

  • dplyr的语法也不支持data.table的滚动联接功能。

  • 我们最近在data.table中实现了重叠联接,以在间隔范围内联接(这里是一个示例),这是当前的一个单独函数foverlaps(),因此可以与管道运算符(magrittr / pipeR?-从未尝试过)一起使用。

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

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

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

  • dplyr的功能是它还使用相同的语法提供了数据库接口,而data.table目前还没有。

因此,您将不得不权衡这些(可能还有其他要点),并根据您是否可以接受这些折衷做出决定。

高温超导


(1)请注意,提高内存效率会直接影响速度(尤其是随着数据变大),因为瓶颈在大多数情况下是将数据从主内存移到缓存中(并尽可能多地利用缓存中的数据-减少缓存未命中-以减少对主存储器的访问)。这里不做详细介绍。


4
绝对辉煌​​。谢谢你
David Arenburg 2014年

6
这是一个很好的答案,但是dplyr可能(如果不太可能)使用dplyr用于SQL的相同方法来实现高效filter()summarise()-即建立一个表达式,然后仅按需执行一次。这不太可能在不久的将来实现,因为dplyr对我来说足够快,而实现查询计划程序/优化程序则相对困难。
hadley 2014年

保持内存高效还有助于另一个重要领域-在内存用完之前实际完成任务。当使用大型数据集时,我遇到了dplyr和熊猫的问题,而data.table可以很好地完成这项工作。
Zaki

25

去尝试一下。

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的评论进行了修订


2
使用该microbenchmark程序包,我发现dplyr在原始版本(数据框)上运行OP的代码diamonds花费的中位时间为0.012秒,而转换diamonds为数据表后花费的中位时间为0.024秒。运行G. Grothendieck的data.table代码花费了0.013秒。至少在我的系统上,它的外观dplyrdata.table性能大致相同。但是,dplyr当数据帧首次转换为数据表时,为什么会变慢?
eipi10 2014年

尊敬的G. Grothendieck,这太好了。感谢您向我展示此基准实用程序。顺便说一句,您在数据表版本中忘记了[order(-Count)]来使dplyr的range(desc(Count))等效。添加此选项后,数据表仍然快了约x1.8(而不是2.9)。
聚合酶

@ eipi10您是否可以在此处再次使用数据表版本重新运行您的工作台(在最后一步中按desc Count添加了排序):diamondsDT [cut!=“ Fair”,列表(AvgPrice =均值(price),MedianPrice = as.numeric(median) (价格)),计数= .N),按=切] [order(-Count)]
聚合酶

仍为0.013秒。排序操作几乎不需要花费任何时间,因为它只是对只有四行的最终表进行了重新排序。
eipi10 2014年

1
从dplyr语法到数据表语法的转换有一些固定的开销,因此尝试改变问题大小可能是值得的。另外,我可能尚未在dplyr中实现最有效的数据表代码;补丁总是受欢迎的
hadley 2014年

22

要回答您的问题:

  • 是的,您正在使用 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只需为最大的结果组分配内存,在这种情况下,结果组只有一行,因此在需要分配多少内存方面有很大的不同。

仅供参考,如果有人在乎,我通过使用 treeprofinstall_github("brodieg/treeprof")),一个实验性的(仍然非常多的alpha)树查看器进行Rprof输出来发现的:

在此处输入图片说明

请注意,以上内容目前仅适用于Mac AFAIK。而且,不幸的是,Rprof该类型的呼叫被记录packagename::funname为匿名呼叫,因此它实际上可能是负责任何datatable::内部的所有呼叫grouped_dt,但是从快速测试来看,它看起来像是datatable::copy一个大呼叫。

就是说,您可以快速了解到, [.data.table呼叫,但是还有一个完全独立的分组分支。


编辑:确认复制:

> 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的可用内存才能获得结果?
聚合酶

2
我觉得我在开发版本中修复了该问题?
hadley 2014年

@hadley,我正在使用CRAN版本。查看开发人员,看起来您已部分解决了该问题,但实际的副本仍然存在(未经测试,仅查看R / grouped-dt.r中的c(20,30:32)行。现在可能更快,但我敢打赌,慢一步是复制。
BrodieG

3
我也在等待data.table中的浅表复制功能;在那之前,我认为安全胜于快速。
hadley 2014年

2

您现在可以使用dtplyr,它是tidyverse的一部分。它允许您照常使用dplyr样式的语句,但是利用了惰性求值并将其语句转换为后台的data.table代码。转换的开销是最小的,但是如果没有的话,您将获得data.table的所有好处。在官方混帐回购协议的更多细节在这里和tidyverse

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.