Data.Table vs DPLYR:一个人能把某件事做好,而另一个人不能或做得不好吗?

data.table vs dplyr: can one do something well the other can't or does poorly?

提问人:BrodieG 提问时间:1/29/2014 最后编辑:MarkBrodieG 更新时间:10/16/2023 访问量:163352

问:

概述

我比较熟悉,没那么多。我已经阅读了一些在 SO 上出现的 dplyr 小插曲和示例,到目前为止,我的结论是:data.tabledplyr

  1. data.table并且在速度上具有可比性,除非有许多(即 >10-100K)组,以及在某些其他情况下(见下面的基准)dplyr
  2. dplyr具有更易于访问的语法
  3. dplyr抽象(或将)潜在的数据库交互
  4. 有一些细微的功能差异(请参阅下面的“示例/用法”)

在我心目中 2.没有太大的重量,因为我相当熟悉,尽管我知道对于两者的新用户来说,这将是一个重要因素。我想避免关于哪个更直观的争论,因为这与我从已经熟悉的人的角度提出的具体问题无关。我还想避免讨论“更直观”如何导致更快的分析(当然是正确的,但同样,这不是我在这里最感兴趣的)。data.tabledata.table

问题

我想知道的是:

  1. 对于熟悉这些软件包的人来说,是否有更容易使用一个或另一个软件包进行编码的分析任务(即所需的击键与所需的深奥程度的某种组合,其中每个都少是一件好事)。
  2. 是否存在分析任务,在一个软件包中执行效率比另一个软件包高出 2 倍以上。

最近的一个 SO 问题让我对此进行了更多思考,因为在那之前,我认为除了我已经可以做的事情之外,没有提供太多东西。这是解决方案(Q 末尾的数据):dplyrdata.tabledplyr

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

这比我尝试解决的要好得多。也就是说,好的解决方案也相当不错(感谢 Jean-Robert、Arun,请注意,这里我更喜欢单个语句而不是严格最优的解决方案):data.tabledata.table

setDT(dat)[,
  .SD[job != "Boss" | year == min(year)][, cumjob := cumsum(job2)], 
  by=list(id, job)
]

后者的语法可能看起来非常深奥,但如果你习惯了,它实际上非常简单(即不使用一些更深奥的技巧)。data.table

理想情况下,我希望看到的是一些很好的例子,即 or 方式更加简洁或性能要好得多。dplyrdata.table

例子

用法

  • dplyr不允许返回任意行数的分组操作(来自 Eddi 的问题,注意:这看起来将在 DPLyr 0.5 中实现,@beginneR显示了在 @eddi 问题的答案中使用的潜在解决方法)。do
  • data.table支持滚动联接(感谢@dholstius)以及重叠联接
  • data.table通过自动索引在内部优化表单的表达式或速度,该索引使用二进制搜索,同时使用相同的基本 R 语法。有关更多详细信息和一个小基准,请参阅此处DT[col == value]DT[col %in% values]
  • dplyr提供函数的标准评估版本(例如,),可以简化编程使用(注意,编程使用绝对是可能的,只需要一些仔细思考、替换/引用等,至少据我所知)regroupsummarize_each_dplyrdata.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))
r data.table dplyr

评论

10赞 eddi 1/30/2014
与阅读类似的解决方案是:dplyras.data.table(dat)[, .SD[job != "Boss" | year == min(year)][, cumjob := cumsum(job2)], by = list(name, job)]
14赞 eddi 1/30/2014
好吧,再次 imO,在 HAS 度量 0 中更清晰地表达的一组问题(d)plyr
41赞 eddi 1/30/2014
@BrodieG,真正让我烦恼的一件事是,在语法方面,基本上是我不喜欢他们的语法的主要原因,是我必须学习太多(阅读超过 1 个)额外的函数(名称对我来说仍然没有意义),记住他们做什么,他们采取什么论点, 等。这一直是我与plyr-philosophy的巨大背离。dplyrplyr
55赞 hadley 1/30/2014
@eddi [开玩笑] 关于 data.table 语法真正让我感到困扰的一件事是,我必须了解太多函数参数是如何交互的,以及神秘的快捷方式意味着什么(例如)。[严重地]我认为这些是合理的设计差异,会吸引不同的人.SD
10赞 eddi 1/30/2014
@hadley re et al - 这是公平的 - 我花了一点时间才理解,但当我到达那里时,我已经能够做很多事情,而 (d)plyr 在前面给你带来了一个很大的障碍。.SD.SD

