Fusionner simultanément plusieurs data.frames dans une liste

259

J'ai une liste de nombreux data.frames que je veux fusionner. Le problème ici est que chaque data.frame diffère en termes de nombre de lignes et de colonnes, mais ils partagent tous les variables clés (que j'ai appelées "var1"et "var2"dans le code ci-dessous). Si les data.frames étaient identiques en termes de colonnes, je pourrais simplement rbind, pour lequel ryrind.fill de plyr ferait le travail, mais ce n'est pas le cas avec ces données.

Parce que la mergecommande ne fonctionne que sur 2 data.frames, je me suis tourné vers Internet pour des idées. J'ai obtenu celui-ci d' ici , qui fonctionnait parfaitement dans R 2.7.2, ce que j'avais à l'époque:

merge.rec <- function(.list, ...){
    if(length(.list)==1) return(.list[[1]])
    Recall(c(list(merge(.list[[1]], .list[[2]], ...)), .list[-(1:2)]), ...)
}

Et j'appellerais la fonction comme ceci:

df <- merge.rec(my.list, by.x = c("var1", "var2"), 
                by.y = c("var1", "var2"), all = T, suffixes=c("", ""))

Mais dans toute version R après 2.7.2, y compris 2.11 et 2.12, ce code échoue avec l'erreur suivante:

Error in match.names(clabs, names(xi)) : 
  names do not match previous names

(Par ailleurs, je vois d'autres références à cette erreur ailleurs sans résolution).

Est-ce qu'il y a un moyen de résoudre ceci?

bshor
la source

Réponses:

183

Une autre question portait précisément comment effectuer plusieurs jointures avec gauche dplyr dans R . La question a été marquée comme un double de celle-ci, je réponds donc ici en utilisant les 3 exemples de trames de données ci-dessous:

x <- data.frame(i = c("a","b","c"), j = 1:3, stringsAsFactors=FALSE)
y <- data.frame(i = c("b","c","d"), k = 4:6, stringsAsFactors=FALSE)
z <- data.frame(i = c("c","d","a"), l = 7:9, stringsAsFactors=FALSE)

Mise à jour de juin 2018 : j'ai divisé la réponse en trois sections représentant trois façons différentes d'effectuer la fusion. Vous voudrez probablement utiliser le purrrchemin si vous utilisez déjà les packages tidyverse . À des fins de comparaison ci-dessous, vous trouverez une version de base R utilisant le même ensemble de données exemple.


1) Rejoignez-les avec reducedu purrrpackage:

Le purrrpackage fournit une reducefonction qui a une syntaxe concise:

library(tidyverse)
list(x, y, z) %>% reduce(left_join, by = "i")
#  A tibble: 3 x 4
#  i       j     k     l
#  <chr> <int> <int> <int>
# 1 a      1    NA     9
# 2 b      2     4    NA
# 3 c      3     5     7

Vous pouvez également effectuer d'autres jointures, telles qu'un full_joinou inner_join:

list(x, y, z) %>% reduce(full_join, by = "i")
# A tibble: 4 x 4
# i       j     k     l
# <chr> <int> <int> <int>
# 1 a     1     NA     9
# 2 b     2     4      NA
# 3 c     3     5      7
# 4 d     NA    6      8

list(x, y, z) %>% reduce(inner_join, by = "i")
# A tibble: 1 x 4
# i       j     k     l
# <chr> <int> <int> <int>
# 1 c     3     5     7

2) dplyr::left_join()avec base R Reduce():

list(x,y,z) %>%
    Reduce(function(dtf1,dtf2) left_join(dtf1,dtf2,by="i"), .)

#   i j  k  l
# 1 a 1 NA  9
# 2 b 2  4 NA
# 3 c 3  5  7

3) Base R merge()avec base R Reduce():

Et à des fins de comparaison, voici une version de base R de la jointure gauche

 Reduce(function(dtf1, dtf2) merge(dtf1, dtf2, by = "i", all.x = TRUE),
        list(x,y,z))
