13 Datos relacionales

13.1 Introducción

Es raro que un análisis de datos involucre una única tabla de datos. Lo típico es que tengas muchas tablas de datos que debes combinar para responder a tus preguntas de interés. De manera colectiva, se le llama datos relacionales a esas múltiples tablas de datos, ya que sus relaciones, y no sólo los conjuntos de datos individuales, son importantes.

Las relaciones siempre se definen sobre un par de tablas. Todas las otras relaciones se construyen sobre esta idea simple: las relaciones entre tres o más tablas son siempre una propiedad de las relaciones entre cada par. ¡A veces ambos elementos de un par pueden ser la misma tabla! Esto se necesita si, por ejemplo, tienes una tabla de personas y cada persona tiene una referencia a sus padres.

Para trabajar con datos relacionales necesitas verbos para funcionen con pares de tablas. Existen tres familias de verbos diseñadas para trabajar con datos relacionales:

  • Uniones de transformación (del inglés mutating joins), que agregan nuevas variables a un data frame a partir de las observaciones coincidentes en otra tabla.

  • Uniones de filtro (del inglés filtering joins), que filtran observaciones en un data frame con base en si coinciden con las observaciones en otra tabla.

  • Operaciones de conjuntos (del inglés set operations), que tratan las observaciones como elementos de un conjunto.

El lugar más común para encontrar datos relacionales es en un sistema relacional de administración de bases de datos (Relational Data Base Management System en inglés), un concepto que abarca casi todas las bases de datos modernas. Si has usado una base de datos con anterioridad, casi seguramente fue SQL. Si es así, los conceptos de este capítulo debiesen ser familiares, aunque su expresión en dplyr es un poco distinta. En términos generales, dplyr es un poco más fácil de usar que SQL ya que dplyr se especializa en el análisis de datos: facilita las operaciones habituales, a expensas de dificultar otras que no se requieren a menudo para el análisis de datos.

13.1.1 Prerrequisitos

Vamos a explorar datos relacionales contenidos en el paquete datos usando los verbos para dos tablas de dplyr.

13.2 datos sobre vuelos

Usaremos datos sobre vuelos desde y hacia Nueva York para aprender sobre datos relacionales1. El paquete datos contiene cuatro tablas que se relacionan con la tabla vuelos que se usó en el capítulo sobre transformación de datos:

  • aerolineas permite observar el nombre completo de la aerolínea a partir de su código abreviado:
  • aeropuertos entrega información de cada aeropuerto, identificado por su código:
  • aviones entrega información de cada avión, identificado por su codigo_cola:
  • clima entrega información del clima en cada aeropuerto de Nueva York para cada hora:

Una forma de mostrar las relaciones entre las diferentes tablas es mediante un diagrama:

Este diagrama es un poco abrumador, ¡pero es simple comparado con algunos que verás en el exterior! La clave para entender estos diagramas es recordar que cada relación siempre involucra un par de tablas. No necesitas entender todo el diagrama, necesitas entender la cadena de relaciones entre las tablas que te interesan.

En estos datos:

  • vuelos se connecta con aviones a través de la variable codigo_cola.

  • vuelos se conecta con aerolineas a través de la variable codigo_carrier.

  • vuelos se conecta con aeropuertos de dos formas: a través de las variables origen y destino.

  • vuelos se conecta con clima a través de las variables origen (la ubicación), anio, mes, dia y hora (el horario).

13.2.1 Ejercicios

  1. Imagina que necesitas dibujar (aproximadamente) la ruta que cada avión vuela desde su origen hasta el destino. ¿Qué variables necesitarías? ¿Qué tablas necesitarías combinar?

  2. Olvidamos dibujar la relación entre clima y aeropuertos. ¿Cuál es la relación y cómo debería aparecer en el diagrama?

  3. clima únicamente contiene información de los aeropuertos de origen (Nueva York). Si incluyera registros para todos los aeropuertos de EEUU, ¿qué relación tendría con vuelos?

  4. Sabemos que hay días “especiales” en el año y pocas personas suelen volar en ellos. ¿Cómo se representarían en un data frame? ¿Cuáles serían las claves primarias de esa tabla? ¿Cómo se conectaría con las tablas existentes?

13.3 Claves

Las variables usadas para conectar cada par de variables se llaman claves (del inglés key). Una clave es una variable (o un conjunto de variables) que identifican de manera única una observación. En casos simples, una sola variable es suficiente para identificar una observación. Por ejemplo, cada avión está identificado de forma única por su codigo_cola. En otros casos, se pueden necesitar múltiples variables. Por ejemplo, para identificar una observación en clima se necesitan cinco variables: anio, mes, dia, hora y origen.

