× ¿Necesitas ayuda para aprender R? Inscríbete en el Curso de introducción a R de Applied Epi, prueba nuestros Tutoriales gratuitos de R, escribe en nuestro Foro de preguntas y respuestas, o pregunta por nuestra Asistencia técnica para R.

14 Unir datos

Arriba: un ejemplo animado de una unión por la izquierda (fuente de la imagen)

Esta página describe diferentes formas de unir datos (“join”, “match”, “link”, “bind”) y combinar dataframes.

14.1 Preparación

Cargar paquetes

Este trozo de código muestra la carga de los paquetes necesarios para los análisis. En este manual destacamos p_load() de pacman, que instala el paquete si es necesario y lo carga para su uso. También puede cargar los paquetes instalados con library() de R base. Consulta la página sobre Fundamentos de R para obtener más información sobre los paquetes de R.

pacman::p_load(
  rio,            # importar y exportar
  here,           # localizar archivos 
  tidyverse,      # gestión y visualización de datos
  RecordLinkage,  # coincidencias probabilísticas
  fastLink        # coincidencias probabilísticas
)

Importar datos

Para empezar, importamos la lista de casos limpiada de una epidemia de ébola simulada. Si quieres seguir el proceso, clica aquí para descargar el listado “limpio” (como archivo .rds). Importa los datos con la función import() del paquete rio (maneja muchos tipos de archivos como .xlsx, .csv, .rds - mira la página de importación y exportación para más detalles).

# importar el listado de casos
linelist <- import("linelist_cleaned.rds")

A continuación se muestran las primeras 50 filas del listado.

Datos de los ejemplos

En la sección de unión que sigue, utilizaremos los siguientes datos:

  1. Una versión “en miniatura” de casos de linelist, que contiene sólo las columnas case_id, date_onset, y hospital, y sólo las 10 primeras filas
  2. Un dataframe separado llamado hosp_info, que contiene más detalles sobre cada hospital

En la sección sobre el emparejamiento probabilístico, utilizaremos dos pequeños conjuntos de datos diferentes. El código para crear esos conjuntos de datos se da en esa sección.

“Miniatura” de casos de linelist

A continuación se muestra la lista de casos en miniatura, que contiene sólo 10 filas y sólo las columnas case_id, date_onset, y hospital.

linelist_mini <- linelist %>%                 # empezar con linelist original
  select(case_id, date_onset, hospital) %>%   # seleccionar columnas
  head(10)                                    # sólo las 10 primeras filas

dataframe de información hospitalaria

A continuación se muestra el código para crear un dataframe separado con información adicional sobre siete hospitales (la población de captación y el nivel de atención disponible). Obsérvese que el nombre “Hospital Militar” pertenece a dos hospitales diferentes: uno de nivel primario que atiende a 10000 residentes y otro de nivel secundario que atiende a 50280 residentes.

# Crear el Data frame de información hospitalaria
hosp_info = data.frame(
  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")
)

Aquí está este dataframe:

Pre-limpieza

Las uniones tradicionales (no probabilísticas) distinguen entre mayúsculas y minúsculas y requieren coincidencias de caracteres exactas entre los valores de los dos dataframes. Para mostrar algunos de los pasos de limpieza que puedes necesitar antes de iniciar una unión, ahora limpiaremos y alinearemos los datos linelist_mini y hosp_info.

Identificar las diferencias

Necesitamos que los valores de la columna hosp_name en el dataframe hosp_info coincidan con los valores de la columna hospital en el dataframe linelist_mini.

Aquí están los valores del dataframe linelist_mini, impresos con la función de R base unique():

unique(linelist_mini$hospital)
## [1] "Other"                                "Missing"                              "St. Mark's Maternity Hospital (SMMH)" "Port Hospital"                        "Military Hospital"

y aquí están los valores del dataframe hosp_info:

unique(hosp_info$hosp_name)
## [1] "central hospital" "military"         "port"             "St. Mark's"       "ignace"           "sisters"

Puedes ver que, aunque algunos de los hospitales existen en ambos dataframes, hay muchas diferencias en la ortografía.

Alinear los valores

Comenzamos limpiando los valores del dataframe hosp_info. Como se explica en la página Limpieza de datos y funciones básicas, podemos recodificar los valores con criterios lógicos utilizando la función case_when() de dplyr. Para los cuatro hospitales que existen en ambos dataframes, cambiamos los valores para alinearlos con los valores de linelist_mini. Para los demás hospitales dejamos los valores como están (TRUE ~ hosp_name).