#   i j  k  l
# 1 a 1 NA  9
# 2 b 2  4 NA
# 3 c 3  5  7
Paul Rougieux
la source
1
La variante full_join fonctionne parfaitement et semble beaucoup moins effrayante que la réponse acceptée. Pas beaucoup de différence de vitesse, cependant.
bshor
1
@Axeman a raison, mais vous pourriez éviter (visiblement) de renvoyer une liste de trames de données en utilisant map_dfr()oumap_dfc()
DaveRGP
Je pensais que je pourrais rejoindre un certain nombre de DF sur la base d'un modèle utilisant ´ls (pattern = "DF_name_contains_this") ´, mais non. J'ai utilisé "noquote (paste (())", mais je produis toujours un vecteur de caractères au lieu d'une liste de DF. J'ai fini par taper les noms, ce qui est odieux.
Stylo de George William Russel le
Une autre question fournit une implémentation de python : liste des trames de données pandas dfs = [df1, df2, df3]alors reduce(pandas.merge, dfs).
Paul Rougieux
222

Réduire rend cela assez simple:

merged.data.frame = Reduce(function(...) merge(..., all=T), list.of.data.frames)

Voici un exemple complet utilisant des données fictives:

set.seed(1)
list.of.data.frames = list(data.frame(x=1:10, a=1:10), data.frame(x=5:14, b=11:20), data.frame(x=sample(20, 10), y=runif(10)))
merged.data.frame = Reduce(function(...) merge(..., all=T), list.of.data.frames)
tail(merged.data.frame)
#    x  a  b         y
#12 12 NA 18        NA
#13 13 NA 19        NA
#14 14 NA 20 0.4976992
#15 15 NA NA 0.7176185
#16 16 NA NA 0.3841037
#17 19 NA NA 0.3800352

Et voici un exemple utilisant ces données pour répliquer my.list:

merged.data.frame = Reduce(function(...) merge(..., by=match.by, all=T), my.list)
merged.data.frame[, 1:12]

#  matchname party st district chamber senate1993 name.x v2.x v3.x v4.x senate1994 name.y
#1   ALGIERE   200 RI      026       S         NA   <NA>   NA   NA   NA         NA   <NA>
#2     ALVES   100 RI      019       S         NA   <NA>   NA   NA   NA         NA   <NA>
#3    BADEAU   100 RI      032       S         NA   <NA>   NA   NA   NA         NA   <NA>

Remarque: il semble que ce soit un bogue merge. Le problème est qu'il n'y a aucune vérification que l'ajout des suffixes (pour gérer les noms non correspondants qui se chevauchent) les rend réellement uniques. À un certain moment, il utilise [.data.framece qui fait make.unique les noms, provoquant l' rbindéchec de.

# first merge will end up with 'name.x' & 'name.y'
merge(my.list[[1]], my.list[[2]], by=match.by, all=T)
# [1] matchname    party        st           district     chamber      senate1993   name.x      
# [8] votes.year.x senate1994   name.y       votes.year.y
#<0 rows> (or 0-length row.names)
# as there is no clash, we retain 'name.x' & 'name.y' and get 'name' again
merge(merge(my.list[[1]], my.list[[2]], by=match.by, all=T), my.list[[3]], by=match.by, all=T)
# [1] matchname    party        st           district     chamber      senate1993   name.x      
# [8] votes.year.x senate1994   name.y       votes.year.y senate1995   name         votes.year  
#<0 rows> (or 0-length row.names)
# the next merge will fail as 'name' will get renamed to a pre-existing field.

Le moyen le plus simple de corriger est de ne pas laisser le champ renommer les champs en double (dont il y en a beaucoup ici) jusqu'à merge. Par exemple:

my.list2 = Map(function(x, i) setNames(x, ifelse(names(x) %in% match.by,
      names(x), sprintf('%s.%d', names(x), i))), my.list, seq_along(my.list))

Le merge/ Reducefonctionnera alors correctement.