Existen dos tipos de claves:

  • Una clave primaria identifica únicamente una observación en su propia tabla. Por ejemplo, aviones$codigo_cola es una clave primaria, ya que identifica de manera única cada avión en la tabla aviones.

  • Una clave foránea únicamente identifica una observación en otra tabla. Por ejemplo, vuelos$codigo_cola es una clave foránea, ya que aparece en la tabla vuelos, en la que une cada vuelo con un único avión.

Una variable puede ser clave primaria y clave foránea a la vez. Por ejemplo, origen es parte de la clave primaria clima y también una clave foránea de aeropuertos.

Una vez que identificas las claves primarias en tus tablas, es una buena práctica verificar que identifican de forma única cada observación. Una forma de hacerlo es usar count() con las claves primarias y buscar las entradas con n mayor a uno:

A veces una tabla puede no tener una clave primaria explícita: cada fila es una observación, pero no existe una combinación de variables que la identifique de forma confiable. Por ejemplo, ¿cuál es la clave primaria en la tabla vuelos? Quizás pienses que podría ser la fecha más el vuelo o el código de cola, pero ninguna de esas variables es única:

Al comenzar a trabajar con estos datos, ingenuamente asumimos que cada número de vuelo sería usado solo una vez al día: eso haría mucho más simple comunicar problemas con un vuelo específico. ¡Desafortunadamente este no es el caso! Si una tabla no tiene una clave primaria, a veces es útil incluir una con mutate() y row_number() (número de fila). Eso simplifica hacer coincidir observaciones una vez que haz hecho algunos filtros y quieres volver a verificar con los datos originales. Esto se llama llave subrogada.

Una clave primaria y su correspondiente clave foránea en otra tabla forman una relación. Las relaciones son típicamente uno a muchos. Por ejemplo, cada vuelo tiene un avión, pero cada avión tiene muchos vuelos. En otros datos, ocasionalmente verás relaciones uno a uno. Puedes pensar esto como un caso especial de uno a muchos. Puedes modelar relaciones muchos a muchos como relaciones de la forma muchos a uno y uno a muchos. Por ejemplo, en estos datos existe una relación muchos a muchos entre aerolíneas y aeropuertos: cada aerolínea vuela a muchos aeropuertos, cada aeropuerto recibe a muchas aerolíneas.

13.3.1 Ejercicios

  1. Agrega una clave subrogada a vuelos.

  2. Identifica las claves en los siguientes conjuntos de datos

  3. datos::bateadores
  4. datos::nombres
  5. datos::atmosfera
  6. datos::vehiculos
  7. datos::diamantes

(Puede que necesites leer un poco de documentación.)

  1. Dibuja un diagrama que ilustre las conexiones entre las tablas bateadores, personas y salarios incluidas en el paquete datos. Dibuja otro diagrama que muestre la relación entre personas, dirigentes y premios_dirigentes.

    ¿Cómo caracterizarías las relación entre bateadores, lanzadores y jardineros?

13.4 Uniones de transformación

La primera herramienta que miraremos para combinar pares de variables es la unión de transformación (mutating join). Una unión de transformación te permite combinar variables a partir de dos tablas. Primero busca coincidencias de observaciones de acuerdo a sus claves y luego copia las variables de una tabla en la otra.

Tal como mutate(), las funciones de unión agregan variables hacia la derecha, por lo que si tienes muchas variables inicialmente, las nuevas variables no se imprimirán. Para estos ejemplos, crearemos un conjunto de datos más angosto para que sea más fácil ver qué es lo que está ocurriendo:

(Recuerda que en RStudio puedes también usar View() para evitar este problema.)

Imagina que quieres incluir el nombre completo de la aerolínea en vuelos2. Puedes combinar los datos de aerolinas y vuelos2 con left_join() (union_izquierda):

El resultado de unir aerolíneas y vuelos2 es la inclusión de una variable adicional: nombre. Esto es la razón de que llamemos unión de transformación a este tipo de unión. En este caso, podrías obtener el mismo resultado usando mutate() junto a las operaciones de filtro de R base:

Sin embargo, esto último es difícil de generalizar cuando necesitas hacer coincidir múltiples variables y requiere hacer una lectura detenida para entender lo que se quiere hacer.

