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相同。

常用的dplyrexpression式是:

 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内存方面有很大的不同。

仅供参考,如果有人关心,我通过使用treeprofinstall_github("brodieg/treeprof") ), Rprof输出的一个实验(并且仍然非常多的alpha)

在这里输入图像说明

注意上面目前只适用于Mac AFAIK。 另外,不幸的是, Rproflogging了packagename::funnametypes的匿名调用,所以它实际上可以是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)