概述

我对data.table比较熟悉,对dplyr不太熟悉.我已经阅读了大约dplyr vignettes个例子,到目前为止,我的结论是:

  1. data.table and dplyr are comparable in speed, except when there are many (i.e. >10-100K) groups, and in some other circumstances (see benchmarks below)
  2. dplyr有更容易理解的语法
  3. dplyr篇摘要(或将)潜在的数据库交互
  4. 存在一些小的功能差异(请参见下面的"示例/用法")

在我心目中2.没有太大的影响力,因为我对它非常熟悉,尽管我知道对于两个领域的新手来说,这将是一个很大的因素.我想避免关于哪个更直观的争论,因为这与我从熟悉data.table的人的Angular 提出的具体问题无关.我还想避免讨论"更直观"如何导致更快的分析(当然是真的,但再一次,这不是我最感兴趣的).

问题

我想知道的是:

  1. 对于熟悉软件包的人来说,使用一个或另一个软件包编写分析任务是否容易得多(例如,所需的击键与所需的深奥程度的某种组合,其中每种击键次数越少越好).
  2. 是否存在在一个包中比在另一个包中更有效地执行分析任务(即超过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))

这比我try 破解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的方式更简洁或性能更好.

例子

Usage
  • dplyr不允许返回任意行数的分组操作(从100开始,注意:这看起来将在101中实现,@初学者在回答@eddi的问题时使用do也显示了一个潜在的解决方案).
  • data.table支持100(感谢@dholstius)和101
  • data.tablespeedautomatic indexingDT[col == value]DT[col %in% values]形式的表达式进行内部优化,该表达式使用binary search,同时使用相同的基本R语法.See here了解更多细节和一个小基准.
  • dplyr提供了标准的函数判断版本(例如regroupsummarize_each_),可以简化dplyr的编程使用(注:data.table的编程使用绝对是可能的,只需要仔细考虑、替换/引用等,至少据我所知)
Benchmarks
  • I ran my own benchmarks and found both packages to be comparable in "split apply combine" style analysis, except when there are very large numbers of groups (>100K) at which point data.table becomes substantially faster.
  • @Arun运行了大约100个,显示随着组数的增加,data.table个组的伸缩性优于dplyr个组(更新了最近两个包中的增强功能和最新版本的R).此外,当一个基准测试试图获得101分时,速度会快data.table~6倍.
  • (未经验证)在较大版本的组/应用/排序上的速度快data.table 75%,而在较小版本上的速度快dplyr 40%(感谢danas,100).
  • data.table》的主要作者马特有benchmarked grouping operations on data.table, dplyr and python pandas on up to 2 billion rows (~100GB in RAM)本书.
  • 100的速度提高了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))

推荐答案

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

我的目的是从数据中尽可能清楚地涵盖其中的每一项.表透视图.

注:除非另有明确说明,我们指的是dplyr的数据.内部构件采用C++语言的C++框架接口.


数据.表语法的形式是一致的-DT[i, j, by].将ijby保持在一起是出于设计.通过将相关操作放在一起,它可以为speed个操作提供easily optimise个操作,更重要的是memory usage个操作,还可以提供大约powerful features个操作,同时保持语法的一致性.

1.速度

在已经显示数据的问题中,添加了相当多的基准测试(尽管主要是分组操作).随着要分组的组和/或行数的增加,表中的dplyr值比dplyr值高faster,包括从10 million to 2 billion rows(RAM中为benchmarks by MattGB)分组到100 - 10 million groupsbenchmarks by Matt,以及不同的分组列,这也比较了pandas.另见updated benchmarks,也包括Sparkpydatatable.

在基准方面,最好也涵盖以下方面:

  • 分组操作涉及subset of rows-即DT[x > val, sum(y), by = z]型操作.

  • updatejoins等其他操作进行基准测试.

  • 除运行时外,每个操作的基准测试也为memory footprint.

