如何向 R 数据帧追加行

我已经查看了 StackOverflow,但是找不到针对我的问题的解决方案,其中包括将行附加到 R 数据框架。

I am initializing an empty 2-column data frame, as follows.

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没有成功。请让我知道,如果你有任何建议。

评论更新: 我并不认为我知道 R 应该如何使用,但是我想忽略在每次迭代中更新索引所需要的额外代码行,我不能轻易地预分配数据帧的大小,因为我不知道它最终会占用多少行。请记住,以上只是一个玩具例子,意味着可重复。不管怎样,谢谢你的建议!

399936 次浏览

Update

不知道您要做什么,我将分享另一个建议: 为每个列预分配所需类型的向量,将值插入到这些向量中,然后在最后创建 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将给我们比 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()中,这种方式生长对象的速度通常很慢,由于预分配,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)

Let's benchmark the three solutions proposed:

# 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

假设您事先不知道 data.frame 的大小。可能是几行,也可能是几百万行。您需要有某种动态增长的容器。考虑到我的经验和所有相关的答案,我提出了4个不同的解决方案:

  1. rbindlist到数据帧

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

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

  4. data.frame自己的增长能力和使用自定义环境(具有引用语义)来存储 data.frame 的能力,这样它就不会在返回时被复制。

下面是对所有方法进行的测试,这些方法适用于大量和小数量的追加行。每个方法有3个相关的函数:

  • 返回放入 first_element的适当备份对象的 create(first_element)

  • append(object, element) that appends the element to the end of the table (represented by object).

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

rbindlist到数据帧

这很简单,也很直接:

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 should be optimized for fast record insertion, so I initially had high hopes for RSQLite solution

这基本上是复制和粘贴的 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's own row-appending + custom environment.

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)
}

测试套件:

For convenience I will use one test function to cover them all with indirect calling. (I checked: using do.call instead of calling the functions directly doesn't makes the code run measurable longer).

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)

Timings for adding n=10 rows

Timings for n=100 rows Timings for n=1000 rows

对于1E5行(在 Intel (R) Core (TM) i7-4710HQ CPU@2.50 GHz 上进行的测量) :

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 + 手动指数增长。差距几乎是两个数量级!

摘要

If you know that you will append rather small number of rows (n<=100), go ahead and use the simplest possible solution: just assign the rows to the data.frame using bracket notation and ignore the fact that the data.frame is not pre-populated.

For everything else use data.table::set and grow the data.table exponentially (e.g. using my code).

更通用的解决方案可能如下所示。

    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

让我们取一个从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. value to be included in the modified vector.
  3. 一个下标,在下标之后要追加值。

简单! 抱歉,以防万一!

用 purrr,tidyr & dplyr 更新

As the question is already dated (6 years), the answers are missing a solution with newer packages tidyr and purrr. So for people working with these packages, I want to add a solution to the previous answers - all quite interesting, especially .

The biggest advantage of purrr and tidyr are better readability IMHO. Purrr 用更灵活的 map ()家族替换掉了 lapplication, tidyr offers the super-intuitive method add_row - just does what it says :)

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

It scales almost linearly, so for 1e5 rows, the performance is:

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

这使得它在基准测试中仅次于 data.table (如果你忽略安慰剂的话)排名第二。作者@Adam Ryczkowski:

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

我的解决方案几乎和原来的答案一样,但是它对我不起作用。

So, I gave names for the columns and it works:

painel <- rbind(painel, data.frame("col1" = xtweets$created_at,
"col2" = xtweets$text))