data.table vs dplyr:它们之间有没有一方能做到另一方无法或做得不好的事情?

911

概述

我对data.table比较熟悉,对dplyr了解不太多。我阅读了一些dplyr的文档和在SO上出现的示例,到目前为止我的结论是:

  1. data.tabledplyr在速度上可比,除非有很多(即>10-100K)组,以及其他一些情况(见下面的基准测试)
  2. dplyr具有更易于理解的语法
  3. dplyr抽象了(或将要抽象)潜在的数据库交互
  4. 存在一些次要的功能差异(见下面的“示例/用法”)
在我看来,第二点并不重要,因为我对data.table相当熟悉,尽管我理解对于两者都不熟悉的用户来说,这将是一个重要因素。我不想争论哪个更直观,因为这与我从已经熟悉data.table角度提出的具体问题无关。我也不想讨论"更直观"如何导致更快的分析(确实如此,但这不是我最感兴趣的)。
问题是:我想知道:
1. 对于熟悉这两个包的人来说,是否有一些分析任务在使用其中一个包进行编码时更容易完成(即所需的按键组合或奇特程度的级别,较少为好)。 2. 是否有某个包在某些分析任务上的性能表现明显更高(即比另一个包更高出2倍以上)。

最近的一个 SO 问题让我多想了一下,因为在那之前我并不认为 dplyr 会提供比 data.table 更多的功能。以下是 dplyr 的解决方案(问题末尾有数据):

dat %.%
  group_by(name, job) %.%
  filter(job != "Boss" | year == min(year)) %.%
  mutate(cumu_job2 = cumsum(job2))

这比我尝试使用`data.table`解决方案好得多。话虽如此,好的`data.table`解决方案也非常好(感谢Jean-Robert、Arun,并且请注意,我更倾向于单语句而不是严格最优解):
setDT(dat)[,
  .SD[job != "Boss" | year == min(year)][, cumjob := cumsum(job2)], 
  by=list(id, job)
]

后者的语法可能看起来非常晦涩,但如果你习惯于使用data.table(即不使用一些较为晦涩的技巧),它实际上非常直观。

理想情况下,我希望看到一些很好的例子,其中dplyrdata.table的方式更加简洁或性能更好。

例子