Charles
la source
Merci! J'ai vu cette solution également sur le lien de Ramnath. Semble assez facile. Mais j'obtiens l'erreur suivante: "Erreur dans match.names (clabs, names (xi)): les noms ne correspondent pas aux noms précédents". Les variables sur lesquelles je compare sont toutes présentes dans tous les cadres de données de la liste, donc je ne comprends pas ce que cette erreur me dit.
bshor
1
J'ai testé cette solution sur R2.7.2 et j'obtiens la même erreur match.names. Il y a donc un problème plus fondamental avec cette solution et mes données. J'ai utilisé le code: réduire (fonction (x, y) fusionner (x, y, tout = T, by.x = match.by, by.y = match.by), ma.list, accumuler = F)
bshor
1
Étrange, j'ai ajouté le code avec lequel je l'ai testé et qui fonctionne bien. Je suppose qu'il y a un changement de nom de champ en fonction des arguments de fusion que vous utilisez? Le résultat fusionné doit toujours avoir les clés appropriées pour être fusionné avec la trame de données suivante.
Charles
Je soupçonne que quelque chose se passe avec des trames de données vides. J'ai essayé quelques exemples comme celui-ci: empty <- data.frame(x=numeric(0),a=numeric(0); L3 <- c(empty,empty,list.of.data.frames,empty,empty,empty)et il s'est passé des trucs bizarres que je n'ai pas encore compris.
Ben Bolker
@Charles Vous êtes sur quelque chose. Votre code fonctionne bien au-dessus pour moi. Et quand je l'adapte au mien, cela fonctionne bien aussi - sauf qu'il fait une fusion en ignorant les variables clés que je veux. Lorsque j'essaie d'ajouter des variables clés plutôt que de les laisser de côté, j'obtiens une nouvelle erreur "Erreur dans is.null (x): 'x' est manquant". La ligne de code est "test.reduce <- Reduce (fonction (...) merge (by = match.by, all = T), my.list)" où match.by est le vecteur des noms de variables clés que je veux fusionner par.
bshor
52

Vous pouvez le faire en utilisant merge_alldans le reshapepackage. Vous pouvez passer des paramètres à l' mergeaide de l' ...argument

reshape::merge_all(list_of_dataframes, ...)

Voici une excellente ressource sur différentes méthodes de fusion de trames de données .

Ramnath
la source
on dirait que je viens de répliquer merge_recurse =) bon de savoir que cette fonction existe déjà.
SFun28
16
Oui. chaque fois que j'ai une idée, je vérifie toujours si @hadley l'a déjà fait, et la plupart du temps il a :-)
Ramnath
1
Je suis un peu confus; dois-je faire merge_all ou merge_recurse? Dans tous les cas, lorsque j'essaie d'ajouter mes arguments supplémentaires à l'un ou l'autre, j'obtiens l'erreur "argument formel" tous "correspondant à plusieurs arguments réels".
bshor
2
Je pense que j'ai abandonné cela de reshape2. Réduire + fusionner est tout aussi simple.
hadley
2
@Ramnath, le lien est mort, y a-t-il un miroir?
Eduardo
4

Vous pouvez utiliser la récursivité pour ce faire. Je n'ai pas vérifié les éléments suivants, mais cela devrait vous donner la bonne idée:

MergeListOfDf = function( data , ... )
{
    if ( length( data ) == 2 ) 
    {
        return( merge( data[[ 1 ]] , data[[ 2 ]] , ... ) )
    }    
    return( merge( MergeListOfDf( data[ -1 ] , ... ) , data[[ 1 ]] , ... ) )
}
SFun28
la source
2

Je vais réutiliser l'exemple de données de @PaulRougieux

x <- data_frame(i = c("a","b","c"), j = 1:3)
y <- data_frame(i = c("b","c","d"), k = 4:6)
z <- data_frame(i = c("c","d","a"), l = 7:9)

Voici une solution courte et douce en utilisant purrrettidyr

library(tidyverse)

 list(x, y, z) %>% 
  map_df(gather, key=key, value=value, -i) %>% 
  spread(key, value)
dmi3kno
la source
1

La fonction eatde mon package safejoin a une telle fonctionnalité, si vous lui donnez une liste de data.frames en tant que deuxième entrée, elles les rejoindront récursivement à la première entrée.

Emprunter et étendre les données de la réponse acceptée:

x <- data_frame(i = c("a","b","c"), j = 1:3)
y <- data_frame(i = c("b","c","d"), k = 4:6)
z <- data_frame(i = c("c","d","a"), l = 7:9)
z2 <- data_frame(i = c("a","b","c"), l = rep(100L,3),l2 = rep(100L,3)) # for later

