Como anexar linhas a um quadro de dados R

121

Examinei o StackOverflow, mas não consigo encontrar uma solução específica para o meu problema, que envolve anexar linhas a um quadro de dados R.

Estou inicializando um quadro de dados de 2 colunas vazio, da seguinte maneira.

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

Então, meu objetivo é percorrer uma lista de valores e, em cada iteração, acrescentar um valor ao final da lista. Comecei com o seguinte código.

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

I também tentou as funções c, appende mergesem sucesso. Por favor, deixe-me saber se você tem alguma sugestão.

Gyan Veda
fonte
2
Não presumo saber como R deveria ser usado, mas queria ignorar a linha de código adicional necessária para atualizar os índices em todas as iterações e não posso pré-alocar facilmente o tamanho do quadro de dados porque não sabe quantas linhas serão necessárias. Lembre-se de que o exposto acima é meramente um exemplo de brinquedo destinado a ser reproduzível. De qualquer forma, obrigado pela sua sugestão!
Gyan Veda

Respostas:

115

Atualizar

Sem saber o que você está tentando fazer, compartilharei mais uma sugestão: Pré-aloque vetores do tipo que você deseja para cada coluna, insira valores nesses vetores e, no final, crie seu data.frame .

Continuando com Julian's f3(um pré-alocado data.frame) como a opção mais rápida até agora, definida como:

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

Aqui está uma abordagem semelhante, mas em que o data.frameé criado como a última etapa.

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

microbenchmarkdo pacote "microbenchmark" nos dará uma visão mais abrangente do que 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()(a abordagem abaixo) é incrivelmente ineficiente por causa da frequência com que chama data.framee porque o crescimento de objetos dessa maneira geralmente é lento na R. f3()é muito melhorado devido à pré-localização, mas a data.frameestrutura em si pode ser parte do gargalo aqui. f4()tenta contornar esse gargalo sem comprometer a abordagem que você deseja adotar.


Resposta original

Isso realmente não é uma boa ideia, mas se você quiser fazer dessa maneira, acho que pode tentar:

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

Observe que no seu código, há outro problema:

  • Você deve usar stringsAsFactorsse quiser que os caracteres não sejam convertidos em fatores. Usar:df = data.frame(x = numeric(), y = character(), stringsAsFactors = FALSE)
A5C1D2H2I1M1N2O1R2T1
fonte
6
Obrigado! Isso resolve meu problema. Por que essa "realmente não é uma boa idéia"? E de que maneira xey são misturados no loop for?
Gyan Veda
5
@ user2932774, É incrivelmente ineficiente cultivar um objeto dessa maneira em R. Uma melhoria (mas ainda não necessariamente a melhor) seria pré-alocar data.frameo tamanho máximo esperado e adicionar os valores com [extração / substituição.
A5C1D2H2I1M1N2O1R2T1
1
Obrigado Ananda. Normalmente, sou pré-alocado, mas discordo que essa não é realmente uma boa ideia. Depende da situação. No meu caso, estou lidando com pequenos dados e a alternativa consumirá mais tempo para codificar. Além disso, esse é um código mais elegante comparado ao necessário para atualizar índices numéricos para preencher as partes apropriadas do quadro de dados pré-alocado em cada iteração. Apenas curioso, qual é a "melhor maneira" de realizar essa tarefa na sua opinião? Eu teria pensado que a pré-localização teria sido melhor.
Gyan Veda
2
@ user2932774, é legal. Também aprecio a sua perspectiva - também nunca trabalho com grandes conjuntos de dados. Dito isto, se vou trabalhar para escrever uma função ou algo assim, normalmente gastaria um pouco de esforço extra tentando ajustar o código para obter melhores velocidades sempre que possível. Veja minha atualização para um exemplo de uma enorme diferença de velocidade.
A5C1D2H2I1M1N2O1R2T1
1
Whoa, isso é uma enorme diferença! Obrigado por executar essa simulação e me ensinar sobre o pacote microbenchmark. Definitivamente, concordo com você que é bom fazer esse esforço extra. No meu caso particular, acho que só queria algo rápido e sujo em algum código que talvez nunca precise ser executado novamente. :)
Gyan Veda
34

Vamos comparar as três soluções propostas:

# 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

A melhor solução é pré-alocar espaço (conforme planejado em R). A próxima melhor solução é usar list, e a pior solução (pelo menos com base nesses resultados de tempo) parece ser rbind.

Julián Urbano
fonte
Obrigado! Embora eu discorde da sugestão de Ananda. Se eu quero que os caracteres sejam convertidos em níveis de um fator ou não, isso dependerá do que eu quero fazer com a saída. Embora eu ache que, com a solução que você propõe, é necessário definir stringsAsFactors como FALSE.
Gyan Veda
Obrigado pela simulação. Percebo que a pré-alocação é melhor em termos de velocidade de processamento, mas esse não é o único fator que considerei ao tomar essa decisão de codificação.
Gyan Veda
1
Em f1 você confundiu atribuindo string ao vetor numérico x. A linha correta é:df <- rbind(df, data.frame(x = i, y = toString(i)))
Eldar Agalarov 01/06
14

