web-dev-qa-db-fra.com

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

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

J'initialise un cadre 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 également essayé les fonctions c, append et merge sans succès. S'il vous plaît laissez-moi savoir si vous avez des suggestions.

107
Gyan Veda

Mise à jour

Ne sachant pas ce que vous essayez de faire, je vais partager une dernière suggestion: préaffectez des vecteurs du type souhaité pour chaque colonne, insérez des valeurs dans ces vecteurs, puis, à la fin, créez votre data.frame.

Poursuivant avec le f3 de Julian (un data.frame préalloué) en tant qu'option la plus rapide à ce jour, défini comme suit:

# 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.frame est 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)
}

microbenchmark du paquet "microbenchmark" nous donnera des informations plus complètes 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 à laquelle il appelle data.frame et parce que la croissance d'objets de cette façon est généralement lente dans R. f3() est beaucoup améliorée en raison de la préallocation data.frame la structure 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 voulez 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 stringsAsFactors si vous voulez que les caractères ne soient pas convertis en facteurs. Utilisez: df = data.frame(x = numeric(), y = character(), stringsAsFactors = FALSE)
103

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 consiste à utiliser list, et la pire solution (du moins sur la base de ces résultats temporels) semble être rbind.

31
Julián Urbano

Supposons simplement que vous ne connaissiez pas à l'avance la taille du nom data.fr. Il peut s'agir de quelques lignes, voire de quelques millions. Vous devez avoir une sorte de conteneur, qui pousse de manière dynamique. Tenant compte de mon expérience et de toutes les réponses connexes dans SO, je propose 4 solutions distinctes:

  1. rbindlist à l'adresse data.frame

  2. Opération rapide set de data.table et associez-la à un doublage manuel du tableau si nécessaire.

  3. tilisez RSQLite et ajoutez-le à la table conservée en mémoire.

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

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

  • create(first_element) qui renvoie l'objet de sauvegarde approprié avec first_element entré.

  • append(object, element) qui ajoute la element à la fin de la table (représentée par object).

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

rbindlist à l'adresse data.frame

C'est assez simple et direct:

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 si nécessaire.

Je vais stocker la vraie longueur de la table dans un attribut 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 doit être optimisé pour une insertion rapide d’enregistrements, j’avais donc de grands espoirs pour la solution RSQLite

Ceci est essentiellement un copier-coller de Karsten W. answer 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.frame propre environnement avec ajout de ligne + personnalisé.

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é, je vais utiliser une fonction de test pour les couvrir tous avec des appels indirects. (J'ai vérifié: utiliser do.call au lieu d'appeler directement les fonctions ne rend pas le code mesurable plus long).

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 la performance pour n = 10 insertions.

J'ai également ajouté une fonction "placebo" (avec le suffixe 0) qui n'effectue rien - il suffit de mesurer le temps système 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)

Timings for adding n=10 rows

Timings for n=100 rowsTimings for n=1000 rows

Pour les rangées 1E5 (mesures effectuées sur un processeur Intel 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, même si elle récupère un peu de vitesse sur des données volumineuses, n’est pas proche de data.table + croissance exponentielle manuelle. La différence est presque deux ordres de grandeur!

Sommaire

Si vous savez que vous allez ajouter un nombre relativement petit de lignes (n <= 100), continuez et utilisez la solution la plus simple possible: il vous suffit d'affecter les lignes à data.frame à l'aide de la notation entre crochets et d'ignorer le fait que le data.frame n'est pas pré-rempli.

Pour tout le reste, utilisez data.table::set et développez exponentiellement le fichier data.table (par exemple, à l'aide de mon code).

12
Adam Ryczkowski

Mise à jour avec purrr, tidyr et dplyr

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

Le plus gros avantage de purrr et tidyr sont 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 qui est écrit :)

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. Ainsi, pour 1e 5 lignes, la performance est la suivante:

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 point de repère 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
3
Agile Bean

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ù à l'intérieur du vecteur, la commande ci-dessous peut s'avérer utile

i) Vecteurs

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

ii) colonnes d'un tablea

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

La commande append prend trois arguments:

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

facile...!! Toutes mes excuses pour tout ...!

2
Praneeth Krishna

Une solution plus générique 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 cadre 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
1
Pisca46