答:

80赞 Thell 11/17/2014 #1

直接回答问题标题...

dplyr 绝对做不了的事情。data.table

你的观点 #3

dplyr 抽象(或将)潜在的数据库交互

是你自己问题的直接答案,但没有提升到足够高的水平。 是真正可扩展到多个数据存储机制的前端,其中 AS 是单个数据存储机制的扩展。dplyrdata.table

看作一个与后端无关的接口,所有目标都使用相同的语法,您可以在其中随意扩展目标和处理程序。 从这个角度来看,是这些目标之一。dplyrdata.tabledplyr

您永远不会(我希望)看到有一天尝试转换您的查询以创建与磁盘或网络数据存储一起操作的 SQL 语句。data.table

dplyr 可以做 data.table 不会或可能不会做的事情。

基于在内存中工作的设计,与 相比,将自身扩展到查询的并行处理中可能要困难得多。data.tabledplyr


在回答体内问题时......

用法

对于熟悉这些软件包的人来说,是否有更容易使用一个或另一个软件包进行编码的分析任务(即所需的击键与所需的深奥程度的某种组合,其中每个都少是一件好事)。

这似乎是一个平底船,但真正的答案是否定的。熟悉工具的人似乎要么使用他们最熟悉的工具,要么使用实际上适合手头工作的工具。话虽如此,有时你想要呈现一种特定的可读性,有时是某种性能水平,当你需要足够高的两者时,你可能只需要另一个工具来配合你已经拥有的东西,以做出更清晰的抽象。

性能

是否存在分析任务,在一个软件包中执行效率比另一个软件包高出 2 倍以上。

同样,没有。 擅长在所做的每一件事上都保持高效,在某些方面受到底层数据存储和注册处理程序的限制。data.tabledplyr

这意味着当你遇到性能问题时,你可以非常确定它出在你的查询函数中,如果它实际上是一个瓶颈,那么你就为自己赢得了提交报告的乐趣。当用作后端时也是如此;您可能会看到一些开销,但很可能是您的查询。data.tabledata.tabledplyrdata.tabledplyr

当后端出现性能问题时,可以通过注册用于混合评估的函数或(对于数据库)在执行之前操作生成的查询来解决它们。dplyr

另请参阅公认的答案,什么时候 plyr 比 data.table 好?

评论

3赞 aaa90210 12/9/2014
dplyr 不能用 tbl_dt 包装 data.table 吗?为什么不两全其美呢?
30赞 jangorecki 1/6/2015
你忘了提到相反的陈述“data.table 肯定能做 dplyr 做不到的事情”,这也是正确的。
29赞 jangorecki 1/6/2015
阿伦的回答很好地解释了这一点。最重要的(就性能而言)是 fread、引用更新、滚动连接、重叠连接。我相信没有任何软件包(不仅是 dplyr)可以与这些功能竞争。一个很好的例子是演示文稿的最后一张幻灯片。
17赞 marbel 12/2/2016
总而言之,data.table 是我仍然使用 R 的原因,否则我会使用 pandas。它甚至比熊猫更好/更快。
9赞 xappppp 12/31/2016
我喜欢 data.table,因为它简单且类似于 SQL 语法结构。我的工作包括每天为统计建模进行非常密集的临时数据分析和图形,我真的需要足够简单的工具来做复杂的事情。现在,我可以将我的工具包简化为在日常工作中仅用于数据的 data.table 和用于图形的 lattice。举个例子,我甚至可以做这样的操作:$DT[group==1,y_hat:=predict(fit1,data=.SD),]$,这真的很整洁,我认为这是经典 R 环境中 SQL 的一大优势。
650赞 32 revs, 8 users 92%Arun #2

我们至少需要涵盖这些方面,以提供全面的答案/比较(没有特别的重要性顺序):、 和 。SpeedMemory usageSyntaxFeatures

我的目的是从 data.table 的角度尽可能清楚地介绍其中的每一个。

注意:除非另有明确说明,否则通过引用 dplyr,我们指的是 dplyr 的 data.frame 接口,其内部使用 Rcpp 在 C++ 中。


