使用do.call和ldply将长列表中的数据框(~100万个)转换为单个数据框时出现问题

25

我知道在stackoverflow上有很多关于如何使用do.call或ldply将数据框列表转换为单个数据框的问题,但这个问题是关于理解这两种方法的内部工作原理,并试图弄清楚为什么我无法使用它们来连接几乎1百万个具有相同结构、字段名称等的df的列表到一个单独的数据框中。每个数据框都是一行和21列。

这些数据最初是作为JSON文件开始的,我使用fromJSON将其转换为列表,然后又运行了一个lapply来提取列表的一部分并将其转换为data.frame,最终得到了一个数据框列表。

我尝试过:

df <- do.call("rbind", list)
df <- ldply(list)

但是我不得不在让它运行了3个小时后杀掉进程,仍然没有任何结果。

有没有更有效的方法?我该如何排除问题并找出为什么会花这么长时间?

顺便说一下 - 我正在使用带有RHEL的72GB四核服务器上的RStudio服务器,所以我不认为内存是问题。以下是sessionInfo:

> sessionInfo()
R version 2.14.1 (2011-12-22)
Platform: x86_64-redhat-linux-gnu (64-bit)

locale:
 [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
 [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
 [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
 [7] LC_PAPER=C                 LC_NAME=C                 
 [9] LC_ADDRESS=C               LC_TELEPHONE=C            
[11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] multicore_0.1-7 plyr_1.7.1      rjson_0.2.6    

loaded via a namespace (and not attached):
[1] tools_2.14.1
> 

2
我不能保证这会带来一个好的解决方案,但是值得研究一下在运行出问题之前你可以将多少个df进行rbind。1k?10k?100k? - joran
4
这里推荐使用data.tablerbindlist - mnel
1
@wahalulu。当你有时间查看data.table中的新函数时,能否考虑将采纳答案移至mnel的答案?但是,我不知道当一个更好的答案在很长一段时间后出现时,S.O.礼仪如何运作,特别是当新答案使用最初不可用的新功能时。 rbindlist是一个确定性解决方案,比do.call("bind",...)快得多,而这个问题和答案都关于大数据的速度。 - Matt Dowle
@MatthewDowle:我在我的回答中添加了我的新解决方案;它只使用基本的R语言。一旦我能够访问具有足够RAM的计算机,我将在一个10GB数据集上对其进行测试。 :) - Joshua Ulrich
哇,我刚刚在另一个案例中尝试了rbindlist,真是太棒了! - wahalulu
显示剩余13条评论
4个回答

18

考虑到您追求高性能,建议使用 data.table 方案。

有一个名为 rbindlist 的函数,它与 do.call(rbind, list) 相同但速度更快。

library(data.table)
X <- replicate(50000, data.table(a=rnorm(5), b=1:5), simplify=FALSE)
system.time(rbindlist.data.table <- rbindlist(X))
##  user  system elapsed 
##  0.00    0.01    0.02

对于一个 data.frame 列表来说,它也非常快速 (very)

Xdf <- replicate(50000, data.frame(a=rnorm(5), b=1:5), simplify=FALSE)

system.time(rbindlist.data.frame <- rbindlist(Xdf))
##  user  system elapsed 
##  0.03    0.00    0.03

作为比较

system.time(docall <- do.call(rbind, Xdf))
##  user  system elapsed 
## 50.72    9.89   60.88 

还需要进行一些适当的基准测试。

library(rbenchmark)
benchmark(rbindlist.data.table = rbindlist(X), 
           rbindlist.data.frame = rbindlist(Xdf),
           docall = do.call(rbind, Xdf),
           replications = 5)
##                   test replications elapsed    relative user.self sys.self 
## 3               docall            5  276.61 3073.444445    264.08     11.4 
## 2 rbindlist.data.frame            5    0.11    1.222222      0.11      0.0 
## 1 rbindlist.data.table            5    0.09    1.000000      0.09      0.0 

并且反对@JoshuaUlrich的解决方案

benchmark(use.rbl.dt  = rbl.dt(X), 
          use.rbl.ju  = rbl.ju (Xdf),
          use.rbindlist =rbindlist(X) ,
          replications = 5)

##              test replications elapsed relative user.self 
## 3  use.rbindlist            5    0.10      1.0      0.09
## 1     use.rbl.dt            5    0.10      1.0      0.09
## 2     use.rbl.ju            5    0.33      3.3      0.31 

我不确定你是否真的需要使用as.data.frame,因为data.table继承了data.frame


你是如何在60秒内运行do.call(rbind,Xdf)的?在我的3.33Ghz双核CPU上需要90秒。也许你的RAM比我的快很多? - Joshua Ulrich
也许——我的机器是一台3.00 Gh Core 2 Duo运行Windows XP的工作电脑。我已经添加了基准测试以获得更可靠的估计。 - mnel
我正在更新接受的答案,以便使用data.table包中的rbindlist。这无疑是最快和最有效的答案,但我想说之前的所有答案也非常有创意! - wahalulu
3
再见勾号。我会永远记得我们在一起的时光... :'( - Joshua Ulrich
@JoshuaUlrich 嘿,我需要查看您的更新答案...我希望每个问题可以有两个勾选标记;) - wahalulu

17

rbind.data.frame 做了很多你不需要的检查。如果你只做你想要的事情,这应该是一个非常快速的转换。

# Use data from Josh O'Brien's post.
set.seed(21)
X <- replicate(50000, data.frame(a=rnorm(5), b=1:5), simplify=FALSE)
system.time({
Names <- names(X[[1]])  # Get data.frame names from first list element.
# For each name, extract its values from each data.frame in the list.
# This provides a list with an element for each name.
Xb <- lapply(Names, function(x) unlist(lapply(X, `[[`, x)))
names(Xb) <- Names          # Give Xb the correct names.
Xb.df <- as.data.frame(Xb)  # Convert Xb to a data.frame.
})
#    user  system elapsed 
#   3.356   0.024   3.388 
system.time(X1 <- do.call(rbind, X))
#    user  system elapsed 
# 169.627   6.680 179.675
identical(X1,Xb.df)
# [1] TRUE

受到data.table答案的启发,我决定尝试使它运行得更快。这是我的更新解决方案,希望仍能保持勾选标记。 ;-)

