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.
::p_load(
pacman# importar y exportar
rio, # localizar archivos
here, # gestión y visualización de datos
tidyverse, # coincidencias probabilísticas
RecordLinkage, # coincidencias probabilísticas
fastLink )
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
<- import("linelist_cleaned.rds") linelist
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:
- Una versión “en miniatura” de casos de
linelist
, que contiene sólo las columnascase_id
,date_onset
, yhospital
, y sólo las 10 primeras filas - 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 %>% # empezar con linelist original
linelist_mini 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
= 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")
)
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"
[2] "Missing"
[3] "St. Mark's Maternity Hospital (SMMH)"
[4] "Port Hospital"
[5] "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"
[5] "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
== "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
) )
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"
[2] "Military Hospital"
[3] "Port Hospital"
[4] "St. Mark's Maternity Hospital (SMMH)"
[5] "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)
<- left_join(df1, df2, by = c("ID" = "identifier")) joined_data
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
<- left_join(df1, df2, by = "ID") joined_data
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
<- left_join(df1, df2, by = c("name" = "firstname", "surname" = "lastname", "Age" = "age")) joined_data
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, df
se 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
ylevel
en la parte izquierda delinelist_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"))
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.
“¿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)
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.
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)
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.
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
yonset
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
ylevel
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"))
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.
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"))
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-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"))
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.
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
::p_load(
pacman# manipulación y visualización de datos
tidyverse, # correspondencia de registros
fastLink )
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
<- 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"
)
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 =
ydfB =
- En
varnames =
indica todos los nombres de columnas que se utilizarán para la comparación. Todos ellos deben existir tanto endfA
como endfB
. - En
stringdist.match =
escribe columnas de las que están envarnames
para ser evaluadas en la cadena “distance”. - En
numeric.match =
dar columnas de las que están envarnames
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.
<- 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.
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
<- 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
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 enresults
, por lo que no está presente enmy_matches
.
Unión en base a las coincidencias probabilísticas
Para utilizar estas coincidencias para unir los resultados a los casos, una estrategia es:
- Utilizar
left_join()
para unirmy_matches
acases
(haciendo coincidir rownames encases
con “inds.a” enmy_matches
) - A continuación, utiliza otro
left_join()
para unirresults
acases
(haciendo coincidir los “inds.b” recién adquiridos encases
con los rownames enresults
)
Antes de las uniones, debemos limpiar los tres dataframes:
- Tanto
dfA
comodfB
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 %>% rownames_to_column()
cases_clean
# convertir filas de test_results en una columna
<- results %>% rownames_to_column()
results_clean
# convertir todas las columnas del set de datos coincidentes en caracteres, para que puedan ser unidas a los nombres
<- my_matches %>%
matches_clean mutate(across(everything(), as.character))
# Unir las coincidencias a dfA, luego añadir dfB
################################################
# la columna "inds.b" se añade a dfA
<- left_join(cases_clean, matches_clean, by = c("rowname" = "inds.a"))
complete
# se añade(n) columna(s) de dfB
<- left_join(complete, results_clean, by = c("inds.b" = "rowname")) complete
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 %>% rownames_to_column()
cases_clean
<- results %>%
results_clean rownames_to_column() %>%
select(rowname, result) # selecciona solo ciertas columnas
<- my_matches %>%
matches_clean mutate(across(everything(), as.character))
# uniones
<- left_join(cases_clean, matches_clean, by = c("rowname" = "inds.a"))
complete <- left_join(complete, results_clean, by = c("inds.b" = "rowname")) complete
Si deseas subconjuntar cualquiera de los dos conjuntos de datos sólo con las filas que coincidan, puedes utilizar los siguientes códigos:
<- cases[my_matches$inds.a,] # Filas en casos que coinciden con una fila en resultados
cases_matched <- results[my_matches$inds.b,] # Filas en resultados que coinciden con una fila de los casos results_matched
O, para ver sólo las filas que no coinciden:
<- cases[!rownames(cases) %in% my_matches$inds.a,] # Filas los casos que NO coinciden con una fila de resultados
cases_not_matched <- results[!rownames(results) %in% my_matches$inds.b,] # Filas de resultados que NO coinciden con una fila de casos results_not_matched
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
<- 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.
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()
<- getMatches(
cases_dedupe 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
$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 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
#######################
<- linelist %>%
hosp_summary 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
###############
<- linelist %>%
totals 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
<- bind_rows(hosp_summary, totals) combined
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
<- linelist %>%
case_info 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.
<- 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%")
)
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[match(case_info$hospital, contact_fu$hospital),] contact_fu_aligned
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 … 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%
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