Datos Relacionales
Rara vez la realización de un análisis de datos involucra una sola tabla de datos. Por lo general, tendremos diversas tablas de datos y será necesario combinarlas para responder a nuestras necesidades e intereses. En general, cuando tenemos varias tablas de datos disponibles se dice que tenemos datos relacionales ya son las relaciones, no solo los conjuntos de datos individuales, las que nos proveen de toda la información para analizar/resolver el problema.
Estas relaciones siempre se definen entre un par de tablas. Todas las demás relaciones se construyen a partir de esta idea simple: las relaciones de tres o más tablas son siempre una propiedad de las relaciones entre cada par de ellas. Es posible que tengamos relaciones dentro de la misma tabla, por ejemplo tenemos una tabla con información relativa a personas y cada una de ellas tiene una referencia a sus padres.
El lugar más común para encontrar este tipo de datos relacionales son los sistemas de administración de bases de datos relacionales, un término que abarca casi todas las bases de datos modernas. Como ya se ha comentado en este curso, SQL es el lenguaje que se usa para la comunicación con las bases de datos. En R tenemos dos maneras de trabajar con este tipo de datos para conseguir relacionar dataframes: con las funciones del núcleo del lenguaje y con dplyr. La función base de R
para unir datos es merge, que ofrece muchos argumentos para controlar esta unión.
En general, dplyr es un poco más fácil de usar que SQL y que R base porque dplyr está especializado para realizar análisis de datos: facilita las operaciones comunes de análisis de datos, a expensas de dificultar otras cosas que normalmente no necesitaremos para el análisis de datos.
Para trabajar con datos relacionales necesitamos operaciones que funcionen con pares de tablas. En dplyr hay tres familias de verbos diseñados para trabajar con datos relacionales:
Mutating joins: que agregan nuevas variables a un dataframe a partir de observaciones coincidentes en otro dataframe.
Filtering joins: que filtran las observaciones de un dataframe en función de si coinciden o no con una observación en otra tabla.
Operaciones de conjuntos: que tratan las observaciones como si fueran elementos de conjunto.
Datos
Para ejemplificar las siguientes operaciones usaremos dos tablas de datos que representan información muy diferente: donaciones en unas elecciones y área inundada tras una tormenta. Las dos tablas tienen un valor en común, el código postal (zip code). Durante esta lección relacionaremos de distinta manera estas dos tablas. El material proviene de aquí. En concreto los datos se encuentran en la siguiente
carpeta, necesitaremos los ficheros zip_contrib.rds y zip_flood.rds.
Si suponemos que tenemos una carpeta donde guardamos los ficheros de datos, para poder cargarlos usaremos el siguiente código:
library(dplyr)
contrib = readRDS("data/zip_contrib.rds")
flood = readRDS("data/zip_flood.rds")
Joins para añadir información
El primer bloque de funciones que veremos para poder combinar información de conjunto de tablas es la de los, mutating joins. Una combinación de la función mutate con la idea de los joins que nos permitirá combinar variables de dos tablas. Primero haremos coincidir las observaciones por sus claves, luego copia las variables de una tabla a otra. Al igual que mutate, las funciones de unión agregan las nuevas variables a la derecha de las que ya existían.