data.table 语法的形式是一致的 - 。保持 , 和在一起是设计使然。通过将相关操作放在一起,它可以轻松优化速度和更重要的是内存使用率的操作,并提供一些强大的功能,同时保持语法的一致性。DT[i, j, by]ijby

1.速度

相当多的基准测试(尽管主要是关于分组操作)已经添加到问题中,表明随着组和/或行数的增加,data.table 变得比 dplyr 更快,包括 Matt 关于在 100 到 1000 万个组上从 1000 万到 20 亿行(RAM 中为 100GB )进行分组的基准测试和不同的分组列, 这也比较了.另请参阅更新的基准,其中包括 和。pandasSparkPolars

在基准测试中,最好还涵盖以下剩余方面:

  • 涉及行子集的分组操作 - 即类型操作。DT[x > val, sum(y), by = z]

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

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

2. 内存使用情况

  1. 涉及 dplyr 或 dplyr 中的操作可能内存效率低下(在 data.frames 和 data.tables 上)。请参阅此帖子filter()slice()

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

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

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

    对此的一个关注点是引用透明度。通过引用更新 data.table 对象(尤其是在函数中)可能并不总是可取的。但这是一个非常有用的功能:有关有趣的案例,请参阅此帖子和帖子。我们想保留它。

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

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

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

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

  3. 加入时聚合:

    假设您有两个 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
    

    并且您希望在按列连接时获取每一行。我们可以:sum(z) * mulDT2x,y

      1. 聚合得到 , 2) 执行连接 和 3) 乘以 (或)DT1sum(z)

        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]
    

    添加/更新 的列 与 的 键列匹配的行上的 from 。我不认为在 中有一个与此操作完全等效的操作,即在不避免操作的情况下,该操作必须复制整个操作才能向其添加新列,这是不必要的。DT1colmulDT2DT2DT1dplyr*_joinDT1

    查看这篇文章了解真实的使用场景。

总而言之,重要的是要意识到每一点优化都很重要。正如格蕾丝·霍珀(Grace Hopper)所说,注意你的纳秒

3. 语法

现在让我们看一下语法。哈德利在这里评论道:

数据表非常快,但我认为它们的简洁性使学习变得更加困难,并且使用它的代码在编写后更难阅读......

我觉得这句话毫无意义,因为它非常主观。我们也许可以尝试对比语法的一致性。我们将并排比较 data.table 和 dplyr 语法。

我们将使用如下所示的虚拟数据:

DT = data.table(x=1:10, y=11:20, z=rep(1:2, each=5))
DF = as.data.frame(DT)
  1. 基本聚合/更新操作。

     # 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])
    
    • data.table 语法很紧凑,dplyr 非常冗长。在情况(a)中,事情或多或少是等价的。

    • 在情况 (b) 中,我们必须在总结时使用 dplyr。但是在更新时,我们不得不将逻辑移到里面。然而,在 data.table 中,我们用相同的逻辑来表达这两种操作 - 对行进行操作,但在第一种情况下,get ,而在第二种情况下,使用其累积总和更新这些行。filter()mutate()x > 2sum(y)y

      当我们说形式是一致的时,这就是我们的意思。DT[i, j, by]

    • 同样,在情况 (c) 中,当我们有条件时,我们能够在 data.table 和 dplyr 中“按原样”表达逻辑。但是,如果我们只想返回条件满足的那些行,否则跳过,我们不能直接使用 (AFAICT)。我们必须先总结,然后总结,因为总是需要一个值if-elseifsummarise()filter()summarise()

      虽然它返回相同的结果,但在此处使用会使实际操作不那么明显。filter()

      在第一种情况下也可以使用(对我来说似乎并不明显),但我的观点是,我们不应该这样做。filter()

  2. 多列的聚合/更新

     # 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))
    
    • 在情况(a)中,代码或多或少是等效的。data.table 使用熟悉的基函数,而引入了一堆函数。lapply()dplyr*_each()funs()

    • data.table 需要提供列名,而 dplyr 会自动生成它。:=

    • 在案例(b)中,dplyr的语法相对简单。改进多个函数的聚合/更新在 data.table 的列表中。

    • 但是,在情况 (c) 中,dplyr 将返回与列数一样多的列,而不是只返回一次。在 data.table 中,我们需要做的就是返回一个列表。列表的每个元素都将成为结果中的一列。因此,我们可以再次使用熟悉的基函数来连接到返回 .n()jc().Nlistlist

    注意:同样,在data.table中,我们需要做的就是在 中返回一个列表。列表的每个元素都将成为结果中的一列。您可以使用 、 、 等...基本函数来实现这一点,而无需学习任何新函数。jc()as.list()lapply()list()

    你只需要学习特殊变量 - 至少。dplyr 中的等价物是 和.N.SDn().

  3. 加入

    dplyr 为每种类型的联接提供了单独的函数,其中 data.table 允许使用相同的语法(并有原因)进行联接。它还提供了一个等效的功能作为替代。DT[i, j, by]merge.data.table()

     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"]
     ??
    
  • 有些人可能会发现每个连接的单独函数要好得多(左、右、内部、反、半等),而其他人可能会喜欢 data.table 的,或者类似于基 R。DT[i, j, by]merge()

  • 但是,dplyr 连接就是这样做的。而已。仅此而已。

  • data.tables 可以在联接 (2) 时选择列,在 dplyr 中,您需要先在两个 data.frame 上进行联接,然后才能联接,如上所示。否则,您将使用不必要的列实现连接,只是为了稍后删除它们,这是低效的。select()

  • data.tables 可以在联接 (3) 时进行聚合,也可以在联接 (4) 时进行更新,使用 by = 。EACHI功能。为什么要将整个联接结果材料化为仅添加/更新几列?

  • data.table 能够滚动连接 (5) - 前滚、LOCF后滚、NOCB、最近

  • data.table 还具有选择第一个最后一个所有匹配项的参数 (6)。mult =

  • data.table 具有 allow.cartesian = TRUE 参数,以防止意外的无效联接。