用法

  • dplyr不允许返回任意行数的分组操作(来自eddi's question,注意:这似乎将在dplyr 0.5中实现,同时,@beginneR在回答@eddi的问题时提供了使用do的潜在解决方法)。
  • data.table支持滚动连接(感谢@dholstius),以及重叠连接
  • data.table通过自动索引内部优化形式为DT[col == value]DT[col %in% values]的表达式,以提高速度,该优化使用二分搜索,并使用相同的基本R语法。在这里可以找到更多细节和一个小型基准测试。
  • dplyr提供了函数的标准评估版本(例如regroupsummarize_each_),可以简化对dplyr的编程使用(请注意,编程使用data.table肯定是可行的,只需要一些谨慎思考、替换/引用等,至少根据我目前的了解)

基准测试

数据

这是我在问题部分展示的第一个例子。

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

10
类似于dplyr的解决方案是:as.data.table(dat)[, .SD[job != "Boss" | year == min(year)][, cumjob := cumsum(job2)], by = list(name, job)] - eddi
14
再次强调,依我之见,那些更适合用(d)plyr来表达的问题集合的概率为零。 - eddi
41
@BrodieG,关于dplyrplyr的语法,有一件事真的让我很烦恼,这基本上是我不喜欢它们语法的主要原因,就是我必须学习太多(不止一个)额外的函数(它们的名称仍然对我来说没有意义),记住它们的作用,它们接受的参数等等。这对于我来说一直是plyr哲学中的巨大障碍。 - eddi
55
@eddi [暗中调侃]我对data.table语法最烦的一件事情是,我必须学习太多函数参数之间的交互方式以及什么是加密快捷方式(例如.SD)。[认真地]我认为这些是合理的设计差异,会吸引不同的人。 - hadley
10
@hadley 关于 .SD 等内容的讨论 - 这很公正 - 我花了一点时间才理解 .SD,但当我掌握它时,我已经能够做很多事情了,而 (d)plyr 则在一开始就给你设置了一个很大的障碍。 - eddi
显示剩余20条评论
5个回答

650
我们需要至少涵盖以下几个方面,以提供全面的答案/比较(重要性无特定顺序):速度、内存使用、语法和功能。
我的意图是从data.table的角度尽可能清楚地涵盖每个方面。
引用: 注意:除非明确说明,否则提到dplyr时,我们指的是使用Rcpp在C++中实现的dplyr的data.frame接口。
数据表的语法在形式上是一致的 - DT[i, j, by]。将ijby放在一起是有意设计的。通过将相关操作放在一起,可以方便地对操作进行优化,提高速度,更重要的是减少内存使用,并提供一些强大的功能,同时保持语法的一致性。

1. 速度

已经在问题中添加了相当多的基准测试(尽管大多数是关于分组操作),显示出随着分组数量和/或分组行数的增加,data.table比dplyr更快,包括Matt关于从1000万到20亿行(100GB内存)进行分组的基准测试,以及100-1000万个分组和不同的分组列,还比较了pandas。另请参阅更新的基准测试,其中还包括Spark和Polars。
在基准测试中,还可以涵盖以下剩余方面,这将是很好的。
  • 涉及到对行的子集进行分组操作 - 即,类似于 DT[x > val, sum(y), by = z] 的操作。

  • 对其他操作(如更新和连接)进行基准测试。

  • 此外,还要对每个操作的内存占用进行基准测试。

2. 内存使用

  1. dplyr中涉及到filter()slice()的操作可能在内存上效率低下(对于data.frame和data.table都是如此)。请参阅此帖子

    请注意Hadley的评论谈到的是速度(对于他来说,dplyr的速度已经足够快),而这里的主要关注点是内存

  2. 目前,data.table接口允许通过引用修改/更新列(注意,我们不需要将结果重新分配给变量)。

     # 通过引用进行子分配,原地更新'y'
     DT[x >= 1L, y := NA]
    

    但是dplyr 永远不会通过引用进行更新。dplyr的等效操作将是(请注意,需要重新分配结果):

     # 复制整个'y'ans <- DF %>% mutate(y = replace(y, which(x >= 1L), NA))
    

    对此的一个担忧是引用透明性。通过引用更新data.table对象,特别是在函数内部,可能并不总是可取的。但这是一个非常有用的功能:请参阅帖子以了解有趣的案例。我们希望保留它。

    因此,我们正在努力导出data.table中的shallow()函数,以为用户提供两种可能性。例如,如果在函数内部不希望修改输入的data.table,则可以执行以下操作:

     foo <- function(DT) {
         DT = shallow(DT)          ## 浅复制DT
         DT[, newcol := 1L]        ## 不会影响原始DT 
         DT[x > 2L, newcol := 2L]  ## 无需复制(内部),因为此列仅存在于浅复制的DT中
         DT[x > 2L, x := 3L]       ## 必须复制(与base R / dplyr一样总是这样);否则原始DT也会被修改。
     }
    

    通过不使用shallow(),保留了旧功能:

     bar <- function(DT) {
         DT[, newcol := 1L]        ## 旧行为,原始DT通过引用更新
         DT[x > 2L, x := 3L]       ## 旧行为,在原始DT中更新列x。
     }
    

    通过使用shallow()创建浅复制,我们理解您不希望修改原始对象。我们在内部处理一切,以确保在确实需要复制列时,仅复制您修改的列。一旦实现,这将完全解决引用透明性问题,并为用户提供两种可能性。

    此外,一旦导出shallow(),dplyr的data.table接口几乎可以避免所有复制。因此,那些喜欢dplyr语法的人可以将其与data.table一起使用。

    但它仍然缺少data.table提供的许多功能,包括(子)引用赋值。

  3. 在连接时进行聚合:

    假设您有两个如下的data.table:

     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
    

    您想要在连接列x,y上,对DT2中的每一行计算sum(z) * mul。我们可以选择:

      1. 聚合DT1以获取sum(z),2)执行连接,3)相乘(或)

        data.table方式

        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)
        
      1. 一次完成所有操作(使用by = .EACHI功能):

        DT1[DT2, list(z=sum(z) * mul), by = .EACHI]
        

    有什么优势?

    • 我们不需要为中间结果分配内存。

    • 我们不需要两次进行分组/哈希(一次用于聚合,另一次用于连接)。

    • 更重要的是,通过查看(2)中的j,我们可以清楚地了解我们想要执行的操作。

    请查看此帖子以获取有关by = .EACHI的详细解释。没有中间结果被实现,连接+聚合一次完成。

    请查看, 帖子以获取实际使用场景。

    dplyr中,您将不得不先连接再聚合或先聚合再连接,这两种方法都不如(从内存的角度来看,也就是速度)高效。

  4. 更新和连接:

    考虑下面的data.table代码:

     DT1[DT2, col := i.mul]
    

    DT2的关键列与DT1匹配的行上,将DT1的列col添加/更新为DT2中的mul。我认为在dplyr中没有这个操作的确切等效方式,即,不避免*_join操作,这将不得不复制整个DT1,只是为了添加一个新列,这是不必要的。

    请查看此帖子以获取实际使用场景。

