使用dplyr时,是否真正在使用data.table?

96
如果我在data.table上使用dplyr语法,我是否可以同时获得所有的data.table速度优势,并仍然使用dplyr的语法?换句话说,如果我使用dplyr语法查询data.table,是否会误用它?还是说我需要使用纯粹的data.table语法来发挥它的全部能力。
代码例子:
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

以下是我提出的datatable等效性。不确定是否符合DT的最佳实践。但我想知道代码是否比幕后的dplyr语法更有效率:

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

9
为什么不使用数据表语法?它既优美又高效。这个问题实际上很难回答,因为它非常广泛。是的,有dplyr方法适用于数据表,但数据表也有自己可比较的方法。 - Rich Scriven
10
我可以使用datatable的语法,当然也能使用dplyr的语法。但是我觉得dplyr的语法更加优雅。尽管我偏爱某个语法,但我真正想知道的是:为了获得100%的datatable功效,我是否需要使用纯datatable语法? - Polymerase
3
请参见此处以了解最近一项基准测试中dplyrdata.frame和相应的data.table上的使用情况(以及其中的参考文献)。 - Henrik
3
@Polymerase - 我认为对于那个问题的答案肯定是“是的”。 - Rich Scriven
1
@Henrik:我后来意识到我误解了那个页面,因为他们只显示了数据框架构造的代码,而没有显示他们用于数据表构造的代码。当我意识到这一点时,我删除了我的评论(希望你没有看到)。 - IRTFM
4个回答

81

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

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

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

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

(1) 给出了在列a > 1DT中行的数量。(2) 对于与(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]

使用data.table更新符合条件的行中的列 a by reference。目前,dplyr会在内部深度复制整个data.table以添加新列。 @BrodieG已在他的回答中提到了这一点。

但是,当实现FR#617时,深拷贝可以被浅拷贝所取代。另外值得注意的是:dplyr:FR#614。请注意,仍然会始终复制您修改的列(因此速度较慢/内存效率较低)。没有办法通过引用来更新列。

其他功能

  • 在data.table中,您可以同时汇总而不加入,并且这更容易理解,并且由于中间连接结果从未被具体化,因此内存效率更高。请查看此帖子以获取示例。您无法(目前)使用dplyr的data.table / data.frame语法执行此操作。

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

  • 我们最近在data.table中实现了重叠连接以在区间范围内进行连接(这里是示例),它是一个单独的函数foverlaps(),因此可以与pipe运算符(magrittr / pipeR?-从未尝试过)一起使用。但是,最终我们的目标是将其集成到[.data.table中,以便我们可以利用其他功能,如分组、同时聚合等等。具有上述相同的限制。

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

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

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

因此,你将需要权衡这些(可能还有其他)方面,并根据这些权衡来决定是否可以接受。

希望能帮到你。


(1)请注意,内存效率直接影响速度(特别是在数据变得更大时),因为在大多数情况下瓶颈是将数据从主内存移动到缓存(尽可能地利用缓存中的数据 - 减少缓存未命中 - 以便减少访问主内存)。这里不详细讨论。


4
非常出色,谢谢。 - David Arenburg
6
这是一个不错的回答,但是如果 dplyr 使用与 SQL 相同的方法——即构建表达式,然后只在需要时执行一次——实现高效的 filter()summarise() 是 _可能的_(尽管不太可能)。由于 dplyr 已经足够快,而且实施查询计划器/优化器相对较难,因此短期内不太可能实现这一点。 - hadley
在另一个重要领域中,内存效率也有所帮助——在耗尽内存之前实际完成任务。当处理大型数据集时,我曾经在使用dplyr和pandas时遇到过这个问题,而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比使用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包,我发现在原始(数据框)版本的diamonds上运行OP的dplyr代码需要中位数为0.012秒的时间,而在将diamonds转换为数据表后,它需要中位数为0.024秒的时间。运行G.Grothendieck的data.table代码需要0.013秒。至少在我的系统上,看起来dplyrdata.table的性能大致相同。但是为什么当数据框首先转换为数据表时,dplyr会变慢呢? - eipi10
亲爱的G. Grothendieck,这太棒了。谢谢你向我展示这个基准实用程序。顺便说一句,在datatable版本中你忘了[order(-Count)]以使其等效于dplyr的arrange(desc(Count))。添加后,datatable仍然比dplyr快大约x1.8(而不是2.9)。 - Polymerase
@eipi10你能否再次运行一下这个基准测试,使用这里的datatable版本(在最后一步添加了按照Count进行降序排序):diamondsDT[cut != "Fair", list(AvgPrice = mean(price), MedianPrice = as.numeric(median(price)), Count = .N), by = cut ] [ order(-Count) ] - Polymerase
仍然是0.013秒。排序操作几乎不需要时间,因为它只是重新排列最终表格,该表格只有四行。 - eipi10
1
将dplyr语法转换为data table语法存在一些固定的开销,因此尝试不同的问题规模可能是值得的。此外,我可能没有在dplyr中实现最有效的data table代码;欢迎提交补丁。 - hadley
@hadley,看一下我的回答,听起来像是问题所在吗?但我不确定这是否容易修复。 - BrodieG

24

回答你的问题:

  • 是的,你正在使用 data.table
  • 但不如纯粹的 data.table 语法高效

在许多情况下,这将是一个可接受的折衷方案,适用于那些想要 dplyr 语法的人,尽管它可能比普通数据帧使用 dplyr 更慢。

一个重要因素似乎是当分组时,dplyr 默认会复制 data.table。考虑以下示例(使用 microbenchmark):

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),这可能不能解释全部的差异,但像diamonds大小这样的一般开销很可能不是全部差异的原因。

问题在于为了保持一致的语法,dplyr需要分两步进行分组。它首先在原始数据表的副本上设置与分组匹配的键,然后才进行分组。而data.table只需为最大结果组分配内存,即此例中仅为一行,因此需要分配的内存量有很大的差别。

如果有人关心的话,我是使用treeprofinstall_github("brodieg/treeprof"))发现这个问题的,它是一个实验性(目前还远未成熟)的用于查看Rprof输出的树形图查看器:

enter image description here

请注意,以上内容目前只在mac上工作(据我所知)。另外,不幸的是,Rprofpackagename::funname类型的调用记录为匿名调用,因此可能是grouped_dt内的任何datatable::调用都有责任,但从快速测试中看来,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()将使内存需求翻倍(与纯datatable语法相比)?也就是说,如果我的datatable对象大小为1GB,并且我使用类似于原始帖子中的dplyr链接语法。我至少需要2GB的可用内存才能获得结果? - Polymerase
2
我感觉我已经在开发版本中修复了这个问题? - hadley
@hadley,我是在CRAN版本上工作的。看了一下dev,似乎你部分解决了这个问题,但实际的复制仍然存在(没有测试,只是查看R/grouped-dt.r中的c(20, 30:32)行)。现在可能更快了,但我敢打赌慢的步骤是复制。 - BrodieG
3
我也在等待data.table中的浅拷贝函数,但在那之前,我认为安全比速度更重要。 - hadley

4
您现在可以使用 dtplyr,它是 tidyverse 的一部分。它允许您像往常一样使用 dplyr 风格的语句,但利用惰性评估并在幕后将您的语句转换为 data.table 代码。翻译的开销很小,但您可以获得所有或大多数 data.table 的好处。更多详情请查看官方 git 仓库 这里 和 tidyverse 页面

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接