r - 数据表与 dplyr : can one do something well the other can't or does poorly?

标签 r data.table dplyr

概述

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

  • data.tabledplyr 在速度上是相当的,除非有很多(即 >10-100K)组,以及在其他一些情况下(参见下面的基准测试)1314239
  • 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 解决方案的 hack 尝试要好得多。也就是说,好的 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显示在回答用do潜在的变通@实现eddi 的问题)。
  • data.table支撑 rolling joins (感谢@dholstius)以及 overlap joins
  • data.table 内部优化了 DT[col == value]DT[col %in% values] 形式的表达式,以通过使用二进制搜索的自动索引来提高速度,同时使用相同的基本 R 语法。 See here 了解更多细节和一个小基准。
  • dplyr提供的功能标准评估版本(例如regroupsummarize_each_),可以简化程序中使用的dplyr(注意程序中使用的data.table是绝对有可能,只是需要一些认真思考,置换/报价,等等,至少据我所知)

  • 基准
  • 我跑 my own benchmarks ,发现两个包是在可比的“ split 应用相结合”的风格分析,当有非常大的数字组(> 100K),在该点data.table变得基本上快除外。
  • @Arun进行了一些 benchmarks on joins ,显示出data.table鳞比dplyr更好,因为群体数量的增加(更新在两个包的最新增强和R最近的版本)。此外,尝试获得 unique values 时的基准测试速度快 data.table ~6x。
  • (未验证)具有在较大的版本中的一个组的更快data.table 75%/应用/排序而dplyr是在较小的( another SO question from comments ,由于达纳斯)快40%。
  • Matt, data.table 的主要作者,有 benchmarked grouping operations on data.table , dplyr and python pandas on up to 2 billion rows (~100GB in RAM)
  • 一个 older benchmark on 80K groups 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))
    

  • 最佳答案

    我们至少需要涵盖这些方面以提供全面的答案/比较(没有特别的重要性顺序): SpeedMemory usageSyntax 和 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]ij 保持在一起是设计使然。通过将相关操作放在一起,它可以轻松优化操作的速度,更重要的是内存使用,还提供一些强大的功能,同时保持语法的一致性。
    1.速度
    相当多的基准测试(尽管主要是关于分组操作)已添加到已经显示 data.table 随着要分组的组和/或行数的增加而变得比 dplyr 更快的问题中,包括 benchmarks by Matt 分组从 1000 万到 20 亿100 - 1000 万组和不同分组列上的行(RAM 中的 100GB),也比较 by 。另见 updated benchmarks ,其中包括 pandasSpark
    在基准测试中,还可以涵盖以下其余方面:
  • 涉及行子集的分组操作 - 即 pydatatable 类型操作。
  • 基准其他操作,如更新和连接。
  • 除了运行时,还对每个操作的内存占用进行基准测试。

  • 2. 内存使用
  • dplyr 中涉及 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.


  • data.table 接口(interface)目前允许通过引用修改/更新列(请注意,我们不需要将结果重新分配回变量)。
     # 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 对象,尤其是在函数内可能并不总是可取的。但这是一个非常有用的功能:有关有趣的案例,请参见 thisthis 帖子。我们想保留它。
    因此,我们正在努力导出 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]


  • 优势是什么?
  • 我们不必为中间结果分配内存。
  • 我们不必分组/散列两次(一次用于聚合,另一次用于加入)。
  • 更重要的是,通过查看(2)中的 by = .EACHI,我们想要执行的操作就很清楚了。

  • 检查 this post 以获取 j 的详细说明。没有中间结果被具体化,join+aggregate 是一次性执行的。
    查看 thisthisthis 帖子以了解实际使用场景。
    by = .EACHI 中,您必须使用 join and aggregate or aggregate first and then join ,就内存而言(这反过来又转化为速度),这两种方法都没有效率。
  • 更新和加入:
    考虑如下所示的 data.table 代码:
     DT1[DT2, col := i.mul]
    
    添加/更新 dplyr 的列 DT1col 来自 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])
    
  • data.table 语法紧凑,dplyr 非常冗长。在情况(a)中,事情或多或少是等价的。
  • 在情况 (b) 中,我们在总结时必须在 dplyr 中使用 dplyr。但是在更新时,我们不得不将逻辑移到 *_join 中。然而,在 data.table 中,我们用相同的逻辑表达两个操作 - 对 DT1 的行进行操作,但在第一种情况下,得到 filter() ,而在第二种情况下,用其累积和更新这些行的 mutate()
    这就是我们说 x > 2 形式一致时的意思。
  • 与情况 (c) 类似,当我们有 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))
    
  • 在情况 (a) 中,代码或多或少是等效的。 data.table 使用熟悉的基函数 summarise() ,而 filter() 引入了 filter() 和一堆函数到 lapply()
  • data.table 的 dplyr 需要提供列名,而 dplyr 会自动生成它。
  • 在情况 (b) 中,dplyr 的语法相对简单。改进多个函数的聚合/更新在 data.table 的列表中。
  • 但是,在情况 (c) 中,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 use c(), 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 are n() 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"]
     ??
    
  • 有些人可能会发现每个连接的单独函数更好(左、右、内、反、半等),而其他人可能喜欢 data.table 的 listDT[i, j, by] 类似于 base R.
  • 但是 dplyr 连接就是这样做的。而已。一点也不少。
  • data.tables 可以在加入 (2) 时选择列,在 dplyr 中,您需要先在两个 data.frames 上 merge.data.table() 才能加入,如上所示。否则,您会使用不必要的列来实现连接,只是为了稍后删除它们,这是低效的。
  • data.tables 可以在加入时聚合,使用 DT[i, j, by] 特性 (3) 并在加入时更新 (4)。为什么将整个连接结果物化为仅添加/更新几列?
  • data.table 能够滚动连接 (5) - 滚动 forward, LOCFroll backward, NOCBnearest
  • data.table 也有 merge() 参数,它选择第一个、最后一个或所有匹配项 (6)。
  • data.table 有 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()
  • 在 data.table 中,您几乎可以在 do() 中抛出任何内容 - 唯一要记住的是它返回一个列表,以便列表中的每个元素都转换为一列。
  • 在 dplyr 中,不能这样做。必须求助于 .SD,具体取决于您对函数是否始终返回单个值的确定程度。而且速度很慢。

  • Once again, data.table's syntax is consistent with DT[i, j, by]. We can just keep throwing expressions in j without having to worry about these things.


    看看 this SO questionthis 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 和这篇文章中的大部分功能。此外:
  • fread - 快速文件阅读器已经存在很长时间了。
  • fwrite - 并行快速文件写入器现已可用。有关实现的详细说明,请参阅 this post,请参阅 #1664 以跟踪进一步的发展。
  • Automatic indexing - 在内部优化基本 R 语法的另一个方便的功能。
  • Ad-hoc 分组 : . 通过对 0x231341 期间的变量进行分组来自动对结果进行排序,这可能并不总是可取的
  • 上面提到的 data.table 连接(速度/内存效率和语法)的众多优点。
  • 非对等连接 :允许使用其他运算符 j 以及 data.table 连接的所有其他优点进行连接。
  • 最近在data.table中实现了
  • Overlapping range joins。检查 this post 以获取基准概述。
  • do() 在 data.table 中的函数允许通过引用真正快速地重新排序 data.tables。
  • dplyr 提供 interface to databases 使用相同的语法,data.table 目前没有。
  • dplyr提供的一组操作更快当量(由Jan Gorecki写入) - summarise()<=, <, >, >=setorder()data.table用额外fsetdiff参数(如SQL)。
  • data.table 加载干净,没有屏蔽警告,并具有描述 here 的机制,用于传递给任何 R 包时的 fintersect 兼容性。 dplyr 更改基本函数 funionfsetequalall ,这可能会导致问题;例如herehere

  • 最后:
  • 在数据库上 - 没有理由 data.table 不能提供类似的接口(interface),但这不是现在的优先事项。如果用户非常喜欢该功能,它可能会被提升.. 不确定。
  • 关于并行性 - 一切都很困难,直到有人继续去做。当然这需要努力(线程安全)。
  • 目前(在 v1.9.7 开发中)正在使用 [.data.frame 并行化已知的耗时部分以提高性能。

  • 关于r - 数据表与 dplyr : can one do something well the other can't or does poorly?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21435339/

    相关文章:

    r - 使用两个变量过滤 data.table,一种优雅的快速方法

    r - do.call 构建并执行 data.table 命令

    r - 如果对象是从文件中新加载的,data.table 不会通过引用修改?

    r - 使用 dplyr 计算共享相似名称的列的按行汇总统计信息,例如平均值、最大值、最小值

    r - 线性回归 - 将预测值附加到同一数据集

    r - 如何在R中的并行任务中删除临时文件

    r - 将中间列表输出保存在 dplyr 管道中,并将其映射回管道下游的另一个列表 - R

    r - 使用另一个条件过滤某些值和多个先前行

    r - 解释 R 中的错​​误消息,将一列分成两列并创建一个新的数据框

    r - 将时间戳(开始、结束)转换为时间序列数据。 align.time() 和 colname 错误