En las siguientes secciones explicaremos en detalle cómo funcionan las uniones de transformación. Comenzarás aprendiendo una representación visual útil de las uniones. Luego usaremos eso para explicar las cuatro uniones de transformación: la unión interior y las tres uniones exteriores. Cuando trabajas con datos reales, las claves no siempre identifican a las observaciones de forma única. Es por eso que a continuación hablaremos de lo que ocurre cuando no existe una coincidencia única. Finalmente, aprenderás cómo decirle a dplyr qué variables son las claves para una unión determinada.

13.4.1 Entendiendo las uniones

Para ayudarte a entender las uniones, usaremos una representación gráfica:

La columna coloreada representa la variable “clave”: estas se usan para unir filas entre las tablas. La columa gris representa la columna “valor” que se usa en todo el proceso. En estos ejemplos te mostraremos una única clave, pero la idea es generalizable de manera directa a múltiples claves y múltiples valores.

Una unión es una forma de conectar cada fila en x con cero, una o más filas en y. El siguiente diagrama muestra cada coincidencia potencial como una intersección de pares de líneas.

(Si observas detenidamente, te darás cuenta de que hemos cambiado el orden de las columnas clave y valor en x. Esto es para enfatizar que las uniones encuentran coincidencias basadas en las claves; el valor simplemente se traslada durante el proceso.)

En la unión que mostramos, las coincidencias se indican con puntos. El número de puntos es igual al numero de coincidencias y al número de filas en la salida.

13.4.2 Unión interior

La forma más simple de unión es la unión interior (del inglés inner join). Una unión interior une pares de observaciones siempre que sus claves sean iguales:

(Para ser precisos, esto corresponde a una unión de igualdad (o equijoin) interior, debido a que las claves se unen usando el operador de igualdad. Dado que muchas uniones son uniones de igualdad, por lo general omitimos esa especificación.)

El output de una unión interior es un nuevo data frame que contiene la clave, los valores de x y los valores de y. Usamos by (según) para indicar a dplyr qué variable es la clave:

La propiedad más importante de una unión interior es que las filas no coincidentes no se incluyen en el resultado. Esto significa que generalmente las uniones interiores no son apropiadas para su uso en el análisis de datos dado que es muy fácil perder observaciones.

13.4.3 Uniones exteriores

Una unión interior mantiene las observaciones que aparecen en ambas tablas. Una unión exterior mantiene las observaciones que aparecen en al menos una de las tablas. Existen tres tipos de uniones exteriores:

  • Una unión izquierda (del inglés left join) mantiene todas las observaciones en x.
  • Una unión derecha (del inglés right join) mantiene todas las observaciones en y.
  • Una unión completa (del inglés full join) mantiene todas las observaciones en x e y.

Estas uniones funcionan agregando una observación “virtual” adicional a cada tabla. Esta observación tiene una clave que siempre coincide (de no haber otras claves coincidentes) y un valor que se llena con NA.

Gráficamente corresponde a lo siguiente:

La unión que más frecuentemente se usa es la unión izquierda: úsala cuando necesites buscar datos adicionales en otra tabla, dado que preserva las observaciones originales incluso cuando no hay coincidencias. La unión izquierda debiera ser tu unión por defecto, a menos que tengas un motivo importante para preferir una de las otras.

Otra forma de ilustrar diferentes tipos de uniones es mediante un diagrama de Venn:

Sin embargo, esta no es una buena representación. Puede ayudar a recordar qué uniones preservan las observaciones en qué tabla pero esto tiene una limitante importante: un diagrama de Venn no puede mostrar qué ocurre con las claves que no identifican de manera única una observación.

13.4.4 Claves duplicadas

Hasta ahora todos los diagramas han asumido que las claves son únicas. Pero ese no siempre es así. Esta sección explica qué ocurre en esos casos. Existen dos posibilidades:

  1. Una tabla tiene claves duplicadas. Esto es útil cuando quieres agregar información adicional dado que típicamente existe una relación uno a muchos.

Nota que hemos puesto la columna   clave en una posición ligeramente distinta en la salida.
Esto refleja que la clave es una clave primaria en `y` y una clave foránea en `x`.
  1. Ambas tablas tienen claves duplicadas. Esto es usualmente un error debido a que en ninguna de las tablas las claves identifican de manera única una observación. Cuando unes claves duplicadas, se obtienen todas las posibles combinaciones, es decir, el producto cartesiano:

13.4.5 Definiendo las columnas clave

