【问题标题】:When should I use setDT() instead of data.table() to create a data.table?什么时候应该使用 setDT() 而不是 data.table() 来创建 data.table?
【发布时间】:2017-06-14 13:07:12
【问题描述】:

我很难掌握setDT() 函数的本质。在阅读 SO 上的代码时,我经常遇到使用 setDT() 创建 data.table 的情况。当然data.table() 的使用无处不在。我觉得我完全理解data.table() 的本质,但setDT() 的相关性却让我无法理解。 ?setDT 告诉我这个:

setDT 将列表(命名和未命名)和 data.frames通过引用转换为 data.tables。

还有:

data.table 的说法中,所有set* 函数都通过引用更改其输入。也就是说,除了一列大小的临时工作内存之外,根本不进行任何复制。

所以这让我觉得我应该只使用setDT() 来制作一个data.table,对吗? setDT() 只是一个到 data.table 转换器的列表吗?

library(data.table)

a <- letters[c(19,20,1,3,11,15,22,5,18,6,12,15,23)]
b <- seq(1,41,pi)
ab <- data.frame(a,b)
d <- data.table(ab)
e <- setDT(ab)

str(d)
#Classes ‘data.table’ and 'data.frame': 13 obs. of  2 variables:
# $ a: Factor w/ 12 levels "a","c","e","f",..: 9 10 1 2 5 7 11 3 8 4 ...
# $ b: num  1 4.14 7.28 10.42 13.57 ...
# - attr(*, ".internal.selfref")=<externalptr>

str(e)
#Classes ‘data.table’ and 'data.frame': 13 obs. of  2 variables:
# $ a: Factor w/ 12 levels "a","c","e","f",..: 9 10 1 2 5 7 11 3 8 4 ...
# $ b: num  1 4.14 7.28 10.42 13.57 ...
# - attr(*, ".internal.selfref")=<externalptr>

在这种情况下似乎没有区别。在另一个例子中,差异很明显:

ba <- list(a,b)
f <- data.table(ba)
g <- setDT(ba)

str(f)
#Classes ‘data.table’ and 'data.frame': 2 obs. of  1 variable:
# $ ba:List of 2
#  ..$ : chr  "s" "t" "a" "c" ...
#  ..$ : num  1 4.14 7.28 10.42 13.57 ...
# - attr(*, ".internal.selfref")=<externalptr>

str(g)
#Classes ‘data.table’ and 'data.frame': 13 obs. of  2 variables:
# $ V1: chr  "s" "t" "a" "c" ...
# $ V2: num  1 4.14 7.28 10.42 13.57 ...
# - attr(*, ".internal.selfref")=<externalptr>

什么时候应该使用setDT()?是什么让setDT() 相关?为什么不让原来的 data.table() 函数能够做 setDT() 能够做的事情?

【问题讨论】:

  • 如果数据已经存在于 data.frame 中,并且您很乐意将该 data.frame(或列表)转换为 data.table,然后使用 setDT()。如果你想从向量或其他东西构造一个 data.table,请使用 data.table()。您永远不应该这样做x &lt;- setDT(y) - 我认为您误解了通过引用修改对象的含义。可能想要浏览一下这些小插曲:r-datatable.com/Getting-Started
  • 并且,看看在将新列分配给deab data.table 会发生什么:d[, newCol := 1]; ab; e[, newCol := 1]; ab
  • @Frank 我会跳回到文献中。您可能知道,对于某些人来说,需要多次迭代才能点击——比如我自己。感谢您为我指明正确的方向。
  • @SymbolixAU 哇!这太疯狂了……他们已经建立了联系。 ab 通过使用 setDT()e 的更改进行更新。诡异的!现在我可以在接下来的几天里尝试解决这个问题。
  • 这就是为什么与获取对象的副本(即data.table())相比,了解“按引用更新”(即setDT())的作用很重要。

标签: r data.table


【解决方案1】:

更新:

@Roland 在 cmets 部分提出了一些好处,帖子对他们来说更好。虽然我最初关注的是内存溢出问题,但他指出,即使没有发生这种情况,各种副本的内存管理也需要大量时间,这是一个更常见的日常问题。现在也添加了这两个问题的示例。

我喜欢 stackoverflow 上的这个问题,因为我认为它实际上是在处理更大的数据集时避免 R 中的堆栈溢出。 ? 不熟悉data.table 家族set 操作的小伙伴们可以受益于本次讨论!

在处理占用大量 RAM 的较大数据集时,应使用 setDT(),因为该操作将修改每个对象,从而节省内存。对于占 RAM 很小比例的数据,使用 data.table 的复制和修改就可以了。

setDT 函数的创建实际上是受到以下堆栈溢出线程的启发,该线程是关于处理大型数据集(几 GB)。您将看到 Matt Dowle 提示“setDT”名称。

Convert a data frame to a data.table without copy

更深入一点:

使用 R,数据存储在内存中。这大大加快了速度,因为 RAM 的访问速度比存储设备快得多。但是,当一个人的数据集是 RAM 的很大一部分时,就会出现问题。为什么?因为当对每个 data.frame 应用某些操作时,base R 倾向于制作每个 data.frame 的副本。这在 3.1 版之后有所改进,但解决这个问题超出了本文的范围。如果将多个data.frames 或lists 拉到一个data.framedata.table 中,则您的内存使用量会迅速增加,因为在操作过程中的某个时刻,您的数据的多个副本存在于RAM 中。如果数据集足够大,当所有副本都生成时,您可能会耗尽内存,并且您的堆栈会溢出。请参阅下面的示例。我们得到一个错误,原始内存地址和对象类没有改变。

