概述
我对 data.table
比较熟悉,对 dplyr
不太熟悉。我已经阅读了一些 dplyr
vignettes 和出现在 SO 上的例子,到目前为止我的结论是:
data.table
和 dplyr
在速度上是相当的,除非有很多(即 >10-100K)组,以及在其他一些情况下(参见下面的基准测试)1314239dplyr
有更易访问的语法 dplyr
抽象(或将)潜在的数据库交互 在我看来 2. 没有太大影响,因为我对它
data.table
相当熟悉,但我知道对于这两个新用户来说,这将是一个重要因素。我想避免争论哪个更直观,因为这与我从已经熟悉 data.table
的人的角度提出的具体问题无关。我也想避免讨论“更直观”如何导致更快的分析(当然是真的,但同样,这不是我在这里最感兴趣的)。问题
我想知道的是:
一个 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
解决方案的 hack 尝试要好得多。也就是说,好的 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显示在回答用do
潜在的变通@实现eddi 的问题)。 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
是绝对有可能,只是需要一些认真思考,置换/报价,等等,至少据我所知) 基准
data.table
变得基本上快除外。 data.table
鳞比dplyr
更好,因为群体数量的增加(更新在两个包的最新增强和R最近的版本)。此外,尝试获得 unique values 时的基准测试速度快 data.table
~6x。 data.table
75%/应用/排序而dplyr
是在较小的( another SO question from comments ,由于达纳斯)快40%。 data.table
的主要作者,有 benchmarked grouping operations on data.table
, dplyr
and python pandas
on up to 2 billion rows (~100GB in RAM) 。 data.table
~823 快 823 10 4 0数据
这是我在问题部分展示的第一个例子。
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))
最佳答案
我们至少需要涵盖这些方面以提供全面的答案/比较(没有特别的重要性顺序): Speed
、 Memory usage
、 Syntax
和 0x251812221313
我的目的是从 data.table 的角度尽可能清楚地涵盖每一个。
Note: unless explicitly mentioned otherwise, by referring to dplyr, we refer to dplyr's data.frame interface whose internals are in C++ using Rcpp.
data.table 语法的形式是一致的 -
Features
。将 DT[i, j, by]
、 i
和 j
保持在一起是设计使然。通过将相关操作放在一起,它可以轻松优化操作的速度,更重要的是内存使用,还提供一些强大的功能,同时保持语法的一致性。1.速度
相当多的基准测试(尽管主要是关于分组操作)已添加到已经显示 data.table 随着要分组的组和/或行数的增加而变得比 dplyr 更快的问题中,包括 benchmarks by Matt 分组从 1000 万到 20 亿100 - 1000 万组和不同分组列上的行(RAM 中的 100GB),也比较
by
。另见 updated benchmarks ,其中包括 pandas
和 Spark
。在基准测试中,还可以涵盖以下其余方面:
pydatatable
类型操作。2. 内存使用
DT[x > val, sum(y), by = z]
或 filter()
的操作可能是内存效率低下的(在 data.frames 和 data.tables 上)。 See this post。Note that Hadley's comment talks about speed (that dplyr is plentiful fast for him), whereas the major concern here is memory.
# sub-assign by reference, updates 'y' in-place
DT[x >= 1L, y := NA]
但是 dplyr 永远不会通过引用更新。 dplyr 等价物将是(请注意,结果需要重新分配): # copies the entire 'y' column
ans <- DF %>% mutate(y = replace(y, which(x >= 1L), NA))
对此的一个关注是 referential transparency 。通过引用更新 data.table 对象,尤其是在函数内可能并不总是可取的。但这是一个非常有用的功能:有关有趣的案例,请参见 this 和 this 帖子。我们想保留它。因此,我们正在努力导出 data.table 中的
slice()
函数,这将为用户提供两种可能性。例如,如果不想修改函数内的输入 data.table,则可以执行以下操作: foo <- function(DT) {
DT = shallow(DT) ## shallow copy DT
DT[, newcol := 1L] ## does not affect the original DT
DT[x > 2L, newcol := 2L] ## no need to copy (internally), as this column exists only in shallow copied DT
DT[x > 2L, x := 3L] ## have to copy (like base R / dplyr does always); otherwise original DT will
## also get modified.
}
通过不使用 shallow()
,旧功能被保留: bar <- function(DT) {
DT[, newcol := 1L] ## old behaviour, original DT gets updated by reference
DT[x > 2L, x := 3L] ## old behaviour, update column x in original DT.
}
通过使用 shallow()
创建浅拷贝,我们了解到您不想修改原始对象。我们在内部处理所有事情,以确保同时确保仅在绝对必要时复制您修改的列。实现后,这应该完全解决引用透明度问题,同时为用户提供两种可能性。Also, once
shallow()
is exported dplyr's data.table interface should avoid almost all copies. So those who prefer dplyr's syntax can use it with data.tables.
But it will still lack many features that data.table provides, including (sub)-assignment by reference.
假设您有两个 data.tables,如下所示:
DT1 = data.table(x=c(1,1,1,1,2,2,2,2), y=c("a", "a", "b", "b"), z=1:8, key=c("x", "y"))
# x y z
# 1: 1 a 1
# 2: 1 a 2
# 3: 1 b 3
# 4: 1 b 4
# 5: 2 a 5
# 6: 2 a 6
# 7: 2 b 7
# 8: 2 b 8
DT2 = data.table(x=1:2, y=c("a", "b"), mul=4:3, key=c("x", "y"))
# x y mul
# 1: 1 a 4
# 2: 2 b 3
并且您想在 shallow()
中的每一行中获得 sum(z) * mul
,同时按列 DT2
加入。我们可以:x,y
得到 DT1
,2)执行连接和 3)乘(或)数据表方式
DT1[, .(z = sum(z)), keyby = .(x,y)][DT2][, z := z*mul][]
dplyr 等价物
DF1 %>% group_by(x, y) %>% summarise(z = sum(z)) %>%
right_join(DF2) %>% mutate(z = z * mul)
sum(z)
功能):DT1[DT2, list(z=sum(z) * mul), by = .EACHI]
优势是什么?
by = .EACHI
,我们想要执行的操作就很清楚了。检查 this post 以获取
j
的详细说明。没有中间结果被具体化,join+aggregate 是一次性执行的。查看 this 、 this 和 this 帖子以了解实际使用场景。
在
by = .EACHI
中,您必须使用 join and aggregate or aggregate first and then join ,就内存而言(这反过来又转化为速度),这两种方法都没有效率。考虑如下所示的 data.table 代码:
DT1[DT2, col := i.mul]
添加/更新 dplyr
的列 DT1
与 col
来自 mul
的那些行,其中 0x251813121313141313131313131313131413143 列匹配。我认为 DT2
中没有与此操作完全等效的操作,即,在不避免 DT2
操作的情况下,它必须复制整个 DT1
只是为了向其中添加一个新列,这是不必要的。检查 this post 以了解实际使用场景。
To summarise, it is important to realise that every bit of optimisation matters. As Grace Hopper would say, Mind your nanoseconds!
3. 语法
现在让我们看看语法。哈德利评论 here :
Data tables are extremely fast but I think their concision makes it harder to learn and code that uses it is harder to read after you have written it ...
我觉得这句话毫无意义,因为它非常主观。我们或许可以尝试对比语法的一致性。我们将并排比较 data.table 和 dplyr 语法。
我们将使用如下所示的虚拟数据:
DT = data.table(x=1:10, y=11:20, z=rep(1:2, each=5))
DF = as.data.frame(DT)
# case (a)
DT[, sum(y), by = z] ## data.table syntax
DF %>% group_by(z) %>% summarise(sum(y)) ## dplyr syntax
DT[, y := cumsum(y), by = z]
ans <- DF %>% group_by(z) %>% mutate(y = cumsum(y))
# case (b)
DT[x > 2, sum(y), by = z]
DF %>% filter(x>2) %>% group_by(z) %>% summarise(sum(y))
DT[x > 2, y := cumsum(y), by = z]
ans <- DF %>% group_by(z) %>% mutate(y = replace(y, which(x > 2), cumsum(y)))
# case (c)
DT[, if(any(x > 5L)) y[1L]-y[2L] else y[2L], by = z]
DF %>% group_by(z) %>% summarise(if (any(x > 5L)) y[1L] - y[2L] else y[2L])
DT[, if(any(x > 5L)) y[1L] - y[2L], by = z]
DF %>% group_by(z) %>% filter(any(x > 5L)) %>% summarise(y[1L] - y[2L])
dplyr
。但是在更新时,我们不得不将逻辑移到 *_join
中。然而,在 data.table 中,我们用相同的逻辑表达两个操作 - 对 DT1
的行进行操作,但在第一种情况下,得到 filter()
,而在第二种情况下,用其累积和更新这些行的 mutate()
。这就是我们说
x > 2
形式一致时的意思。sum(y)
条件时,我们能够在 data.table 和 dplyr 中“按原样”表达逻辑。但是,如果我们只想返回满足 y
条件的那些行,否则就跳过,我们不能直接使用 DT[i, j, by]
(AFAICT)。我们必须先 if-else
然后总结,因为 if
总是期望一个值。虽然它返回相同的结果,但在此处使用
summarise()
会使实际操作不那么明显。在第一种情况下也很可能使用
filter()
(对我来说似乎并不明显),但我的观点是我们不应该这样做。 # case (a)
DT[, lapply(.SD, sum), by = z] ## data.table syntax
DF %>% group_by(z) %>% summarise_each(funs(sum)) ## dplyr syntax
DT[, (cols) := lapply(.SD, sum), by = z]
ans <- DF %>% group_by(z) %>% mutate_each(funs(sum))
# case (b)
DT[, c(lapply(.SD, sum), lapply(.SD, mean)), by = z]
DF %>% group_by(z) %>% summarise_each(funs(sum, mean))
# case (c)
DT[, c(.N, lapply(.SD, sum)), by = z]
DF %>% group_by(z) %>% summarise_each(funs(n(), mean))
summarise()
,而 filter()
引入了 filter()
和一堆函数到 lapply()
。dplyr
需要提供列名,而 dplyr 会自动生成它。*_each()
的列数,而不是一次。在 data.table 中,我们需要做的就是返回 funs()
中的列表。列表的每个元素都将成为结果中的一列。因此,我们可以再次使用熟悉的基函数 :=
将 n()
连接到 j
,后者返回 c()
。Note: Once again, in data.table, all we need to do is return a list in
j
. Each element of the list will become a column in result. You can usec()
,as.list()
,lapply()
,list()
etc... base functions to accomplish this, without having to learn any new functions.
You will need to learn just the special variables -
.N
and.SD
at least. The equivalent in dplyr aren()
and.
dplyr 为每种类型的连接提供单独的函数,其中 data.table 允许使用相同的语法
.N
(并有理由)进行连接。它还提供等效的 list
函数作为替代。 setkey(DT1, x, y)
# 1. normal join
DT1[DT2] ## data.table syntax
left_join(DT2, DT1) ## dplyr syntax
# 2. select columns while join
DT1[DT2, .(z, i.mul)]
left_join(select(DT2, x, y, mul), select(DT1, x, y, z))
# 3. aggregate while join
DT1[DT2, .(sum(z) * i.mul), by = .EACHI]
DF1 %>% group_by(x, y) %>% summarise(z = sum(z)) %>%
inner_join(DF2) %>% mutate(z = z*mul) %>% select(-mul)
# 4. update while join
DT1[DT2, z := cumsum(z) * i.mul, by = .EACHI]
??
# 5. rolling join
DT1[DT2, roll = -Inf]
??
# 6. other arguments to control output
DT1[DT2, mult = "first"]
??
list
或 DT[i, j, by]
类似于 base R.merge.data.table()
才能加入,如上所示。否则,您会使用不必要的列来实现连接,只是为了稍后删除它们,这是低效的。DT[i, j, by]
特性 (3) 并在加入时更新 (4)。为什么将整个连接结果物化为仅添加/更新几列?merge()
参数,它选择第一个、最后一个或所有匹配项 (6)。select()
参数来防止意外的无效连接。Once again, the syntax is consistent with
DT[i, j, by]
with additional arguments allowing for controlling the output further.
by = .EACHI
...dplyr 的汇总是专门为返回单个值的函数设计的。如果您的函数返回多个/不相等的值,您将不得不求助于
mult =
。您必须事先了解所有函数的返回值。 DT[, list(x[1], y[1]), by = z] ## data.table syntax
DF %>% group_by(z) %>% summarise(x[1], y[1]) ## dplyr syntax
DT[, list(x[1:2], y[1]), by = z]
DF %>% group_by(z) %>% do(data.frame(.$x[1:2], .$y[1]))
DT[, quantile(x, 0.25), by = z]
DF %>% group_by(z) %>% summarise(quantile(x, 0.25))
DT[, quantile(x, c(0.25, 0.75)), by = z]
DF %>% group_by(z) %>% do(data.frame(quantile(.$x, c(0.25, 0.75))))
DT[, as.list(summary(x)), by = z]
DF %>% group_by(z) %>% do(data.frame(as.list(summary(.$x))))
allow.cartesian = TRUE
的等价物是 do()
do()
中抛出任何内容 - 唯一要记住的是它返回一个列表,以便列表中的每个元素都转换为一列。.SD
,具体取决于您对函数是否始终返回单个值的确定程度。而且速度很慢。Once again, data.table's syntax is consistent with
DT[i, j, by]
. We can just keep throwing expressions inj
without having to worry about these things.
看看 this SO question 和 this one 。我想知道是否可以使用 dplyr 的语法直接表达答案......
To summarise, I have particularly highlighted several instances where dplyr's syntax is either inefficient, limited or fails to make operations straightforward. This is particularly because data.table gets quite a bit of backlash about "harder to read/learn" syntax (like the one pasted/linked above). Most posts that cover dplyr talk about most straightforward operations. And that is great. But it is important to realise its syntax and feature limitations as well, and I am yet to see a post on it.
data.table has its quirks as well (some of which I have pointed out that we are attempting to fix). We are also attempting to improve data.table's joins as I have highlighted here.
But one should also consider the number of features that dplyr lacks in comparison to data.table.
4. 特点
我已经指出了 here 和这篇文章中的大部分功能。此外:
.
通过对 0x231341 期间的变量进行分组来自动对结果进行排序,这可能并不总是可取的j
以及 data.table 连接的所有其他优点进行连接。do()
在 data.table 中的函数允许通过引用真正快速地重新排序 data.tables。dplyr
提供的一组操作更快当量(由Jan Gorecki写入) - summarise()
,<=, <, >, >=
,setorder()
和data.table
用额外fsetdiff
参数(如SQL)。fintersect
兼容性。 dplyr 更改基本函数 funion
、 fsetequal
和 all
,这可能会导致问题;例如here 和 here 。最后:
[.data.frame
并行化已知的耗时部分以提高性能。 关于r - 数据表与 dplyr : can one do something well the other can't or does poorly?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21435339/