Hasta ahora, los pares de tablas siempre se han unido de acuerdo a una única variable y esa variable tiene el mismo nombre en ambas tablas. Esta restricción se expresa de la forma by = "key". Puedes usar otros valores de by para conectar las tablas de otras maneras:

  • Por defecto, by = NULL, usa todas las variables que aparecen en ambas tablas, lo que se conoce como unión natural. Por ejemplo, las tablas vuelos y clima coinciden en sus variables comunes: anio, mes, dia, hora y origen.
  • Un vector de caracteres, by = "x". Esto es similar a una unión natural, pero usa algunas de las variables comunes. Por ejemplo, vuelos y aviones tienen la variable anio, pero significa cosas distintas en cada tabla por lo que queremos unir por codigo_cola.

Nota que la variable anio (que aparece en los dos data frames de entrada, pero que no es igual en ambos casos) se desambigua con un sufijo en el output.

  • Un vector de caracteres con nombres: by = c("a" = "b"). Esto va a unir la variable a en la tabla x con la variabla b en la tabla y. Las variables de x se usarán en el output.

Por ejemplo, si queremos dibujar un mapa necesitamos combinar los datos de vuelos con los datos de aeropuertos, que contienen la ubicación de cada uno (latitud y longitud). Cada vuelo tiene un aeropuerto de origen y destino, por lo que necesitamos especficar cuál queremos unir:

13.4.6 Ejercicios

  1. Calcula el atraso promedio por destino y luego une los datos en aeropuertos para que puedas mostrar la distribución espacial de los atrasos. Esta es una forma fácil de dibujar un mapa de los Estados Unidos:

(No te preocupes si no entiendes que hace semi_join(); lo aprenderás a continuación.)

Quizás quieras usar `size` o `colour` para editar los puntos y mostrar 
el atraso promedio de cada aeropuerto.
  1. Agrega la ubicación de origen y destino (por ejemplo, latitud y longitud) a vuelos.

  2. ¿Existe una relación entre la antiguedad de un avión y sus atrasos?

  3. ¿Qué condiciones climáticas hacen más probables los atrasos?

  4. ¿Qué sucedió el día 13 de junio de 2013? Muestra el patrón espacial de los atrasos. Luego, usa un buscador para encontrar referencias cruzadas con el clima.

13.4.7 Otras implementaciones

base::merge() puede realizar los cuatro tipos de uniones de transformación:

dplyr merge
inner_join(x, y) merge(x, y)
left_join(x, y) merge(x, y, all.x = TRUE)
right_join(x, y) merge(x, y, all.y = TRUE),
full_join(x, y) merge(x, y, all.x = TRUE, all.y = TRUE)

La ventaja de los verbos específicos de dplyr es que muestran de manera clara la intención del código: la diferencia entre las uniones es realmente importante pero se esconde en los argumentos de merge(). Las uniones de dplyr son considerablemente más rápidas y no generan problemas con el orden de las filas

SQL es una inspiración para las convenciones de dplyr, por lo que su traducción es directa:

dplyr SQL
inner_join(x, y, by = "z") SELECT * FROM x INNER JOIN y USING (z)
left_join(x, y, by = "z") SELECT * FROM x LEFT OUTER JOIN y USING (z)
right_join(x, y, by = "z") SELECT * FROM x RIGHT OUTER JOIN y USING (z)
full_join(x, y, by = "z") SELECT * FROM x FULL OUTER JOIN y USING (z)

Nota que “INNER” y “OUTER” son opcionales, por lo que a menudo se omiten.

Unir distintas variables entre tablas, por ejemplo inner_join(x, y, by = c("a" = "b")), usa una sintaxis ligeramente distinta en SQL: SELECT * FROM x INNER JOIN y ON x.a = y.b. Como la sintaxis sugiere, SQL soporta un rango más amplio de tipos de uniones que dplyr, ya que puedes conectar tablas usando restricciones distintas a las de igualdad (a veces llamadas de no-igualdad).

13.5 Uniones de filtro

Las uniones de filtro unen observaciones de la misma forma que las uniones de transformación pero afectan a las observaciones, no a las variables. Existen dos tipos:

  • semi_join(x, y) mantiene todas las observaciones en x con coincidencias en y.
  • anti_join(x, y) descarta todas las observaciones en x con coincidencias en y.

Las semi uniones son útiles para unir tablas resumen previamente filtradas con las filas originales. Por ejemplo, imagina que encontraste los diez destinos más populares:

Ahora quieres encontrar cada vuelo que fue a alguno de esos destinos. Puedes construir un filtro:

Pero es difícil extender este enfoque a varias variables. Por ejemplo, imagina que encontraste los diez días con los atrasos promedio más altos. ¿Cómo construirías un filtro que use anio, mes y dia para buscar coincidencias con vuelos?

Puedes, en cambio, usar semi_join(), que conecta dos tablas de manera similar a una unión de transformación, pero en lugar de agregar nuevas columnas, mantiene las filas en x que tienen coincidencias en y:

Gráficamente, un semi_join() se ve de la siguiente forma:

Solo la existencia de coincidencias es importante; da igual qué observaciones son coincidentes. Esto significa que las uniones de filtro nunca duplican filas como lo hacen las uniones de transformación:

La operación inversa de semi_join() es anti_join(). anti_join() mantiene las filas que no tienen coincidencias:

Las anti uniones son útiles para encontrar desajustes. Por ejemplo, al conectar aviones y vuelos, podría interesarte saber que existen muchos vuelos que no tienen coincidencias en aviones:

13.5.1 Ejercicios

  1. ¿Qué significa que un vuelo le falte codigo_cola? ¿Qué tienen en común los códigos de cola que no tienen registros coincidentes en aviones? (Pista: Una variable explica ~90% de los problemas.)

  2. Filtra los vuelos para mostrar únicamente los aviones que han realizado al menos cien viajes.

  3. Combina datos::vehiculos y datos::comunes para encontrar los registros de los modelos más comunes.

  4. Encuentra las 48 horas (en el transcurso del año) que tengan los peores atrasos. Haz una referencia cruzada con la tabla clima. ¿Puedes observar patrones?

  5. ¿Qué te indica anti_join(vuelos, aeropuertos, by = c("destino" = "codigo_aeropuerto"))? ¿Qué te indica anti_join(aeropuertos, vuelos, by = c("codigo_aeropuerto" = "destino"))?

  6. Puedes esperar que exista una relación implícita entre aviones y aerolíneas, dado que cada avión es operado por una única aerolínea. Confirma o descarta esta hipótesis usando las herramientas que aprendiste más arriba.

13.6 Problemas con las uniones

Los datos con los que has estado trabajando en este capítulo han sido limpiados de modo que tengas la menor cantidad de problemas posibles. Tus datos difícilmente estarán tan ordenados, por lo que hay algunas consideraciones y pasos a tener en cuenta para que las uniones sobre tus propios datos funcionen adecuadamente.

  1. Comienza identificando las variables que forman las claves primarias en cada tabla. Usualmente debieras hacerlo considerando tus conocimientos de los datos, no observando empíricamente las combinaciones de variables que resultan en un identificador único. Si te centras en las variables sin pensar en sus significados, puedes tener la (mala) suerte de encontrar una combinación única en tus datos pero que no sea válida en general.

    Por ejemplo, la altura y la longitud identifican de manera única cada aeropuerto, ¡pero no son buenos identificadores!

  2. Verifica que ninguna de las variables en la clave primaria esté perdida. ¡Si hay un valor faltante no podrá identificar una observación!

  3. Verifica que las claves foráneas coincidan con las claves primarias en otra tabla. La mejor forma de hacerlo es mediante anti_join(). Es común que las claves no coincidan debido a errores en la entrada de datos. Arreglar este problema requiere mucho trabajo.

    Si tienes claves perdidas, debes tener cuidado en el uso de unión interior versus unión exterior y considerar cuidadosamente si quieres descartar las filas que no tengan coincidencias.

Ten en cuenta que verificar el número de filas antes y después de unir no es suficiente para asegurar que la unión funcionó de forma exitosa. Si tienes una unión interior con claves duplicadas en ambas tablas, puedes tener la mala suerte de que el número de filas descartadas sea igual al número de filas duplicadas.

13.7 Operaciones de conjuntos

El tipo final de verbo para dos tablas son las operaciones de conjunto. Si bien lo usamos de manera poco frecuente, en ocasiones es útil cuando quieres dividir un filtro complejo en partes maś simples. Todas estas operaciones funcionan con una fila completa, comparando los valores de cada variable. Esto espera que los imput x e y tengan las mismas variables y trata las observaciones como conjuntos:

  • intersect(x, y): entregas las observaciones comunes en x e y.
  • union(x, y): entregas las observaciones únicas en x e y.
  • setdiff(x, y): entregas las observaciones en x pero no en y.

Dados los siguientes datos simples:

Las cuatro posibilidades son:


  1. NdT. El texto original se refiere al paquete nycflights13 cuya traducción se incluye en el paquete datos.