如何将行追加到R数据框


121

我环顾了StackOverflow,但是找不到针对我的问题的解决方案,该解决方案涉及将行附加到R数据帧。

我正在初始化一个空的2列数据帧,如下所示。

df = data.frame(x = numeric(), y = character())

然后,我的目标是遍历值列表,并在每次迭代中将一个值附加到列表末尾。我从以下代码开始。

for (i in 1:10) {
    df$x = rbind(df$x, i)
    df$y = rbind(df$y, toString(i))
}

我也试图功能cappend以及merge没有成功。如果您有任何建议,请告诉我。


2
我不知道如何使用R,但是我想忽略在每次迭代中更新索引所需要的额外代码行,因为我不愿意预先分配数据帧的大小不知道最终将需要多少行。请记住,以上内容仅是一个玩具示例,旨在再现。无论哪种方式,谢谢您的建议!
Gyan Veda

Answers:


115

更新资料

不知道您要做什么,我将再分享一个建议:为每列预分配所需类型的向量,在这些向量中插入值,然后最后创建您的 data.frame

继续使用朱利安f3(预先分配的data.frame)作为迄今为止最快的选项,定义为:

# pre-allocate space
f3 <- function(n){
  df <- data.frame(x = numeric(n), y = character(n), stringsAsFactors = FALSE)
  for(i in 1:n){
    df$x[i] <- i
    df$y[i] <- toString(i)
  }
  df
}

这是一种类似的方法,但是data.frame在最后一步中创建。

# Use preallocated vectors
f4 <- function(n) {
  x <- numeric(n)
  y <- character(n)
  for (i in 1:n) {
    x[i] <- i
    y[i] <- i
  }
  data.frame(x, y, stringsAsFactors=FALSE)
}

microbenchmark来自“ microbenchmark”程序包的内容将为我们提供比system.time以下内容更全面的见解:

library(microbenchmark)
microbenchmark(f1(1000), f3(1000), f4(1000), times = 5)
# Unit: milliseconds
#      expr         min          lq      median         uq         max neval
#  f1(1000) 1024.539618 1029.693877 1045.972666 1055.25931 1112.769176     5
#  f3(1000)  149.417636  150.529011  150.827393  151.02230  160.637845     5
#  f4(1000)    7.872647    7.892395    7.901151    7.95077    8.049581     5

f1()(下面的方法)由于调用频率高而效率极低data.frame,因为这种方法通常在R.中生长较慢,f3()这由于预分配而得到了很大的改善,但是data.frame结构本身可能是瓶颈所在。f4()尝试绕过该瓶颈而又不影响您要采用的方法。


原始答案

这确实不是一个好主意,但是如果您想这样做,我想您可以尝试:

for (i in 1:10) {
  df <- rbind(df, data.frame(x = i, y = toString(i)))
}

请注意,在您的代码中,还有另一个问题:

  • stringsAsFactors如果不希望将字符转换为因数,则应使用。用:df = data.frame(x = numeric(), y = character(), stringsAsFactors = FALSE)

6
谢谢!那解决了我的问题。为什么这“真的不是一个好主意”?在for循环中x和y以何种方式混合?
Gyan Veda

5
@ user2932774,在R中以这种方式增长对象的效率非常低。一种改进(但仍未必是最好的方法)将是预分配data.frame您期望的最终大小,并在值中添加[提取/替换。
A5C1D2H2I1M1N2O1R2T1

1
谢谢,阿南达 我通常会进行预分配,但是我不同意这确实不是一个好主意。这取决于实际情况。就我而言,我正在处理小数据,而替代方法将花费更多的时间进行编码。另外,与在每次迭代中更新数字索引以填充预分配数据帧的适当部分所需的代码相比,这是更优雅的代码。只是好奇,您认为完成此任务的“最佳方法”是什么?我本以为预分配是最好的。
Gyan Veda

2
@ user2932774,很酷。我也很欣赏您的观点-我几乎也从未真正使用大型数据集。就是说,如果我要写一个函数之类的东西,我通常会花一些额外的精力来尝试调整代码以尽可能地提高速度。请参阅我的更新以获取一个巨大的速度差异示例。
2013年

1
哇,那是一个巨大的差异!感谢您运行该模拟并教给我有关微基准测试包的信息。我绝对同意您的意见,为此付出额外的努力是很高兴的。在我的特定情况下,我想我只是想对一些我可能永远不必再次运行的代码进行快速和肮脏的处理。:)
Gyan Veda 2013年

