data.table vs dplyr:一个人可以做得很好,而另一个人不能或做得很差?

Posted

技术标签:

【中文标题】data.table vs dplyr:一个人可以做得很好,而另一个人不能或做得很差?【英文标题】:data.table vs dplyr: can one do something well the other can't or does poorly? 【发布时间】:2014-02-21 12:10:37 【问题描述】:

概述

我对@9​​87654334@ 比较熟悉,对dplyr 不太熟悉。我已经阅读了一些 dplyr vignettes 和 SO 上出现的示例,到目前为止,我的结论是:

    data.tabledplyr 在速度上相当,除非有很多(即 >10-100K)组,以及在某些其他情况下(请参阅下面的基准) dplyr 具有更易于访问的语法 dplyr 抽象(或将要)潜在的数据库交互 存在一些细微的功能差异(请参阅下面的“示例/用法”)

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

问题

我想知道的是:

    对于熟悉软件包的人来说,是否有更容易使用一个或另一个软件包编写代码的分析任务(例如,所需的击键与所需的深奥水平的某种组合,其中少一些是好事)。 是否有分析任务在一个包中比在另一个包中的执行效率显着提高(即超过 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 解决方案的破解尝试要好得多。也就是说,好的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 支持 rolling joins(感谢@dholstius)以及 overlap joins data.table 在内部优化 DT[col == value]DT[col %in% values] 形式的表达式以实现 speed 通过 自动索引 使用 二分搜索 同时使用相同基本 R 语法。 See here 了解更多细节和小基准。 dplyr 提供函数的标准评估版本(例如regroupsummarize_each_),可以简化dplyr 的编程使用(注意data.table 的编程使用绝对是可能的,只需要仔细考虑,替换/引用等,至少据我所知) 基准 我运行了 my own benchmarks 并发现两个包在“拆分应用组合”样式分析中具有可比性,除非此时有大量组 (>100K) data.table变得更快。 @Arun 运行了一些 benchmarks on joins,表明随着组数量的增加,data.table 的扩展性优于 dplyr(更新为包和 R 的最新版本中的最新增强功能)。此外,在尝试获得 unique values 时的基准测试使 data.table 的速度提高了约 6 倍。 (未验证)data.table 在较大版本的组/应用/排序上快 75%,而 dplyr 在较小版本上快 40%(another SO question from comments,感谢 danas) . data.table的主要作者Matt,拥有benchmarked grouping operations on data.table, dplyr and python pandas on up to 2 billion rows (~100GB in RAM)older benchmark on 80K groupsdata.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))

【问题讨论】:

dplyr类似的解决方案是:as.data.table(dat)[, .SD[job != "Boss" | year == min(year)][, cumjob := cumsum(job2)], by = list(name, job)] 好吧,再次提醒,(d)plyr 中更清晰地表达的问题集的度量为 0 @BrodieG 关于dplyrplyr 在语法方面真正困扰我的一件事,基本上是我不喜欢它们的语法的主要原因,我也必须学习许多(阅读超过 1 个)额外的函数(名称对我来说仍然没有意义),记住它们的作用,它们采用的参数等。这一直是一个巨大的关闭对我来说来自 plyr 哲学。 @eddi [tongue-in-cheek] data.table 语法真正困扰我的一件事是,我必须了解太多函数参数如何交互,以及神秘的快捷方式意味着什么(例如.SD)。 [认真] 我认为这些是合理的设计差异,会吸引不同的人 @hadley re .SD 等 - 这很公平 - .SD 花了我一点时间来理解,但是当我到达那里时,我已经能够做很多事情了,而( d)plyr 为您提供了一个巨大的障碍。 【参考方案1】:

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

我的目的是从 data.table 的角度尽可能清楚地涵盖其中的每一项。

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


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

1。速度

相当多的基准(尽管主要是关于分组操作)已经添加到已经显示 data.table 比 dplyr的问题中,因为要分组的组和/或行数增加,包括benchmarks by Matt 在100-1000 万组上从1000 万到20 亿行(RAM 中100GB)分组和不同的分组列,这也与pandas 进行比较。另见updated benchmarks,其中还包括Sparkpydatatable

在基准测试中,最好也涵盖这些剩余方面:

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

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

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