# My "rbind list" function
rbl.ju <- function(x) {
  u <- unlist(x, recursive=FALSE)
  n <- names(u)
  un <- unique(n)
  l <- lapply(un, function(N) unlist(u[N==n], FALSE, FALSE))
  names(l) <- un
  d <- as.data.frame(l)
}
# simple wrapper to rbindlist that returns a data.frame
rbl.dt <- function(x) {
  as.data.frame(rbindlist(x))
}

library(data.table)
if(packageVersion("data.table") >= '1.8.2') {
  system.time(dt <- rbl.dt(X))  # rbindlist only exists in recent versions
}
#    user  system elapsed 
#    0.02    0.00    0.02
system.time(ju <- rbl.ju(X))
#    user  system elapsed 
#    0.05    0.00    0.05 
identical(dt,ju)
# [1] TRUE

+1 -- 这是一个很棒的想法。我的建议在速度上足够接近,我想知道如果更仔细地调整,它是否能够击败这种方法。但实际上,这是我会选择的方式。 - Josh O'Brien
1
+1 -- 如果所有列都具有相同的类,则我也会考虑使用 as.data.frame(matrix(unlist(X), nrow = length(X), byrow=TRUE)) - flodel
谢谢大家,这看起来很棒。我明天早上会实现它并告诉你们结果。 - wahalulu
太棒了!将大约1百万个dfs合并成一个df只需要约600秒。我可以接受这样的性能,而且这将成为以后合并列表中df的方法,只要它们具有相同的结构,这通常是我的情况。 - wahalulu

8

您的观察表明,随着data.frames数量的增加,所需时间呈指数增长。这意味着将 rbind 分成两个阶段可能会加速处理。

这个简单的实验似乎证实了这是一个非常有成效的方法:

## Make a list of 50,000 data.frames
X <- replicate(50000, data.frame(a=rnorm(5), b=1:5), simplify=FALSE)

## First, rbind together all 50,000 data.frames in a single step
system.time({
    X1 <- do.call(rbind, X)
})
#    user  system elapsed 
# 137.08   57.98  200.08 


## Doing it in two stages cuts the processing time by >95%
##   - In Stage 1, 100 groups of 500 data.frames are rbind'ed together
##   - In Stage 2, the resultant 100 data.frames are rbind'ed
system.time({
    X2 <- lapply(1:100, function(i) do.call(rbind, X[((i*500)-499):(i*500)]))
    X3 <- do.call(rbind, X2)
}) 
#    user  system elapsed 
#    6.14    0.05    6.21 


## Checking that the results are the same
identical(X1, X3)
# [1] TRUE

rbind.fill 给我带来了几乎与你的 X2/X3 解决方案相同的时间(差异小于 10%)。 - Eduardo Leoni

4
你有一组数据框,每个数据框只有一行。如果可能将它们转换为向量,我认为这将大大提高速度。
但是,假设它们需要是数据框,我将创建一个函数,其中的代码借鉴自Dominik在Can rbind be parallelized in R?中的答案。
do.call.rbind <- function (lst) {
  while (length(lst) > 1) {
    idxlst <- seq(from = 1, to = length(lst), by = 2)
    lst <- lapply(idxlst, function(i) {
      if (i == length(lst)) {
        return(lst[[i]])
      }
      return(rbind(lst[[i]], lst[[i + 1]]))
    })
  }
  lst[[1]]
}

我已经使用这个函数数月了,发现它比do.call(rbind, ...)更快且占用的内存更少[免责声明是我几乎只在xts对象上使用它]

每个数据框具有的行数越多,列表具有的元素越多,此函数的好处就越多。

如果你有一个包含100,000个数字向量的列表,do.call(rbind, ...)会更好。 如果你有10亿长度的列表,那么这个函数会更好。

> df <- lapply(1:10000, function(x) data.frame(x = sample(21, 21)))
> library(rbenchmark)
> benchmark(a=do.call(rbind, df), b=do.call.rbind(df))
test replications elapsed relative user.self sys.self user.child sys.child
1    a          100 327.728 1.755965   248.620   79.099          0         0
2    b          100 186.637 1.000000   181.874    4.751          0         0

如果你增加列表的长度,相对加速将指数级地提高。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接