PRECAUCIÓN: Normalmente, al limpiar se debe crear una nueva columna (por ejemplo, hosp_name_clean), pero para facilitar la demostración mostramos la modificación de la antigua columna

hosp_info <- hosp_info %>% 
  mutate(
    hosp_name = case_when(
      # criterio                         # valor nuevo
      hosp_name == "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",
      TRUE                             ~ hosp_name
      )
    )

Los nombres de los hospitales que aparecen en ambos dataframes están alineados. Hay dos hospitales en hosp_info que no están presentes en linelist_mini - nos ocuparemos de ellos más adelante, en la unión.

unique(hosp_info$hosp_name)
## [1] "Central Hospital"                     "Military Hospital"                    "Port Hospital"                        "St. Mark's Maternity Hospital (SMMH)" "ignace"                              
## [6] "sisters"

Antes de una unión, a menudo es más fácil convertir en una columna todas a minúsculas o todas a mayúsculas. Si necesitas convertir todos los valores de una columna a MAYÚSCULAS o minúsculas, utiliza mutate() y envuelva la columna con una de estas funciones de stringr, como se muestra en la página sobre Caracteres y cadenas.

str_to_upper()
str_to_upper()
str_to_title()

14.2 Uniones en dplyr

El paquete dplyr ofrece varias funciones de unión. dplyr está incluido en el paquete tidyverse. Estas funciones de unión se describen a continuación, con casos de uso sencillos.

Muchas gracias a https://github.com/gadenbuie por los gifs informativos.

Sintaxis general

Los comandos de unión pueden ejecutarse como comandos independientes para unir dos dataframes en un nuevo objeto, o pueden utilizarse dentro de una cadena de pipes (%>%) para fusionar un dataframe en otro mientras se limpia o se modifica de alguna manera.

En el siguiente ejemplo, la función left_join() se utiliza como un comando independiente para crear un nuevo dataframe joined_data. Las entradas son los dataframes 1 y 2 (df1 y df2). El primer dataframe es el dataframe de referencia, y el segundo se une a él.

El tercer argumento by = es donde se especifican las columnas de cada dataframe que se utilizarán para alinear las filas de los dos dataframes. Si los nombres de estas columnas son diferentes, proporciónelos dentro de un vector c() como se muestra a continuación, donde las filas se emparejan sobre la base de valores comunes entre la columna ID en df1 y la columna identifier en df2.

# Unión basada en valores comunes entre la columna "ID" (primer data frame) y la columna "identifier  (segundo data frame)
joined_data <- left_join(df1, df2, by = c("ID" = "identifier"))

Si las columnas by de ambos dataframes tienen exactamente el mismo nombre, puedes proporcionar sólo este nombre, entre comillas.

# Unión basada en valores comunes en la columna "ID" en ambos data frames
joined_data <- left_join(df1, df2, by = "ID")

Si estás uniendo los dataframes basándote en valores comunes en varios campos, enumera estos campos dentro del vector c(). Este ejemplo une filas si los valores de tres columnas de cada conjunto de datos se alinean exactamente.

# unión basada en el mismo nombre, apellido y edad
joined_data <- left_join(df1, df2, by = c("name" = "firstname", "surname" = "lastname", "Age" = "age"))

Los comandos de unión también pueden ejecutarse dentro de una cadena de pipes. Esto modificará el dataframe que se está canalizando.

En el ejemplo siguiente, df1 se pasa por los pipes, df2 se une a él y, por tanto, dfse modifica y se redefine.

df1 <- df1 %>%
  filter(date_onset < as.Date("2020-03-05")) %>% # limpieza miscelánea
  left_join(df2, by = c("ID" = "identifier"))    # unión df2 a df1

ATENCIÓN: ¡Las uniones son específicas para cada caso! Por lo tanto, es útil convertir todos los valores a minúsculas o mayúsculas antes de la unión. Consulta la página sobre caracteres/cadenas.

Uniones izquierda y derecha

Una unión a la izquierda o a la derecha se utiliza habitualmente para añadir información a un dataframe: la nueva información se añade sólo a las filas que ya existían en el dataframe de referencia. Estas uniones son comunes en el trabajo epidemiológico, ya que se utilizan para añadir información de unos datos a otro.