2。内存使用情况

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

    请注意,Hadley's comment 谈论的是 speed(dplyr 对他来说非常快),而这里主要关注的是 memory

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

    对此的担忧是referential transparency。通过引用更新 data.table 对象,尤其是在函数内可能并不总是可取的。但这是一个非常有用的功能:有关有趣的案例,请参阅 this 和 this 帖子。我们想保留它。

    因此,我们正在努力在 data.table 中导出 shallow() 函数,这将为用户提供两种可能性。例如,如果不希望在函数中修改输入 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() 创建浅拷贝,我们了解到您不想修改原始对象。我们会在内部处理所有事情,以确保在复制列的同时仅在绝对必要时修改。实施后,这应该完全解决引用透明度问题,同时为用户提供两种可能性。

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

    但它仍然缺少 data.table 提供的许多功能,包括通过引用进行(子)分配。

    加入时聚合:

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

    您希望在DT2 中的每一行获得sum(z) * mul,同时通过列x,y 加入。我们可以:

      聚合 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) %>% 变异(z = z * mul)

      一次性完成所有操作(使用by = .EACHI 功能):

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

    有什么优势?

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

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

    更重要的是,通过查看(2)中的j,我们想要执行的操作很清楚。

    查看this post 了解by = .EACHI 的详细说明。没有中间结果被物化,并且join+aggregate是一次性完成的。

    查看this、this 和this 的帖子了解实际使用场景。

    dplyr 中,您必须使用join and aggregate or aggregate first and then join,就内存(这反过来转化为速度)而言,两者都没有那么高效。

    更新和加入:

    考虑如下所示的data.table代码:

     DT1[DT2, col := i.mul]
    

    DT2 的键列与DT1 匹配的行上添加/更新DT1 的列col 和来自DT2mul。我认为dplyr 中没有与此操作完全等效的操作,即,在不避免 *_join 操作的情况下,该操作必须复制整个 DT1 以添加新列,这是不必要的.

    查看this post 了解真实使用场景。

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

3。语法

现在让我们看看语法。哈德利评论here:

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

我觉得这句话毫无意义,因为它非常主观。我们或许可以尝试对比语法的一致性。我们将并排比较 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])
    

    data.table 语法紧凑,dplyr 相当冗长。在情况 (a) 中,事情或多或少是等价的。

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

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

    类似情况 (c),当我们有if-else 条件时,我们能够在 data.table 和 dplyr 中表达逻辑“原样”。但是,如果我们只想返回满足if 条件的行并跳过其他行,我们不能直接使用summarise() (AFAICT)。我们必须先filter() 然后总结,因为summarise() 总是期望一个单个值

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

    在第一种情况下也很可能使用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))
    

    在情况 (a) 中,代码或多或少是等价的。 data.table 使用熟悉的基本函数lapply(),而dplyr*_each() 以及一堆函数引入funs()

    data.table 的:= 要求提供列名,而 dplyr 会自动生成。

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

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

    注意:再一次,在 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. 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 的 DT[i, j, by]merge(),它类似于基本 R。

但是 dplyr 加入就是这样做的。而已。一点也不差。

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

data.tables 可以在加入时聚合,使用by = .EACHI 功能 (3) 以及 在加入时更新 (4)。为什么要具体化整个连接结果来添加/更新几列?

data.table 能够滚动连接 (5) - 滚动 forward, LOCF、roll backward, NOCB、nearest。

data.table 也有 mult = 参数,用于选择 firstlastall 匹配 (6)。

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

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

    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 的等效项是 .

在 data.table 中,您几乎可以在 j 中输入任何内容 - 唯一需要记住的是它会返回一个列表,以便将列表中的每个元素转换为一列。

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

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

看看this SO question 和this one。我想知道是否可以使用 dplyr 的语法直接表达答案...

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

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

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

4。特点

我已经指出了here 以及这篇文章中的大部分功能。另外:

fread - 快速文件阅读器已经存在很长时间了。

fwrite - 并行 快速文件写入器现已推出。请参阅this post 了解有关实施的详细说明,并参阅#1664 以跟踪进一步的发展。

Automatic indexing - 另一个方便的功能,可在内部按原样优化基本 R 语法。

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

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

非等值联接:允许使用其他运算符 &lt;=, &lt;, &gt;, &gt;= 进行联接以及 data.table 联接的所有其他优点。

Overlapping range joins 最近在 data.table 中实现。查看this post 了解基准测试概览。

