当前位置: 首页 > 知识库问答 >
问题:

data.table vs dplyr:一个能做得很好而另一个做不到或做得很差吗?

艾骏喆
2023-03-14
    null

中在我的脑海中。不太重要,因为我对data.table相当熟悉,但我知道对于刚接触这两个工具的用户来说,这将是一个很大的因素。我希望避免关于哪一个更直观的争论,因为这与我从已经熟悉data.table的人的角度提出的特定问题无关。我还想避免讨论“更直观”如何导致更快的分析(当然是正确的,但这里不是我最感兴趣的)。

我想知道的是:

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

最近的一个SO问题让我对此有了更多的思考,因为在此之前,我并不认为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的问题,注意:这似乎将在dplyr 0.5中实现,而且,@beginner在@eddi问题的答案中显示了使用do的潜在解决方法)。
  • data.table支持滚动联接(谢谢@dholstius)和重叠联接
  • data.table通过使用二进制搜索同时使用相同的基本R语法的自动索引,在内部优化了dt[col==value]dt[col%in%value]形式的表达式,以提高速度。有关更多详细信息和一个小基准,请参阅此处。
  • dplyr提供了函数的标准评估版本(例如regroupsummarize_each_),这些函数可以简化dplyr的编程使用(注意,data.table的编程使用肯定是可能的,只是需要一些仔细的思考、替换/引用等,至少据我所知)
  • 我运行了我自己的基准测试,发现这两个包在“拆分应用组合”风格分析中是可以比较的,除非有非常多的组(>100k)时,data.table会变得快得多。
  • @arun在联接上运行了一些基准测试,显示随着组数的增加,data.table的扩展性优于dplyr(在包和R的最新版本中都进行了更新)。此外,在尝试获取唯一值时,基准测试的data.table速度快6x
  • (未经验证)在较大版本的组/apply/sort上具有数据表快75%,而在较小版本上dplyr快40%(另一个来自注释的SO问题,谢谢danas)。
  • Matt是data.table的主要作者,他对data.tabledplyr和pythonpandas进行了基准分组操作,最多可处理20亿行(~100GB内存)。
  • 80k组上的较旧基准具有数据.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))

共有1个答案

辛意智
2023-03-14

我们至少需要涵盖这些方面来提供全面的答案/比较(没有特定的重要性顺序):速度内存使用语法特性

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

注意:除非另外明确提到,通过引用dplyr,我们指的是dplyr的data.frame接口,其内部是使用rcpp的C++。

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

随着要分组的组和/或行的数量的增加,Table的速度比dplyr快,包括Matt对1亿到1000万组和不同分组列的1000万到20亿行(内存为100GB)进行分组的基准测试,这也比较了pandas。另请参阅更新的基准测试,其中还包括sparkpydataTable

关于基准,最好也包括以下几个方面:

> 涉及行子集的

  • 分组操作--即dt[x>val,sum(y),by=z]类型操作。

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

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

    当前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对象,特别是在函数中,可能并不总是可取的。但这是一个非常有用的特性:有关有趣的案例,请参阅这篇文章和这篇文章。我们想保留它。

     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.
     }
    
     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一起使用。

    但是它仍然缺乏Data.Table提供的许多特性,包括(sub)-引用赋值。

    加入时聚合:

    假设您有两个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。我们可以:

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

    DF1%>%group_by(x,y)%>%summise(z=sum(z))%>%right_join(DF2)%>%mutate(z=z*mul)

    一次性完成所有操作(使用by=.eachi特性):

    DT1[DT2,列表(z=SUM(z)*mul),by=.EACHI]

    优势在哪里?

    >

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

    dplyr中,您必须连接并聚合或先聚合再连接,这两种方法在内存方面都没有那么高效(这反过来又转化为速度)。

    更新和连接:

    请考虑下面显示的数据表代码:

     DT1[DT2, col := i.mul]
    

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

    总而言之,重要的是要认识到每一个优化都很重要。就像格蕾丝·霍珀说的,小心你的毫微秒!

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

    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>2的行进行操作,但在第一种情况下,得到sum(y),而在第二种情况下,用其累计和更新y的行。

    类似地,在情况(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中返回一个列表。列表中的每个元素都将成为Result中的一列。您可以使用c()as.list()lapply()list()等。基本函数来完成这一点,而不需要学习任何新的函数。

    您至少需要学习特殊变量.n.sd。dplyr中的等价物是n().

    联接

    dplyr为每种类型的联接提供了单独的函数,其中as 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.tables可以在连接(2)时选择列,在dplyr中,您需要在两个data.frames上首先使用select()才能连接,如上所示。否则,您将使用不必要的列对联接进行物化,以便稍后删除它们,这是低效的。

    Data.Tables可以使用by=.eachi特性(3)在连接时进行聚合,也可以在连接时进行更新(4)。为什么要将整个连接结果物化为只添加/更新几列?

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

    Data.Table具有allow.cartesian=true参数以防止意外的无效联接。

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

    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(),这取决于您对函数是否总是返回一个值的确定程度。而且相当慢。

    看看这道SO题和这道题。我想知道是否可以用DPLYR的语法将答案表达得简单明了...

    总而言之,我特别强调了DPLYR语法效率低、有限或操作不简单的几个实例。这特别是因为data.table在“更难读/学习”语法(如上面粘贴/链接的语法)方面受到了很大的反对。大多数涉及dplyr的帖子都讨论了最简单的操作。这太好了。但是认识到它的语法和特性限制也是很重要的,我还没有看到关于它的帖子。

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

    但是还应该考虑dplyr与Data.Table相比所缺少的特性的数量。

    我已经在这里和这篇文章中指出了大部分的特点。此外:

    >

  • Fread-快速文件阅读器已经提供了很长时间。

    上面提到了Data.Table联接的许多优点(速度/内存效率和语法)。

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

    最近在Data.Table中实现了重叠的范围连接。请查看本篇文章,了解基准测试的概述。

    data.table提供了集合操作(由Jan Gorecki编写)更快的等价物-fsetdifffintersectfunionfsetequal以及附加的all参数(如SQL中的)。

    data.table可以清晰地加载,没有屏蔽警告,并且在传递到任何R包时,这里描述了[.data.frame兼容性的机制。dplyr更改了基函数filterlag[,这些函数可能会导致问题,例如这里和这里。

    最后:

    >

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

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

      null

  •  类似资料:
    • 我用Java重写了我的第一个程序,现在它看起来像这样: 实际上一切都很顺利,除了: > 当我在输入a、B和C的值时输入一个非数字类型时,会显示一个双重警告,然后它就会正常工作。(仅1个警告) 当我回答Y或N时,程序终止,但如果我输入任何其他内容,如“g”、“2”或“w”等,程序会再次询问我(第一次也是两次),然后当我输入Y或N,它工作得很好。 我真的不知道这是怎么回事,因为在编译过程中没有警告或错

    • 这是take II,前几周我发了帖子,我的问题被搁置,我调整了我的文本,但无法得到评论,系统关闭了原来的帖子。 服务器端:只读-服务器打开管道,然后定期检查是否有内容(即不在流末尾)并读取信息。此检查必须以轮询为基础,因为只有在轮询期间,才有有效的上下文来传递数据。。 客户端:仅写-打开管道、写入管道、关闭(Client.exe多次调用,生命周期短,下面的代码是测试代码),例如,其他一些脚本将“使

    • 我要离开一个网站(https://realpython.com/how-to-make-a-discord-bot-python/#how-to-make-a-discord-bot-in-python)和我下面的步骤,但我一直得到一个错误msg Traceback(最近一次调用最后一次): File"C:\用户\Bryce.Persello346\Desktop\bot.py",第15行,cli

    • 问题内容: 一般而言,制作所有字段是一个好主意,但有时我发现自己在构造函数中做了所有事情。最近,我结束了一个类实际上做的 一切都 在构造函数中,包括读取属性文件并访问数据库。 一方面,这就是该类的用途,它封装了读取的数据,我喜欢创建完全初始化的对象。构造函数完全不复杂,因为它委派了大部分工作,因此看起来不错。 另一方面,感觉有点奇怪。此外,在大约17:58的演讲中,有充分的理由不对构造函数进行过多

    • 在讲述有关list的时候,提到做游戏的事情,后来这个事情一直没有接续。不是忘记了,是在想在哪个阶段做最合适。经过一段时间学习,看官已经不是纯粹小白了,已经属于python初级者了。现在就是开始做那个游戏的时候了。 游戏内容:猜数字游戏 太简单了吧。是的,游戏难度不大,不过这个游戏中蕴含的东西可是值得玩味的。 游戏过程描述 程序运行起来,随机在某个范围内选择一个整数。 提示用户输入数字,也就是猜程序