> N <- 1e8
> P <- 1e2
> data <- as.data.frame(rep(data.frame(rnorm(N)), P))
> 
> pryr::object_size(data)
800 MB
> 
> tracemem(data)
[1] "<0000000006D2DF18>"
> 
> data <- data.table(data)
Error: cannot allocate vector of size 762.9 Mb
> 
> tracemem(data)
[1] "<0000000006D2DF18>"
> class(data)
[1] "data.frame"
>

无需复制就可以修改对象的能力很重要。这就是setDT 在接受listdata.frame 并返回data.table 时所做的事情。与上面使用setDT 的示例相同,现在可以正常工作且没有错误。类和内存地址都发生了变化,并且没有副本发生。

> tracemem(data)
[1] "<0000000006D2DF18>"
> class(data)
[1] "data.frame"
> 
> setDT(data)
>  
> tracemem(data)
[1] "<0000000006A8C758>"
> class(data)
[1] "data.table" "data.frame"

@Roland 指出,对于大多数人来说,更大的关注点是速度,这是由于大量使用内存管理而产生的副作用。这是一个不会使 cpu 崩溃的较小数据的示例,并说明了 setDT 对这项工作的速度有多快。注意data &lt;- data.table(data) 之后的“tracemem”的结果,复制了data。与不打印单个副本的setDT(data) 形成对比。然后我们必须调用tracemem(data) 来查看新的内存地址。

> N <- 1e5
> P <- 1e2
> data <- as.data.frame(rep(data.frame(rnorm(N)), P))
> pryr::object_size(data)
808 kB

> # data.table method
> tracemem(data)
[1] "<0000000019098438>"
> data <- data.table(data)
tracemem[0x0000000019098438 -> 0x0000000007aad7d8]: data.table 
tracemem[0x0000000007aad7d8 -> 0x0000000007c518b8]: copy as.data.table.data.frame as.data.table data.table 
tracemem[0x0000000007aad7d8 -> 0x0000000018e454c8]: as.list.data.frame as.list vapply copy as.data.table.data.frame as.data.table data.table 
> class(data)
[1] "data.table" "data.frame"
> 
> # setDT method
> # back to data.frame
> data <- as.data.frame(data)
> class(data)
[1] "data.frame"
> tracemem(data)
[1] "<00000000125BE1A0>"
> setDT(data)
> tracemem(data)
[1] "<00000000125C2840>"
> class(data)
[1] "data.table" "data.frame"
> 

这对时间有什么影响?正如我们所见,setDT 的速度要快得多。

> # timing example
> data <- as.data.frame(rep(data.frame(rnorm(N)), P))
> microbenchmark(setDT(data), data <- data.table(data))
Unit: microseconds
                     expr       min         lq        mean    median            max neval        uq
              setDT(data)    49.948    55.7635    69.66017    73.553        100.238   100    79.198
 data <- data.table(data) 54594.289 61238.8830 81545.64432 64179.131     611632.427   100 68647.917

Set 函数可用于许多领域,而不仅仅是将对象转换为 data.tables 时。您可以通过调用该主题的小插图来找到有关参考语义以及如何在其他地方应用它们的更多信息。

library(data.table)    
vignette("datatable-reference-semantics")

这是一个很好的问题,那些考虑将 R 用于更大数据集或只想加快数据操作活动的人,可以从熟悉 data.table 引用语义的显着性能改进中受益。

【讨论】:

  • 你过分关注记忆。通过引用与通过副本进行的更改也具有巨大的速度影响,这对大多数人来说更相关。一个好的答案应该包含一些基准。此外,您不应过分依赖古代帖子中有关这些问题的信息。 Base R 的内存管理自 2009 年以来不断发展。
  • @Roland 感谢 cmets,在这些问题上我总是尽量采取谦虚的态度。但是,在深入研究后,我必须说我的回答与 data.table 的作者在他们的文档中所写的一致。具体来说,他们创建了setDT 将data.frames 转换为data.tables 无需复制。请参阅?setDT。事实上,它们甚至指向我上面列出的同一个堆栈溢出帖子。谢谢
  • 我认为你没有理解我的意思。副本不仅需要额外的内存,而且内存管理需要大量时间。这就是为什么避免复制不仅可以节省内存,还可以节省时间。
  • 好的,是的,我现在明白你的意思了!我更新了上面的帖子,我认为这对您的 cmets 更好。感谢您在这方面的坚持。
【解决方案2】:

setDT() 不能替代data.table()。它是as.data.table() 的更有效替代品,可用于某些类型的对象。

  • mydata &lt;- as.data.table(mydata) 将复制mydata 后面的对象,将副本转换为data.table,然后将mydata 符号更改为指向副本。
  • setDT(mydata) 会将mydata 后面的对象更改为data.table。没有复制。

那么使用setDT() 的现实情况是什么?当您无法控制原始数据的类别时。例如,大多数用于处理数据库的包都提供data.frame 输出。在这种情况下,您的代码将类似于

mydata <- dbGetQuery(conn, "SELECT * FROM mytable")  # Returns a data.frame
setDT(mydata)                                        # Make it a data.table

什么时候应该使用as.data.table(x)?只要x 不是listdata.frame。最常见的用途是矩阵。

【讨论】:

  • 小问题:在您的第一个要点中,当您的意思是 mydata symbol 时,您指的是一个变量。
  • 最后一个问题不会按原样回答。最好单独举个例子。我的 2 美分
猜你喜欢
  • 2019-06-15
  • 2011-06-22
  • 2015-12-11
  • 2012-02-12
  • 2012-06-27
  • 2010-11-29
  • 2015-12-23
  • 1970-01-01
相关资源
最近更新 更多