34

让我们对提出的三种解决方案进行基准测试:

# use rbind
f1 <- function(n){
  df <- data.frame(x = numeric(), y = character())
  for(i in 1:n){
    df <- rbind(df, data.frame(x = i, y = toString(i)))
  }
  df
}
# use list
f2 <- function(n){
  df <- data.frame(x = numeric(), y = character(), stringsAsFactors = FALSE)
  for(i in 1:n){
    df[i,] <- list(i, toString(i))
  }
  df
}
# pre-allocate space
f3 <- function(n){
  df <- data.frame(x = numeric(1000), y = character(1000), stringsAsFactors = FALSE)
  for(i in 1:n){
    df$x[i] <- i
    df$y[i] <- toString(i)
  }
  df
}
system.time(f1(1000))
#   user  system elapsed 
#   1.33    0.00    1.32 
system.time(f2(1000))
#   user  system elapsed 
#   0.19    0.00    0.19 
system.time(f3(1000))
#   user  system elapsed 
#   0.14    0.00    0.14

最好的解决方案是预分配空间(如R所预期)。次佳的解决方案是使用list,而最差的解决方案(至少基于这些计时结果)似乎是rbind


谢谢!尽管我不同意阿南达的建议。是否要将字符转换为因子水平取决于我要对输出执行的操作。尽管我猜想您提出的解决方案是必要的,但必须将stringAsFactors设置为FALSE。
Gyan Veda 2013年

感谢您的模拟。我意识到预分配在处理速度方面是最好的,但这并不是我在做出此编码决策时考虑的唯一因素。
吉安·韦达

1
在f1中,您通过将字符串分配给数字矢量x感到困惑。正确的行是:df <- rbind(df, data.frame(x = i, y = toString(i)))
Eldar Agalarov 2014年

14

假设您根本不知道data.frame的大小。它可以是几行,也可以是几百万。您需要具有某种可以动态增长的容器。考虑到我的经验以及SO中的所有相关答案,我提供了4种不同的解决方案:

  1. rbindlist 到data.frame

  2. 使用data.table的快速set操作,并在需要时将其与手动将表加倍。

  3. 使用RSQLite并追加到内存中保存的表。

  4. data.frame自身的成长和使用自定义环境(具有参考语义)来存储data.frame的能力,因此不会在返回时将其复制。

这是对大量和少量附加行的所有方法的测试。每种方法都有3个与之关联的功能:

  • create(first_element)返回first_element放入的适当支持对象。

  • append(object, element)将追加element到表格的末尾(由表示object)。

  • access(object)获取data.frame所有插入的元素。

rbindlist 到data.frame

这非常简单明了:

create.1<-function(elems)
{
  return(as.data.table(elems))
}

append.1<-function(dt, elems)
{ 
  return(rbindlist(list(dt,  elems),use.names = TRUE))
}

access.1<-function(dt)
{
  return(dt)
}

data.table::set +在需要时手动将表格加倍。

我将表的真实长度存储在一个rowcount属性中。

create.2<-function(elems)
{
  return(as.data.table(elems))
}