同样,语法与允许进一步控制输出的附加参数一致。DT[i, j, by]

  1. do()...

    DPLYR 的 Summarise 专为返回单个值的函数而设计。如果函数返回多个/不相等的值,则必须求助于 .您必须事先了解所有函数的返回值。do()

     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))))
    
  • .SD的等价物是.

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

  • 在 dplyr 中,不能这样做。必须求助于取决于您对函数是否始终返回单个值的确定性。而且它很慢。do()

同样,data.table 的语法与 一致。我们可以不停地抛出表情,而不必担心这些事情。DT[i, j, by]j

看看这个 SO 问题这个问题。我想知道是否有可能使用 dplyr 的语法将答案表达为简单明了......

总而言之,我特别强调了几个例子,在这些例子中,dplyr 的语法要么效率低下,要么受到限制,要么无法使操作变得简单明了。这主要是因为 data.table 在“更难阅读/学习”语法(如上面粘贴/链接的语法)方面遭到了相当多的反对。大多数涵盖 dplyr 的帖子都谈到了最直接的操作。这太好了。但意识到它的语法和功能限制也很重要,我还没有看到关于它的帖子。

data.table 也有它的怪癖(我已经指出了我们正在尝试修复的一些怪癖)。正如我在这里强调的那样,我们也在尝试改进 data.table 的连接。

但与 data.table 相比,还应该考虑 dplyr 缺乏的功能数量。

4. 特点