Al utilizar estas uniones, el orden de escritura de los dataframes en el comando es importante*.

  • En una unión a la izquierda, el primer dataframe escrito es el de base
  • En una unión a la derecha, el segundo dataframe escrito es el de base

Se conservan todas las filas del dataframe de referencia. La información del otro dataframe (secundario) se une al dataframe de referencia sólo si hay una coincidencia a través de la(s) columna(s) del identificador. Además:

  • Las filas del dataframe secundario que no coinciden se eliminan.
  • Si hay muchas filas de la línea de base que coinciden con una fila del dataframe secundarios (muchos a uno), la información secundaria se añade a cada fila de la línea de base que coincide.
  • Si una fila del de base coincide con varias filas del dataframe secundario (uno a varios), se dan todas las combinaciones, lo que significa que se pueden añadir nuevas filas al dataframe devuelto.

Ejemplos animados de uniones a la izquierda y a la derecha (fuente de la imagen)

Ejemplo

A continuación se muestra el resultado de un left_join() de hosp_info (dataframe secundario, ver aquí) en linelist_mini (dataframe de referencia, ver aquí). linelist_mini original tiene filas nrow(linelist_mini). Se muestra linelist_mini modificada. Observa lo siguiente:

  • Se han añadido dos nuevas columnas, catchment_pop y level en la parte izquierda de linelist_mini
  • Se mantienen todas las filas originales del dataframe de referencia linelist_mini
  • Cualquier fila original de linelist_mini para “Hospital Militar” está duplicada porque coincide con dos filas en el dataframe secundario, por lo que se devuelven ambas combinaciones
  • La columna del identificador de la unión del set de datos secundario (hosp_name) ha desaparecido porque es redundante con la columna del identificador primario (hospital)
  • Cuando una fila de referencia no coincide con ninguna fila secundaria (por ejemplo, cuando el hospital is “Other” or “Missing”), NA (en blanco) rellena las columnas del dataframe secundario
  • Se eliminaron las filas del dataframe secundario que no coincidían con el dataframe de referencia (hospitales “sisters” e “ignace”)
linelist_mini %>% 
  left_join(hosp_info, by = c("hospital" = "hosp_name"))

“¿Debo usar una unión a la derecha o a la izquierda?”

Para responder a la pregunta anterior, hay que tener claro “¿qué dataframe debe conservar todas sus filas?” - Utiliza éste como base. Una unión a la izquierda conserva todas las filas del primer dataframe escrito en el comando, mientras que una unión a la derecha conserva todas las filas del segundo dataframe.

Los dos comandos de abajo consiguen el mismo resultado - 10 filas de hosp_info unidas en base a linelist_mini, pero utilizan diferentes uniones. El resultado es que el orden de las columnas variará en función de si hosp_info llega por la derecha (en la unión izquierda) o llega por la izquierda (en la unión derecha). El orden de las filas también puede cambiar en consecuencia. Pero ambas consecuencias pueden ser tratadas posteriormente, utilizando select() para reordenar las columnas o arrange() para ordenar las filas.

# Los dos comandos siguientes obtienen los mismos datos, pero con filas y columnas ordenadas de forma diferente
left_join(linelist_mini, hosp_info, by = c("hospital" = "hosp_name"))
right_join(hosp_info, linelist_mini, by = c("hosp_name" = "hospital"))

Este es el resultado de hosp_info en linelist_mini a través de una unión a la izquierda (nuevas columnas entrando por la derecha)

Este es el resultado de hosp_info en linelist_mini a través de una unión a la derecha (nuevas columnas entrando desde la izquierda)

Considera también si tu caso de uso está dentro de una cadena de pipes (%>%). Si los datos del pipe son la base, es probable que utilices una unión izquierda para añadir datos a ella.

Unión completa

Una unión completa (Full join) es la más inclusiva de las uniones: devuelve todas las filas de ambos dataframes.

Si hay filas presentes en una y no en la otra (donde no se encontró ninguna coincidencia), el dataframe las incluirá y se hará más largo. Los valores faltantes NA se utilizan para rellenar los huecos creados. A medida que se une, observa el número de columnas y filas con cuidado para solucionar el problema de las coincidencias de mayúsculas y minúsculas y de los caracteres exactos.

El dataframe de “base” es el que se escribe primero en el comando. El ajuste de esto no afectará a los registros devueltos por la unión, pero puede afectar al orden de las columnas resultantes, al orden de las filas y a las columnas de los identificadores que se conservan.