2.内存使用

  1. dplyr中涉及filter()slice()的操作可能会导致内存效率低下(在data.frames和data.tables上).See this post

    请注意,Hadley's comment代表speed(dplyr对他来说足够快),而这里主要关注的是memory.

  2. 数据目前的表接口允许修改/更新列by reference(注意,我们不需要将结果重新分配回变量).

     # sub-assign by reference, updates 'y' in-place
     DT[x >= 1L, y := NA]
    

    但dplyr will never通过引用进行更新.dplyr等效值为(请注意,需要重新分配结果):

     # copies the entire 'y' column
     ans <- DF %>% mutate(y = replace(y, which(x >= 1L), NA))
    

    对此的担忧是referential transparency.更新数据.引用表对象,尤其是在函数中,可能并不总是可取的.但这是一个非常有用的功能:有关有趣的 case ,请参见thisthis篇帖子.我们想保留它.

    因此,我们正在努力导出数据中的shallow()个函数.表将为用户提供both possibilities.例如,如果希望不修改输入数据.在函数中的表中,可以执行以下操作:

     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()创建shallow copy,我们知道您不想修改原始对象.我们会在内部处理一切,以确保在复制您修改的列的同时.当实现时,这将解决referential transparency问题,同时为用户提供两种可能性.

    此外,一旦导出shallow(),dplyr的数据.表接口应避免几乎所有副本.因此,那些喜欢dplyr语法的人可以将其用于数据.桌子.

    但它仍将缺乏许多可以提供数据的功能.表提供了,包括(子)参考赋值.

  3. 加入时聚合:

    假设你有两个数据.下表:

     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
    

    你希望DT2中每行得到sum(z) * mul,同时通过x,y列连接.我们可以:

      1. 聚合DT1得到sum(z),2)执行连接,3)乘法(或)

        data.table way

        DT1[,(z=sum(z)),keyby=(x,y)][DT2][,z:=z*mul][]

        dplyr equivalent

        DF1%>;%按(x,y)%分组总结(z=总和(z))%>;%

      1. 一气呵成(使用by = .EACHI功能):

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

    优势是什么?

    • 我们不必为中间结果分配内存.

    • 我们不必两次分组/散列(一次用于聚合,另一次用于加入).

    • 更重要的是,通过查看第(2)条中的j条,我们想要执行的操作是明确的.

    关于by = .EACHI的详细解释,请勾选this post.没有中间结果,join+聚合是一次性完成的.

    看看this篇、this篇和this篇文章中的真实使用场景.

    dplyr中,你必须达到join and aggregate or aggregate first and then join,这两种方法在内存方面都没有那么有效(这反过来又转化为速度).

  4. 更新并加入:

    考虑数据.表代码如下所示:

     DT1[DT2, col := i.mul]
    

    DT2的键列与DT1匹配的行上,使用DT2中的mul来添加/更新DT1的列col.我不认为有一个完全等同于dplyr中的这个操作,也就是说,如果不避免*_join操作,就必须复制整个DT1,只需向其中添加一个新列,这是不必要的.

    判断this post以了解实际使用情况.

总之,重要的是要认识到每一点优化都很重要.正如Grace Hopper所说,Mind your nanoseconds

3.语法

现在让我们看看syntax.哈德利 comments 道:

数据表速度非常快,但我认为它们的简洁性使其成为harder to learncode that uses it is harder to read after you have written it...

我觉得这句话毫无意义,因为它很主观.我们或许可以try 的是对比consistency in syntax.我们将比较数据.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])
    
    • 数据表语法非常简洁,dplyr非常冗长.在 case (a)中,情况大致相同.

    • 在 case (b)中,我们必须在dplyr中使用filter(),而在dplyr中使用summarising.但是在updating的时候,我们必须把逻辑移到mutate()里面.在数据中.然而,我们用相同的逻辑表示这两个操作——对x > 2行进行操作,但在第一种情况下,得到sum(y),而在第二种情况下,用其累积和更新y行.

      这就是我们说的DT[i, j, by]表格is consistent的意思.

    • 类似地,在 case (c)中,当我们有if-else个条件时,我们能够在两个数据中表达逻辑"as-is".表和dplyr.然而,如果我们只想返回if条件满足的那些行,否则就跳过,我们不能直接使用summarise()(AFAICT).我们必须先filter(),然后总结,因为summarise()总是期望single value.

      虽然它返回相同的结果,但在这里使用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))
    
    • 在 case (a)中,代码或多或少是等效的.数据表使用了大家熟悉的基本函数lapply(),而dplyr引入了*_each()以及一系列函数.

    • 数据表的:=要求提供列名,而dplyr会自动生成列名.

    • 在 case (b)中,dplyr的语法相对简单.改善多个功能的聚合/更新取决于数据.表的列表.

    • 不过,在(c)的情况下,dplyr将返回n()倍的列,而不是一次.在数据中.表,我们只需要在j中返回一个列表.列表中的每个元素都将成为结果中的一列.因此,我们可以再次使用熟悉的基函数c().N连接到list,后者返回list.

    注:再一次,在数据中.表,我们需要做的就是在j中返回一个列表.列表中的每个元素都将成为结果中的一列.你可以使用c()as.list()lapply()list()等...基本函数来实现这一点,而无需学习任何新函数.

    你只需要学习特殊变量——至少.N.SD.dplyr中的等效值为n().

  3. 加入

    dplyr为每种类型的连接提供单独的函数,其中作为数据.表允许使用相同的语法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"]
     ??
    
  • 有些人可能会发现每个连接都有一个单独的函数更好(左、右、内、反、半等),而其他人可能喜欢数据.表中的DT[i, j, by]merge()类似于基数R.

  • 然而,dplyr连接就是这样做的.没别的了.同样如此.

  • 数据表可以在连接(2)时 Select 列,在dplyr中,两个数据都需要先输入select().如上图所示,连接前的框架.否则,您将使用不必要的列来实现连接,只是为了以后删除它们,这是低效的.

  • 数据表格可以使用by = .EACHI特征(3)和update while joining特征(4).为什么要具体化整个连接结果,只添加/更新几列?

  • 数据工作台能够进行rolling joins(5)-滚动forward, LOCFroll backward, NOCBnearest.

  • 数据表中还有mult =个参数,可以 Select firstlastall个匹配项(6).

  • 数据表中有allow.cartesian = TRUE个参数可以防止意外的无效联接.

