mutate是否通过引用更改tbl?

Bea*_*eld 16 r dplyr data.table

我真正喜欢的data.table:=通过引用更改表格的习惯用语,而不需要昂贵的副本.据我所知,data.table与其他方法相比,这是超快的方面之一.

现在,我开始玩dplyr这个看似同样高效的包.但由于结果仍然需要使用<-运营商进行分配,因此我预计此级别的性能会下降.然而,似乎没有.

举个例子:

library(dplyr)
library(Lahman)
library(microbenchmark)
library(ggplot2)

df <- Batting[ c("yearID", "teamID", "G_batting") ]

mb <- microbenchmark(
  dplyr = {
    tb <- tbl_df( df )
    tb <- tb %.%
      group_by( yearID, teamID ) %.%
      mutate( G_batting = max(G_batting) )
  },
  data.table = {
    dt <- as.data.table( df )
    dt[ , G_batting := max(G_batting), by = list( yearID, teamID ) ]
  },
  times = 500
)

qplot( data = mb, x = expr, y = time * 1E-6, geom = "boxplot", ylab="time [ms]", xlab = "approach" )
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

我只是想知道这是怎么可能的?或者我的基准测试方式存在概念错误?我对<-错误的理解是什么?

Aru*_*run 18

好问题.一般来说,我会根据数据大小进行基准测试,该数据大小足以(几乎)完全适合缓存.在"初始设置"下查看此处.将(内存中)大数据开发的工具与运行在毫秒级的任务进行比较实际上没有意义.我们计划在未来对相对较大的数据进行基准测试.

此外,如果您的目的是要确定是否mutate正在执行副本,那么您所要做的就是检查address之前和之后(这可以使用.Internal(inspect(.))in base R或使用函数changes()来完成dplyr).


关于是否正在制作副本:

这里有两个不同的东西需要检查.A)创建新列,以及B)修改现有列.

A)创建新列:

require(dplyr)
require(data.table)
df <- tbl_df(data.frame(x=1:5, y=6:10))

df2 <- mutate(df, z=1L)
changes(df, df2)
# Changed variables:
#           old new
# z             0x105ec36d0
Run Code Online (Sandbox Code Playgroud)

它告诉你,x和的地址没有变化y,并指出z我们刚刚添加.这里发生了什么事?

dplyr 浅拷贝data.frame,然后又增加了新列.一个浅拷贝,而不是一个深拷贝只是复制列指针的载体,而不是数据本身.因此它应该很快.基本上df2是使用3列创建的,其中前两列指向与刚创建的第3列相同的地址位置df.

另一方面,data.table不需要浅拷贝,因为它通过引用修改列(就地).data.table也(巧妙地)过度分配列向量列表,允许通过引用快速添加(新)列.

只要你的列数太多,浅拷贝的时间就不应该有很大差异.这是5000列(1e4行)的小基准:

require(data.table) # 1.8.11
require(dplyr)      # latest commit from github

dt <- as.data.table(lapply(1:5e3, function(x) sample(1e4)))
ans1 <- sapply(1:1e2, function(x) {
    dd <- copy(dt) # so as to create the new column each time
    system.time(set(dd, i=NULL, j="V1001", value=1L))['elapsed'] 
    # or equivalently of dd[, V1001 := 1L]
})

df <- tbl_df(as.data.frame(dt))
ans2 <- sapply(1:1e2, function(x) {
    system.time(mutate(df, V1001 = 1L))['elapsed']
})
> summary(ans1) # data.table
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
0.00000 0.00000 0.00100 0.00061 0.00100 0.00100
> summary(ans2) # dplyr
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
0.03800 0.03900 0.03900 0.04178 0.04100 0.07900
Run Code Online (Sandbox Code Playgroud)

你可以在这里看到"平均时间"的差异(0.00061 vs 0.04178)..

B)修改现有列:

df2 <- mutate(df, y=1L)
changes(df, df2)
# Changed variables:
#           old         new
# y         0x105e5a850 0x105e590e0 
Run Code Online (Sandbox Code Playgroud)

