mutate是否通过引用更改tbl?

时间:2014-02-11 12:12:46

标签: r data.table dplyr

我真正喜欢的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" )

enter image description here

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

2 个答案:

答案 0 :(得分:17)

好问题。一般来说,我会对数据大小进行基准测试,这个数据大小足以完全适合(几乎)完全在缓存中。在“初始设置”下查看here。将针对(内存中)大数据开发的工具与运行在毫秒内运行的任务进行比较确实没有意义。我们计划在未来对相对较大的数据进行基准测试。

此外,如果您的目的是了解mutate是否正在执行副本,那么您要做的就是检查address之前和之后(此可以使用.Internal(inspect(.))基础中的Rchanges()中的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

它告诉您xy的地址没有变化,并指出我们刚刚添加的z。这里发生了什么?

dplyr 浅层副本 data.frame,然后添加了新列。 浅拷贝而不是深拷贝只是复制列指针的向量,而不是数据本身。因此它应该很快。基本上df2创建了3列,其中前两列指向df 相同的地址位置,并且刚创建了第3列。

另一方面,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

你可以在这里看到“平均时间”的差异(0.00061对比0.04178)..

B)修改现有列:

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

它告诉您y已更改 - 已经制作了y 的副本。它必须创建一个新的内存位置来更改y的值,因为它之前指的是与df的{​​{1}}相同的位置。

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

  

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

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

dplyr

据我所知,如果没有使用基础DT[x >= 5L, y := 1L] # y is an existing column R的data.frame的完整副本,则无法完成此操作。


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

答案 1 :(得分:10)

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

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

df <- data.frame(x = 1:10, y = 1:10)
transform(df, z = x + y)

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

如果我们在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

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

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