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

wb1gzix0  于 2022-12-20  发布在  其他
关注(0)|答案(4)|浏览(221)

如果我在一个datatable之上使用dplyr语法,我是否可以在使用dplyr语法的同时获得datatable的所有速度优势?换句话说,如果我使用dplyr语法查询它,我是否会误用datatable?或者我是否需要使用纯datatable语法来利用它的所有功能。
代码示例:

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) ]
mwecs4sa

mwecs4sa1#

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

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

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

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

(1)给出DT中的行数,其中列a > 1.(2)返回mean(b),该列按c,d分组,i中的表达式与(1)相同。
常用的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]

它只在满足条件的行上更新列a * by reference *。此时dplyr deep在内部复制整个data.table以添加新列。@BrodieG在他的回答中已经提到了这一点。
但在实施FR #617时,深层副本可以替换为浅层副本。dplyr: FR#614。请注意,您修改的列将始终被复制(因此稍慢/内存效率较低)。将无法通过引用更新列。

其他功能

  • 在data.table中,你可以在连接的同时进行聚合,这更容易理解,而且由于中间连接结果永远不会实体化,所以内存效率更高。查看this post的例子。你不能(目前?)使用dplyr的data.table/data.frame语法来做这件事。
  • data.table的rolling joins特性在dplyr的语法中也不受支持。
  • 我们最近在data.table中实现了在区间范围上进行连接的重叠连接(here's an example),目前它是一个单独的函数foverlaps(),因此可以与管道操作符一起使用(magrittr/pipeR?-我自己从未尝试过)。

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

  • 从1.9.4开始,data.table使用辅助键实现自动索引,以便基于规则R语法的子集进行快速二进制搜索。例如:DT[x == 1]DT[x %in% some_vals]将在第一次运行时自动创建索引,然后使用二分搜索将该索引用于从同一列到快速子集的连续子集。此功能将继续发展。有关此功能的简短概述,请查看this gist

从为data.tables实现filter()的方式来看,它没有利用这个特性。

  • dplyr的一个特性是它还提供了使用相同语法的interface to databases,而data.table目前还没有。

因此,您必须权衡这些(可能还有其他几点),并根据这些权衡是否为您所接受来做出决定。
高温加热
(1)请注意,内存效率直接影响速度(尤其是当数据变得更大时),因为大多数情况下的瓶颈是将数据从主内存移动到缓存(并尽可能多地利用缓存中的数据-减少缓存未命中-从而减少访问主内存)。

a2mppw5e

a2mppw5e2#

你试试看

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的意见修订

nwlls2ji

nwlls2ji3#

要回答您的问题:

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

在许多情况下,对于那些想要dplyr语法的人来说,这是一个可以接受的折衷方案,尽管它可能比使用普通 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中的这一行:

if (copy) {
    data <- data.table::copy(data)
}

其中copy默认为TRUE(并且不能轻易地更改为FALSE,我可以看到)。这可能没有解释100%的差异,但是diamonds大小的东西的一般开销很可能不是全部差异。
问题在于,为了保持语法的一致性,dplyr分两步进行分组:首先在原始数据表的副本上设置与分组相匹配的键,然后才进行分组。data.table只为最大的结果组分配内存,在本例中,最大的结果组只有一行,因此需要分配的内存量有很大差异。
仅供参考,如果有人关心的话,我是通过使用treeprofinstall_github("brodieg/treeprof"))发现这一点的,Rprof输出的一个实验性(仍然非常alpha)树查看器:

请注意,上述内容目前仅适用于macs AFAIK。此外,不幸的是,Rprofpackagename::funname类型的调用记录为匿名,因此它实际上可能是grouped_dt内部负责的任何和所有datatable::调用,但从快速测试来看,datatable::copy似乎是一个大调用。
也就是说,您可以很快看到[.data.table调用并没有太多的开销,但是分组也有一个完全独立的分支。

EDIT:确认复制:

> 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)
eit6fx6z

eit6fx6z4#

现在可以使用dtplyr了,它是tidyverse的一部分。它允许您像往常一样使用dplyr样式的语句,但是利用了惰性求值,并在幕后将您的语句转换为data.table代码。转换的开销很小,但是您可以派生所有的,如果不是,data.table的大部分好处。更多细节请访问官方git repo here和tidyverse page

相关问题