它告诉您y已经更改 - 已经创建了一个y副本.它必须创建一个新的存储位置改变的值y,因为它是指向同一位置作为的dfy前.

但是,由于data.table修改到位,在(B)的情况下不会复制.它会df在适当的地方修改.因此,如果要修改列,应该会看到性能差异.

这是两个包之间哲学的根本区别之一.dplyr不喜欢就地修改因此在修改现有列时通过复制进行交易.

因此,如果没有深层复制,就无法更改data.frame的特定列的某些行的值.那是:

DT[x >= 5L, y := 1L] # y is an existing column
Run Code Online (Sandbox Code Playgroud)

这不可能不使用基地data.frame的完整副本来完成Rdplyr,据我所知.


另外,在具有32GB RAM的计算机上考虑一个大小为20GB的2列数据集(每列10GB两列).其data.table理念是提供一种通过引用来更改这些10GB列的子集的方法,而不需要复制一列一列.一列的副本需要额外的10GB,并且可能会因内存不足而失败,更不用说快速与否了.这个概念(:=)类似于SQL中的UPDATE.

  • 很好的解释,我发现这种灵活性能够决定何时复制以及何时不复制非常有价值,因为我一直做非常昂贵的B类操作(最常见的是它像DT1 [DT2,newcolinDT1:= somecolfromDT2] `) (3认同)
  • @hadley假装缺失值怎么样?我假设tbl_dt()不允许更改? (3认同)
  • @hadley我同意这是一个预测问题.它可能涉及来自同一列和其他列的数据.但是它应该只更改该列中的一些值,至少如果数据不是太糟糕:) (3认同)
  • 非常感谢你如此详细的解释.我也看到了关于`DT [x> = 5L,y:= 1L]的观点. (2认同)

had*_*ley 11

要了解发生了什么,您需要了解究竟是什么被复制.数据框实际上很便宜,因为它基本上只是一个指向列的指针数组.做一个数据帧的浅表副本非常便宜,因为你只需要复制那些指针.

但是,大多数基本R函数都执行深层复制.所以当你这样做时:

df <- data.frame(x = 1:10, y = 1:10)
transform(df, z = x + y)
Run Code Online (Sandbox Code Playgroud)

R不仅复制数据帧,它实际上复制每个单独的列.dplyr提供了changes()使这更容易看到的功能.对于数据框中的每一列,它显示该列所在的内存位置.如果已更改,则已复制完整列:

df2 <- transform(df, z = x + y)
changes(df, df2)
#> Changed variables:
#>           old            new           
#> x         0x7fb19adcd378 0x7fb19ab9bcb8
#> y         0x7fb19adcd3d0 0x7fb19ab9bd10
#> z                        0x7fb19ab9bd68
#> 
#> Changed attributes:
#>           old            new           
#> names     0x7fb19adcce98 0x7fb1944e4558
#> row.names 0x7fb19ab2bd10 0x7fb19ab2bf20
#> class     0x7fb19ad5d208 0x7fb19ab51b28
Run Code Online (Sandbox Code Playgroud)

如果我们在dplyr中执行相同的操作,则不会复制原始列:

df3 <- dplyr::mutate(df, z = x + y)
changes(df, df3)
#> Changed variables:
#>           old new           
#> z             0x7fb19adcd060
#> 
#> Changed attributes:
#>           old            new           
#> names     0x7fb19adcce98 0x7fb1944e8b18
#> row.names 0x7fb19ab9c0d8 0x7fb19ab9c340
#> class     0x7fb19ad5d208 0x7fb19ad69408
Run Code Online (Sandbox Code Playgroud)

这使得dplyr比基础R快得多.

Data.table再次快一点,因为它允许您修改数据表 - 它甚至不必将指针复制到列.我认为不进行修改会使dplyr更容易理解(因为它坚持通常的R语义),代价是速度稍慢(但成本会随着列数而不是行数而增加).