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.
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.
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:
stringsAsFactors
si vous voulez que les caractères ne soient pas convertis en facteurs. Utilisez: df = data.frame(x = numeric(), y = character(), stringsAsFactors = FALSE)
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
.
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:
rbindlist
à l'adresse data.frame
Opération rapide set
de data.table
et associez-la à un doublage manuel du tableau si nécessaire.
tilisez RSQLite
et ajoutez-le à la table conservée en mémoire.
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.frameC'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,]))
}
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)
}
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)
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!
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).
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
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:
facile...!! Toutes mes excuses pour tout ...!
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