349 votes

Fusionner simultanément plusieurs data.frames dans une liste

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

Parce que le merge ne fonctionne que sur 2 data.frames, je me suis tourné vers l'Internet pour trouver des idées. J'ai obtenu celle-ci de aquí qui fonctionnait parfaitement dans R 2.7.2, qui est la version 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 ça :

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

Mais dans toute version de R postérieure à 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

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

Y a-t-il un moyen de résoudre ce problème ?

330voto

Paul4forest Points 75

Une autre question posée spécifiquement comment réaliser des jointures multiples à gauche en utilisant dplyr dans R . La question a été marquée comme étant un doublon de celle-ci, je réponds donc ici, en utilisant les 3 échantillons 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 voulez probablement utiliser la méthode purrr si vous utilisez déjà le tidyverse paquets. À des fins de comparaison, vous trouverez ci-dessous une version R de base utilisant le même ensemble de données d'échantillon.


1) Les rejoindre avec reduce de la purrr paquet :

El purrr fournit un reduce 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_join o 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 la 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 la base R Reduce() :

Et à titre de comparaison, voici une version R de base de la jointure gauche basée sur la réponse de Charles.

 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

243voto

Charles Points 2069

Reduce rend cela assez facile :

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

Note : Il semble que ce soit un bogue dans le système de gestion de l'information. merge . Le problème est qu'il n'y a aucun moyen de vérifier que l'ajout de suffixes (pour gérer les noms superposés qui ne correspondent pas) les rend réellement uniques. À un moment donné, il utilise [.data.frame dont hace make.unique les noms, provoquant le rbind d'échouer.

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

La solution la plus simple est de ne pas laisser le renommage des champs en double (il y en a beaucoup ici) à la discrétion de l'utilisateur. merge . Eg :

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

El merge / Reduce fonctionnera alors parfaitement.

60voto

Ramnath Points 24798

Vous pouvez le faire en utilisant merge_all dans le reshape paquet. Vous pouvez passer des paramètres à merge en utilisant le ... argument

reshape::merge_all(list_of_dataframes, ...)

Voici une excellente ressource sur les différentes méthodes de fusion des cadres de données. .

8voto

Moody_Mudskipper Points 18115

La fonction eat de mon paquet safejoin possède une telle fonctionnalité, si vous lui donnez une liste de data.frames comme deuxième entrée, il les joindra récursivement à la 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 ne sommes pas obligés de prendre toutes les colonnes, nous pouvons utiliser des aides de sélection de tidyselect et choisir (comme nous partons de .x tous .x 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 de supprimer certains d'entre eux :

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, le .conflict vous permet de le résoudre, par exemple en prenant le premier/le deuxième, en les additionnant, en les fusionnant, ou en les imbriquant.

garder le 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

coalescent :

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

NA peuvent être remplacées en utilisant l'option .fill argument.

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, il s'agit d'une version améliorée left_join mais tous dplyr Les jointures sont soutenues par le site .mode les jointures floues sont également supportées par l'argument match_fun (il est enveloppé autour du package fuzzyjoin ) ou en donnant une formule telle que ~ X("var1") > Y("var2") & X("var3") < Y("var4") au by argument.

6voto

SFun28 Points 6444

Vous pouvez utiliser la récursion pour ce faire. Je n'ai pas vérifié ce qui suit, mais cela devrait vous donner une bonne idée :

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X