我已经在这里和这篇文章中指出了大部分功能。另外:

  • FREAD - 快速文件读取器已经存在了很长时间。

  • fwrite - 现在可以使用并行化的快速文件编写器。有关实施的详细说明,请参阅此帖子,并查看 #1664 以跟踪进一步的发展。

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

  • 临时分组:通过对 期间的变量进行分组来自动对结果进行排序,这可能并不总是可取的。dplyrsummarise()

  • 上面提到的 data.table 连接(速度/内存效率和语法)的众多优点。

  • 非等价联接:允许使用其他运算符进行联接,以及 data.table 联接的所有其他优点。<=, <, >, >=

  • 最近在 data.table 中实现了重叠范围连接。查看这篇文章,了解基准测试的概述。

  • setorder()data.table 中的函数,允许通过引用对 data.tables 进行非常快速的重新排序。

  • dplyr 使用相同的语法为数据库提供接口,而 data.table 目前没有。

  • data.table提供更快的集合操作(由 Jan Gorecki 编写)- 、 和附加参数(如在 SQL 中)。fsetdifffintersectfunionfsetequalall

  • data.table 干净地加载,没有屏蔽警告,并且具有此处所述的机制,以便在传递给任何 R 包时实现兼容性。dplyr 更改基本函数,这可能会导致问题;例如这里和这里[.data.framefilterlag[


最后:

  • 在数据库上 - data.table 没有理由不能提供类似的接口,但这现在不是优先事项。如果用户非常喜欢该功能,它可能会受到冲击。不确定。

  • 关于并行性 - 一切都很困难,直到有人继续去做。当然,这需要付出努力(确保线程安全)。

    • 目前(在 v1.9.7 开发版中)正在取得进展,以使用 并行化已知耗时的部件,以提高性能。OpenMP

评论

11赞 IRTFM 1/6/2015
@bluefeet:我不认为你把讨论转移到聊天中对我们其他人有什么好处。在我的印象中,Arun 是开发人员之一,这可能会带来有用的见解。
7赞 David Arenburg 1/7/2015
我认为您在使用引用赋值()的每个地方都应该使用as,而不仅仅是:=dplyr<-DF <- DF %>% mutate...DF %>% mutate...
5赞 jangorecki 1/15/2015
关于语法。我相信对于习惯语法的用户来说会更容易,但对于习惯查询语言语法(如)及其背后的关系代数(这都是关于表格数据转换)的用户来说会更容易。@Arun您应该注意,集合运算符很容易通过包装函数实现,当然会带来显着的加速。dplyrplyrdata.tableSQLdata.table
28赞 Ufos 12/18/2018
我已经读了很多遍这篇文章,它对我理解 data.table 并能够更好地使用它有很大帮助。在大多数情况下,我更喜欢 data.table 而不是 dplyr 或 pandas 或 PL/pgSQL。但是,我无法停止思考如何表达它。语法简单、清晰或冗长。事实上,即使我经常使用 data.table,我仍然经常难以理解我自己的代码,我一周前就写过了。这是只写语言的一个生动的例子。en.wikipedia.org/wiki/Write-only_language所以,让我们希望有一天我们能够在 data.table 上使用 dplyr。
9赞 its.me.adam 3/6/2020
实际上,很多 dplyr 代码不再适用(由于更新?...这个答案可以使用刷新,因为它是一个很好的资源。
443赞 hadley 1/8/2015 #3

这是我试图从 dplyr 的角度给出一个全面的答案, 遵循阿伦回答的大致轮廓(但有些重新排列 基于不同的优先级)。

语法

语法有一些主观性,但我坚持我的说法 Data.Table 的简洁性使其更难学习和阅读。 这在一定程度上是因为 dplyr 正在解决一个更容易的问题!

dplyr 为您做的一件非常重要的事情是它限制了您的选择。我声称大多数单表问题都可以 只需五个关键动词即可解决 filter、select、mutate、arrange 和 总结,以及“按组”副词。这种限制是一个很大的帮助 当您学习数据操作时,因为它有助于订购您的 思考问题。在 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. (我也无法弄清楚如何以一种看起来不错的方式缩进重复的内容)。就我个人而言,当我查看代码时,我 6个月前写的,就像看陌生人写的代码一样, 因此,我更喜欢简单明了的代码。[

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

  • 由于几乎每个数据表操作都使用额外的 上下文来弄清楚发生了什么。例如,是联接两个数据表还是从数据框中提取列? 这只是一个小问题,因为在编写良好的代码中, 变量名称应表明正在发生的事情。[x[y]

  • 我喜欢这是 dplyr 中的一个单独操作。它 从根本上改变了计算,所以我认为应该是显而易见的 在浏览代码时,它比 的参数。group_by()group_by()by[.data.table

我也喜欢管道不仅限于一个包装。你可以从整理你的 带有 tidyr 的数据,以及 在GGVIS中完成一个情节。而你是 不限于我写的包 - 任何人都可以写一个函数 这构成了数据操作管道的无缝部分。事实上,我 而是更喜欢用以下方法重写的以前的 data.table 代码:%>%

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

管道的想法不仅限于数据帧和 很容易推广到其他上下文:交互式网络 图形网络 抓取要点运行时 合同,...%>%

内存和性能

我把它们混为一谈,因为对我来说,它们并不那么重要。 大多数 R 用户使用的数据行数不到 100 万行,而 dplyr 是 足够快,足以处理您不知道的数据大小 处理时间。我们优化了 dplyr 在中等数据上的表现力; 随意使用 Data.Table 以获得处理更大数据的原始速度。

dplyr 的灵活性还意味着您可以轻松调整性能 特征,使用相同的语法。如果 dplyr 的性能与 数据帧后端对你来说还不够好,你可以使用 data.table 后端(尽管功能集有些受限)。 如果您正在处理的数据不适合内存,则可以使用 数据库后端。

综上所述,从长远来看,dplyr 的性能会变得更好。我们将 一定要实现 Data.Table 的一些好主意,比如 radix 对联接和筛选器进行排序和使用相同的索引。我们也是 致力于并行化,以便我们可以利用多个内核。

特征

我们计划在 2015 年开展以下几项工作:

  • 该软件包,以便于从磁盘和 到内存,类似于 .readrfread()

  • 更灵活的联接,包括对非等式联接的支持。

  • 更灵活的分组,如引导示例、汇总等

我还投入时间改进 R 的数据库 连接器,能够与 Web API 通信,并更轻松地抓取 HTML 页面

评论

35赞 David Arenburg 1/8/2015
顺便说一句,我确实同意你的许多论点(尽管我自己更喜欢语法),但如果你不喜欢风格,你可以很容易地使用管道操作。 不是特定于 ,而是来自一个单独的包(您恰好也是该包的合著者),所以我不确定我是否理解您在大部分语法段落中想说什么。data.table%>%data.table[%>%dplyr
16赞 hadley 1/8/2015
@DavidArenburg好点子。我重新编写了语法,希望能更清楚地说明我的主要观点是什么,并强调你可以与data.table一起使用%>%
7赞 BrodieG 1/8/2015
谢谢哈德利,这是一个有用的观点。我通常会做(要点)的缩进,这实际上效果很好。我将 Arun 的答案作为答案,因为他更直接地回答了我的具体问题,这些问题与其说是关于语法的可访问性,不如说是我认为对于试图大致了解 和 之间的差异/共性的人来说,这是一个很好的答案。DT[\n\texpression\n][\texpression\n]dplyrdata.table
40赞 EDi 1/28/2015
当已经有快速阅读时,为什么要在 fastread 上工作?把时间花在改进 fread() 或处理其他(欠发达)事情上不是更好吗?fread()
16赞 Paul 1/28/2016
的 API 建立在对符号的大量滥用之上。这是它最大的优势,也是它最大的弱点。data.table[]
19赞 Iyar Lin 6/15/2020 #4

阅读 Hadley 和 Arun 的答案,人们会觉得那些喜欢语法的人在某些情况下会为了长时间的运行时间而切换或妥协。dplyrdata.table

但正如一些人已经提到的,可以用作后端。这是使用最近发布 1.0.0 版的软件包完成的。学习几乎不需要额外的努力。dplyrdata.tabledtplyrdtplyr

使用时,使用该函数声明一个惰性 data.table,之后使用标准语法来指定对其的操作。这如下所示:dtplyrlazy_dt()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_tableas.data.table()as.data.frame()as_tibble()data.table

重新创建了作者 Matt Dowle 在 2018 年 12 月完成的基准分析,其中涵盖了大量组的操作情况。我发现,这确实使那些喜欢语法的人在大多数情况下能够继续使用它,同时享受 .data.tabledtplyrdplyrdata.table

评论

1赞 jangorecki 6/24/2020
您可能没有很多 dplyr 中没有 API 的功能,例如按引用进行子赋值、滚动连接、重叠连接、非等价连接、连接时更新,可能还有很多其他功能。
0赞 Iyar Lin 6/25/2020
我不得不承认,这些功能都没有敲响警钟。您能否在data.table中提供具体示例?
3赞 jangorecki 6/27/2020
?data.table 示例,我提到的所有示例,除了重叠连接。有吗
0赞 Arthur Yip 10/16/2020
连接、滚动、重叠连接的更新可以通过管道的几个部分直接构建。
0赞 Arthur Yip 10/16/2020
有关非 equi 连接,请参阅 fuzzyjoin(似乎比 data.table 的非 equi 连接具有更多的特性和功能)。
-1赞 Yetta Jager 3/25/2023 #5

我从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 3/25/2023
这在一定程度上是一个答案(尽管尚不完全清楚它为现有答案添加了什么),但它的后半部分是一个不同的问题,它应该作为它自己的问题发布,而不是标记到现有的相关问题......