data.table 中的setorder() 函数允许通过引用快速重新排序 data.tables。

dplyr 使用相同的语法提供 interface to databases,而 data.table 目前没有。

data.table 提供更快的 set 操作 等效项(由 Jan Gorecki 编写) - fsetdifffintersectfunionfsetequal 以及额外的 all 参数(如SQL)。

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


最后:

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

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

目前(在 v1.9.7 开发版中)正在使用 OpenMP 并行化已知耗时的部分以提高性能。

【讨论】:

@bluefeet:我认为您将讨论转移到聊天中并没有为我们其他人提供任何出色的服务。我的印象是 Arun 是开发人员之一,这可能会产生有用的见解。 我认为,在您使用引用赋值 (:=) 的每个地方,dplyr 等效项也应该使用 &lt;-,就像在 DF &lt;- DF %&gt;% mutate... 中一样,而不仅仅是 DF %&gt;% mutate...跨度> 关于语法。我相信dplyr 对于习惯于plyr 语法的用户来说会更容易,但data.table 对于习惯于查询诸如SQL 之类的语言语法以及它背后的关系代数的用户来说会更容易,这就是表格数据转换。 @Arun 你应该注意到 set operator 通过包装 data.table 函数非常容易实现,当然可以带来显着的加速。 这篇文章我已经读了很多遍了,它对我理解 data.table 和更好地使用它有很大帮助。在大多数情况下,我更喜欢 data.table 而不是 dplyr 或 pandas 或 PL/pgSQL。但是,我无法停止思考如何表达它。语法简单、清晰或冗长。事实上,即使在我大量使用 data.table 之后,我仍然经常难以理解我自己的代码,这是我一周前写的。这是只写语言的生活示例。 en.wikipedia.org/wiki/Write-only_language 所以,希望有一天我们能够在 data.table 上使用 dplyr。 实际上很多 dplyr 代码不再适用(由于更新?)...这个答案可以使用刷新,因为它是一个很好的资源。【参考方案2】:

这是我从 dplyr 的角度尝试综合回答, 遵循阿伦答案的大致轮廓(但有些重新排列 基于不同的优先级)。

语法

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

dplyr 为您做的一件非常重要的事情是 限制您的选择。我声称大多数单表问题可以 只需五个关键动词 filter、select、mutate、arrange 和 总结,以及“按组”副词。这个约束是一个很大的帮助 当您学习数据操作时,因为它有助于订购您的 思考问题。在 dplyr 中,这些动词中的每一个都映射到一个 单一功能。每个功能做一项工作,并且易于理解 孤立无援。

通过将这些简单的操作与 %&gt;%。这是来自 Arun linked to 的帖子之一的示例:

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

