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

时间:2012-07-14 18:39:03

标签: r memory dataframe

根据Creating an R dataframe row-by-row,使用data.frame附加到rbind并不理想,因为它每次都会创建整个data.frame的副本。如何在R中累积数据,从而产生data.frame而不会产生此处罚?中间格式不需要是data.frame

5 个答案:

答案 0 :(得分:40)

第一种方法

我尝试访问预先分配的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)
}

但是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)

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

> tracemem(res)
[1] "<0x79b98b0>"
> res[[2]] <- data.frame()
tracemem[0x79b98b0 -> 0x71da500]: 

换句话说,替换列表中的元素会导致列表被复制。我假设整个列表,但它可能只是列表中的那个元素。我对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

但正如@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)

(结果如下所示)

<强>基准

循环运行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

benchmarks

:=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

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

答案 1 :(得分:7)

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

答案 2 :(得分:6)

嗯,我很惊讶没有人提到转换为矩阵......

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

CODE:

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)

结果:

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

使用矩阵的优点:

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

使用矩阵的结论:

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

答案 3 :(得分:5)

我喜欢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 

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

答案 4 :(得分:1)

This post建议使用data.frame剥离tibble / as.list的类属性,按常规方式分配列表元素,然后将结果转换回{再次{1}} / data.frame。该方法的计算复杂度呈线性增长,但增长率不到10e-6。

tibble

这是原始文章的图片: enter image description here