Ejemplo animado de una unión completa (fuente de la imagen)

Ejemplo

A continuación se muestra la salida de un full_join() de hosp_info (originalmente nrow(hosp_info), view here) into linelist_mini (originalmente nrow(linelist_mini), view here). Nota lo siguiente:

  • Se mantienen todas las filas de la base (linelist_mini)
  • Se conservan las filas de los datos secundarios que no coinciden con la de base (“ignace” y “sisters”), con los valores de las columnas correspondientes de la de base case_id y onset rellenados con los valores que faltan
  • Del mismo modo, se conservan las filas de los datos de referencia que no coinciden con el secundario (“Otros” y “Falta”), y las columnas secundarias catchment_pop y level se rellenan con los valores que faltan
  • En el caso de coincidencias de uno a muchos o de muchos a uno (por ejemplo, filas para “Hospital Militar”), se devuelven todas las combinaciones posibles (alargando el conjunto de datos final)
  • Sólo se mantiene la columna del identificador de la línea de base (hospital)
linelist_mini %>% 
  full_join(hosp_info, by = c("hospital" = "hosp_name"))

Unión interna

Una unión interna es la más restrictiva de las uniones: sólo devuelve las filas que coinciden en ambos dataframes. Esto significa que el número de filas en el dataframe de referencia puede reducirse. El ajuste de qué dataframe es el de “base” (escrito en primer lugar en la función) no afectará a las filas que se devuelven, pero sí al orden de las columnas, al orden de las filas y a las columnas de los identificadores que se conservan.

Ejemplo animado de una unión interna (fuente de la imagen)

Ejemplo

A continuación se muestra la salida de un inner_join() de linelist_mini (base) con hosp_info (secundario). Observa lo siguiente:

  • Se eliminan las filas del de base que no coinciden con los datos secundarios (filas en las que el hospital es “Missing” u “Other”) * Asimismo, se eliminan las filas del dataframe secundario que no tenían ninguna coincidencia en la de base (filas en las que hosp_name es “sisters” o “ignace”)
  • Sólo se conserva la columna del identificador del de base (hospital)
linelist_mini %>% 
  inner_join(hosp_info, by = c("hospital" = "hosp_name"))

Semi-unión

Una semi-unión join es una “unión filtrada” que utiliza otro conjunto de datos no para añadir filas o columnas, sino para realizar un filtrado.

Un semi-join mantiene todas las observaciones en el dataframe de referencia que tienen una coincidencia con el dataframe secundario (pero no añade nuevas columnas ni duplica ninguna fila para las coincidencias múltiples). Lee más sobre estas uniones de “filtrado” aquí.

Ejemplo animado de una semiunión (fuente de la imagen)

Como ejemplo, el siguiente código devuelve las filas del dataframe hosp_info que tienen coincidencias en linelist_mini basadas en el nombre del hospital.

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 unión

La anti unión es otra “unión filtrada” que devuelve las filas del dataframe de referencia que no tienen una coincidencia en el dataframe secundario.

Lee más sobre el filtrado de las uniones aquí.

Los anti-join son útiles para la identificación de registros que no están presentes en otro dataframe, la solución de problemas de ortografía en un join (revisión de registros que deberían haber coincidido) y el examen de registros que fueron excluidos después de otro join.

Al igual que con right_join() y left_join(), el dataframe de base (que aparece primero) es importante. Las filas devueltas son sólo las del dataframe de referencia. Observa en el siguiente gif que la fila del dataframe secundario (fila púrpura 4) no se devuelve a pesar de que no coincide con la línea de base.

Ejemplo animado de una anti-unión (fuente de la imagen)

Ejemplo de anti_join() sencillo

Para un ejemplo sencillo, encontremos los hospitales de hosp_info que no tienen ningún caso en linelist_mini. Enumeramos primero hosp_info, como dataframe de referencia. Se devuelven los hospitales que no están presentes en linelist_mini.

hosp_info %>% 
  anti_join(linelist_mini, by = c("hosp_name" = "hospital"))

Ejemplo de anti_join() complejo

Para otro ejemplo, digamos que ejecutamos un inner_join() entre linelist_mini y hosp_info. Esto devuelve sólo un subconjunto de los registros originales de linelist_mini, ya que algunos no están presentes en hosp_info.

