Comment ajouter des lignes à un bloc de données R

121

J'ai regardé autour de StackOverflow, mais je ne trouve pas de solution spécifique à mon problème, qui consiste à ajouter des lignes à un bloc de données R.

J'initialise une trame de données vide à 2 colonnes, comme suit.

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

Ensuite, mon objectif est de parcourir une liste de valeurs et, à chaque itération, d'ajouter une valeur à la fin de la liste. J'ai commencé avec le code suivant.

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

J'ai aussi essayé les fonctions c, appendet mergesans succès. Veuillez me faire savoir si vous avez des suggestions.

Gyan Veda
la source
2
Je ne présume pas de savoir comment R était censé être utilisé, mais je voulais ignorer la ligne de code supplémentaire qui serait nécessaire pour mettre à jour les index à chaque itération et je ne peux pas facilement préallouer la taille de la trame de données car je ne le fais pas Je ne sais pas combien de lignes il faudra finalement. N'oubliez pas que ce qui précède n'est qu'un exemple de jouet destiné à être reproductible. Dans tous les cas, merci pour votre suggestion!
Gyan Veda

Réponses:

115

Mettre à jour

Ne sachant pas ce que vous essayez de faire, je vais partager une autre suggestion: préallouer des vecteurs du type que vous souhaitez pour chaque colonne, insérer des valeurs dans ces vecteurs, puis, à la fin, créer votre fichier data.frame.

Poursuivre avec Julian f3(une pré-allouée data.frame) comme l'option la plus rapide à ce jour, définie comme:

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

Voici une approche similaire, mais dans laquelle le data.frameest créé à la dernière étape.

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

microbenchmarkdu package "microbenchmark" nous donnera un aperçu plus complet 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()(l'approche ci-dessous) est incroyablement inefficace en raison de la fréquence d'appels data.frameet du fait que la croissance des objets de cette façon est généralement lente dans R. f3()est beaucoup améliorée en raison de la préallocation, mais la data.framestructure elle-même pourrait faire partie du goulot d'étranglement ici. f4()essaie de contourner ce goulot d'étranglement sans compromettre l'approche que vous souhaitez adopter.


Réponse originale

Ce n'est vraiment pas une bonne idée, mais si vous vouliez le faire de cette façon, je suppose que vous pouvez essayer:

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

Notez que dans votre code, il y a un autre problème:

  • Vous devez utiliser stringsAsFactorssi vous souhaitez que les caractères ne soient pas convertis en facteurs. Utilisation:df = data.frame(x = numeric(), y = character(), stringsAsFactors = FALSE)
A5C1D2H2I1M1N2O1R2T1
la source
6
Merci! Cela résout mon problème. Pourquoi est-ce "vraiment pas une bonne idée"? Et de quelle manière x et y sont-ils mélangés dans la boucle for?
Gyan Veda
5
@ user2932774, Il est incroyablement inefficace de développer un objet de cette façon dans R. Une amélioration (mais pas nécessairement la meilleure façon) serait de préallouer un objet data.framede la taille ultime attendue et d'ajouter les valeurs avec[ extraction / remplacement.
A5C1D2H2I1M1N2O1R2T1
1
Merci, Ananda. Je vais normalement avec la préallocation, mais je ne suis pas d'accord pour dire que ce n'est vraiment pas une bonne idée. Ça dépend de la situation. Dans mon cas, je traite de petites données et l'alternative prendra plus de temps à coder. De plus, il s'agit d'un code plus élégant que celui requis pour mettre à jour les index numériques afin de remplir les parties appropriées de la trame de données pré-allouée à chaque itération. Juste curieux, quelle est la «meilleure façon» d'accomplir cette tâche à votre avis? J'aurais pensé que la préallocation aurait été la meilleure solution.
Gyan Veda
2
@ user2932774, c'est cool. J'apprécie également votre point de vue - je ne travaille pratiquement jamais non plus avec de grands ensembles de données. Cela dit, si je vais travailler sur l'écriture d'une fonction ou quelque chose du genre, je consacrerais généralement un peu plus d'efforts à essayer de modifier le code pour obtenir de meilleures vitesses chaque fois que possible. Voir ma mise à jour pour un exemple d'une différence de vitesse assez énorme.
A5C1D2H2I1M1N2O1R2T1
1
Whoa, c'est une énorme différence! Merci d'avoir exécuté cette simulation et de m'avoir appris le package microbenchmark. Je suis tout à fait d'accord avec vous pour dire que c'est agréable de faire cet effort supplémentaire. Dans mon cas particulier, je suppose que je voulais juste quelque chose de rapide et de sale sur du code que je n'aurais peut-être plus jamais à exécuter. :)
Gyan Veda
34