Inner Join
El tipo más simple de combinación es el inner join. Un inner join empareja pares de observaciones que tienen las mismas claves en ambas tablas. Nuestro resultado contendrá solo filas con códigos postales que estén tanto en contrib como en flood. La función inner_join() es equivalente a usar base::merge() con los parámetros predeterminados.
datos_ij = inner_join(contrib, flood, by = "zip")
str(datos_ij)
Es necesario destacar el uso del parámetro by en el que especificamos el nombre de la columna por la cual queremos realizar la unión. En el caso que no tenga el mismo nombre en las dos tablas podemos usar la siguiente expresión by = c("nombre_en_tabla1"="nombre_en_tabla2").
Si observamos los resultados, ahora tenemos un nuevo dataframe llamado datos_ij que tiene 3 columnas, una de ellas el código postal zip code y que no puede tener mayor número de filas que cualquiera de los dos conjuntos de datos originales. Esto es debido a que solamente hemos unido las informaciones de los códigos postales que coinciden. El problema es que hemos perdido muchas observaciones: todos los códigos postales que no han hecho ninguna contribución a la campaña. Esto puede no
tener mucho sentido, ya que estas cantidades en realidad deberían ser cero. Para cumplir nuestro objetivo deberemos usar una función de unión diferente.
Left / Right Join
Estas funciones devuelven un nuevo dataframe que contiene todas las observaciones en un dataframe y las observaciones coincidentes del otro. Veamos que sucede cuando realizamos la operación left_join:
datos_lj <- left_join(contrib, flood, by = "zip")
str(datos_lj)
La función left_join toma el dataframe de la “izquierda” (es decir, contrib, o el primero pasado a left_join) y agrega las columnas coincidentes del dataframe “derecho”. El resultado es un dataframe con el mismo número de filas que contrib. Comprueba que si los códigos postales no están contenidos en flood_extent, hay valores NA en el dataframe resultado (data_lj).
Por otra parte, la función right_join es el reflejo de left_join. Es como si cambiaras de orden x e y en la función left_join.
datos_rj <- right_join(contrib, flood, by = "zip")
str(datos_rj)
Full Join
La función full_join devuelve un dataframe que contiene todas las observaciones de ambos dataframes. Esto es útil cuando no desea excluir ninguna observación.
data_fj <- full_join(contrib, flood, by = "zip")
existe.na = function(x){sum(is.na(x))}
sapply(data_fj, FUN =existe.na)
Una vez que hemos ejecutado el código anterior, podemos observar como aparecen valores NA en las dos columnas que hemos agregado.
Claves duplicadas
Hasta ahora, en nuestros ejemplos hemos asumido que las claves que usamos para la unión (el parámetro by) son únicas. Pero ese no es siempre el caso. Esta sección explica lo que sucede cuando las claves no son únicas. Hay dos posibilidades:
Una de las tablas tiene claves duplicadas: Esto es útil cuando deseamos agregar información adicional, ya que normalmente existe una relación de uno a muchos.
Las dos tablas tienen claves duplicadas: Este caso suele ser un error y nos es recomendable realizar la unión porque en ninguna de las tablas las claves identifican de forma única una observación. Cuando unimos claves duplicadas, obtenemos todas las combinaciones posibles, es decir, el producto cartesiano.
Joins para filtrar
Los joins de filtrado trabajan con las observaciones coincidentes de la misma manera que los joins de mutación, pero estos afectan a las observaciones, en lugar de a las variables. Hay dos tipos:
semi_join(x,y): mantiene todas las observaciones enxque tienen una coincidencia eny.anti_join(x, y): elimina todas las observaciones enxque tienen una coincidencia eny.
Por ejemplo si queremos mantener todas las observaciones en la tabla de contribuciones (contrib) que han tenido alguna inundación (flood), deberíamos ejecutar el siguiente código:
datos_semi = semi_join(contrib, flood, by="zip")
str(datos_semi)
Si nos fijamos, en este caso seguimos teniendo 2 columnas en el dataframe resultado.
Operaciones de conjuntos
El último tipo de función que permite trabajar con dos tablas son las operaciones de conjuntos. En general, se usan con menor frecuencia, pero ocasionalmente son útiles cuando deseamos dividir un solo filtro complejo en partes más simples. Todas estas operaciones funcionan con una fila completa, comparando los valores de cada variable. Estos esperan que las entradas x e y tengan las mismas variables, y tratan las observaciones como conjuntos matemáticos:
intersect(x, y): devuelve solo observaciones tanto enxcomo eny.union(x, y): devuelve observaciones únicas enxey.setdiff(x, y): devuelve observaciones enx, pero no eny.
Veamos un ejemplo muy sencillo, copiad este código en RStudio:
df1 <- tribble(
~x, ~y,
1, 1,
2, 1
)
df2 <- tribble(
~x, ~y,
1, 1,
1, 2
)
Las cuatro operaciones posibles son:
intersect(df1, df2)
union(df1, df2)
setdiff(df1, df2)
setdiff(df2, df1)
Ejercicios
Los archivos books.csv, authors.csv y book-authors.csv nos indican detalles de las lecturas para el verano de un estudiante de estadística. Ha construido una pequeña base de datos organizada en 3 archivos:
Las tareas a realizar son las siguientes:
Leer los tres archivos en R, nombrándolos libros, autores y autores_libro.
Agregar los ISBN a la tabla de datos de los autores. ¿Por qué el dataframe resultante tiene 11 filas?
Agregar la tabla de información de autores del ejercicio anterior a la tabla de datos de libros.
4¿Hay algún autor en la tabla de datos de autores que no tenga ningún libro de la tabla de datos de libros? Utilizar el join adecuado para este trabajo
Después de leer Juego de tronos, el estudiante decide leer el resto de la serie durante el verano. El archivo CSV books2.csv contiene los libros actualizados en la lista de lectura del estudiante. Leer este archivo en R, nombrándolo
books2.Usa la misma declaración de unión que hiciste en el ejercicio 3, pero usando el dataframe libros2.
Recursos
Para la elaboración de este material se ha recurrido a diversas fuentes de información, entre ellas destaca el capítulo 13 del libro R4DS enlace que recomiendo leer en profundidad.
Otros recursos que se han usado son:
Combining (joining/merging) data sets with dplyr Cole Tanigawa-Lau
Si se quiere practicar más, existen 2 bloques de ejercicios muy interesantes a realizar: