dplyr data.table,我真的使用data.table吗?
如果我在数据表顶部使用dplyr语法,是否仍然使用dplyr的语法获得数据表的所有速度优势? 换句话说,如果我用dplyr语法来查询数据表,是否会误用数据表? 还是我需要使用纯数据表语法来利用其所有的权力。
预先感谢您的任何build议。 代码示例:
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中的filter()
和slice()
)
假设有10列的DT
。 考虑这些data.tableexpression式:
DT[a > 1, .N] ## --- (1) DT[a > 1, mean(b), by=.(c, d)] ## --- (2)
(1)给出了DT
中列数a > 1
的行数。 (2)返回的mean(b)
按c,d
分组,与(1)中的i
相同。
常用的dplyr
expression式是:
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()
列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)
突出这两套scheme之间的主要哲学差异至关重要:
在
data.table
,我们喜欢把这些相关的操作放在一起,并且允许查看j-expression
(来自相同的函数调用),并且认识到不需要(1)中的任何列。i
的expression式被计算出来,而.N
只是给.N
数的那个逻辑向量的总和; 整个子集从来没有实现。 在(2)中,只有列b,c,d
在子集中被物化,其他列被忽略。但在
dplyr
,哲学就是要有一个function做好一件事。 目前(至less目前)没有办法确定filter()
之后的操作是否需要我们过滤的所有列。 如果你想有效地执行这些任务,你需要提前思考。 在这种情况下,我个人觉得它是反直觉的。
请注意,在(5)和(6)中,我们仍然a
我们不需要的列a
子集。 但我不知道如何避免这一点。 如果filter()
函数有一个参数来select要返回的列,我们可以避免这个问题,但是这个函数不会只执行一个任务(这也是一个dplyrdeviseselect)。
通过引用进行分配
dplyr 永远不会通过引用来更新。 这是两个软件包之间又一个巨大的(哲学)差异。
例如,在data.table中,您可以执行以下操作:
DT[a %in% some_vals, a := NA]
它通过引用仅仅满足条件的那些行来更新列a
。 此刻,dplyr在内部深度复制整个data.table以添加一个新列。 @BrodieG在他的回答中已经提到了这一点。
但是,实施FR#617时,可以用浅拷贝replace深层副本。 也相关: dplyr:FR#614 。 请注意,仍然会修改您修改的列(因此内存效率会更低/更less)。 没有办法通过引用更新列。
其他function
-
在data.table中,可以在join时进行聚合,而且由于中间连接结果永远不会实现,所以这对于理解和记忆效率更为直观。 检查这个post的例子。 你不能(目前?)使用dplyr的data.table / data.frame语法来做到这一点。
-
data.table的滚动连接function在dplyr的语法中也不受支持。
-
我们最近在data.table中实现了重叠连接,通过区间范围( 这里是一个例子 ),这是一个单独的函数
foverlaps()
,因此可以与pipe道操作符一起使用(magrittr / pipeR?) – 从来没有尝试过我)。但最终,我们的目标是将其整合到
[.data.table
以便我们可以收集其他function,如分组,聚集等,同时也将具有上述相同的限制。 -
从1.9.4开始,data.table实现了使用二级密钥的自动索引,用于基于正则R语法的基于快速二分search的子集。 例如:
DT[x == 1]
和DT[x %in% some_vals]
将在第一次运行时自动创build一个索引,然后将使用二进制search将相同列中的连续子集用于快速子集。 此function将继续演变。 请查看这个要点来简要介绍一下这个function。从
filter()
为data.tables实现的方式,它没有利用这个特性。 -
dplyr的特点是它也提供了与数据库的接口,使用相同的语法,data.table目前没有。
所以,你将不得不权衡这些(也可能是其他的观点),并根据这些权衡是否可以接受来决定。
HTH
(1)注意,高效的内存直接影响速度(特别是数据越来越大),因为大多数情况下的瓶颈是将数据从主内存转移到高速caching(尽可能多地利用高速caching中的数据 – 减less高速caching未命中 – 以减less访问主内存)。 这里不详细讨论。
去尝试一下。
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
根据聚合酶的评论修改。
回答你的问题:
- 是的,你正在使用
data.table
- 但是不像纯数据
data.table
语法那样高效
在许多情况下,对于那些想要dplyr
语法的人来说,这是一个可以接受的折衷办法,尽pipe它可能比使用普通dataframe的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
这一行dplyr:::grouped_dt
:
if (copy) { data <- data.table::copy(data) }
其中copy
默认为TRUE
(并且不能轻易更改为FALSE,我可以看到)。 这可能不占100%的差异,但是diamonds
大小的一般开销最有可能不是完全的差异。
问题是,为了有一个一致的语法, dplyr
进行分组。 它首先在与之匹配的原始数据表的副本上设置密钥,并且只在稍后进行分组。 data.table
只是为最大的结果组分配内存,在这种情况下,它只是一行,所以在需要分配多less内存方面有很大的不同。
仅供参考,如果有人关心,我通过使用treeprof
( install_github("brodieg/treeprof")
), Rprof
输出的一个实验(并且仍然非常多的alpha)
注意上面目前只适用于Mac AFAIK。 另外,不幸的是, Rprof
logging了packagename::funname
types的匿名调用,所以它实际上可以是packagename::funname
中的任何和所有的datatable::
calls,但是从快速testing看起来像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)