append.2<-function(dt, elems)
{
  n<-attr(dt, 'rowcount')
  if (is.null(n))
    n<-nrow(dt)
  if (n==nrow(dt))
  {
    tmp<-elems[1]
    tmp[[1]]<-rep(NA,n)
    dt<-rbindlist(list(dt, tmp), fill=TRUE, use.names=TRUE)
    setattr(dt,'rowcount', n)
  }
  pos<-as.integer(match(names(elems), colnames(dt)))
  for (j in seq_along(pos))
  {
    set(dt, i=as.integer(n+1), pos[[j]], elems[[j]])
  }
  setattr(dt,'rowcount',n+1)
  return(dt)
}

access.2<-function(elems)
{
  n<-attr(elems, 'rowcount')
  return(as.data.table(elems[1:n,]))
}

应该为快速记录插入而优化SQL,因此我最初对RSQLite解决方案寄予厚望

这基本上是在类似线程上复制和粘贴Karsten W.答案

create.3<-function(elems)
{
  con <- RSQLite::dbConnect(RSQLite::SQLite(), ":memory:")
  RSQLite::dbWriteTable(con, 't', as.data.frame(elems))
  return(con)
}

append.3<-function(con, elems)
{ 
  RSQLite::dbWriteTable(con, 't', as.data.frame(elems), append=TRUE)
  return(con)
}

access.3<-function(con)
{
  return(RSQLite::dbReadTable(con, "t", row.names=NULL))
}

data.frame自己的行附加+自定义环境。

create.4<-function(elems)
{
  env<-new.env()
  env$dt<-as.data.frame(elems)
  return(env)
}

append.4<-function(env, elems)
{ 
  env$dt[nrow(env$dt)+1,]<-elems
  return(env)
}

access.4<-function(env)
{
  return(env$dt)
}

测试套件:

为方便起见,我将使用一个测试函数通过间接调用将它们全部覆盖。(我检查过:使用do.call而不是直接调用函数不会使代码的运行时间更长)。

test<-function(id, n=1000)
{
  n<-n-1
  el<-list(a=1,b=2,c=3,d=4)
  o<-do.call(paste0('create.',id),list(el))
  s<-paste0('append.',id)
  for (i in 1:n)
  {
    o<-do.call(s,list(o,el))
  }
  return(do.call(paste0('access.', id), list(o)))
}

让我们看看n = 10插入的性能。

我还添加了一个“安慰剂”功能(带有后缀0),该功能什么都不做-只是为了衡量测试设置的开销。

r<-microbenchmark(test(0,n=10), test(1,n=10),test(2,n=10),test(3,n=10), test(4,n=10))
autoplot(r)

添加n = 10行的时间

n = 100行的时间 n = 1000行的时间

对于1E5行(在2.50 GHz @Intel®Core™i7-4710HQ CPU上完成的测量):

nr  function      time
4   data.frame    228.251 
3   sqlite        133.716
2   data.table      3.059
1   rbindlist     169.998 
0   placebo         0.202

看起来像基于SQLite的解决方案,尽管可以在大数据上重新获得一定的速度,但远不及data.table +手动指数增长。区别几乎是两个数量级!

摘要

如果您知道要追加的行数很少(n <= 100),请继续使用最简单的解决方案:只需使用方括号表示法将行分配给data.frame,而忽略data.frame是未预先填充。

对于其他一切,请使用data.table::set和增长data.table(例如,使用我的代码)。


2
SQLite运行缓慢的原因是,在每个INSERT INTO上,它都必须重新索引,即O(n),其中n是行数。这意味着一次插入一行到SQL数据库是O(n ^ 2)。如果您一次插入整个data.frame,SQLite可能会非常快,但是逐行增长并不是最好的选择。
朱利安·扎克

5

使用purrr,tidyr和dplyr更新

由于问题已经过时(6年),答案缺少使用较新软件包tidyr和purrr的解决方案。因此,对于使用这些软件包的人们,我想为先前的答案添加解决方案-所有这些都非常有趣,尤其是。

