【问题标题】:Does mutate change tbl by reference?mutate 是否通过引用改变 tbl?
【发布时间】:2014-03-09 05:17:27
【问题描述】:

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

我只是想知道这怎么可能?或者我的基准测试方式是否存在概念错误?难道我对&lt;-的理解错了吗?

【问题讨论】:

  • 并不是说这里一定是这种情况,但是使用.Call C API 可以潜在地改变任何对象。 API 不强制开发人员返回一个新对象,并且传入的对象的所有数据都可以通过SEXP 结构的 C 指针提供给开发人员。我自己已经完成了对图像数据的快速就地操作(不,这是不可取的!)。
  • 不管它值多少钱,对我来说,dplyr 在你的基准测试的所有迭代中都比data.table 慢 50%(在微基准测试上是 100 倍,尽管在早期的运行中我得到了更差的结果 @ 987654333@ 案例)。这是在 Windows 64 位机器上。
  • @BrodieG 很有趣。为完整起见,此基准测试是在 R 3.0.2、Mac (x86_64-apple-darwin10.8.0 (64-bit))、dplyrversion 0.1.1 (2014-02-09) 和 data.table 上进行的1.8.10.
  • 我也在 3.0.2 / 0.1.1 / 1.8.10。数据表计时:10.52806 10.91406 11.51819 11.91552 14.73834,Dplyr:15.69537 16.29676 16.71768 17.43426 24.86194(min、lq、med、uq、max、毫秒)。
  • 数据集需要更大,复制需要更小。否则它是在比较开销,而不是任务本身。

标签: r data.table dplyr


【解决方案1】:

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

此外,如果您的意图是确定mutate 是否正在执行复制,那么您所要做的就是检查之前和之后的address(这可以使用.Internal(inspect(.)) in base R 或使用函数 changes() in 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 相同的地址位置,而第三列刚刚创建。

另一方面,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 的值,因为它指向的位置与之前dfy 的位置相同。

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

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

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

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

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


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

【讨论】:

  • 很好的解释,而且我发现这种能够决定何时复制和何时不复制的灵活性非常有价值,因为我一直在执行非常昂贵的 B 类操作(最常见的情况是 DT1 [DT2, newcolinDT1 := somecolfromDT2]`)
  • 非常感谢您提供如此详细的解释。我也看到你关于DT[x &gt;= 5L, y := 1L] 的观点。
  • @matt-dowle 没有必要进行编辑(最后一段),我已经从上面所说的内容中得到了它。无论如何,我想知道为什么我在过去几天读到的所有讨论都涵盖了data.tabledplyr,恕我直言,情绪化了。我不是在问哪个包更好。
  • @hadley 缺失值的插补怎么样?我假设 tbl_dt() 也不允许就地更改?
  • @hadley 我同意这是一个预测问题。它可能涉及来自同一列和其他列的数据。但是,它应该只更改该列中的几个值,至少在数据不太糟糕的情况下:)
【解决方案2】:

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

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

【讨论】:

  • 非常感谢。但也许还有一个问题:为什么像dt &lt;- tbl_df( data.frame( a = 1:10, b = LETTERS[1:10] ) ); dt2 &lt;- dt; dt2[1,1] &lt;- 0; changes(dt2, dt) 这样的东西会复制dt 的所有列,尽管只有一列被操纵?
  • @Basterfield 那是因为您使用的是基本 R [ 方法。我们最终可能会在 C++ 中实现它,但它目前不是高优先级。
猜你喜欢
  • 2021-10-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-02-14
  • 2020-01-27
  • 2014-07-20
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多