Comparons les trois solutions proposées:

# 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

La meilleure solution consiste à pré-allouer de l'espace (comme prévu dans R). La meilleure solution suivante consiste à utiliser list, et la pire solution (au moins sur la base de ces résultats de synchronisation) semble être rbind.

Julián Urbano
la source
Merci! Bien que je ne sois pas d'accord avec la suggestion d'Ananda. Le fait que je souhaite que les caractères soient convertis ou non en niveaux d'un facteur dépendra de ce que je veux faire avec la sortie. Bien que je suppose qu'avec la solution que vous proposez, il est nécessaire de définir stringsAsFactors sur FALSE.
Gyan Veda
Merci pour la simulation. Je me rends compte que la préallocation est la meilleure en termes de vitesse de traitement, mais ce n'est pas le seul facteur que j'ai pris en compte pour prendre cette décision de codage.
Gyan Veda
1
Dans f1, vous avez confondu en attribuant une chaîne au vecteur numérique x. La ligne correcte est:df <- rbind(df, data.frame(x = i, y = toString(i)))
Eldar Agalarov
14

Supposons que vous ne connaissiez tout simplement pas la taille du data.frame à l'avance. Il peut s'agir de quelques lignes ou de quelques millions. Vous devez avoir une sorte de conteneur, qui se développe dynamiquement. En tenant compte de mon expérience et de toutes les réponses connexes en SO, je viens avec 4 solutions distinctes:

  1. rbindlist au data.frame

  2. Utilisez data.tablele setfonctionnement rapide de et associez-le au doublement manuel de la table en cas de besoin.

  3. Utilisez RSQLiteet ajoutez au tableau conservé en mémoire.

  4. data.frameLa propre capacité de développer et d'utiliser un environnement personnalisé (qui a une sémantique de référence) pour stocker le data.frame afin qu'il ne soit pas copié au retour.

Voici un test de toutes les méthodes pour le petit et le grand nombre de lignes ajoutées. Chaque méthode est associée à 3 fonctions:

  • create(first_element)qui renvoie l'objet de support approprié avec first_elementput in.

  • append(object, element)qui ajoute le elementà la fin du tableau (représenté par object).

  • access(object)obtient le data.frameavec tous les éléments insérés.

rbindlist au data.frame

C'est assez simple et simple:

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 + doubler manuellement la table en cas de besoin.

Je vais stocker la vraie longueur de la table dans un rowcountattribut.

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 devrait être optimisé pour une insertion rapide des enregistrements, donc j'avais au départ de grands espoirs de RSQLitesolution

Il s'agit essentiellement d'un copier-coller de la réponse de Karsten W. sur un fil similaire.

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.framepropre environnement personnalisé d'ajout de lignes.

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

La suite de tests:

Pour plus de commodité, j'utiliserai une fonction de test pour les couvrir tous avec un appel indirect. (J'ai vérifié: utiliser do.callau lieu d'appeler directement les fonctions ne rend pas le code mesurable plus longtemps).

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

Voyons les performances pour n = 10 insertions.

