概述
我对 data.table
比较熟悉,而不是 dplyr
. 我已经阅读了一些dplyr vignettes以及在SO上出现的例子,到目前为止,我的结论是:
-
data.table
和dplyr
在速度上具有可比性,除非有许多(即> 10-100K)组,并且在某些其他情况下(参见下面的基准) -
dplyr
具有更易于访问的语法 -
dplyr
摘要(或将)潜在的数据库交互 -
存在一些细微的功能差异(请参阅下面的"Examples/Usage")
在我看来2.没有多大的重量,因为我对它非常熟悉 data.table
,虽然我明白对于两者都是新用户来说这将是一个很重要的因素 . 我想避免争论哪个更直观,因为这与我从熟悉 data.table
的人的角度提出的具体问题无关 . 我还想避免讨论"more intuitive"如何导致更快的分析(当然是真的,但同样,不是我最感兴趣的) .
问题
我想知道的是:
-
对于熟悉软件包的人来说,是否需要使用一个或另一个软件包进行编码的分析任务更容易(即需要按键的一些组合与所需的深奥水平相结合,其中每个小组都是好事) .
-
是否存在在一个包装与另一个包装中更有效地执行分析任务(即,大于2倍)的分析任务 .
一个recent SO question让我更多地思考这个问题,因为直到那时我才认为 dplyr
会提供超出我在 data.table
已经做过的事情 . 这是 dplyr
解决方案(Q末尾的数据):
dat %.%
group_by(name, job) %.%
filter(job != "Boss" | year == min(year)) %.%
mutate(cumu_job2 = cumsum(job2))
这比我在解决方案上的黑客尝试要好得多 . 也就是说,好的 data.table
解决方案也相当不错(感谢Jean-Robert,Arun,并注意到这里我赞成对最严格的最佳解决方案的单一陈述):
setDT(dat)[,
.SD[job != "Boss" | year == min(year)][, cumjob := cumsum(job2)],
by=list(id, job)
]
后者的语法可能看起来非常深奥,但如果你习惯 data.table
(即不使用一些更深奥的技巧),它实际上非常简单 .
理想情况下,我希望看到的是一些很好的例子 dplyr
或 data.table
方式实质上更简洁或表现更好 .
例子
用法
-
dplyr
不允许返回任意行数的分组操作(来自 eddi's question ,注意:这看起来好像它将在 dplyr 0.5 中实现,同样,@ beginneR在回答@ eddi的问题时显示了使用do
的潜在解决方法) . -
data.table
支持 rolling joins (感谢@dholstius)以及 overlap joins -
data.table
在内部通过自动索引优化表单DT[col == value]
或DT[col %in% values]
的表达式,自动索引使用二进制搜索,同时使用相同的基本R语法 . See here了解更多细节和一个小基准 . -
dplyr
提供了函数的标准评估版本(例如regroup
,summarize_each_
),可以简化dplyr
的编程使用(注意编程使用data.table
绝对是可能的,只需要仔细考虑,替换/引用等,至少据我所知)
基准
-
我跑了 my own benchmarks 并发现两个包在"split apply combine"样式分析中具有可比性,除非有非常多的组(> 100K),此时
data.table
变得非常快 . -
@Arun运行了一些 benchmarks on joins ,显示
data.table
比dplyr
更好地扩展,因为组的数量增加(使用最近增强的包和最新版本的R更新) . 此外,尝试获得 unique values 时的基准测试速度提高了data.table
~6倍 . -
(未验证)在较大版本的组/ apply / sort上的速度提高了
data.table
,而dplyr
在较小版本的组件上速度提高了40%( another SO question from comments ,感谢danas) . -
马特,
data.table
的主要作者,benchmarked grouping operations on data.table, dplyr and python pandas on up to 2 billion rows (~100GB in RAM) . -
older benchmark on 80K groups 的速度快了
data.table
~8倍
数据
这是我在问题部分中展示的第一个例子 .
dat <- structure(list(id = c(1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 2L, 2L,
2L, 2L, 2L, 2L, 2L, 2L), name = c("Jane", "Jane", "Jane", "Jane",
"Jane", "Jane", "Jane", "Jane", "Bob", "Bob", "Bob", "Bob", "Bob",
"Bob", "Bob", "Bob"), year = c(1980L, 1981L, 1982L, 1983L, 1984L,
1985L, 1986L, 1987L, 1985L, 1986L, 1987L, 1988L, 1989L, 1990L,
1991L, 1992L), job = c("Manager", "Manager", "Manager", "Manager",
"Manager", "Manager", "Boss", "Boss", "Manager", "Manager", "Manager",
"Boss", "Boss", "Boss", "Boss", "Boss"), job2 = c(1L, 1L, 1L,
1L, 1L, 1L, 0L, 0L, 1L, 1L, 1L, 0L, 0L, 0L, 0L, 0L)), .Names = c("id",
"name", "year", "job", "job2"), class = "data.frame", row.names = c(NA,
-16L))
3 回答
这是我从dplyr角度全面回答的尝试,遵循Arun答案的大致轮廓(但根据不同的优先级进行了一些重新安排) .
语法
语法有一些主观性,但我坚持认为data.table的简洁使得学习更难学,更难阅读 . 这部分是因为dplyr解决了一个更容易的问题!
dplyr为您做的一件非常重要的事情是它限制了您的选择 . 我声称大多数单表问题都可以通过五个关键动词过滤,选择,变异,排列和汇总以及"by group"副词来解决 . 当您学习数据操作时,这种约束是一个很大的帮助,因为它有助于您对问题进行思考 . 在dplyr中,每个动词都映射到一个函数 . 每个功能都有一份工作,很容易理解 .
您可以通过将这些简单操作与
%>%
一起管道来创建复杂性 . 以下是其中一个帖子Arun linked to的示例:即使你发生了因为这些功能都是英语动词 . 英语动词的缺点是它们需要比
[
更多的打字,但我认为可以通过更好的自动完成来大大减轻这种情况 .这是等效的data.table代码:
它's harder to follow this code unless you'已经熟悉data.table . (我也无法弄清楚如何以一种看起来很好看的方式缩进重复的
[
) . 就个人而言,当我看到我在6个月前写的代码时,它已经变得更加直接,如果详细,代码 .我认为其他两个小因素会略微降低可读性:
由于几乎每个数据表操作都使用
[
,因此需要额外的上下文来确定发生了什么 . 例如,是x[y]
连接两个数据表还是从数据框中提取列?这只是一个小问题,因为在编写良好的代码中,变量名称应该表明发生了什么 .我喜欢
group_by()
是dplyr中的一个单独操作 . 它从根本上改变了计算,所以我认为在略读代码时应该是显而易见的,并且比by
的by
参数更容易发现group_by()
.我也喜欢the pipe不仅限于一个包 . 您可以先使用tidyr整理数据,然后使用ggvis中的绘图结束 . 而且你不仅限于我写的软件包 - 任何人都可以编写一个函数来构成数据操作管道的无缝部分 . 实际上,我更喜欢先前用
%>%
重写的data.table代码:使用
%>%
进行管道的想法不仅限于数据帧,而且很容易推广到其他上下文:interactive web graphics,web scraping,gists,run-time contracts,...)记忆和表现
我把它们混在一起,因为对我来说,它们并不那么重要 . 大多数R用户的工作量低于100万行,而dplyr足够快,足以满足您不了解处理时间的数据量 . 我们优化dplyr以表达对中等数据的表达;随时可以使用data.table获取更大数据的原始速度 .
dplyr的灵活性还意味着您可以使用相同的语法轻松调整性能特征 . 如果dplyr与数据帧后端的性能不够好,则可以使用data.table后端(尽管功能有限) . 如果您使用的数据不适合内存,则可以使用数据库后端 .
总而言之,dplyr的表现会在长期内变得更好 . 我们肯定会实现data.table的一些好主意,比如基数排序和联接和过滤器使用相同的索引 . 我们还致力于并行化,因此我们可以利用多个核心 .
功能
我们计划在2015年开展的一些工作:
readr
包,使文件从磁盘和内存中轻松获取,类似于fread()
.更灵活的连接,包括对非等连接的支持 .
更灵活的分组,如bootstrap样本,汇总等
我'm also investing time into improving R' s database connectors,能够与web apis交谈,并且更容易scrape html pages .
直接回答问题 Headers ......
dplyr肯定会做data.table不能做的事情 .
你的观点#3
是你自己问题的直接答案,但没有提升到足够高的水平 .
dplyr
是多个数据存储机制的真正可扩展前端,其中data.table
是单个数据存储机制的扩展 .将
dplyr
视为后端不可知接口,所有目标都使用相同的语法,您可以随意扩展目标和处理程序 . 从dplyr
的角度来看,data.table
是其中一个目标 .您永远不会(我希望)看到
data.table
尝试翻译您的查询以创建与磁盘或网络数据存储一起运行的SQL语句 .dplyr可能会做data.table不会或可能不会做 .
基于内存工作的设计,
data.table
可能会比将更多的时间延伸到查询的并行处理中,而不是dplyr
.回应体内问题......
用法
这可能看起来像是一个平底船,但真正的答案是否定的 . 熟悉工具的人似乎使用他们最熟悉的工具或实际上适合工作的工具 . 有人说,有时候你想要提供一个特定的可读性,有时候是一个性能水平,当你需要足够高的两者时,你可能只需要另一个工具来配合你已经拥有的东西来做出更清晰的抽象 .
表现
再一次,没有 .
data.table
擅长提高它所做的一切dplyr
在某些方面受限于底层数据存储和注册处理程序的负担 .这意味着当您遇到
data.table
的性能问题时,您可以非常肯定它在您的查询功能中,如果它实际上是data.table
的瓶颈,那么您已经赢得了提交报告的乐趣 . 当dplyr
使用data.table
作为后端时也是如此;您可能会看到dplyr
的一些开销,但可能是您的查询 .当
dplyr
具有后端性能问题时,您可以通过注册混合评估函数或(在数据库的情况下)在执行之前操作生成的查询来绕过它们 .另见when is plyr better than data.table?的接受答案
我们需要至少涵盖这些方面,以提供全面的答案/比较(没有特别重要的顺序):
Speed
,Memory usage
,Syntax
和Features
.我的目的是从data.table的角度尽可能清楚地涵盖其中的每一个 .
data.table语法的形式一致 -
DT[i, j, by]
. 保持i
,j
和by
在一起是设计的 . 通过将相关操作保持在一起,它允许轻松优化操作以提高速度,更重要的是内存使用,并提供一些强大的功能,同时保持语法的一致性 .1.速度
相当多的基准测试(尽管主要是分组操作)已经添加到已经显示data.table的问题比dplyr更快,因为要分组的组和/或行的数量增加,包括benchmarks by Matt分组从1000万到20亿在100-1000万个组和不同的分组列上的行(100GB RAM),也比较了
pandas
.在基准测试中,覆盖这些剩余方面也很棒:
涉及行子集的分组操作 - 即
DT[x > val, sum(y), by = z]
类型的操作 .对其他操作进行基准测试,例如更新和连接 .
除运行时外,还为每个操作 Build 基准内存占用量 .
2.内存使用情况
filter()
或slice()
的操作可能内存效率低(在data.frames和data.tables上) . See this post .但dplyr永远不会通过引用更新 . dplyr等价物将是(注意结果需要重新分配):
对此的担忧是referential transparency . 通过引用更新data.table对象,尤其是在函数内,可能并不总是需要 . 但这是一个非常有用的功能:请参阅this和this帖子以了解有趣的案例 . 我们想保留它 .
因此,我们正在努力在data.table中导出
shallow()
函数,该函数将为用户提供两种可能性 . 例如,如果希望不修改函数中的输入data.table,则可以执行以下操作:通过不使用
shallow()
,保留旧功能:通过使用
shallow()
创建浅表副本,我们知道您不想修改原始对象 . 我们在内部处理所有事情,以确保在确保复制列时,只有在绝对必要时才修改 . 实施时,这应该完全解决参考透明度问题,同时为用户提供两种可能性 .假设您有两个data.tables如下:
并且您希望在
DT2
中的每一行获得sum(z) * mul
,同时按列x,y
加入 . 我们可以:DT1
得到sum(z)
,2)执行连接和3)乘(或)by = .EACHI
功能):有什么好处?
我们不必为中间结果分配内存 .
我们不必分组/哈希两次(一次用于聚合,另一次用于连接) .
更重要的是,通过查看(2)中的
j
,可以清楚地了解我们想要执行的操作 .检查this post以获取
by = .EACHI
的详细说明 . 没有实现中间结果,并且一次性执行连接聚合 .查看实际使用场景的this,this和this帖子 .
在
dplyr
中,你必须join and aggregate or aggregate first and then join,在内存方面都不是那么有效(这反过来转化为速度) .考虑下面显示的data.table代码:
在
DT2
的键列与DT1
匹配的行上,从DT2
添加/更新DT1
的col
列col
. 我认为dplyr
中没有完全等效的操作,即没有避免*_join
操作,只需要复制整个DT1
只是为了向它添加一个新列,这是不必要的 .检查this post以获取实际使用情况 .
3.语法
我们现在看一下语法 . 哈德利评论here:
我发现这句话毫无意义,因为它非常主观 . 我们可以尝试的是对比语法的一致性 . 我们将并排比较data.table和dplyr语法 .
我们将使用下面显示的虚拟数据:
data.table语法是紧凑的,dplyr非常详细 . 在(a)的情况下,事情或多或少是等价的 .
如果是(b),我们必须在总结时在dplyr中使用
filter()
. 但在更新时,我们不得不在mutate()
内移动逻辑 . 但是,在data.table中,我们使用相同的逻辑表示两个操作 - 在x > 2
的行上操作,但在第一种情况下,获取sum(y)
,而在第二种情况下,使用其累积总和更新y
的那些行 .当我们说
DT[i, j, by]
表格一致时,这就是我们的意思 .if-else
条件时,我们能够在data.table和dplyr中表达逻辑"as-is" . 但是,如果我们只想返回if
条件满足的那些行,否则我们不能直接使用summarise()
(AFAICT) . 我们必须首先filter()
然后总结,因为summarise()
总是期望单个值 .虽然它返回相同的结果,但在这里使用
filter()
会使实际操作不那么明显 .在第一种情况下也很可能使用
filter()
(对我来说似乎并不明显),但我的观点是我们不应该这样做 .在情况(a)中,代码或多或少相等 . data.table使用熟悉的基函数
lapply()
,而dplyr
将*_each()
和一堆函数引入funs()
.data.table的
:=
需要提供列名,而dplyr会自动生成列名 .在情况(b)中,dplyr的语法相对简单 . 改进多个函数的聚合/更新是在data.table的列表中 .
但是在(c)的情况下,dplyr将返回
n()
列的次数,而不是一次 . 在data.table中,我们需要做的就是在j
中返回一个列表 . 列表的每个元素都将成为结果中的一列 . 因此,我们可以再次使用熟悉的基函数c()
将.N
连接到list
,返回list
.dplyr为每种类型的连接提供单独的函数,其中data.table允许使用相同的语法
DT[i, j, by]
(和有原因)连接 . 它还提供了等效的merge.data.table()
函数作为替代 .有些人可能会为每个连接找到一个单独的函数(左,右,内,反,半等),而其他人可能喜欢data.table的
DT[i, j, by]
或merge()
,类似于基础R.然而dplyr加入就是这么做的 . 而已 . 没什么 .
data.tables可以在加入(2)时选择列,而在dplyr中,如上所示,在加入之前,您需要先在两个data.frames上使用
select()
. 否则,您将仅使用不必要的列来实现连接,以便稍后删除它们,这是低效的 .data.tables可以聚合加入(3)并使用
by = .EACHI
功能在加入(4)时更新 . 为什么要将整个连接结果添加/更新几列?data.table能够滚动连接(5) - 滚动forward, LOCF,roll backward, NOCB,nearest .
data.table也有
mult =
参数,用于选择第一个,最后一个或所有匹配(6) .data.table具有allow.cartesian = TRUE参数以防止意外的无效连接 .
do()
......dplyr的总结是专门为返回单个值的函数而设计的 . 如果函数返回多个/不相等的值,则必须使用
do()
. 您必须事先知道所有函数的返回值 ..SD
的等价物是.
在data.table中,你可以在_161989中抛出几乎任何东西 - 唯一要记住的是它返回一个列表,以便列表的每个元素都转换为一列 .
在dplyr中,不能那样做 . 必须求助于
do()
,具体取决于您的功能是否始终返回单个值 . 它很慢 .看看this SO question和this one . 我想知道使用dplyr的语法是否可以直截了当地表达答案...
4.功能
我已经指出了大部分功能here以及这篇文章 . 此外:
fread - 快速文件阅读器已经有很长一段时间了 .
fwrite - 当前devel版本v1.9.7中的新功能,现在可以使用并行快速文件编写器 . 有关实施的详细说明,请参见this post;有关进一步发展的信息,请参阅#1664 .
Automatic indexing - 另一个在内部优化基本R语法的便利功能 .
Ad-hoc grouping :
dplyr
在summarise()
期间通过对变量进行分组来自动对结果进行排序,这可能并不总是令人满意 .上面提到的data.table连接(用于速度/内存效率和语法)的许多优点 .
Non-equi joins :是v1.9.7中的新功能 . 它允许使用其他运算符
<=, <, >, >=
连接以及data.table连接的所有其他优点 .Overlapping range joins最近在data.table中实现 . 检查this post以获得基准测试的概述 .
data.table中的
setorder()
函数允许通过引用真正快速重新排序data.tables .dplyr使用相同的语法提供interface to databases,而data.table目前不提供 .
data.table
从v1.9.7(由Jan Gorecki编写)提供更快的等效设置操作 -fsetdiff
,fintersect
,funion
和fsetequal
以及额外的all
参数(如在SQL中) .data.table干净地加载而没有屏蔽警告,并且在传递给任何R软件包时具有here
[.data.frame
兼容性的机制 . dplyr更改了可能导致问题的基本函数filter
,lag
和[
;例如here和here .最后:
在数据库上 - 没有理由说data.table不能提供类似的接口,但现在这不是优先事项 . 如果用户非常喜欢这个功能,它可能会被提升......不确定 .
关于并行性 - 一切都很困难,直到有人继续前进并做到这一点 . 当然需要付出努力(线程安全) .
目前正在取得进展(在v1.9.7开发中),使用
OpenMP
将已知耗时部件并行化,以实现增量性能增益 .