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

Pol*_*ase 83 r dplyr data.table

如果我使用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))
Run Code Online (Sandbox Code Playgroud)

结果:

#         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
Run Code Online (Sandbox Code Playgroud)

这是我想出的数据表等价.不确定它是否符合DT良好做法.但我想知道代码是否比场景背后的dplyr语法更有效:

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

Aru*_*run 73

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

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

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

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

(1)给出的行的数量DT,其中柱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)
Run Code Online (Sandbox Code Playgroud)

显然,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)
Run Code Online (Sandbox Code Playgroud)

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

  • 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]
Run Code Online (Sandbox Code Playgroud)

它 仅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目前还没有.

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

HTH


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

  • 这是一个很好的答案,但是dplyr实现一个有效的`filter()`加上`summarize()`是使用dplyr用于SQL的相同方法 - 即构建一个表达式然后只有_possible_(如果不可能)按需执行一次.这不太可能在不久的将来实现,因为dplyr对我来说足够快,并且实现查询规划器/优化器相对比较困难. (6认同)
  • 绝对精彩.感谢那 (4认同)

G. *_*eck 23

就试一试吧.

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]
Run Code Online (Sandbox Code Playgroud)

在这个问题上,似乎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
Run Code Online (Sandbox Code Playgroud)

根据Polymerase的评论进行修订.

  • 使用`microbenchmark`软件包,我发现在'diamond`的原始(数据框)版本上运行OP的`dplyr`代码的中位时间为0.012秒,而转换`diamond后的中位时间为0.024秒`到数据表.运行G. Grothendieck的`data.table`代码花了0.013秒.至少在我的系统上,看起来`dplyr`和`data.table`具有相同的性能.但是,当数据帧首次转换为数据表时,为什么`dplyr`会变慢? (2认同)

Bro*_*ieG 20

回答你的问题:

  • 是的,你正在使用 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
Run Code Online (Sandbox Code Playgroud)

过滤速度相当,但分组不是.我相信罪魁祸首是这一行dplyr:::grouped_dt:

if (copy) {
    data <- data.table::copy(data)
}
Run Code Online (Sandbox Code Playgroud)

其中copy缺省为TRUE(并且不能轻易地改变为FALSE,我可以看到).这可能不会占到差异的100%,但仅在一般情况下,对于diamonds最有可能的大小的开销不是完全不同的.

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

仅供参考,如果有人关心,我发现这是通过使用treeprof(install_github("brodieg/treeprof")),一个实验(并且仍然非常阿尔法)树查看器Rprof输出:

在此输入图像描述

注意以上目前仅适用于macs 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)
Run Code Online (Sandbox Code Playgroud)

  • 我也在等data.table中的浅拷贝函数; 在那之前,我认为安全比快. (3认同)
  • 我觉得我在开发版中修复了这个问题? (2认同)