除非您已经熟悉此代码,否则很难遵循此代码 数据表。 (我也不知道如何缩进重复的[ 以一种我觉得不错的方式)。就个人而言,当我查看代码时,我 6个月前写的,就像看一个陌生人写的代码, 所以我开始更喜欢简单明了的代码。

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

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

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

我也喜欢the pipe 不仅限于一个包。你可以从整理你的 数据与 tidyr,和 在ggvis 中完成一个情节。而你 不限于我写的包——任何人都可以写一个函数 它构成了数据操作管道的无缝部分。事实上,我 更喜欢之前用%&gt;%重写的data.table代码:

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

使用%&gt;% 管道的想法不仅限于数据帧和 很容易推广到其他上下文: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()

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

更灵活的分组,如引导样本、汇总等

我还在投入时间来提高 R 的 database connectors,即与人交谈的能力 web apis,并使其更容易 scrape html pages.

【讨论】:

只是一个旁注,我确实同意你的许多论点(虽然我自己更喜欢data.table 语法),但你可以轻松地使用%&gt;% 来管道data.table 操作,如果你不喜欢[ 风格。 %&gt;% 并非特定于 dplyr,而是来自一个单独的包(您恰好也是该包的合著者),所以我不确定我是否理解您在大多数 语法段落。 @DavidArenburg 好点。我重写了语法,希望能更清楚地说明我的要点是什么,并强调您可以将 %&gt;% 与 data.table 一起使用 感谢哈德利,这是一个有用的观点。重新缩进我通常会使用 DT[\n\texpression\n][\texpression\n] (gist),这实际上效果很好。我将 Arun 的答案作为答案,因为他更直接地回答了我的具体问题,这些问题与语法的可访问性无关,但我认为这对于试图大致了解 @ 之间的差异/共性的人们来说是一个很好的答案987654357@ 和 data.table. 既然已经有fread(),为什么还要使用fastread?把时间花在改进 fread() 或处理其他(不发达的)事情上不是更好吗? data.table 的 API 建立在对 [] 表示法的大量滥用之上。这是它最大的优势,也是它最大的弱点。【参考方案3】:

直接回复问题标题...

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

你的观点 #3

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

是对您自己问题的直接回答,但没有提升到足够高的水平。 dplyr 是一个真正可扩展的前端到多种数据存储机制,而data.table 是对单个机制的扩展。

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

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

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

基于内存中工作的设计,data.table 可能比dplyr 更难将自己扩展到并行查询处理。


回答体内问题...

用法

对于熟悉这些软件包的人来说,是否有更容易使用其中一个软件包编写代码的分析任务(即所需的按键组合与所需的深奥水平,其中较少的每个都是好事)。

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

性能

是否有分析任务在一个包中比在另一个包中的执行效率大大提高(即超过 2 倍)。

再次,不。 data.table 擅长在 it 所做的所有事情中保持高效,而 dplyr 在某些方面受限于底层数据存储和注册的处理程序。

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

dplyr 存在后端性能问题时,您可以通过注册混合评估函数或(在数据库的情况下)在执行之前处理生成的查询来解决这些问题。

另请参阅when is plyr better than data.table? 接受的答案

【讨论】:

不能 dplyr 用 tbl_dt 包装 data.table 吗?为什么不两全其美? 你忘了提相反的说法“data.table 肯定做了dplyr 做不到的事情” 这也是正确的。 Arun 回答很好地解释了这一点。最重要的(就性能而言)将是 fread、通过引用更新、滚动连接、重叠连接。我相信没有任何软件包(不仅是 dplyr)可以与这些功能竞争。一个很好的例子可以是来自this 演示文稿的最后一张幻灯片。 总的来说,data.table 是我仍然使用 R 的原因。否则我会使用 pandas。它甚至比 pandas 更好/更快。 我喜欢 data.table,因为它简单且类似于 SQL 语法结构。我的工作涉及每天为统计建模做非常密集的临时数据分析和图形,我真的需要足够简单的工具来完成复杂的事情。现在,我可以在日常工作中将我的工具包简化为仅用于数据的 data.table 和用于图形的 lattice。举个例子,我什至可以做这样的操作:$DT[group==1,y_hat:=predict(fit1,data=.SD),]$,这真的很简洁,我认为它是 SQL 的一大优势经典的 R 环境。【参考方案4】:

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

但正如一些人已经提到的,dplyr 可以使用data.table 作为后端。这是使用最近版本为 1.0.0 release 的 dtplyr 包完成的。学习 dtplyr 几乎不需要付出额外的努力。

当使用dtplyr 时,使用函数lazy_dt() 声明一个惰性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

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

recreateddata.table 作者 Matt Dowle 在 2018 年 12 月完成了一项基准分析,其中涵盖了对大量组进行操作的情况。我发现dtplyr 确实使大多数喜欢dplyr 语法的人在享受data.table 提供的速度的同时继续使用它。

【讨论】:

你可能不会有很多在 dplyr 中没有 API 的特性,比如引用的子分配、滚动连接、重叠连接、非 equi 连接、连接更新,可能还有很多其他的. 我不得不承认这些功能都没有敲响警钟。能否请您在 data.table 中提供具体示例? ?data.table examples,所有我提到的,除了重叠连接。有吗 连接更新、滚动连接、重叠连接可以用管道的几个部分直接构建。 查看非等值连接的模糊连接(似乎比 data.table 的非等值连接具有更多的特性和功能)。

以上是关于data.table vs dplyr:一个人可以做得很好,而另一个人不能或做得很差?的主要内容,如果未能解决你的问题,请参考以下文章

重新审视 data.table 与 dplyr 的内存使用

一个接一个地加载 Data.Table 和 dplyr 会出错

data.table 按行求和,平均值,最小值,最大值,如 dplyr?

使用 count()、aggregate()、data.table() 或 dplyr() 汇总数据(均值、标准差)

data.table相当于dplyr :: filter_at

使用dplyr汇总并保持相同的变量名称