14 Joindre des données
Ci-dessus : animation d’une jointure par la gauche (source de l’image)
Ce chapitre décrit les méthodes permettant de joindre / fusionner / faire correspondre / lier / “merger” / unir / combiner des tableaux.
Lors de l’analyse de données épidémiologiques, il est rare que votre processus de nettoyage des données n’implique pas des sources de données multiples, et donc leur mise en relation. Par exemple, vous aurez peut-être à joindre des données de laboratoire aux résultats cliniques des patients, ou des données de mobilité Google aux tendances des maladies infectieuses, ou même un jeu de données à un stade donné de l’analyse et une version transformée de lui-même.
Dans ce chapitre, nous allons :
- Réaliser des jointures de deux dataframes en faisant correspondre les lignes sur la base d’une clef primaire (dans une ou plusieurs colonnes).
- Joindre des jeux de données sur la base de correspondances probabilistes (probables) entre les observations
- Étendre un jeu de données en concaténant des lignes ou des colonnes d’un autre jeu de données.
14.1 Étapes préliminaires
Importation des paquets
Ces lignes de code chargent les paquets nécessaires aux analyses. Dans ce guide, nous mettons l’accent sur p_load()
de pacman, qui installe le paquet si nécessaire puis l’importe pour l’utiliser. Vous pouvez également charger les paquets installés avec library()
de base R. Voir la page sur bases de R pour plus d’informations sur les paquets R.
::p_load(
pacman# import des fichiers
rio, # chemins d'accès
here, # gestion des données + graphiques (ggplot2)
tidyverse, # correspondances probabilistes
RecordLinkage, # correspondances probabilistes
fastLink )
Importation des données
Nous importons un jeu de données de cas d’une épidémie d’ébola fictive. Pour reproduire les étapes, cliquez pour télécharger la linelist “propre” (as .rds file). Importez vos données avec la fonction import()
du paquet rio (elle accepte de nombreux types de fichiers comme .xlsx, .rds, .csv - voir la page Importation et exportation des données pour plus de détails).
# importer la linelist dans R
<- import("linelist_cleaned.rds") linelist
Les cinquantes premières lignes sont affichées ci-dessous :
Jeux de données simplifiés
Dans les exemples ci-dessous, nous utiliserons des jeux de données simplifiés pour mieux voir ce qui se passe :
- Une version “miniature” de la linelist (liste des cas), contenant seulement les colonnes
case_id
,date_onset
, ethospital
, et seulement les 10 premières lignes.
- Une table nommée
hosp_info
, qui contient des détails sur chaque hôpital.
Dans la section sur l’appariement probabiliste, nous utiliserons deux petits ensembles de données différents. Le code pour créer ces jeux de données sera donné dans cette section.
Linelist miniature
Nous générons ici la linelist miniature des cas, qui contient seulement 10 lignes et seulement les colonnes case_id
, date_onset
, et hospital
.
<- linelist %>%
linelist_mini select(case_id, date_onset, hospital) %>% # sélectionne les colonnes
head(10) # garde les 10 premières lignes
Jeu de données des hôpitaux
Le code ci-dessous permet de créer un jeu de données contenant des informations supplémentaires sur sept hôpitaux (la population desservie et le niveau de soins disponible). Notez que le nom “Hôpital militaire” appartient à deux hôpitaux différents, l’un de niveau primaire desservant 10000 résidents et l’autre de niveau secondaire desservant 50280 résidents.
# Crée des informations sur les hôpitaux :
= data.frame(
hosp_info hosp_name = c("central hospital", "military", "military",
"port", "St. Mark's", "ignace", "sisters"),
catchment_pop = c(1950280, 40500, 10000, 50280,
12000, 5000, 4200),
level = c("Tertiary", "Secondary", "Primary", "Secondary",
"Secondary", "Primary", "Primary")
)
Voici le tableau ainsi produit :
Nettoyage préliminaire
Les jointures “traditionnelles” (i.e. non-probabilistes) sont sensibles à la casse et nécessitent des correspondances exactes entre les valeurs des colonnes utilisées comme clef/identifiant. Pour démontrer certaines des étapes de nettoyage que vous pourriez avoir besoin de faire avant de joindre vos données, nous allons commencer par nettoyer et aligner les dataframe linelist_mini
et hosp_info
.
Identifier les différences
Le nom de l’hôpital étant notre identifiant/clef commun aux deux jeux de données, nous avons besoin que les valeurs de la colonne hosp_name
dans le tableau hosp_info
correspondent aux valeurs de la colonne hospital
dans le tableau linelist_mini
.
Voici le dataframe linelist_mini
, affiché avec la fonction base R unique()
:
unique(linelist_mini$hospital)
[1] "Other"
[2] "Missing"
[3] "St. Mark's Maternity Hospital (SMMH)"
[4] "Port Hospital"
[5] "Military Hospital"
… et voici les valeurs dans le dataframe hosp_info
:
unique(hosp_info$hosp_name)
[1] "central hospital" "military" "port" "St. Mark's"
[5] "ignace" "sisters"
Il est clair que si certains hôpitaux sont présents dans les deux dataframes, leurs noms ne sont pas toujours orthographiés de la même manière.
Aligner les valeurs
Nettoyons les valeurs du jeu de données hosp_info
. Comme expliqué dans le chapitre sur le Nettoyage de données et fonctions essentielles, il est possible de recoder les valeurs à partir de critères logiques en utilisant la fonction case_when()
de dplyr. Pour les quatre hôpitaux communs dans les deux dataframes, nous modifions les noms pour les aligner avec les noms dans le tableau linelist_mini
(en ne touchant pas aux noms des autres hôpitaux grâce à l’argument TRUE ~ hosp_name
).
ATTENTION: Normalement on devrait créer une nouvelle colonne pour ce type de nettoyage (hosp_name_clean
par exemple), mais pour mieux comprendre ce qui se passe lors des étapes suivantes, nous modifions directement la colonne contenant les données “brutes”
<- hosp_info %>%
hosp_info mutate(
hosp_name = case_when(
# critère # nouvelles valeur
== "military" ~ "Military Hospital",
hosp_name == "port" ~ "Port Hospital",
hosp_name == "St. Mark's" ~ "St. Mark's Maternity Hospital (SMMH)",
hosp_name == "central hospital" ~ "Central Hospital",
hosp_name TRUE ~ hosp_name
) )
Les noms des hôpitaux qui apparaissent dans les deux bases de données sont désormais identiques. Il y a deux hôpitaux dans hosp_info
qui ne sont pas présents dans linelist_mini
, nous les traiterons plus tard, lors de la jointure.
unique(hosp_info$hosp_name)
[1] "Central Hospital"
[2] "Military Hospital"
[3] "Port Hospital"
[4] "St. Mark's Maternity Hospital (SMMH)"
[5] "ignace"
[6] "sisters"
Avant une jointure, il est souvent rassurant de convertir une colonne tout en minuscules ou majuscules. Pour cela, on peut utiliser mutate()
et une des colonnes de stringr (voir le chapitre sur les chaînes de caractères):
str_to_upper()
str_to_lower()
str_to_title()
14.2 Jointures à l’aide de dplyr
Le package dplyr offre plusieurs fonctions qui permettent d’effectuer des jointures différentes. dplyr est inclus dans le paquet tidyverse.
Un grand merci à https://github.com/gadenbuie pour les gifs informatifs !
Syntaxe générale
Les fonctions de jointure peuvent s’utiliser seules pour unir deux dataframes et créer un nouveau dataframe, mais aussi au sein d’un enchaînement de commandes (pipe avec %>%
) pour fusionner un dataframe dans un autre à la volée.
Dans l’exemple ci-dessous, la fonction left_join()
est utilisée de manière autonome pour créer un nouveau jeu de données (joined_data
). Les arguments à l’entrée sont les dataframes à unir/fusionner/joindre (df1
et df2
). Le premier dataframe listé est le dataframe de base, et le deuxième dataframe listé est joint à celui-ci.
Le troisième argument by =
précise quelle(s) colonne(s) sera utilisée pour faire la correspondance entre les lignes des deux dataframes (la clef). Si les noms de ces colonnes sont différents, fournissez-les dans un vecteur c()
comme dans l’exemple ci-dessous, les identifiants communs sont dans la colonne ID
dans df1
et dans la colonne identifier
dans df2
.
# Jointure basée sur les valeurs communes dans la colonne ID (df1) et la colonne "identifier" (df2)
<- left_join(df1, df2,
joined_data by = c("ID" = "identifier"))
Si la ou les colonnes “clef” à le même nom dans les deux tableaux, alors leur nom peut juste être fourni directement, avec des guillemets :
# Jointure basée sur les valeurs communes dans la colonne ID présente dans df1 et df2
<- left_join(df1, df2,
joined_data by = "ID")
S’il y a besoin de plusieurs colonnes pour identifier de manière unique les observations (i.e. créer une clef primaire), on peut lister plusieurs colonnes dans un vecteur et le passer à by
. Dans cet exemple, les lignes des deux dataframes sont unies si les valeurs sont identiques dans les trois colonnes.
# Jointure basée sur le prénom, le nom de famille et l'age : les lignes sont fusionnées si les valeurs sont alignées exactement
<- left_join(df1, df2,
joined_data by = c("name" = "firstname",
"surname" = "lastname",
"Age" = "age"))
Les fonctions de jointure peuvent également être exécutées dans un enchaînement d’instructions (ou pipe). Cela modifiera le jeu de données qui est passée dans le pipe.
Dans l’exemple ci-dessous, df1
est pipé, df2
lui est joint, et df1
est ainsi modifié et redéfini.
<- df1 %>%
df1 filter(date_onset < as.Date("2020-03-05")) %>% # nettoyage divers
left_join(df2, by = c("ID" = "identifier")) # jointure de df2 à df1
ATTENTION: Les jointures respectent les majuscules/minuscules ! Il peut donc être utile de convertir les colonnes utilisées comme clefs en minuscules ou majuscules. Voir le chapitre sur les chaînes de caractères
Jointures à gauche et droite
Une jointure à gauche ou droite est une opération très couramment utilisée pour ajouter des informations à un dataframe, en particulier dans les analyses épidémiologiques. Les nouvelles informations sont ajoutées uniquement aux lignes qui existaient déjà dans le dataframe de “référence”.
En utilisant ces jointures, l’ordre d’écriture des dataframes dans la commande est important.
- Dans une jointure à gauche, le premier dataframe écrit est utilisé comme “référence” à laquelle on adjoint les informations venant de l’autre table.
- Dans une jointure à droite, le second dataframe est la référence à laquelle on rajoute les informations venant du premier dataframe.
Plus précisement :
* Toutes les lignes présentes dans le dataframe de référence sont conservées. Les informations contenues dans le dataframe secondaire sont adjointes au dataframe de référence uniquement s’il existe une correspondance via la ou les colonnes d’identification/clefs.
Les lignes du dataframe secondaire qui ne correspondent pas sont abandonnées.
Si plusieurs lignes du dataframe utilisé comme référence correspondent à une ligne dans le dataframe secondaire (many-to-one), les informations du dataframe secondaire sont ajoutées à chaque ligne du dataframe de référence correspondantes.
Si une ligne du dataframe de référence correspond à plusieurs lignes dans le dataframe secondaire (one-to-many), toutes les combinaisons sont données, ce qui signifie que de nouvelles lignes sont ajoutées au dataframe de référence !.
Exemples animés de jointures gauche et droite (source de l’image)
Exemple
Voici le résultat d’un left_join()
de hosp_info
(dataframe secondaire, voir ici) dans/vers linelist_mini
(dataframe de référence, voir ici). La linelist_mini
originale a nrow(linelist_mini)
lignes. La linelist_mini
modifiée est affichée. On constate que :
Deux nouvelles colonnes,
catchment_pop
etlevel
ont été ajoutées sur le côté gauche delinelist_mini
.Toutes les lignes originales du dataframe de référence
linelist_mini
ont été conservées.Toutes les lignes originales de
linelist_mini
pour “Military Hospital” ont été dupliquées car elles correspondaient à deux lignes dans le dataframe secondaire, et donc les deux combinaisons ont été retournées.La colonne d’identifiant/clef de jointure du dataframe secondaire (
hosp_name
) a disparu car elle est redondante avec la colonne d’identifiant du dataframe de référence (hospital
)Lorsqu’une ligne du dataframe de gauche ne correspond à aucune du dataframe de droite (par exemple, lorsque
hospital
est “Autre” ou “Manquant”), les observations renvoyées dans les colonnes venant du dataframe de droite sontNA
.Les lignes du dataframe de droite qui ne correspondent pas au dataframe gauche (hôpitaux “sisters” et “ignace”) ont été abandonnées.
%>%
linelist_mini left_join(hosp_info, by = c("hospital" = "hosp_name"))
Warning in left_join(., hosp_info, by = c(hospital = "hosp_name")): Detected an unexpected many-to-many relationship between `x` and `y`.
ℹ Row 5 of `x` matches multiple rows in `y`.
ℹ Row 4 of `y` matches multiple rows in `x`.
ℹ If a many-to-many relationship is expected, set `relationship =
"many-to-many"` to silence this warning.
Jointure à gauche ou jointure à droite ?
Pour répondre à la question, il faut décider quel dataframe doit conserver toutes ses lignes, et l’utiliser comme dataframe de référence. Une jonction à gauche conserve toutes les lignes du premier dataframe écrit dans la commande, tandis qu’une jonction à droite conserve toutes les lignes du second dataframe.
Les deux instructions ci-dessous retournent le même résultat : 10 lignes de hosp_info
jointes dans un dataframe linelist_mini
, mais elles utilisent des jointures différentes. Le résultat est que l’ordre des colonnes sera différent selon que hosp_info
arrive “par la droite” (dans la jointure à gauche) ou arrive “par la gauche” (dans la jointure à droite). L’ordre des lignes peut également changer en conséquence. Néanmoins ces deux conséquences peuvent être traitées ultérieurement, en utilisant select()
pour réordonner les colonnes ou arrange()
pour trier les lignes.
# On obtient le même jeu de données, mais avec l'ordre des colonnes et des lignes différent
left_join(linelist_mini, hosp_info, by = c("hospital" = "hosp_name"))
right_join(hosp_info, linelist_mini, by = c("hosp_name" = "hospital"))
Voici le résultat d’une fusion de hosp_info
dans linelist_mini
par une jointure à gauche (nouvelles colonnes ajoutées par la droite) :
Warning in left_join(linelist_mini, hosp_info, by = c(hospital = "hosp_name")): Detected an unexpected many-to-many relationship between `x` and `y`.
ℹ Row 5 of `x` matches multiple rows in `y`.
ℹ Row 4 of `y` matches multiple rows in `x`.
ℹ If a many-to-many relationship is expected, set `relationship =
"many-to-many"` to silence this warning.
Voici le résultat de la fusion de hosp_info
dans linelist_mini
par une jointure à droite (nouvelles colonnes ajoutées par la gauche) :
Warning in right_join(hosp_info, linelist_mini, by = c(hosp_name = "hospital")): Detected an unexpected many-to-many relationship between `x` and `y`.
ℹ Row 4 of `x` matches multiple rows in `y`.
ℹ Row 5 of `y` matches multiple rows in `x`.
ℹ If a many-to-many relationship is expected, set `relationship =
"many-to-many"` to silence this warning.
Une autre chose à considérer est si la jointure est réalisée au sein d’une chaîne d’instructions pipées (%>%
). Si le dataframe transmis par le pipe doit être utilisé comme référence, utilisez une jointure à gauche pour lui adjoindre de nouvelles données.
Jointure complète
La jointure complète est la plus inclusive des jointures, elle renvoie toutes les lignes des deux dataframes fusionnés.
Si des lignes ne sont présentes que dans l’un des dataframes fusionnés (i.e aucune correspondance n’est trouvée), elles seront inclues dans le dataframe final (qui s’allonge donc), avec des NA
pour combler les vides.
Lorsque vous effectuez une jointure, surveillez attentivement le nombre de colonnes et de lignes pour vérifier le nombre de lignes des dataframes en entrée, et du dataframe fusionné. Cela vous permettra notamment de détecter des problèmes de correspondance dus à la sensibilité à la casse ou à des correspondances inexactes.
Le dataframe de référence utilisé comme base est celui qui est écrit en premier dans la commande. Dans une jointure complète, Lequel des deux dataframes est écrit en premier n’affecte que l’ordre des lignes, l’ordre des colonnes et le nom des colonnes clef retenues.
Exemple animé d’une jointure complète (image source)
Exemple
Voici la sortie d’une jointure complète du dataframe hosp_info
(originellement nrow(hosp_info)
lignes, voir ici) avec linelist_mini
(originellement nrow(linelist_mini)
lignes, voir ici). On constate que :
Toutes les lignes du dataframe de référence sont conservées (
linelist_mini
).Les lignes du second dataframe qui n’ont pas de correspondance avec le premier dataframe sont conservées (“ignace” et “sisters”), et les valeurs des colonnes apportées par le dataframe de référence,
case_id
etonset
, sont complétées par des valeurs manquantes.De même, les lignes du dataframe de référence qui ne correspondent pas à la ligne secondaire (“Autre” et “Manquant”) sont conservées, les colonnes secondaires
catchment_pop
etlevel
étant remplies de valeurs manquantes.Dans le cas d’une correspondance un-à-plusieurs ou plusieurs-à-un (par exemple, des lignes pour “Hôpital militaire”), toutes les combinaisons possibles sont retournées (ce qui allonge le dataframe final).
Seule la colonne d’identification du dataframe de référence est conservée (
hospital
).
%>%
linelist_mini full_join(hosp_info, by = c("hospital" = "hosp_name"))
Warning in full_join(., hosp_info, by = c(hospital = "hosp_name")): Detected an unexpected many-to-many relationship between `x` and `y`.
ℹ Row 5 of `x` matches multiple rows in `y`.
ℹ Row 4 of `y` matches multiple rows in `x`.
ℹ If a many-to-many relationship is expected, set `relationship =
"many-to-many"` to silence this warning.
Jointure interne
La jointure interne est la plus restrictive des jointures : elle renvoie uniquement les lignes avec des correspondances dans les deux dataframes.
Le nombre de lignes dans le dataframe de référence peut ainsi diminuer. Le choix du dataframe à passer en premier à la fonction n’aura pas d’impact sur les lignes conservées, mais affectera l’ordre des colonnes, l’ordre des lignes et clefs d’identification retenues.
Exemple animé d’une jointure complète : (image source)
Exemple
Voici la sortie d’un inner_join()
de la linelist_mini
(référence) avec hosp_info
(secondaire). On constate que :
- Les lignes du dataframe de référence sans correspondance dans le second dataframe sont supprimées (lignes où
hospital
est “Missing” ou “Other”).
- De même, les lignes du dataframe secondaires qui n’ont pas de correspondance dans le dataframe de référence (lignes où
hosp_name
est “sisters” ou “ignace”) sont supprimées.
- Seule la colonne clef du dataframe de référence est conservée (
hospital
).
%>%
linelist_mini inner_join(hosp_info,
by = c("hospital" = "hosp_name"))
Warning in inner_join(., hosp_info, by = c(hospital = "hosp_name")): Detected an unexpected many-to-many relationship between `x` and `y`.
ℹ Row 5 of `x` matches multiple rows in `y`.
ℹ Row 4 of `y` matches multiple rows in `x`.
ℹ If a many-to-many relationship is expected, set `relationship =
"many-to-many"` to silence this warning.
Semi jointure
Les semi jointures sont des jointures dites “filtrantes”, qui utilisent la correspondance avec un second dataframe pour filtrer le dataframe de référence.
La semi jointure garde toutes les observations du dataframe de référence qui ont une correspondance dans le dataframe secondaire. Mais les colonnes du dataframe secondaire ne sont pas ajoutées et les lignes du dataframe de référence ne sont pas dupliquées s’il y a des correspondances multiples.
Plus d’explications sur les semi-jointures ici.
Exemple animé d’une semi jointure (image source)
Voici un exemple qui retourne les lignes du dataframe hosp_info
qui ont une correspondance dans le dataframe linelist_mini
en utilisant le nom de l’hôpital comme clé de jointure/identifiant.
%>%
hosp_info semi_join(linelist_mini,
by = c("hosp_name" = "hospital"))
hosp_name catchment_pop level
1 Military Hospital 40500 Secondary
2 Military Hospital 10000 Primary
3 Port Hospital 50280 Secondary
4 St. Mark's Maternity Hospital (SMMH) 12000 Secondary
Anti-jointure anti_join()
L’anti-jointure est une autre type de jointure filtrante, qui, à l’opposé du semi-join, ne renvoie que les lignes du dataframe de référence qui n’ont PAS de correspondance dans le dataframe secondaire.
Plus de détails sur les jointures filtrantes ici.
Voici quelques cas d’usage de l’anti-jointure : identifier les observations non présentes dans un autre dataframe, identifier les typos qui compliquent une jointure (se focaliser sur les observations qui auraient du correspondre), examiner les observations qui ont été exclues d’une jointure etc.
Comme pour les jointures à droite (right_join()
) et à gauche (left_join()
), l’ordre dans lequel sont passés les dataframe a de l’importance. Dans les joins filtrants, on ne renvoie que les lignes présentes dans le dataframe de référence (écrit en premier), comme on peut le voir dans l’animation ci-dessous (la ligne 4, violette, du dataframe secondaire n’est pas retournée, alors qu’elle ne matche avec aucune ligne du dataframe de référence).
Exemple animé d’une anti-jointure (image source)
Exemple simple d’anti-jointure
Un cas d’utilisation simple est de rechercher les hôpitaux dans le tableau hosp_info
qui n’ont pas de cas présents dans le tableau linelist_mini
. Nous rentrons hosp_info
en premier, comme dataframe de référence, puis linelist_mini
, la seconde table à comparer pour trouver les hôpitaux qui n’y sont pas présents.
%>%
hosp_info anti_join(linelist_mini, by = c("hosp_name" = "hospital"))
Exemple d’anti-jointure plus complexe
Imaginons cette fois-ci que nous avons exécuté une jointure interne (inner_join()
) entre les dataframes linelist_mini
et hosp_info
. Cette opération ne retourne qu’un sous-ensemble des lignes originales de linelist_mini
, car certains hôpitaux ne sont pas présents dans hosp_info
.
%>%
linelist_mini inner_join(hosp_info, by = c("hospital" = "hosp_name"))
Warning in inner_join(., hosp_info, by = c(hospital = "hosp_name")): Detected an unexpected many-to-many relationship between `x` and `y`.
ℹ Row 5 of `x` matches multiple rows in `y`.
ℹ Row 4 of `y` matches multiple rows in `x`.
ℹ If a many-to-many relationship is expected, set `relationship =
"many-to-many"` to silence this warning.
Nous pouvons utiliser une anti jointure pour inspecter les éléments de linelist_mini
qui ont été exclus lors de la jointure interne, avec les mêmes paramètres (linelist_mini
comme dataframe de référence).
%>%
linelist_mini anti_join(hosp_info, by = c("hospital" = "hosp_name"))
Pour voir les lignes d’hosp_info
exclues lors de la jointure interne, nous pourrions aussi exécuter une anti-jointure en utilisant hosp_info
comme table de référence.
14.3 Apparienement probabiliste
Si l’on ne dispose pas d’un identifiant unique commun à tous les dataframes sur lequel se baser, il est possible d’utiliser un algorithme de correspondance probabiliste. Cet algorithme cherche des correspondances entre les observations sur la base de la similarité (par exemple, la distance entre les chaînes de caractères Jaro-Winkler ou la distance numérique). Nous illustrons ce concept ci-dessous à l’aide du paquet fastLink.
Charger les paquets
::p_load(
pacman# manipulation de données et visualisation
tidyverse, # appariement d'observations
fastLink )
Nous créons d’abord deux petits jeux de données d’exemple que nous utiliserons pour démontrer l’appariement/la correspondance probabiliste (cases
et test_results
) :
# Création des jeux de données
<- tribble(
cases ~gender, ~first, ~middle, ~last, ~yr, ~mon, ~day, ~district,
"M", "Amir", NA, "Khan", 1989, 11, 22, "River",
"M", "Anthony", "B.", "Smith", 1970, 09, 19, "River",
"F", "Marialisa", "Contreras", "Rodrigues", 1972, 04, 15, "River",
"F", "Elizabeth", "Casteel", "Chase", 1954, 03, 03, "City",
"M", "Jose", "Sanchez", "Lopez", 1996, 01, 06, "City",
"F", "Cassidy", "Jones", "Davis", 1980, 07, 20, "City",
"M", "Michael", "Murphy", "O'Calaghan",1969, 04, 12, "Rural",
"M", "Oliver", "Laurent", "De Bordow" , 1971, 02, 04, "River",
"F", "Blessing", NA, "Adebayo", 1955, 02, 14, "Rural"
)
<- tribble(
results ~gender, ~first, ~middle, ~last, ~yr, ~mon, ~day, ~district, ~result,
"M", "Amir", NA, "Khan", 1989, 11, 22, "River", "positive",
"M", "Tony", "B", "Smith", 1970, 09, 19, "River", "positive",
"F", "Maria", "Contreras", "Rodriguez", 1972, 04, 15, "Cty", "negative",
"F", "Betty", "Castel", "Chase", 1954, 03, 30, "City", "positive",
"F", "Andrea", NA, "Kumaraswamy", 2001, 01, 05, "Rural", "positive",
"F", "Caroline", NA, "Wang", 1988, 12, 11, "Rural", "negative",
"F", "Trang", NA, "Nguyen", 1981, 06, 10, "Rural", "positive",
"M", "Olivier" , "Laurent", "De Bordeaux", NA, NA, NA, "River", "positive",
"M", "Mike", "Murphy", "O'Callaghan", 1969, 04, 12, "Rural", "negative",
"F", "Cassidy", "Jones", "Davis", 1980, 07, 02, "City", "positive",
"M", "Mohammad", NA, "Ali", 1942, 01, 17, "City", "negative",
NA, "Jose", "Sanchez", "Lopez", 1995, 01, 06, "City", "negative",
"M", "Abubakar", NA, "Abullahi", 1960, 01, 01, "River", "positive",
"F", "Maria", "Salinas", "Contreras", 1955, 03, 03, "River", "positive"
)
Le dataframe cases
a 9 observations de patients attendant les résultats de leur test.
Le dataframe test_results
a 14 observations et contient la colonne result
, qui contient des informations que nous voudrions rapatrier dans le jeu de données cases
en utilisant un algorithme probabiliste pour faire correspondre les observations.
Appariement probabiliste
La fonction fastLink()
du paquet fastLink peut être utilisée pour appliquer un algorithme probabiliste de correspondance. Voici quelques informations basiques, mais vous pouvez en savoir plus en tapant ?fastLink
dans la console.
- Définir les deux dataframes à comparer grâce aux arguments
dfA =
etdfB =
.
- Dans
varnames =
, indiquer les noms de toutes les colonnes à utiliser pour la comparaison. Ces colonnes doivent exister à la fois dansdfA
etdfB
.
- Dans
stringdist.match =
, donner les colonnes sur lesquelles effectuer le calcul de la distance de similarité entre les chaînes de caractère (ce ou ces colonnes doivent être présentes dansvarnames
).
- Dans
numeric.match =
, donner les colonnes sur lesquelles calculer une mesure de distance numérique (ce ou ces colonnes doivent être présentes dansvarnames
).
- Les valeurs manquantes sont ignorées.
- Par défaut, chaque ligne de l’un des deux dataframes est comparée à une ligne au maximum de l’autre dataframe. Si vous voulez voir toutes les correspondances évaluées, choisissez
dedupe.matches = FALSE
. La dé-duplication est faite en utilisant la technique de programmation linéaire de Winkler.
Astuce : divisez une colonne de date en trois colonnes numériques distinctes en utilisant day()
, month()
, et year()
du package lubridate.
Le seuil par défaut pour les correspondances est de 0.94 (threshold.match =
) mais il peut être ajusté. Un seuil plus élevé peut produire plus de faux négatifs (des lignes qui ne correspondent pas alors qu’elles devraient correspondre) et un seuil plus bas peut produire plus de faux positifs.
Ci-dessous, les données sont comparées sur la base de la distance de similarité entre les chaînes de caractères dans les colonnes du nom et du district, et sur la base de la distance numérique pour l’année, le mois et le jour de naissance. Un seuil de correspondance de 95% de probabilité est fixé.
<- fastLink::fastLink(
fl_output dfA = cases,
dfB = results,
varnames = c("gender", "first", "middle", "last", "yr", "mon", "day", "district"),
stringdist.match = c("first", "middle", "last", "district"),
numeric.match = c("yr", "mon", "day"),
threshold.match = 0.95)
====================
fastLink(): Fast Probabilistic Record Linkage
====================
If you set return.all to FALSE, you will not be able to calculate a confusion table as a summary statistic.
Calculating matches for each variable.
Getting counts for parameter estimation.
Parallelizing calculation using OpenMP. 1 threads out of 8 are used.
Running the EM algorithm.
Getting the indices of estimated matches.
Parallelizing calculation using OpenMP. 1 threads out of 8 are used.
Deduping the estimated matches.
Getting the match patterns for each estimated match.
Vérification des correspondance
L’objet fl_output
contient la sortie de la fonction fastLink()
. Cet objet est une liste (classe list
) qui contient plusieurs dataframes détaillant les résultats de l’analyse des correspondances. Le dataframe matches
contient les correspondances les plus probables entre cases
et results
. On peut y accéder avec la commande fl_output$matches
. Ci-dessous, nous l’enregistrons sous le nom de my_matches
pour faciliter son accès ultérieur.
Le dataframe my_matches
contient deux colonnes contenant les numéros de lignes/indices (aussi appelés “rownames”) de cases
(“inds.a”) et de results
(“inds.b”), représentant les meilleures correspondances. Si un numéro de ligne d’un dataframe est manquant, alors aucune correspondance n’a été trouvée dans l’autre dataframe au seuil de correspondance spécifié.
# print matches
<- fl_output$matches
my_matches my_matches
inds.a inds.b
1 1 1
2 2 2
3 3 3
4 4 4
5 8 8
6 7 9
7 6 10
8 5 12
On observe que :
- Les correspondances ont été trouvées malgré les légères différences dans l’orthographe des noms et les dates de naissance, c’est la beauté de ce type d’approche :
- “Tony B. Smith” correspond à “Anthony B. Smith”.
- “Maria Rodriguez” correspond à “Marialisa Rodrigues”.
- “Betty Chase” correspond à “Elizabeth Chase”.
- “Olivier Laurent De Bordeaux” correspond à “Oliver Laurent De Bordow” (date de naissance manquante ignorée).
- “Tony B. Smith” correspond à “Anthony B. Smith”.
- Une ligne de
cases
(pour “Blessing Adebayo”, ligne 9) n’avait pas de bonne correspondance dansresults
, elle n’est donc pas présente dansmy_matches
.
Jointure basée sur les correspondances probabilistes.
Pour utiliser ces correspondances afin de joindre les results
aux cases
, une stratégie consiste à :
- Utiliser
left_join()
pour joindremy_matches
àcases
(en faisant correspondre les rownames danscases
à “inds.a” dansmy_matches
)
- Utiliser ensuite un autre
left_join()
pour joindreresults
àcases
(en faisant correspondre les “inds.b” nouvellement acquis danscases
aux noms de domaine dansresults
).
Avant les jointures, nous devons nettoyer les trois dataframes :
- Les numéros de ligne (“rowname”) de
dfA
etdfB
doivent être convertis en une colonne.
- Les deux colonnes de
my_matches
sont converties en chaînes de caractères, donc elles peuvent être jointes aux caractères rownames.
# Préparation des dataframes avant la jointure
#############################
# Covertir les numéros de lignes en colonne
<- cases %>% rownames_to_column()
cases_clean <- results %>% rownames_to_column()
results_clean
# Convertir toutes les colonnes du dataframe des correspondances en texte pour pouvoire les joindre aux numéros de ligne
<- my_matches %>%
matches_clean mutate(across(everything(), as.character))
# Joindre `clean_matches` à dfA, puis ajouer dfB
###################################
# la colonne "inds.b" est ajoutée à dfA
<- left_join(cases_clean, matches_clean, by = c("rowname" = "inds.a"))
complete
# les colonnes de dfB sont rappatriées
<- left_join(complete, results_clean, by = c("inds.b" = "rowname")) complete
Le dataframe complete
ainsi crée contient toutes les colonnes de cases
et de results
. Beaucoup d’entre elles se retrouvent ajoutées avec les suffixes “.x” et “.y”, parce que les noms des colonnes seraient dupliqués sinon.
Pour obtenir seulement les 9 observations “originales” dans cases
avec la ou les nouvelles colonnes de results
, utiliser select()
sur results
avant les jointures, de sorte que e dataframe ne contienne que les rownames et les colonnes que vous voulez ajouter à cases
(par exemple la colonne result
).
<- cases %>% rownames_to_column()
cases_clean
<- results %>%
results_clean rownames_to_column() %>%
select(rowname, result) # Sélectionner certaines colonnes
<- my_matches %>%
matches_clean mutate(across(everything(), as.character))
# jointure
<- left_join(cases_clean, matches_clean, by = c("rowname" = "inds.a"))
complete <- left_join(complete, results_clean, by = c("inds.b" = "rowname")) complete
Si vous voulez ne garder que les lignes qui avec des correspondances dans l’un ou l’autre des des dataframe, vous pouvez utiliser les codes ci-dessous :
<- cases[my_matches$inds.a,] # Lignes de `cases` qui matchent une ligne dans 'results'
cases_matched <- results[my_matches$inds.b,] # Lignes de 'results qui matchent une ligne dans `cases` results_matched
Ou pour ne voir que les lignes sans correspondances :
<- cases[!rownames(cases) %in% my_matches$inds.a,] # Lignes dans `cases` sans matchs dans `results`
cases_not_matched <- results[!rownames(results) %in% my_matches$inds.b,] # Lignes dans `results` sans matchs dans `cases` results_not_matched
Déduplication probabiliste
La correspondance probabiliste peut également être utilisée pour dé-dupliquer un jeu de données. Voir la page sur la dé-duplication pour d’autres méthodes de dé-duplication.
Ici, nous modifions le tableau cases
, en ajoutant des lignes supplémentaires qui peuvent être des doublons de lignes existantes, et l’appelons cases_dup
; voir “Tony” avec “Anthony”, et “Marialisa Rodrigues” avec “Maria Rodriguez”.
On peut désormais utiliser la fonction fastLink()
comme précédemment, mais en comparant le jeu de données cases_dup
à lui-même. Lorsque les dataframes fournis en argument sont identiques, la fonction suppose que vous voulez dé-dupliquer.
Notez que nous ne spécifions pas stringdist.match =
ou numeric.match =
comme nous le faisions précédemment.
## Utiliser fastLink sur le même jeu de données
<- fastLink(
dedupe_output dfA = cases_dup,
dfB = cases_dup,
varnames = c("gender", "first", "middle", "last", "yr", "mon", "day", "district")
)
====================
fastLink(): Fast Probabilistic Record Linkage
====================
If you set return.all to FALSE, you will not be able to calculate a confusion table as a summary statistic.
dfA and dfB are identical, assuming deduplication of a single data set.
Setting return.all to FALSE.
Calculating matches for each variable.
Getting counts for parameter estimation.
Parallelizing calculation using OpenMP. 1 threads out of 8 are used.
Running the EM algorithm.
Getting the indices of estimated matches.
Parallelizing calculation using OpenMP. 1 threads out of 8 are used.
Calculating the posterior for each pair of matched observations.
Getting the match patterns for each estimated match.
La fonction getMatches()
permet d’examiner la sortie de fastLink
pour rechercher les doublons potentiels. Il faut fournir le dataframe d’origine à dfA =
et dfB =
, ainsi que la sortie de fastLink()
à fl.out =
.
Note : fl.out
doit nécessairement être de la classe fastLink.dedupe
, ou en d’autres termes, le résultat de fastLink()
.
## Executer getMatches()
<- getMatches(
cases_dedupe dfA = cases_dup,
dfB = cases_dup,
fl.out = dedupe_output)
La colonne la plus à droite indique les identifiants des doublons. Ici les deux dernières lignes sont identifiées comme étant des doublons probables des lignes 2 et 3.
Pour obtenir les numéros de ligne des lignes qui sont potentiellement des doublons, il suffit de compter le nombre de lignes par valeur unique dans la colonne dedupe.ids
, puis de filtrer pour ne garder que celles qui ont plus d’une ligne. Dans ce cas, nous obtenons laisse les lignes 2 et 3.
%>%
cases_dedupe count(dedupe.ids) %>%
filter(n > 1)
dedupe.ids n
1 2 2
2 3 2
Pour inspecter les doublons, on peut utiliser le numéro de ligne pour obtenir la ligne complète :
# Afficher la ligne 2 et ses duplicats probables
$dedupe.ids == 2,] cases_dedupe[cases_dedupe
gender first middle last yr mon day district dedupe.ids
2 M Anthony B. Smith 1970 9 19 River 2
10 M Tony B. Smith 1970 9 19 River 2
14.4 Assembler et aligner des dataframes
Une autre manière de combiner deux dataframes consiste à les assembler/concaténer/coller/aligner. On peut également considérer cette méthode comme un ajout de lignes ou de colonnes.
Cette section explique également comment “aligner” l’ordre des lignes d’un dataframe sur celui d’un autre dataframe. Ce sujet est abordé ci-dessous dans la section consacrée à la liaison des colonnes.
Assembler verticallement
La fonction bind_rows()
de dplyr permet de coller les lignes d’un dataframe à la suite d’un autre dataframe (verticalement, donc). Elle est très inclusive : toute colonne présente dans l’un ou l’autre des dataframes sera incluse dans la sortie.
Quelques notes :
Contrairement à la version base de R
row.bind()
, dplyr’sbind_rows()
n’exige pas que l’ordre des colonnes soit le même dans les deux dataframes à assembler. Du moment que les noms des colonnes sont orthographiés de manière identique, la fonction les alignera correctement.Il est possible de fournir une chaîne de caractères à l’argument
.id =
pour produire une nouvelle colonne qui servira à identifier de quel dataframe chaque ligne provient à l’origine.Il est possible d’utiliser
bind_rows()
sur une liste de dataframes possédant la même structure pour les combiner en un seul. Vous trouverez un exemple de cette action dans la page Itération, boucles et listes qui importe plusieurs linelists avec le paquet purrr.
Un exemple classique d’assemblage de dataframes est d’ajouter une ligne “total” à un tableau descriptif créé avec la fonction summarise()
de dplyr. Ci-dessous, nous créons un tableau des nombres de cas et des valeurs médianes de CT par hôpital et y ajoutons une ligne de total.
La fonction summarise()
est utilisée sur des données groupées par hôpital pour retourner un dataframe récapitulatif par hôpital. Malheureusement, elle ne produit pas automatiquement une ligne de “totaux”, donc nous devons la rajouter nous même. Nous l’obtenons en résumant à nouveau ces données, mais sans grouper par hôpital. Cela produit un deuxième dataframe d’une seule ligne, que nous concaténons ensuite au premier pour obtenir le tableau final.
Vous trouverez d’autres exemples de ce type dans les pages Tableaux descriptifs et Tableaux de présentation.
# Créer la table de base
###################
<- linelist %>%
hosp_summary group_by(hospital) %>% # grouper les données par hôpital
summarise( # Créer résumé :
cases = n(), # NNombre de lignes par hôpital
ct_value_med = median(ct_blood, na.rm=T)) # Médiane du CT par hôpital
Voici à quoi ressemble hosp_summary
:
Maintenant nous créons un dataframe d’une seule ligne contenant les statistiques pour tous les hôpitaux (données non groupées) :
# Créer la ligne de totaux
###############
<- linelist %>%
totals summarise(
cases = n(), # Nb lignes dataframe entier
ct_value_med = median(ct_blood, na.rm=T)) # Médiane du CT
Ci-dessous, le dataframe totals
. Remarquez qu’il n’y a que deux colonnes alors que hosp_summary
en contenant trois. Ce n’est pas un problème.
Nous utilisons à présent bind_rows()
pour assembler les deux dataframes :
# Combiner les deux dataframes ensemble
<- bind_rows(hosp_summary, totals) combined
Pour le résultat ci-dessous. Dans la dernière ligne, une valeur vide NA
a été automatiquement insérée dans la colonne hospital
, qui n’était pas dans hosp_summary
. Comme expliqué dans la page Tableaux de présentation, il est possible de “remplir” cette cellule avec “Total” en utilisant replace_na()
.
Assembler des colonnes latéralement
De manière assez similaire, il existe une fonction bind_cols()
dans dplyr qui combine deux dataframes latéralement. En revanche, contrairement aux jointures, les lignes sont alignées les unes aux autres par position : la ligne X du dataframe 1 sera alignée à la ligne X du dataframe 2.
Par exemple, nous allons assembler plusieurs tableaux récapitulatifs. Nous montrerons au passage comment réarranger l’ordre des lignes d’un dataframe pour qu’il corresponde à l’ordre dans un autre dataframe, à l’aide de la fonction match()
.
Ici, nous définissons case_info
comme un dataframe récapitulatif des cas de la liste linéaire, par hôpital, avec le nombre de cas et le nombre de décès.
# Information sur les cas
<- linelist %>%
case_info group_by(hospital) %>%
summarise(
cases = n(),
deaths = sum(outcome == "Death", na.rm=T)
)
Nous définissons également contact_fu
, un autre dataframe contenant des informations sur le pourcentage de contacts exposés ayant fait l’objet d’une enquête et d’un “suivi”, toujours par hôpital.
<- data.frame(
contact_fu hospital = c("St. Mark's Maternity Hospital (SMMH)", "Military Hospital", "Missing", "Central Hospital", "Port Hospital", "Other"),
investigated = c("80%", "82%", NA, "78%", "64%", "55%"),
per_fu = c("60%", "25%", NA, "20%", "75%", "80%")
)
Notez que les hôpitaux sont les mêmes, mais dans un ordre différent dans les deux dataframes. La solution la plus simple serait d’utiliser un left_join()
sur la colonne hospital
, mais il est aussi possible d’utiliser bind_cols()
avec une étape supplémentaire.
Utiliser match()
pour homogénéiser l’ordre des lignes
Comme l’ordre des lignes est différent, un simple bind_cols()
entraînerait une mauvaise correspondance des données. Pour résoudre ce problème, nous pouvons utiliser la fonction match()
de base R pour aligner les lignes d’un dataframes dans le même ordre que dans un autre. Pour cette approche, nous supposons qu’il n’y a pas de doublons dans les dataframes.
Lorsque nous utilisons match()
, la syntaxe est match(vecteur/colonne dans l'ordre désiré, colonne de dataframe à ordonner)
, où le premier argument est l’ordre souhaité (soit un vecteur autonome, soit une colonne d’un dataframe), et le second argument est la colonne du dataframe qui sera réordonnée. La sortie de match()
est un vecteur de nombres représentant l’ordre correct des positions. Vous pouvez en savoir plus avec ?match
.
match(case_info$hospital, contact_fu$hospital)
[1] 4 2 3 6 5 1
On peut utiliser ce vecteur numérique pour réorganiser le dataframe : placez-le entre des crochets [ ]
avant la virgule. Pour en savoir plus sur la syntaxe des crochets pour séléctionner les lignes et/ou colonnes d’un dataframe en base R, consultez la page Bases de R. La commande ci-dessous crée un nouveau dataframe, défini comme l’ancien dataframe dans lequel les lignes sont ordonnées selon le tableau numérique ci-dessus.
<- contact_fu[match(case_info$hospital, contact_fu$hospital),] contact_fu_aligned
Maintenant nous pouvons lier les colonnes du dataframe ensemble, avec l’ordre correct des lignes respecté. Notez que certaines colonnes sont dupliquées et devront être nettoyées avec rename()
. Pour en savoir plus sur bind_rows()
, rendez-vous ici.
bind_cols(case_info, contact_fu)
New names:
• `hospital` -> `hospital...1`
• `hospital` -> `hospital...4`
# A tibble: 6 × 6
hospital...1 cases deaths hospital...4 investigated per_fu
<chr> <int> <int> <chr> <chr> <chr>
1 Central Hospital 454 193 St. Mark's … 80% 60%
2 Military Hospital 896 399 Military Ho… 82% 25%
3 Missing 1469 611 Missing <NA> <NA>
4 Other 885 395 Central Hos… 78% 20%
5 Port Hospital 1762 785 Port Hospit… 64% 75%
6 St. Mark's Maternity Hospital (… 422 199 Other 55% 80%
Une alternative base R à bind_cols
est cbind()
, qui effectue la même opération.
14.5 Resources
La page du tidyverse sur les jointures
Le chapitre sur les données relationelles dans R for Data Science
La page de dplyr sur bind on binding
Une vignette surfastLink sur la page Github du paquet
Publication décrivant la méthodologie de fastLink
Publication décrivant le paquetRecordLinkage