purrr和tidyr的最大优点是更好的可读性IMHO。purrr用更灵活的map()系列代替了lapply,tidyr提供了超直观的方法add_row-就像它所说的那样:)

map_df(1:1000, function(x) { df %>% add_row(x = x, y = toString(x)) })

该解决方案简短易懂,并且相对较快:

system.time(
   map_df(1:1000, function(x) { df %>% add_row(x = x, y = toString(x)) })
)
   user  system elapsed 
   0.756   0.006   0.766

它几乎线性缩放,因此对于1e5行,性能为:

system.time(
  map_df(1:100000, function(x) { df %>% add_row(x = x, y = toString(x)) })
)
   user  system elapsed 
 76.035   0.259  76.489 

这将使其在@Adam Ryczkowski基准测试中的data.table(如果您忽略安慰剂)之后排在第二位:

nr  function      time
4   data.frame    228.251 
3   sqlite        133.716
2   data.table      3.059
1   rbindlist     169.998 
0   placebo         0.202

您无需使用add_row。例如: map_dfr(1:1e5, function(x) { tibble(x = x, y = toString(x)) })
user3808394

@ user3808394谢谢,这是一个有趣的选择!如果有人想从头开始创建数据框,则您的数据框较短,因此是更好的解决方案。如果您已经有一个数据框,我的解决方案当然更好。
敏捷豆

如果您已经有一个数据框,则可以bind_rows(df, map_dfr(1:1e5, function(x) { tibble(x = x, y = toString(x)) }))使用代替add_row
user3808394

2

让我们取一个向量“点”,它的数字从1到5

point = c(1,2,3,4,5)

如果我们想在向量内的任何地方附加数字6,则下面的命令可能会派上用场

i)向量

new_var = append(point, 6 ,after = length(point))

ii)表格的栏

new_var = append(point, 6 ,after = length(mtcars$mpg))

该命令append采用三个参数:

  1. 要修改的向量/列。
  2. 要包含在修饰向量中的值。
  3. 下标,之后要附加值。

简单...!!如有任何歉意...!


1

以下是一个更通用的解决方案。

    extendDf <- function (df, n) {
    withFactors <- sum(sapply (df, function(X) (is.factor(X)) )) > 0
    nr          <- nrow (df)
    colNames    <- names(df)
    for (c in 1:length(colNames)) {
        if (is.factor(df[,c])) {
            col         <- vector (mode='character', length = nr+n) 
            col[1:nr]   <- as.character(df[,c])
            col[(nr+1):(n+nr)]<- rep(col[1], n)  # to avoid extra levels
            col         <- as.factor(col)
        } else {
            col         <- vector (mode=mode(df[1,c]), length = nr+n)
            class(col)  <- class (df[1,c])
            col[1:nr]   <- df[,c] 
        }
        if (c==1) {
            newDf       <- data.frame (col ,stringsAsFactors=withFactors)
        } else {
            newDf[,c]   <- col 
        }
    }
    names(newDf) <- colNames
    newDf
}

函数extendDf()将数据帧扩展为n行。

举个例子:

aDf <- data.frame (l=TRUE, i=1L, n=1, c='a', t=Sys.time(), stringsAsFactors = TRUE)
extendDf (aDf, 2)
#      l i n c                   t
# 1  TRUE 1 1 a 2016-07-06 17:12:30
# 2 FALSE 0 0 a 1970-01-01 01:00:00
# 3 FALSE 0 0 a 1970-01-01 01:00:00

system.time (eDf <- extendDf (aDf, 100000))
#    user  system elapsed 
#   0.009   0.002   0.010
system.time (eDf <- extendDf (eDf, 100000))
#    user  system elapsed 
#   0.068   0.002   0.070

0

我的解决方案与原始答案几乎相同,但对我不起作用。

因此,我为列命名,并且可以正常工作:

painel <- rbind(painel, data.frame("col1" = xtweets$created_at,
                                   "col2" = xtweets$text))
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.