# devtools::install_github("moodymudskipper/safejoin")
library(safejoin)
eat(x, list(y,z), .by = "i")
# # A tibble: 3 x 4
#   i         j     k     l
#   <chr> <int> <int> <int>
# 1 a         1    NA     9
# 2 b         2     4    NA
# 3 c         3     5     7

Nous n'avons pas à prendre toutes les colonnes, nous pouvons utiliser certains assistants de tidyselect et choisir (comme nous partons de .xtoutes les .xcolonnes sont conservées):

eat(x, list(y,z), starts_with("l") ,.by = "i")
# # A tibble: 3 x 3
#   i         j     l
#   <chr> <int> <int>
# 1 a         1     9
# 2 b         2    NA
# 3 c         3     7

ou supprimez ceux spécifiques:

eat(x, list(y,z), -starts_with("l") ,.by = "i")
# # A tibble: 3 x 3
#   i         j     k
#   <chr> <int> <int>
# 1 a         1    NA
# 2 b         2     4
# 3 c         3     5

Si la liste est nommée, les noms seront utilisés comme préfixes:

eat(x, dplyr::lst(y,z), .by = "i")
# # A tibble: 3 x 4
#   i         j   y_k   z_l
#   <chr> <int> <int> <int>
# 1 a         1    NA     9
# 2 b         2     4    NA
# 3 c         3     5     7

S'il y a des conflits de colonnes, l' .conflictargument vous permet de le résoudre, par exemple en prenant le premier / second, en les ajoutant, en les fusionnant ou en les imbriquant.

garder en premier:

eat(x, list(y, z, z2), .by = "i", .conflict = ~.x)
# # A tibble: 3 x 4
#   i         j     k     l
#   <chr> <int> <int> <int>
# 1 a         1    NA     9
# 2 b         2     4    NA
# 3 c         3     5     7

garder en dernier:

eat(x, list(y, z, z2), .by = "i", .conflict = ~.y)
# # A tibble: 3 x 4
#   i         j     k     l
#   <chr> <int> <int> <dbl>
# 1 a         1    NA   100
# 2 b         2     4   100
# 3 c         3     5   100

ajouter:

eat(x, list(y, z, z2), .by = "i", .conflict = `+`)
# # A tibble: 3 x 4
#   i         j     k     l
#   <chr> <int> <int> <dbl>
# 1 a         1    NA   109
# 2 b         2     4    NA
# 3 c         3     5   107

se fondre:

eat(x, list(y, z, z2), .by = "i", .conflict = dplyr::coalesce)
# # A tibble: 3 x 4
#   i         j     k     l
#   <chr> <int> <int> <dbl>
# 1 a         1    NA     9
# 2 b         2     4   100
# 3 c         3     5     7

nid:

eat(x, list(y, z, z2), .by = "i", .conflict = ~tibble(first=.x, second=.y))
# # A tibble: 3 x 4
#   i         j     k l$first $second
#   <chr> <int> <int>   <int>   <int>
# 1 a         1    NA       9     100
# 2 b         2     4      NA     100
# 3 c         3     5       7     100

NAles valeurs peuvent être remplacées en utilisant l' .fillargument.

eat(x, list(y, z), .by = "i", .fill = 0)
# # A tibble: 3 x 4
#   i         j     k     l
#   <chr> <int> <dbl> <dbl>
# 1 a         1     0     9
# 2 b         2     4     0
# 3 c         3     5     7

Par défaut, c'est une amélioration, left_joinmais toutes les jointures dplyr sont prises en charge via l' .modeargument, les jointures floues sont également prises en charge via l' match_fun argument (il est enroulé autour du package fuzzyjoin) ou en donnant une formule telle que ~ X("var1") > Y("var2") & X("var3") < Y("var4")l' byargument.

Moody_Mudskipper
la source
0

J'avais une liste de trames de données sans colonne d'ID commune.
Il me manquait des données sur de nombreux DFS. Il y avait des valeurs nulles. Les trames de données ont été produites à l'aide de la fonction de table. La fonction Réduire, Fusionner, rbind, rbind.fill et leurs semblables ne pouvaient pas m'aider à atteindre mon objectif. Mon objectif était de produire une trame de données fusionnée compréhensible, sans rapport avec les données manquantes et la colonne d'identification commune.