总结一下,意识到每一点优化的重要性是很重要的。正如Grace Hopper所说,要注意你的纳秒!
3. 语法
现在让我们来看看语法。Hadley在这里评论说:
数据表非常快,但我认为它们的简洁性使得学习起来更加困难,而且编写后的代码也更难阅读...
我觉得这个评论没有意义,因为它非常主观。我们可以尝试对比语法的一致性。我们将比较data.table和dplyr语法的差异。
我们将使用下面显示的虚拟数据进行工作:
DT = data.table(x=1:10, y=11:20, z=rep(1:2, each=5))
DF = as.data.frame(DT)

基本的聚合/更新操作。
# 情况(a) DT[, sum(y), by = z] ## data.table 语法 DF %>% group_by(z) %>% summarise(sum(y)) ## dplyr 语法 DT[, y := cumsum(y), by = z] ans <- DF %>% group_by(z) %>% mutate(y = cumsum(y))
# 情况(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)))
# 情况(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])
data.table 语法简洁,而 dplyr 的语法相对冗长。在情况(a)中,两者几乎是等价的。
在情况(b)中,我们在 dplyr 中必须在 summarise 中使用 filter(),但在更新时,我们必须将逻辑放在 mutate() 中。然而,在 data.table 中,我们可以使用相同的逻辑表达这两个操作 - 在 x > 2 的行上操作,但在第一种情况下,获取 sum(y),而在第二种情况下,将这些行的 y 更新为其累积和。
这就是我们所说的 DT[i, j, by] 形式“一致”的含义。
类似地,在情况(c)中,当我们有 if-else 条件时,我们能够在 data.table 和 dplyr 中以“原样”表达逻辑。然而,如果我们只想返回满足 if 条件的行并跳过其他行,我们不能直接使用 summarise()(据我所知)。我们必须先使用 filter(),然后再使用 summarise(),因为 summarise() 总是期望一个“单个值”。
虽然它返回相同的结果,但在这里使用 filter() 使实际操作不太明显。
在第一种情况下,可能也可以使用 filter()(对我来说不太明显),但我的观点是我们不应该这样做。
多列的聚合/更新
# 情况(a) DT[, lapply(.SD, sum), by = z] ## data.table 语法 DF %>% group_by(z) %>% summarise_each(funs(sum)) ## dplyr 语法 DT[, (cols) := lapply(.SD, sum), by = z] ans <- DF %>% group_by(z) %>% mutate_each(funs(sum))
# 情况(b) DT[, c(lapply(.SD, sum), lapply(.SD, mean)), by = z] DF %>% group_by(z) %>% summarise_each(funs(sum, mean))
# 情况(c) DT[, c(.N, lapply(.SD, sum)), by = z] DF %>% group_by(z) %>% summarise_each(funs(n(), mean))
在情况(a)中,代码几乎是等价的。data.table 使用熟悉的基本函数 lapply(),而 dplyr 引入了 *_each() 以及一堆函数来定义 funs()。
data.table 的 := 需要提供列名,而 dplyr 会自动生成列名。
在情况(b)中,dplyr 的语法相对直观。改进多函数的聚合/更新是 data.table 的目标。
然而,在情况(c)中,dplyr 会将 n() 返回多次,而不是只返回一次。在 data.table 中,我们只需要在 j 中返回一个列表。列表的每个元素将成为结果中的一列。因此,我们可以再次使用熟悉的基本函数 c() 将 .N 连接到返回的列表中,从而返回一个列表。
注意:再次强调,在 data.table 中,我们只需要在 j 中返回一个列表。列表的每个元素将成为结果中的一列。您可以使用 c()、as.list()、lapply()、list() 等基本函数来实现这一点,而无需学习任何新的函数。
您只需要学习特殊变量 - .N 和 .SD。dplyr 中的等价物是 n() 和 .。
连接
dplyr 为每种类型的连接提供了单独的函数,而 data.table 允许使用相同的语法 DT[i, j, by] 进行连接(有原因)。它还提供了一个等价的 merge.data.table() 函数作为替代。
setkey(DT1, x, y)
# 1. 普通连接 DT1[DT2] ## data.table 语法 left_join(DT2, DT1) ## dplyr 语法
# 2. 在连接时选择列 DT1[DT2, .(z, i.mul)] left_join(select(DT2, x, y, mul), select(DT1, x, y, z))
# 3. 在连接时聚合 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. 在连接时更新 DT1[DT2, z := cumsum(z) * i.mul, by = .EACHI] ??
# 5. 滚动连接 DT1[DT2, roll = -Inf] ??
# 6. 控制输出的其他参数 DT1[DT2, mult = "first"] ??
有些人可能会觉得每个连接都有单独的功能更好(左连接、右连接、内连接、反连接、半连接等),而另一些人可能喜欢data.table的DT[i, j, by]或类似于base R的merge()。
然而,dplyr的连接只是这样做。没有多余的东西,也没有少了的东西。
data.table可以在连接时选择列(2),而在dplyr中,您需要在连接之前先在两个数据框上使用select()。否则,您将只能使用不必要的列来实现连接,然后再将它们删除,这是低效的。
data.table可以在连接时进行聚合(3),也可以在连接时进行更新(4),使用by = .EACHI功能。为什么要将整个连接结果材料化,只是为了添加/更新几列呢?
data.table能够进行滚动连接(5)-滚动向前,LOCF向后滚动,NOCB最近
data.table还有一个mult =参数,可以选择第一个、最后一个或所有匹配项(6)。
data.table有一个allow.cartesian = TRUE参数,以防止意外的无效连接。
再次强调,语法与DT[i, j, by]一致,还可以通过额外的参数进一步控制输出。
  • do()...

    dplyr的summarise函数专门设计用于返回单个值的函数。如果您的函数返回多个/不同的值,您将不得不使用do()函数。您必须事先了解所有函数的返回值。

     DT[, list(x[1], y[1]), by = z]                 ## data.table语法
     DF %>% group_by(z) %>% summarise(x[1], y[1]) ## dplyr语法
     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))))
    
    • .SD的等价物是.

    • 在data.table中,你可以在j中放入几乎任何东西 - 唯一需要记住的是要返回一个列表,以便将列表的每个元素转换为列。

    • 在dplyr中,不能这样做。必须使用do(),具体取决于你对函数是否总是返回单个值的确定程度。而且速度相当慢。

    再次强调,data.table的语法与DT[i, j, by]一致。我们可以继续在j中添加表达式,而不必担心这些问题。

    请看这个SO问题这个问题。我想知道是否可能用dplyr的语法来直接表达答案...

    总结一下,我特别强调了几个dplyr语法效率低下、功能有限或操作不直观的实例。这主要是因为data.table在“难以阅读/学习”的语法方面受到了很多批评(就像上面粘贴/链接的那个)。大多数涉及dplyr的帖子都谈到了最直接的操作,这很好。但是也很重要意识到它的语法和功能限制,我还没有看到过相关的帖子。
    data.table也有它自己的怪癖(其中一些我已经指出我们正在努力修复)。我们也在努力改进data.table的连接操作,我在这里here也提到了。
    但是人们也应该考虑到dplyr相对于data.table缺少的功能数量。
    4. 功能
    我在这里here和本文中指出了大部分功能。另外:
    • fread - 快速文件阅读器现在已经可用很长时间了。

    • fwrite - 现在可用的并行化快速文件写入器。详细说明请参见this post,了解实现方式,并参考#1664以跟踪进一步的发展。

    • 自动索引 - 另一个方便的功能,用于优化基本的 R 语法。

    • 临时分组:在summarise()期间,dplyr会自动按分组变量对结果进行排序,但这可能并不总是理想的。

    • 如上所述,data.table连接具有许多优势(用于提高速度/内存效率和语法)。

    • 非等值连接:允许使用其他运算符<=, <, >, >=进行连接,同时享受data.table连接的所有其他优势。

    • 最近在data.table中实现了重叠范围连接。请查看this post以获取概述和基准测试。

    • setorder()函数在data.table中可以通过引用实现非常快速的数据表重新排序。

    • dplyr使用相同的语法提供了与数据库的接口,而data.table目前还没有这个功能。

    • data.table提供了更快的集合操作等效函数(由Jan Gorecki编写)- fsetdifffintersectfunionfsetequal,并附加了all参数(与SQL中的用法相同)。

    • data.table在加载时不会产生掩盖警告,并且在传递给任何R包时,有一个在这里描述的机制,用于与[.data.frame兼容。dplyr更改了基本函数filterlag[,可能会引起问题;例如这里这里


    最后:
    关于数据库 - 没有理由为什么data.table不能提供类似的接口,但这并不是现在的重点。如果用户非常希望这个功能,可能会提高优先级..不确定。
    关于并行性 - 一切都很困难,直到有人去做为止。当然,这需要付出努力(确保线程安全)。
    目前正在进行进展(在v1.9.7开发版中),通过使用OpenMP来并行化已知的耗时部分,以实现增量性能提升。

    11
    @bluefeet:我认为你把那个讨论移到聊天室并没有给我们其他人带来很大的帮助。我认为Arun是其中一位开发者,这可能会带来有用的见解。 - IRTFM
    7
    我认为在任何使用引用赋值(:=)的地方,应该使用dplyr中等价的<-,例如DF <- DF %>% mutate...而不是仅仅使用DF %>% mutate... - David Arenburg
    5
    关于语法。我认为对于习惯于plyr语法的用户来说,dplyr可能更容易上手,但是对于习惯于查询语言(如SQL)及其背后的关系代数进行表格数据处理的用户来说,data.table可能更容易上手。@Arun,你应该注意到可以通过调用data.table函数很容易地实现集合操作,而且当然会带来显著的速度提升。 - jangorecki
    28
    我已经读了这篇文章很多次,它帮助我更好地理解data.table并能够更好地使用它。在大多数情况下,我更喜欢使用data.table而不是dplyr或pandas或PL/pgSQL。然而,我无法停止思考如何表达它。语法简单、清晰或详细。事实上,即使在我已经大量使用data.table后,我仍经常难以理解自己一周前写的代码。这是一个只写语言的生动例子。https://en.wikipedia.org/wiki/Write-only_language所以,让我们期望有一天能够在data.table上使用dplyr。 - Ufos
    9
    实际上,由于更新,许多dplyr代码不再适用...此答案可能需要更新,因为它是如此重要的资源。 - its.me.adam
    显示剩余16条评论

    443

    以下是我从dplyr的角度尝试全面回答的内容,遵循Arun的答案概述(但基于不同的重点略有重新排列)。

    语法

    语法存在一定主观性,但我坚持认为data.table的简洁使其更难学习和阅读。部分原因是因为dplyr解决的问题要简单得多!

    dplyr对你做的一件非常重要的事情是限制你的选择。我声称,大多数单表问题可以使用仅五个关键动词filter、select、mutate、arrange和summarise以及“by group”副词来解决。这种限制在学习数据操作时是一个巨大的帮助,因为它有助于有序地思考问题。在dplyr中,每个这些动词都映射到一个单独的函数。每个函数只执行一项任务,易于单独理解。

    通过使用%>% 将这些简单的操作管道连接在一起,您可以创建复杂性。以下是Arun其中一篇帖子中的示例(链接)

    diamonds %>%
      filter(cut != "Fair") %>%
      group_by(cut) %>%
      summarize(
        AvgPrice = mean(price),
        MedianPrice = as.numeric(median(price)),
        Count = n()
      ) %>%
      arrange(desc(Count))
    

    即使您以前从未见过dplyr(甚至是R!),您仍然可以理解发生了什么,因为这些函数都是英语动词。英语动词的缺点是它们需要比[更多的打字,但我认为这可以通过更好的自动完成来大大缓解。

    这是相应的data.table代码:

    diamondsDT <- data.table(diamonds)
    diamondsDT[
      cut != "Fair", 
      .(AvgPrice = mean(price),
        MedianPrice = as.numeric(median(price)),
        Count = .N
      ), 
      by = cut
    ][ 
      order(-Count) 
    ]
    

    除非您已经熟悉data.table,否则很难跟随此代码。 (我也无法找到如何缩进重复的[以使其看起来更好)。就个人而言, 当我看着自己六个月前写的代码时,就好像在看一个陌生人写的代码一样, 所以我更喜欢简单直接但冗长的代码。

    我认为会稍微降低可读性的另外两个小因素:

    • 由于几乎每个数据表操作都使用[,您需要更多上下文才能弄清楚正在发生什么。 例如,x[y]是将两个数据表连接还是从数据框中提取列? 这只是一个小问题,因为在编写良好的代码时,变量名称应该能够提示正在发生什么。

    • 我喜欢dplyr中的group_by()是一个单独的操作。它从根本上改变了计算方式, 所以我认为在浏览代码时应该很明显,而且比[.data.tableby参数更容易发现group_by()

    我还喜欢管道不仅限于一个包。 您可以从tidyr开始整理数据, 并在ggvis中完成绘图。而且您不限于我编写的软件包, 任何人都可以编写一个函数,它是数据操作管道的无缝部分。 实际上,我更喜欢用%>% 重写以前的data.table代码:

    diamonds %>% 
      data.table() %>% 
      .[cut != "Fair", 
        .(AvgPrice = mean(price),
          MedianPrice = as.numeric(median(price)),
          Count = .N
        ), 
        by = cut
      ] %>% 
      .[order(-Count)]
    

    使用%>% 进行管道操作不仅限于数据框,而且很容易推广到其他上下文环境中:交互式Web图形网页抓取代码片段运行时合同 ...)

    内存和性能

    我将它们归为一类,因为对我来说它们并不重要。大多数R用户处理的数据行数远远低于100万行,并且dplyr足够快以处理这样大小的数据,您不会意识到处理时间。我们针对中等数据的表达性进行了dplyr性能优化;如果您需要处理更大的数据,则可以使用data.table实现原始速度。

    dplyr的灵活性也意味着您可以使用相同的语法轻松调整性能特征。如果使用数据框后端的dplyr的性能不够好,您可以使用data.table后端(尽管具有一些受限制的功能集)。如果您正在使用的数据无法适应内存,则可以使用数据库后端。

    所有这些都说过,dplyr性能将在长期内得到改善。我们一定会实现像基数排序和使用相同的索引进行连接和过滤等data.table中的好想法。我们还正在进行并行化工作,以便利用多个核心。

    特点

    我们计划在2015年完成以下几件事:

    • readr,使得从磁盘获取文件并加载到内存中变得容易,类似于fread()函数。

    • 更灵活的连接操作,包括支持非均匀连接。

    • 更灵活的分组操作,例如引导样本、汇总等等。

    我也投入时间改进R的数据库连接器,提高与Web APIs通信的能力,并使其更容易抓取HTML页面


    35
    只是一则边注,我同意你的很多论点(尽管我更喜欢 data.table 的语法),但如果你不喜欢 [ 风格,你可以轻松地使用 %>% 来连接 data.table 操作。%>% 不是特定于 dplyr 的,而是来自一个独立的包(你碰巧也是共同作者),所以我不确定你在大部分 Syntax 段落中想表达什么。 - David Arenburg
    16
    @DavidArenburg 很好的观点。我已经重新编写了语法,希望更清楚地表达我的主要观点,并强调您可以在data.table中使用%>% - hadley
    7
    谢谢 Hadley,这是一个有用的观点。关于缩进,我通常做 DT[\n\texpression\n][\texpression\n](**gist**),这实际上效果很好。虽然 Arun 的回答更直接地回答了我的具体问题,但我将把它作为答案保留,因为它对于那些试图对 dplyrdata.table 的差异/共同点有一个总体了解的人来说也是一个好答案。 - BrodieG
    40
    既然已经有了 fread(),为什么还要开发 FastRead?把时间花在改进 fread() 或者其他(不太发展完善的)事情上不是更好吗? - EDi
    16
    data.table 的 API 基于对 [] 符号的大量滥用。这是它最大的优点,也是最大的缺点。 - Paul
    显示剩余10条评论

    80

    在直接回应问题标题的情况下...

    dplyr 肯定有一些 data.table 做不到的功能。

    你的第三点

    dplyr 抽象了潜在的数据库交互

    是对自己问题的直接回答,但没有被提升到足够高的水平。将 dplyr 视为可扩展的前端界面,用于多个数据存储机制,而 data.table 是一个单一机制的扩展。

    dplyr 视为后端不可知接口,所有的目标都使用相同的语法,您可以随意扩展目标和处理程序。从 dplyr 的角度来看,data.table 就是其中之一。

    您永远不会(希望如此)看到 data.table 尝试翻译您的查询以创建操作磁盘或联网数据存储的 SQL 语句。

    dplyr 可能做到 data.table 不会或可能无法做到的事情。

    基于内存工作设计,data.table在扩展到查询并行处理方面可能比dplyr更加困难。

    针对内部问题的回答...

    用法

    对于那些熟悉这些软件包的人来说,是否有一些分析任务使用其中一个软件包比另一个更容易编码(即需要的按键组合或专业知识水平的要求较低,其中每个方面的要求越少越好)。

    这可能看起来像是在回避问题,但真正的答案是否定的。熟悉工具的人似乎会使用最熟悉的工具或实际上适合当前任务的工具。话虽如此,有时您想呈现特定的可读性,有时需要一定的性能水平,当您需要足够高的两者水平时,您可能需要另外一个工具来配合您已经拥有的工具以使抽象更清晰。

    性能

    是否有一些分析任务在一个软件包中执行的效率比另一个软件包高出很多(即超过2倍)。

    再次强调,data.table 在所有方面都非常高效,而 dplyr 则在某些方面受限于底层数据存储和已注册的处理程序。这意味着当你在使用 data.table 时遇到性能问题时,你可以相当确定它是在你的查询函数中,如果确实是 data.table 的瓶颈,那么你需要提交一份报告。当 dplyr 使用 data.table 作为后端时,你可能会看到一些来自 dplyr 的开销,但很可能是你的查询引起的。
    dplyr 在后端出现性能问题时,你可以通过注册混合评估函数或(在数据库的情况下)在执行之前操纵生成的查询来解决它们。
    还可以参见何时使用 plyr 要比 data.table 好?的接受答案。

    3
    dplyr不能将一个data.table包装成tbl_dt吗?为什么不兼顾两者的优点呢? - aaa90210
    30
    你忘了提及相反的陈述:“data.table绝对做不到的事情,dplyr可以做到”,这也是正确的。 - jangorecki
    29
    Arun的回答解释得很好。就性能而言,最重要的是fread,引用更新,滚动连接和重叠连接。我认为没有任何一个包(不仅仅是dplyr)可以与这些特性竞争。一个很好的例子可以从这个演示文稿的最后一页看到。 - jangorecki
    17
    完全正确,data.table 是我仍然使用 R 的原因。否则我会使用 pandas。它甚至比 pandas 更好/更快。 - marbel
    9
    我喜欢使用data.table,因为它的简单性和类似于SQL语法结构。我的工作涉及每天进行非常强烈的即席数据分析和统计建模图形,我真的需要一个足够简单的工具来完成复杂的任务。现在我可以将我的工具箱仅限于每天工作中的data.table用于数据和lattice用于图形。例如,我甚至可以执行这样的操作:$DT[group==1,y_hat:=predict(fit1,data=.SD),]$,这真的很不错,我认为这是从经典R环境中的SQL得到的巨大优势。 - xappppp
    显示剩余2条评论

    19

    阅读 Hadley 和 Arun 的答案,人们会觉得那些喜欢 dplyr 语法的人在某些情况下必须转到 data.table 或妥协于长时间运行。

    但是正如一些人已经提到的,dplyr 可以使用 data.table 作为后端。这是通过使用 dtplyr 包实现的,该包最近发布了版本1.0.0 release。学习 dtplyr 几乎不需要额外的努力。

    当使用 dtplyr 时,使用函数 lazy_dt() 声明一个lazy data.table,然后使用标准的 dplyr 语法来指定对它的操作。这看起来像以下内容:

    new_table <- mtcars2 %>% 
      lazy_dt() %>%
      filter(wt < 5) %>% 
      mutate(l100k = 235.21 / mpg) %>% # liters / 100 km
      group_by(cyl) %>% 
      summarise(l100k = mean(l100k))
    
      new_table
    
    #> Source: local data table [?? x 2]
    #> Call:   `_DT1`[wt < 5][, `:=`(l100k = 235.21/mpg)][, .(l100k = mean(l100k)), 
    #>     keyby = .(cyl)]
    #> 
    #>     cyl l100k
    #>   <dbl> <dbl>
    #> 1     4  9.05
    #> 2     6 12.0 
    #> 3     8 14.9 
    #> 
    #> # Use as.data.table()/as.data.frame()/as_tibble() to access results
    

    new_table对象在调用as.data.table()/as.data.frame()/as_tibble()时才被评估,此时执行底层的data.table操作。

    我已经重新创建了由data.table作者Matt Dowle于2018年12月进行的基准分析,涵盖了对大量组的操作。我发现dtplyr确实使那些喜欢dplyr语法的人在享受data.table提供的速度的同时保持使用它的能力。


    1
    你可能不会有太多dplyr API中没有的功能,比如通过引用进行子赋值、滚动连接、重叠连接、非等连接、联接更新等等。 - jangorecki
    我必须承认,这些特性都不太熟悉。能否请您在data.table中提供具体的例子? - Iyar Lin
    3
    ?data.table examples,除了重叠连接之外,我提到的都有。 - jangorecki
    使用管道的几个部分可以直接构建关于连接、滚动和重叠连接的更新。 - Arthur Yip
    请查看fuzzyjoin以进行非等值连接(似乎比data.table的非等值连接具有更多的功能和特性)。 - Arthur Yip

    -1

    我一开始使用data.table,但为了适应工作团队(并且因为我更喜欢其语法),我现在使用dplyr。由于它们都使用链式结构,所以两者都很难调试。一个重要的限制是,它们都难以处理需要从多个行或列中获取信息的计算。对于data.table,可以使用mapply、map或只需添加参数与lapply来完成多个列的函数。我还没有在dplyr中追求过这一点,但我认为这是可能的。然而,我没有找到一种dplyr的方法来直接指向另一行中的信息进行计算。例如,可以在data.table中使用以下方式完成:DT[, DDspawn := DD - .SD[jday==Jday.spawn, 'DD'], by=bys]。我认为在数据库术语中,这种能力被称为“直接访问”,但我不是专家。是否有一种dplyr的方法?虽然有滞后函数,所以这一定是可能的,但我没有找到在dplyr中实现此功能的语法。


    1
    这部分是回答(尽管不太清楚它对现有回答有什么补充),但后半部分是一个不同的问题,应该作为自己的问题发布,而不是标记在现有相关问题上... - Ben Bolker

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接