最高效的列表转数据框方法是什么?

37

刚刚和同事们讨论了这个问题,我们想知道SO上的人有什么看法。假设我有一个包含N个元素的列表,其中每个元素都是长度为X的向量。现在假设我想将其转换为数据框架。与R中的大多数事物一样,有多种方法可以解决它,例如使用as.dataframe、使用plyr软件包、结合do.callcbind、预先分配DF并填充它等。

提出的问题是当N或X(在我们的例子中是X)变得非常大时会发生什么。如果效率(特别是内存方面)很重要,是否有一种明显优越的方法来解决这个问题呢?

2个回答

29

由于data.frame已经是一个列表,而且您知道每个列表元素的长度都相同(X),最快的方法可能就是更新classrow.names属性:

set.seed(21)
n <- 1e6
x <- list(x=rnorm(n), y=rnorm(n), z=rnorm(n))
x <- c(x,x,x,x,x,x)

system.time(a <- as.data.frame(x))
system.time(b <- do.call(data.frame,x))
system.time({
  d <- x  # Skip 'c' so Joris doesn't down-vote me! ;-)
  class(d) <- "data.frame"
  rownames(d) <- 1:n
  names(d) <- make.unique(names(d))
})

identical(a, b)  # TRUE
identical(b, d)  # TRUE

更新 - 这比创建 d 快了约2倍:

system.time({
  e <- x
  attr(e, "row.names") <- c(NA_integer_,n)
  attr(e, "class") <- "data.frame"
  attr(e, "names") <- make.names(names(e), unique=TRUE)
})

identical(d, e)  # TRUE

更新2 - 我忘记了内存消耗。最后一次更新会复制两个e。使用attributes函数可以将其减少到一个副本。

set.seed(21)
f <- list(x=rnorm(n), y=rnorm(n), z=rnorm(n))
f <- c(f,f,f,f,f,f)
tracemem(f)
system.time({  # makes 2 copies
  attr(f, "row.names") <- c(NA_integer_,n)
  attr(f, "class") <- "data.frame"
  attr(f, "names") <- make.names(names(f), unique=TRUE)
})

set.seed(21)
g <- list(x=rnorm(n), y=rnorm(n), z=rnorm(n))
g <- c(g,g,g,g,g,g)
tracemem(g)
system.time({  # only makes 1 copy
  attributes(g) <- list(row.names=c(NA_integer_,n),
    class="data.frame", names=make.names(names(g), unique=TRUE))
})

identical(f,g)  # TRUE

2
不要在答案中使用“可能”,这样就正确了。如果你使用这些调用并用长度命令替换知道n的作弊方法来创建一个函数,那么这也是正确的。你的新函数在移除所有的详尽检查后,大致等同于data.frame()。所以,如果你确定你正在传递正确的输入给这个调用,那就按照Josh推荐的方法来提高速度。如果你不确定,那么使用data.frame更安全,而do.call(data.frame, x))是下一个最快的选择(奇怪的是)。 - John
3
请查看 plyr::quickdf 函数以获取与此完全相同的功能。 - hadley
1
好的,不完全准确,但非常接近(唯一列名不是有效数据框的先决条件)。我不确定基于attributes<-未记录行为的内存黑客是否是一个好主意。 - hadley
2
演示了tracemem的好例子,很好地说明了列表和数据框之间的区别。 - Richie Cotton
3
@hadley:按照谁的标准来定义规范?我在手册中找不到任何讨论,而在核心R源代码中,attr<-structure似乎被同样频繁地使用...而且structure使用了attributes<- - Joshua Ulrich
显示剩余5条评论

10

这似乎需要使用 data.table 建议,因为需要处理大型数据集。特别地,setattr 通过引用进行设置而不是复制。

library(data.table)
set.seed(21)
n <- 1e6
h <- list(x=rnorm(n), y=rnorm(n), z=rnorm(n))
h <- c(h,h,h,h,h,h)
tracemem(h)

system.time({h <- as.data.table(h)
            setattr(h, 'names', make.names(names(h), unique=T))})

as.data.table会进行复制操作。


编辑 - 无复制版本

使用@MatthewDowle的建议setattr(h,'class','data.frame'),这将通过引用转换为data.frame(无复制)。

set.seed(21)
n <- 1e6
i <- list(x=rnorm(n), y=rnorm(n), z=rnorm(n))
i <- c(i,i,i,i,i,i)
tracemem(i)

system.time({  
  setattr(i, 'class', 'data.frame')
  setattr(i, "row.names", c(NA_integer_,n))

  setattr(i, "names", make.names(names(i), unique=TRUE))

})

1
setattr(h,"class","data.frame") 应该是即时的,完全不需要复制。 - Matt Dowle
@MatthewDowle -- 就像这样 setattr(h, "class", "data.table") ;) (非常酷,顺便说一下)。 - Josh O'Brien
@JoshO'Brien 确实 :) 最近几天才意识到 ?setattrx 必须是 data.table (感谢在datatable-help上的评论)。 setattr 实际上用于任何东西。将修复文档。它也会返回输入,因此如果需要,您可以在之后组合 [i,j,by] (例如,如果您将其封装到别名中:setDT(DF)[i,j,by])。 - Matt Dowle
@MatthewDowle -- 是的,我尝试了你的代码,并很高兴地看到它在不复制任何内容的情况下完成了转换为data.frame。很棒的编程技巧! - Josh O'Brien
@JoshO'Brien setattr 实际上只是 R 的 C 级别 setAttrib API 函数的一行包装器。顺便说一下,bit 包也有相同的函数。它还有 vecseq(我刚刚看到)看起来非常方便。值得回顾一下 bit 包,看看它还有哪些宝藏(自己做个笔记)。 - Matt Dowle

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