linelist_mini %>% 
  inner_join(hosp_info, by = c("hospital" = "hosp_name"))

Para revisar los registros de linelist_mini que fueron excluidos durante el inner join, podemos ejecutar un anti-join con la misma configuración (linelist_mini como base).

linelist_mini %>% 
  anti_join(hosp_info, by = c("hospital" = "hosp_name"))

Para ver los registros de hosp_info que se excluyeron en la unión interna, también podríamos ejecutar una anti unión con hosp_info como dataframe de referencia.

14.3 Emparejamiento probabilístico

Si no dispones de un identificador único común a todos los conjuntos de datos para unirlos, considera la posibilidad de utilizar un algoritmo de coincidencia probabilística. Este algoritmo buscaría coincidencias entre los registros basándose en la similitud (por ejemplo, la distancia de cadena de Jaro-Winkler o la distancia numérica). A continuación se muestra un ejemplo sencillo utilizando el paquete fastLink .

Cargar paquetes

pacman::p_load(
  tidyverse,      # manipulación y visualización de datos
  fastLink        # correspondencia de registros
  )

A continuación se presentan dos pequeños conjuntos de datos de ejemplo que utilizaremos para demostrar la correspondencia probabilística (cases y test_results):

Aquí está el código utilizado para hacer estos conjuntos de datos:

# crear un set de datos

cases <- tribble(
  ~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"
)

results <- tribble(
  ~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"
  )

El dataset cases tiene 9 registros de pacientes que están a la espera de los resultados de las pruebas.

El set de datos test_results tiene 14 registros y contiene la columna resultado, que queremos añadir a los registros en cases basado en la coincidencia probabilística de registros.

Correspondencia probabilística

La función fastLink() del paquete fastLink puede utilizarse para aplicar un algoritmo de coincidencia. Esta es la información básica. Puedes leer más detalles escribiendo ?fastLink en tu consola.

  • Define los dos dataframes para la comparación con los argumentos dfA =y dfB =
  • En varnames = indica todos los nombres de columnas que se utilizarán para la comparación. Todos ellos deben existir tanto en dfA como en dfB.
  • En stringdist.match = escribe columnas de las que están en varnames para ser evaluadas en la cadena “distance”.
  • En numeric.match = dar columnas de las que están en varnames para ser evaluadas en la distancia numérica.
  • Los valores faltantes se ignoran
  • Por defecto, cada fila de cualquiera de los dos dataframes coincide como máximo con una fila del otro dataframe. Si deseas ver todas las coincidencias evaluadas, establece dedupe.matches = FALSE. La deduplicación se realiza mediante la solución de asignación lineal de Winkler.

Sugerencia: divide una columna de fecha en tres columnas numéricas separadas utilizando day(), month(), and year() del paquete lubridate

El umbral por defecto para las coincidencias es de 0,94 (threshold.match =), pero puedes ajustarlo más alto o más bajo. Si defines el umbral, ten en cuenta que los umbrales más altos podrían producir más falsos negativos (filas que no coinciden y que en realidad deberían coincidir) y, del mismo modo, un umbral más bajo podría producir más falsos positivos.

A continuación, los datos se emparejan según la distancia de las cadenas en las columnas de nombre y distrito, y según la distancia numérica para el año, el mes y el día de nacimiento. Se establece un umbral de coincidencia del 95% de probabilidad.

