19

What I really like about data.table is the := idiom for changing the table by reference, without the need for costly copies. From what I understand, this is one of the aspects which makes data.table so ultrafast compared to other methods.

Now, I started playing around with the dplyr package which seems to be equally performant. But since results still have to be assigned using the <- operator, I was expecting a performance drain at this level. However, there seems to be none.

As an example:

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

I am just wondering how this is possible? Or is there a conceptual mistake in the way I benchmark? Is my understanding of <- wrong?

4

2 回答 2

22

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

此外,如果您的意图是确定是否mutate正在执行复制,那么您所要做的就是检查address之前和之后(这可以使用.Internal(inspect(.))in baseR或使用 in 函数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

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

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

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

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

这是两个软件包在理念上的根本区别之一。dplyr不喜欢就地修改,因此在修改现有列时通过复制进行权衡。

正因为如此,如果没有深拷贝,就不可能更改 data.frame 特定列的某些行的值。那是:

DT[x >= 5L, y := 1L] # y is an existing column

据我所知,如果没有使用 baseR和的 data.frame 的完整副本,这将无法完成。dplyr


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

于 2014-02-11T14:31:07.683 回答
11

要了解发生了什么,您需要了解正在复制的确切内容。复制数据帧实际上非常便宜,因为它基本上只是一个指向列的指针数组。对数据帧进行浅拷贝非常便宜,因为您只需要复制这些指针。

但是,大多数基本 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 语义),但代价是稍微慢了一点(但成本随着列数而不是行数而增加)。

于 2014-02-11T14:31:47.627 回答