Par conséquent, j'ai fait la fonction suivante. Peut-être que cette fonction peut aider quelqu'un.

##########################################################
####             Dependencies                        #####
##########################################################

# Depends on Base R only

##########################################################
####             Example DF                          #####
##########################################################

# Example df
ex_df           <- cbind(c( seq(1, 10, 1), rep("NA", 0), seq(1,10, 1) ), 
                         c( seq(1, 7, 1),  rep("NA", 3), seq(1, 12, 1) ), 
                         c( seq(1, 3, 1),  rep("NA", 7), seq(1, 5, 1), rep("NA", 5) ))

# Making colnames and rownames
colnames(ex_df) <- 1:dim(ex_df)[2]
rownames(ex_df) <- 1:dim(ex_df)[1]

# Making an unequal list of dfs, 
# without a common id column
list_of_df      <- apply(ex_df=="NA", 2, ( table) )

il suit la fonction

##########################################################
####             The function                        #####
##########################################################


# The function to rbind it
rbind_null_df_lists <- function ( list_of_dfs ) {
  length_df     <- do.call(rbind, (lapply( list_of_dfs, function(x) length(x))))
  max_no        <- max(length_df[,1])
  max_df        <- length_df[max(length_df),]
  name_df       <- names(length_df[length_df== max_no,][1])
  names_list    <- names(list_of_dfs[ name_df][[1]])

  df_dfs <- list()
  for (i in 1:max_no ) {

    df_dfs[[i]]            <- do.call(rbind, lapply(1:length(list_of_dfs), function(x) list_of_dfs[[x]][i]))

  }

  df_cbind               <- do.call( cbind, df_dfs )
  rownames( df_cbind )   <- rownames (length_df)
  colnames( df_cbind )   <- names_list

  df_cbind

}

Exécuter l'exemple

##########################################################
####             Running the example                 #####
##########################################################

rbind_null_df_lists ( list_of_df )
Elias EstatisticsEU
la source
0

Lorsque vous avez une liste de dfs et qu'une colonne contient l '"ID", mais dans certaines listes, certains ID sont manquants, vous pouvez utiliser cette version de Reduce / Merge afin de joindre plusieurs Dfs d'ID de ligne ou d'étiquettes manquantes:

Reduce(function(x, y) merge(x=x, y=y, by="V1", all.x=T, all.y=T), list_of_dfs)
Elias EstatisticsEU
la source
0

Voici un wrapper générique qui peut être utilisé pour convertir une fonction binaire en fonction multi-paramètres. L'avantage de cette solution est qu'elle est très générique et peut être appliquée à toutes les fonctions binaires. Vous n'avez qu'à le faire une fois et vous pouvez l'appliquer n'importe où.

Pour faire une démonstration de l'idée, j'utilise une récursivité simple à implémenter. Il peut bien sûr être implémenté de manière plus élégante qui bénéficie du bon support de R pour le paradigme fonctionnel.

fold_left <- function(f) {
return(function(...) {
    args <- list(...)
    return(function(...){
    iter <- function(result,rest) {
        if (length(rest) == 0) {
            return(result)
        } else {
            return(iter(f(result, rest[[1]], ...), rest[-1]))
        }
    }
    return(iter(args[[1]], args[-1]))
    })
})}

Ensuite, vous pouvez simplement envelopper toutes les fonctions binaires avec lui et appeler avec les paramètres de position (généralement data.frames) dans les premières parenthèses et les paramètres nommés dans les secondes parenthèses (comme by =ou suffix =). Si aucun paramètre nommé, laissez les deuxièmes parenthèses vides.

merge_all <- fold_left(merge)
merge_all(df1, df2, df3, df4, df5)(by.x = c("var1", "var2"), by.y = c("var1", "var2"))

left_join_all <- fold_left(left_join)
left_join_all(df1, df2, df3, df4, df5)(c("var1", "var2"))
left_join_all(df1, df2, df3, df4, df5)()
englealuze
la source