Suponha que você simplesmente não saiba o tamanho do data.frame com antecedência. Pode muito bem ser algumas linhas ou alguns milhões. Você precisa ter algum tipo de recipiente, que cresça dinamicamente. Levando em consideração minha experiência e todas as respostas relacionadas no SO, venho com 4 soluções distintas:

  1. rbindlist para o data.frame

  2. Use data.tablea setoperação rápida e junte-a à duplicação manual da mesa, quando necessário.

  3. Use RSQLitee acrescente à tabela mantida na memória.

  4. data.frameprópria capacidade de crescer e usar o ambiente personalizado (que tem semântica de referência) para armazenar o data.frame, para que ele não seja copiado no retorno.

Aqui está um teste de todos os métodos para o número pequeno e grande de linhas anexadas. Cada método possui 3 funções associadas:

  • create(first_element)que retorna o objeto de suporte apropriado com a first_elementinserção.

  • append(object, element)que anexa elementao final da tabela (representado por object).

  • access(object)obtém o data.framecom todos os elementos inseridos.

rbindlist para o data.frame

Isso é bastante fácil e direto:

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 + dobrar manualmente a tabela quando necessário.

Vou armazenar o comprimento verdadeiro da tabela em um rowcountatributo.

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

O SQL deve ser otimizado para inserção rápida de registros, então eu inicialmente tinha grandes esperanças de RSQLitesolução

Isso é basicamente copiar e colar a resposta de Karsten W. em tópicos semelhantes.

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.framepróprio ambiente personalizado + com adição de linhas.

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

A suíte de testes:

Por conveniência, usarei uma função de teste para cobrir todas elas com chamadas indiretas. (Eu verifiquei: usar em do.callvez de chamar diretamente as funções não torna o código mensurável por mais tempo).

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

Vamos ver o desempenho de n = 10 inserções.

Também adicionei funções 'placebo' (com sufixo 0) que não realizam nada - apenas para medir a sobrecarga da configuração do teste.

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

Horários para adicionar n = 10 linhas

Horários para n = 100 linhas Tempos para n = 1000 linhas

Para linhas 1E5 (medições feitas na CPU Intel (R) Core (i) i7-4710HQ a 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

Parece que a suluição baseada em SQLite, embora recupere alguma velocidade em dados grandes, não chega nem perto de data.table + crescimento exponencial manual. A diferença é quase duas ordens de magnitude!

Resumo

Se você souber que anexará um número bastante pequeno de linhas (n <= 100), vá em frente e use a solução mais simples possível: apenas atribua as linhas ao data.frame usando a notação entre colchetes e ignore o fato de que o data.frame é não pré-preenchido.

Para todo o resto, use data.table::sete aumente exponencialmente o data.table (por exemplo, usando meu código).

Adam Ryczkowski
fonte
2
A razão pela qual o SQLite é lento é que, em cada INSERT INTO, ele precisa REINDEX, que é O (n), onde n é o número de linhas. Isso significa que a inserção em um banco de dados SQL, uma linha por vez, é O (n ^ 2). O SQLite pode ser muito rápido, se você inserir um data.frame inteiro de uma só vez, mas não é o melhor para crescer linha por linha.
Julian Zucker
5

Atualizar com purrr, tidyr e dplyr

Como a pergunta já está datada (6 anos), as respostas estão faltando uma solução com os pacotes mais recentes tidyr e purrr. Portanto, para as pessoas que trabalham com esses pacotes, quero adicionar uma solução às respostas anteriores - todas bastante interessantes, especialmente.

A maior vantagem do ronronar e do tidyr é a melhor legibilidade do IMHO. O purrr substitui o lapply pela família map () mais flexível, a tidyr oferece o método super intuitivo add_row - apenas faz o que diz :)

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

Esta solução é curta e intuitiva de ler, e é relativamente rápida:

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

Ele é dimensionado quase linearmente, portanto, para 1e5 linhas, o desempenho é:

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 

que o colocaria em segundo lugar logo após data.table (se você ignorar o placebo) no benchmark de @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
Feijão ágil
fonte
Você não precisa usar add_row. Por exemplo: map_dfr(1:1e5, function(x) { tibble(x = x, y = toString(x)) }).
user3808394
@ user3808394 obrigado, essa é uma alternativa interessante! se alguém quiser criar um quadro de dados a partir do zero, o seu é mais curto, portanto, a melhor solução. caso você já tenha um dataframe, minha solução é obviamente melhor.
Bean Agile
Se você já possui um dataframe, faria em bind_rows(df, map_dfr(1:1e5, function(x) { tibble(x = x, y = toString(x)) }))vez de usá-lo add_row.
user3808394
2

Vamos pegar um vetor 'point' com números de 1 a 5

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

se quisermos adicionar um número 6 em qualquer lugar dentro do vetor, o comando abaixo pode ser útil

i) Vetores

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

ii) colunas de uma tabela

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

O comando appendusa três argumentos:

  1. o vetor / coluna a ser modificada.
  2. valor a ser incluído no vetor modificado.
  3. um subscrito, após o qual os valores devem ser anexados.

simples...!! Desculpas em caso de qualquer ...!

Praneeth Krishna
fonte
1

Uma solução mais genérica para pode ser a seguinte.

    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
}

A função extendDf () estende um quadro de dados com n linhas.

Como um exemplo:

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
Pisca46
fonte
0

Minha solução é quase a mesma que a resposta original, mas não funcionou para mim.

Então, eu dei nomes para as colunas e funciona:

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