同样,语法与DT[i, j, by]一致,并带有额外的参数,允许进一步控制输出.

  1. do()...

    dplyr的摘要是专门为返回单个值的函数设计的.如果函数返回多个/不等的值,则必须使用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等于.

  • 在数据中.表中,您几乎可以在j中抛出任何内容——唯一需要记住的是,它返回一个列表,以便列表中的每个元素都转换为一列.

  • 在dplyr中,不能这样做.必须求助于do(),这取决于您对函数是否总是返回单个值的确定程度.而且速度很慢.

再一次,数据.表的语法与DT[i, j, by]一致.我们可以继续在j中输入表达式,而不必担心这些事情.

看看this SO questionthis one.我想知道是否有可能使用dplyr的语法将答案表达为简单明了...

总之,我特别强调了several个例子,其中dplyr的语法要么效率低下,要么有限,要么无法让操作变得简单.这尤其是因为数据.对于"更难阅读/学习"的语法(如上面粘贴/链接的语法),table受到了相当大的反对.大多数关于dplyr的帖子都谈到了最简单的操作.这很好.但认识到它的语法和功能限制也很重要,我还没有看到关于它的帖子.

数据表也有它的怪癖(其中一些我已经指出,我们正在try 修复).我们还试图改善数据.表的联接,如我突出显示的here.

但是,也应该考虑DPLYR与数据相比缺少的特征数量.桌子

4.特点

我已经指出了here个功能中的大部分,在这篇文章中也是如此.此外:

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

  • fwrite-parallelised快速文件写入器现在可用.关于实现的详细说明见this post,跟踪进一步发展见#1664.

  • Automatic indexing-另一个方便的功能是在内部优化base R语法.

  • Ad-hoc grouping:dplyr通过在summarise()期间对变量进行分组,自动对结果进行排序,这可能并不总是可取的.

  • 在数据方面有很多优势.上面提到的表联接(用于速度/内存效率和语法).

  • Non-equi joins:允许使用其他运算符<=, <, >, >=以及数据的所有其他优点进行联接.表联接.

  • Overlapping range joins在数据中实现.最近的桌子.查看this post以了解基准测试的概述.

  • setorder()数据中的函数.表,它允许对数据进行真正快速的重新排序.参考表格.

  • dplyr使用相同的语法提供了interface to databases个数据.桌子现在不在.

  • data.table提供了set operations(由Jan Gorecki编写)的更快类似功能fsetdifffintersectfunionfsetequal,以及额外的all参数(如SQL).

  • 数据表干净地加载,没有掩蔽警告,并且有一个机制,当传递到任何R包时,它的兼容性为here.dplyr更改可能导致问题的基本函数filterlag[;e、 g.herehere.


最后:

  • 在数据库上——没有理由不使用数据.table无法提供类似的接口,但现在这不是优先考虑的问题.如果用户非常喜欢这个功能,它可能会被提升..不确定.

  • 在平行性上——一切都很困难,直到有人go 做.当然这需要努力(线程安全).

    • 目前(在v1.9.7-devel中)在使用OpenMP并行化已知耗时部件以获得增量性能提升方面正在取得进展.

R相关问答推荐

更改网格的crs以匹配简单要素点对象的crs

行式dppr中的变量列名

无法定义沿边轨迹的 colored颜色 渐变(与值无关)

函数可以跨多个列搜索多个字符串并创建二进制输出变量

R如何计算现有行的总和以添加新的数据行

从多层嵌套列表构建Tibble?

使用ggplot2中的sec_axis()调整次轴

将列表中的字符串粘贴到R中for循环内的dplyr筛选器中

为什么函数toTitleCase不能处理english(1),而toupper可以?

根据r中每行中的日期序列,使用列名序列创建新列

计算多变量的加权和

在ggplot2图表中通过端点连接点

是否从列中删除★符号?

conditionPanel不考虑以下条件

组合名称具有模式的列表的元素

如何捕获这个shiny 的、可扩展的react 性用户输入矩阵作为另一个react 性对象,以便进一步操作?

将字符变量出现次数不相等的字符框整形为pivot_wider,而不删除重复名称或嵌套字符变量

R:水平旋转图

如何在分组蜂群小区中正确定位标签

如何在类应用函数中访问函数本身