fl_output <- fastLink::fastLink(
  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 4 are used.
## Running the EM algorithm.
## Getting the indices of estimated matches.
##     Parallelizing calculation using OpenMP. 1 threads out of 4 are used.
## Deduping the estimated matches.
## Getting the match patterns for each estimated match.

Revisar los coincidentes

Definimos el objeto devuelto por fastLink() como fl_output. Es de tipo list, y en realidad contiene varios dataframes dentro de él, detallando los resultados de la coincidencia. Uno de estos dataframes es matches, que contiene las coincidencias más probables entre cases y results. Puedes acceder a este dataframe “coincidencias” con fl_output$matches. A continuación, se guarda como my_matches para facilitar el acceso posterior.

Cuando se imprime my_matches, se ven dos vectores de columnas: los pares de números de fila/índices (también llamados “rownames”) en cases (“inds.a”) y en results (“inds.b”) que representan las mejores coincidencias. Si falta un número de fila de un dataframe, entonces no se ha encontrado ninguna coincidencia en el otro dataframe con el umbral de coincidencia especificado.

# imprimir coincidencias
my_matches <- fl_output$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

Cosas a tener en cuenta:

  • Las coincidencias se produjeron a pesar de las ligeras diferencias en la ortografía del nombre y las fechas de nacimiento:
    • “Tony B. Smith” coincide con “Anthony B Smith”
    • “María Rodríguez” coincide con “Marialisa Rodrigues”
    • “Betty Chase” coincide con “Elizabeth Chase”
    • “Olivier Laurent De Bordeaux” coincide con “Oliver Laurent De Bordow” (se ignora la fecha de nacimiento que falta)
  • Una fila de cases (para “Blessing Adebayo”, fila 9) no tuvo una buena coincidencia en results, por lo que no está presente en my_matches.

Unión en base a las coincidencias probabilísticas

Para utilizar estas coincidencias para unir los resultados a los casos, una estrategia es:

  1. Utilizar left_join() para unir my_matches a cases (haciendo coincidir rownames en cases con “inds.a” en my_matches)
  2. A continuación, utiliza otro left_join() para unir results a cases (haciendo coincidir los “inds.b” recién adquiridos en cases con los rownames en results)

Antes de las uniones, debemos limpiar los tres dataframes:

  • Tanto dfA como dfB deben tener sus números de fila (“rowname”) convertidos en una columna propia.
  • Las dos columnas de my_matches se convierten en tipo carácter, por lo que pueden unirse a las filas
# Limpiar los datos antes de unirlos
####################################

# convertir filas casos en una columna 
cases_clean <- cases %>% rownames_to_column()

# convertir filas de test_results en una columna
results_clean <- results %>% rownames_to_column()  

# convertir todas las columnas del set de datos coincidentes en caracteres, para que puedan ser unidas a los nombres
matches_clean <- my_matches %>%
  mutate(across(everything(), as.character))



# Unir las coincidencias a dfA, luego añadir dfB
################################################
# la columna "inds.b" se añade a dfA
complete <- left_join(cases_clean, matches_clean, by = c("rowname" = "inds.a"))

# se añade(n) columna(s) de dfB 
complete <- left_join(complete, results_clean, by = c("inds.b" = "rowname"))

Como se realiza utilizando el código anterior, el dataframe resultante complete contendrá todas las columnas tanto de cases como de results. A muchas de ellas se les añadirán los sufijos “.x” e “.y”, ya que de lo contrario los nombres de las columnas estarían duplicados.

Alternativamente, para conseguir sólo los 9 registros “originales” en los casos con la(s) nueva(s) columna(s) de results, usa select() en results antes de las uniones, de forma que sólo contenga los nombres y las columnas que deseas añadir a cases (por ej. la columna result).

cases_clean <- cases %>% rownames_to_column()

results_clean <- results %>%
  rownames_to_column() %>% 
  select(rowname, result)    # selecciona solo ciertas columnas 

matches_clean <- my_matches %>%
  mutate(across(everything(), as.character))

# uniones
complete <- left_join(cases_clean, matches_clean, by = c("rowname" = "inds.a"))
complete <- left_join(complete, results_clean, by = c("inds.b" = "rowname"))

Si deseas subconjuntar cualquiera de los dos conjuntos de datos sólo con las filas que coincidan, puedes utilizar los siguientes códigos:

cases_matched <- cases[my_matches$inds.a,]  # Filas en casos que coinciden con una fila en resultados
results_matched <- results[my_matches$inds.b,]  # Filas en resultados que coinciden con una fila de los casos

O, para ver sólo las filas que no coinciden:

cases_not_matched <- cases[!rownames(cases) %in% my_matches$inds.a,]  # Filas los casos que NO coinciden con una fila de resultados
results_not_matched <- results[!rownames(results) %in% my_matches$inds.b,]  # Filas de resultados que NO coinciden con una fila de casos

De-duplicación probabilística

La coincidencia probabilística también puede utilizarse para de-duplicar unos datos. Consulta la página sobre de-duplicación para conocer otros métodos de de-duplicación.

Aquí comenzamos con el conjunto de datos cases, pero ahora lo llamamos cases_dup, ya que tiene 2 filas adicionales que podrían ser duplicados de filas anteriores: Ver “Tony” con “Anthony”, y “Marialisa Rodrigues” con “Maria Rodriguez”.

Ejecuta fastLink() como antes, pero compara el dataframe cases_dup consigo mismo. Cuando los dos dataframes proporcionados son idénticos, la función asume que se quiere de-duplicar. Observa que no especificamos stringdist.match = o numeric.match = como hicimos anteriormente.

## Ejecutar fastLink en el mismo conjunto de datos
dedupe_output <- fastLink(
  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 4 are used.
## Running the EM algorithm.
## Getting the indices of estimated matches.
##     Parallelizing calculation using OpenMP. 1 threads out of 4 are used.
## Calculating the posterior for each pair of matched observations.
## Getting the match patterns for each estimated match.

Ahora, puedes revisar los duplicados potenciales con getMatches(). Proporciona el dataframe como dfA = y dfB =, y proporciona la salida de la función fastLink() como fl.out =. fl.out debe ser del tipo fastLink.dedupe, o en otras palabras, el resultado de fastLink().

## Ejecutar getMatches()
cases_dedupe <- getMatches(
  dfA = cases_dup,
  dfB = cases_dup,
  fl.out = dedupe_output)

Véase la columna de la derecha, que indica los IDs duplicados: las dos últimas filas se identifican como probables duplicados de las filas 2 y 3.

Para devolver los números de fila de las filas que probablemente sean duplicadas, puede contar el número de filas por valor único en la columna dedupe.ids, y luego filtrar para mantener sólo aquellas con más de una fila. En este caso, esto deja las filas 2 y 3.

cases_dedupe %>% 
  count(dedupe.ids) %>% 
  filter(n > 1)
##   dedupe.ids n
## 1          2 2
## 2          3 2

Para inspeccionar las filas completas de los probables duplicados, pon el número de fila en este comando:

# muestra la fila 2 y todos sus posibles duplicados
cases_dedupe[cases_dedupe$dedupe.ids == 2,]   
##    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 Enlazamiento y alineación

Otro método para combinar dos dataframes es “unirlos”. También se puede pensar en esto como “anexar” o “añadir” filas o columnas.

En esta sección también se discutirá cómo “alinear” el orden de las filas de un dataframe con el orden de otro dataframe. Este tema se discute más adelante en la sección sobre Vinculación de columnas.

Enlazar filas

Para unir las filas de un dataframe con el fondo de otro dataframe, utiliza bind_rows() de dplyr. Es muy inclusivo, por lo que cualquier columna presente en cualquiera de los dataframes se incluirá en la salida. Algunas notas:

  • A diferencia de la versión de R base de R row.bind(), bind_rows() de dplyr no requiere que el orden de las columnas sea el mismo en ambos dataframes. Siempre que los nombres de las columnas se escriban de forma idéntica, las alineará correctamente.
  • Puedes especificar opcionalmente el argumento .id =. Proporcionar un nombre de columna de caracteres. Esto producirá una nueva columna que sirve para identificar de qué dataframe procede originalmente cada fila.
  • Puedes utilizar bind_rows() en una lista de dataframes de estructura similar para combinarlos en un dataframe. Mira un ejemplo en la página Iteración, bucles y listas que implica la importación de múltiples listas de líneas con purrr.

Un ejemplo común de vinculación de filas es vincular una fila “total” a una tabla descriptiva hecha con la función summarise() de dplyr. A continuación, creamos una tabla de recuentos de casos y valores medianos de TC por hospital con una fila de totales.

La función summarise() se utiliza en los datos agrupados por hospital para devolver un dataframe resumido por hospital. Pero la función summarise() no produce automáticamente una fila de “totales”, así que la creamos resumiendo los datos de nuevo, pero con los datos no agrupados por hospital. Esto produce un segundo dataframe de una sola fila. A continuación, podemos unir estos dataframes para obtener la tabla final.

Mira otros ejemplos trabajados como éste en las páginas de Tablas descriptivas y Tablas para presentaciones.

# Crear tabla principal
#######################
hosp_summary <- linelist %>% 
  group_by(hospital) %>%                        # Agrupar los datos por hospitales
  summarise(                                    # Crear nuevas columnas resumen de indicadores de interés
    cases = n(),                                  # Número de filas por grupo hospital-resultado     
    ct_value_med = median(ct_blood, na.rm=T))     # mediana del valor CT por grupo

Este es el dataframe de hosp_summary:

Crea un dataframe con las estadísticas “totales” (no agrupadas por hospital). Esto devolverá una sola fila.

# crear totales
###############
totals <- linelist %>% 
  summarise(
    cases = n(),                               # Número de filas para todo el conjunto de datos      
    ct_value_med = median(ct_blood, na.rm=T))  # Mediana de CT para todo el conjunto de datos

Y a continuación está el dataframe totals. Observa que sólo hay dos columnas. Estas columnas también están en hosp_summary, pero hay una columna en hosp_summary que no está en totals (hospital).

Ahora podemos unir las filas con bind_rows().

# Unir los data frames
combined <- bind_rows(hosp_summary, totals)

Ahora podemos ver el resultado. Observa cómo en la última fila se rellena un valor NA vacío para la columna hospital que no estaba en hosp_summary. Como se explica en la página de Tablas para presentaciones, podrías “rellenar” esta celda con “Total” utilizando replace_na().

Enlazar columnas

Existe una función similar de dplyr bind_cols() que se puede utilizar para combinar dos dataframes de forma lateral. Ten en cuenta que las filas se emparejan entre sí por posición (no como una unión anterior) - por ejemplo, la fila 12 en cada dataframe se alineará.

Como ejemplo, unimos varias tablas de resumen. Para ello, también mostramos cómo reordenar el orden de las filas de un dataframe para que coincida con el orden de otro dataframe, con match().

Aquí definimos case_info como un dataframe resumido de los casos del listado, por hospital, con el número de casos y el número de muertes.

# Información de casos
case_info <- linelist %>% 
  group_by(hospital) %>% 
  summarise(
    cases = n(),
    deaths = sum(outcome == "Death", na.rm=T)
  )

Y digamos que aquí hay un dataframe diferente contact_fu que contiene información sobre el porcentaje de contactos expuestos investigados y “seguidos”, de nuevo por hospital.

contact_fu <- data.frame(
  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%")
)

Observa que los hospitales son los mismos, pero están en diferente orden en cada dataframe. La solución más sencilla sería utilizar un left_join() en la columna hospitals, pero también podría utilizar bind_cols() con un paso adicional.

Utiliza match() para alinear la ordenación

Debido a que los órdenes de las filas son diferentes, un simple comando bind_cols() daría lugar a un desajuste de los datos. Para solucionarlo podemos utilizar match() de R base para alinear las filas de un dataframe en el mismo orden que en otro. Asumimos para este enfoque que no hay valores duplicados en ninguno de los dos dataframes.

Cuando utilizamos match(), la sintaxis es match(TARGET ORDER VECTOR, DATA FRAME COLUMN TO CHANGE), donde el primer argumento es el orden deseado (ya sea un vector independiente, o en este caso una columna en un dataframe), y el segundo argumento es la columna del dataframe que se reordenará. La salida de match() es un vector de números que representa el ordenamiento correcto de las posiciones. Puedes obtener más información con ?match.

match(case_info$hospital, contact_fu$hospital)
## [1] 4 2 3 6 5 1

Puedes utilizar este vector numérico para reordenar el dataframe - colócalo dentro de los subcorchetes [ ] antes de la coma. Lee más sobre la sintaxis del subconjunto de corchetes en la página de Fundamentos de R. El comando de abajo crea un nuevo dataframe, definido como el anterior en el que las filas están ordenadas en el vector numérico de arriba.

contact_fu_aligned <- contact_fu[match(case_info$hospital, contact_fu$hospital),]

Ahora podemos unir las columnas del dataframe, con el orden correcto de las filas. Ten en cuenta que algunas columnas están duplicadas y será necesario limpiarlas con rename(). Lee más sobre bind_rows() aquí.

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 Maternity Hospital (SMMH) 80%          60%   
## 2 Military Hospital                      896    399 Military Hospital                    82%          25%   
## 3 Missing                               1469    611 Missing                              <NA>         <NA>  
## 4 Other                                  885    395 Central Hospital                     78%          20%   
## 5 Port Hospital                         1762    785 Port Hospital                        64%          75%   
## 6 St. Mark's Maternity Hospital (SMMH)   422    199 Other                                55%          80%

Una alternativa en R base a bind_cols es cbind(), que realiza la misma operación.

14.5 Recursos

Las páginas de tidyverse sobre join

La página de R for Data Science sobre datos relacionales

La página de tidyverse en dplyr en la encuadernación

Una viñeta sobre fastLink en la página de Github del paquete

Publicación que describe la metodología de fastLink

Publicación que describe el paquete RecordLinkage