以节省内存的方式增长data.frame

Rea*_*onk 45 memory r dataframe

根据逐行创建R数据帧,附加到data.frame使用是不理想的rbind,因为它每次都会创建整个data.frame的副本.如何累积数据R导致data.frame不会产生这种惩罚?中间格式不需要是a data.frame.

Ari*_*man 43

第一种方法

我尝试访问预先分配的data.frame的每个元素:

res <- data.frame(x=rep(NA,1000), y=rep(NA,1000))
tracemem(res)
for(i in 1:1000) {
  res[i,"x"] <- runif(1)
  res[i,"y"] <- rnorm(1)
}
Run Code Online (Sandbox Code Playgroud)

但tracemem变得疯狂(例如每次都将data.frame复制到一个新地址).

替代方法(也不起作用)

一种方法(不确定它更快,因为我尚未进行基准测试)是创建一个data.frames列表,然后将stack它们全部组合在一起:

makeRow <- function() data.frame(x=runif(1),y=rnorm(1))
res <- replicate(1000, makeRow(), simplify=FALSE ) # returns a list of data.frames
library(taRifx)
res.df <- stack(res)
Run Code Online (Sandbox Code Playgroud)

不幸的是,在创建列表时,我认为你很难预先分配.例如:

> tracemem(res)
[1] "<0x79b98b0>"
> res[[2]] <- data.frame()
tracemem[0x79b98b0 -> 0x71da500]: 
Run Code Online (Sandbox Code Playgroud)

换句话说,替换列表的元素会导致列表被复制.我假设整个列表,但它可能只是列表中的那个元素.我对R的内存管理细节并不十分熟悉.

可能是最好的方法

与目前许多速度或内存限制的流程一样,最好的方法可能是使用data.table而不是使用data.frame.由于data.table具有:=通过引用操作符分配,它可以更新而无需重新复制:

library(data.table)
dt <- data.table(x=rep(0,1000), y=rep(0,1000))
tracemem(dt)
for(i in 1:1000) {
  dt[i,x := runif(1)]
  dt[i,y := rnorm(1)]
}
# note no message from tracemem
Run Code Online (Sandbox Code Playgroud)

但是正如@MatthewDowle指出的那样,set()是在循环中执行此操作的适当方法.这样做会让它更快:

library(data.table)
n <- 10^6
dt <- data.table(x=rep(0,n), y=rep(0,n))

dt.colon <- function(dt) {
  for(i in 1:n) {
    dt[i,x := runif(1)]
    dt[i,y := rnorm(1)]
  }
}

dt.set <- function(dt) {
  for(i in 1:n) {
    set(dt,i,1L, runif(1) )
    set(dt,i,2L, rnorm(1) )
  }
}

library(microbenchmark)
m <- microbenchmark(dt.colon(dt), dt.set(dt),times=2)
Run Code Online (Sandbox Code Playgroud)

(结果如下所示)

标杆

循环运行10,000次,数据表几乎快了整整一个数量级:

Unit: seconds
          expr        min         lq     median         uq        max
1    test.df()  523.49057  523.49057  524.52408  525.55759  525.55759
2    test.dt()   62.06398   62.06398   62.98622   63.90845   63.90845
3 test.stack() 1196.30135 1196.30135 1258.79879 1321.29622 1321.29622
Run Code Online (Sandbox Code Playgroud)

基准

:=与之比较set():

> m
Unit: milliseconds
          expr       min        lq    median       uq      max
1 dt.colon(dt) 654.54996 654.54996 656.43429 658.3186 658.3186
2   dt.set(dt)  13.29612  13.29612  15.02891  16.7617  16.7617
Run Code Online (Sandbox Code Playgroud)

注意,n这里是10 ^ 6而不是10 ^ 5,如上面绘制的基准.因此,工作量会增加一个数量级,结果以毫秒而非秒为单位进行测量.确实令人印象深刻

  • 据我所知,你的最后一个例子没有增长data.table.您只需将第一行覆盖1,000次. (3认同)
  • 这很好但是你看到了`?':="````````````````````````````````````````````````````````````` `:=`有开销(例如检查传递给`[.data.table`)的参数的存在和类型,这就是提供在循环中使用`set()`的原因. (3认同)

Mar*_*box 8

您还可以拥有一个空列表对象,其中元素用数据帧填充; 然后用sapply或类似的方法收集结果.这里可以找到一个例子.这不会招致成长对象的惩罚.


Jea*_*lie 7

好吧,我很惊讶没人提到转换到矩阵了......

Ari B. Friedman定义的dt.colondt.set函数相比,转换为矩阵的运行时间最短(比dt.colon略快).矩阵内的所有影响都是通过引用完成的,因此在此代码中不会执行不必​​要的内存复制.

码:

library(data.table)
n <- 10^4
dt <- data.table(x=rep(0,n), y=rep(0,n))

use.matrix <- function(dt) {
  mat = as.matrix(dt)  # converting to matrix
  for(i in 1:n) {
    mat[i,1] = runif(1)
    mat[i,2] = rnorm(1)
  }
  return(as.data.frame(mat))  # converting back to a data.frame
}


dt.colon <- function(dt) { # same as Ari's function
  for(i in 1:n) {
    dt[i,x := runif(1)]
    dt[i,y := rnorm(1)]
  }
}

dt.set <- function(dt) { # same as Ari's function
  for(i in 1:n) {
    set(dt,i,1L, runif(1) )
    set(dt,i,2L, rnorm(1) )
  }
}

library(microbenchmark)
microbenchmark(dt.colon(dt), dt.set(dt), use.matrix(dt),times=10)
Run Code Online (Sandbox Code Playgroud)

结果:

Unit: milliseconds
           expr        min         lq     median         uq        max neval
   dt.colon(dt) 7107.68494 7193.54792 7262.76720 7277.24841 7472.41726    10
     dt.set(dt)   93.25954   94.10291   95.07181   97.09725   99.18583    10
 use.matrix(dt)   48.15595   51.71100   52.39375   54.59252   55.04192    10
Run Code Online (Sandbox Code Playgroud)

使用矩阵的优点:

  • 这是目前为止最快的方法
  • 您不必学习/使用data.table对象

使用矩阵的Con:

  • 你只能在矩阵中处理​​一种数据类型(特别是,如果你在data.frame的列中有混合类型,那么它们将全部按行转换为字符:mat = as.matrix(dt)#convertation到矩阵)


Kar*_* W. 6

我喜欢RSQLite这件事:dbWriteTable(...,append=TRUE)收集时的dbReadTable陈述和最后的陈述.

如果数据足够小,可以使用":memory:"文件,如果它很大,则使用硬盘.

当然,它无法在速度方面进行竞争:

makeRow <- function() data.frame(x=runif(1),y=rnorm(1))

library(RSQLite)
con <- dbConnect(RSQLite::SQLite(), ":memory:")

collect1 <- function(n) {
  for (i in 1:n) dbWriteTable(con, "test", makeRow(), append=TRUE)
  dbReadTable(con, "test", row.names=NULL)
}

collect2 <- function(n) {
  res <- data.frame(x=rep(NA, n), y=rep(NA, n))
  for(i in 1:n) res[i,] <- makeRow()[1,]
  res
}

> system.time(collect1(1000))
   User      System verstrichen 
   7.01        0.00        7.05  
> system.time(collect2(1000))
   User      System verstrichen 
   0.80        0.01        0.81 
Run Code Online (Sandbox Code Playgroud)

但如果data.frames有多行,它看起来可能会更好.而且您不需要事先知道行数.