J'ai également ajouté des fonctions «placebo» (avec suffixe 0) qui n'effectuent rien - juste pour mesurer la surcharge de la configuration de test.

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

Délais d'ajout de n = 10 lignes

Timings pour n = 100 lignes Timings pour n = 1000 lignes

Pour les lignes 1E5 (mesures effectuées sur un processeur Intel (R) Core (TM) i7-4710HQ à 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

Il semble que la solution basée sur SQLite, bien qu'elle regagne un peu de vitesse sur des données volumineuses, est loin d'être proche de data.table + croissance exponentielle manuelle. La différence est de près de deux ordres de grandeur!

Résumé

Si vous savez que vous allez ajouter un nombre assez petit de lignes (n <= 100), allez-y et utilisez la solution la plus simple possible: affectez simplement les lignes au data.frame en utilisant la notation entre crochets et ignorez le fait que le data.frame est non prérempli.

Pour tout le reste, utilisez data.table::setet développez la table data.table de manière exponentielle (par exemple en utilisant mon code).

Adam Ryczkowski
la source
2
La raison pour laquelle SQLite est lent est qu'à chaque INSERT INTO, il doit REINDEX, qui est O (n), où n est le nombre de lignes. Cela signifie que l'insertion dans une base de données SQL une ligne à la fois est O (n ^ 2). SQLite peut être très rapide, si vous insérez un data.frame entier à la fois, mais ce n'est pas le meilleur pour croître ligne par ligne.
Julian Zucker
5

Mise à jour avec Purrr, Tidyr & Dplyr

Comme la question est déjà datée (6 ans), les réponses manquent une solution avec les nouveaux packages tidyr et purrr. Donc, pour les personnes travaillant avec ces packages, je souhaite ajouter une solution aux réponses précédentes - toutes très intéressantes, en particulier.

Le plus grand avantage de purrr et tidyr est une meilleure lisibilité à mon humble avis. purrr remplace lapply par la famille map () plus flexible, tidyr propose la méthode super intuitive add_row - fait juste ce qu'elle dit :)

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

Cette solution est courte et intuitive à lire, et elle est relativement rapide:

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

Il évolue presque linéairement, donc pour 1e5 lignes, les performances sont:

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 

ce qui le placerait au deuxième rang juste après data.table (si vous ignorez le placebo) dans le 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
Haricot agile
la source
Vous n'avez pas besoin d'utiliser add_row. Par exemple: map_dfr(1:1e5, function(x) { tibble(x = x, y = toString(x)) }).
user3808394
@ user3808394 merci, c'est une alternative intéressante! si quelqu'un veut créer un dataframe à partir de zéro, le vôtre est plus court donc la meilleure solution. au cas où vous auriez déjà un dataframe, ma solution est bien sûr meilleure.
Agile Bean
Si vous avez déjà un dataframe, vous le feriez bind_rows(df, map_dfr(1:1e5, function(x) { tibble(x = x, y = toString(x)) }))au lieu d'utiliser add_row.
user3808394
2

Prenons un vecteur 'point' qui a des nombres de 1 à 5

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

si nous voulons ajouter un numéro 6 n'importe où dans le vecteur, la commande ci-dessous peut être utile

i) Vecteurs

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

ii) colonnes d'un tableau

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

La commande appendprend trois arguments:

  1. le vecteur / colonne à modifier.
  2. valeur à inclure dans le vecteur modifié.
  3. un indice, après quoi les valeurs doivent être ajoutées.

Facile...!! Toutes mes excuses en cas de ...!

Praneeth Krishna
la source
1

Une solution plus générique pour pourrait être la suivante.

    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
}

La fonction extendDf () étend un bloc de données avec n lignes.

Par exemple:

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
la source
0

Ma solution est presque la même que la réponse originale mais cela ne fonctionne pas pour moi.

Alors, j'ai donné des noms aux colonnes et ça marche:

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