Combinar DataFrames en Julia (16ª parte – ¡Hola Julia!)

Publicado el 03 septiembre 2020 por Daniel Rodríguez @analyticslane

Al trabajar con diferentes conjuntos de datos puede que estos no se encuentren un único DataFrame. Por ejemplo, si lo importamos de una base de datos en el que tengamos una tabla de clientes y otra de factura. En tal caso se puede usar SQL para crear una consulta en la que unir las tablas e importar el resultado. Aunque sí queremos tener un mayor control sobre las operaciones se puede unir y combinar DataFrames en Julia con las herramientas que existen en la librería DataFrames.

Creación de un conjunto de datos

Vamos a crear un conjunto de datos similar al que usamos en su momento para explicar cómo combinar DataFrames en Python. Creando una tabla de clientes y otra tabla con algunas facturas.

using DataFrames

clients = DataFrame(
    id=[1,2,3,4,5],
    first_name=["Oralie" ,"Imojean" ,"Michele", "Ailbert", "Stevy"],
    last_name=["Fidgeon" ,"Benet" ,"Woodlands", "Risdale", "MacGorman"],
    age=[30 ,21 ,29 ,22, 24])

invoices = DataFrame(
    invoice_id=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
    client_id=[3, 2, 7, 2, 7, 3, 1, 4 ,2, 3, 6, 2],
    amount=[77.91, 24.36, 74.65, 19.75, 27.46, 17.13, 45.77, 81.7, 14.41, 52.69, 32.03, 12.78])
5×4 DataFrame
│ Row │ id    │ first_name │ last_name │ age   │
│     │ Int64 │ String     │ String    │ Int64 │
├─────┼───────┼────────────┼───────────┼───────┤
│ 1   │ 1     │ Oralie     │ Fidgeon   │ 30    │
│ 2   │ 2     │ Imojean    │ Benet     │ 21    │
│ 3   │ 3     │ Michele    │ Woodlands │ 29    │
│ 4   │ 4     │ Ailbert    │ Risdale   │ 22    │
│ 5   │ 5     │ Stevy      │ MacGorman │ 24    │

12×3 DataFrame
│ Row │ invoice_id │ client_id │ amount  │
│     │ Int64      │ Int64     │ Float64 │
├─────┼────────────┼───────────┼─────────┤
│ 1   │ 1          │ 3         │ 77.91   │
│ 2   │ 2          │ 2         │ 24.36   │
│ 3   │ 3          │ 7         │ 74.65   │
│ 4   │ 4          │ 2         │ 19.75   │
│ 5   │ 5          │ 7         │ 27.46   │
│ 6   │ 6          │ 3         │ 17.13   │
│ 7   │ 7          │ 1         │ 45.77   │
│ 8   │ 8          │ 4         │ 81.7    │
│ 9   │ 9          │ 2         │ 14.41   │
│ 10  │ 10         │ 3         │ 52.69   │
│ 11  │ 11         │ 6         │ 32.03   │
│ 12  │ 12         │ 2         │ 12.78   │

Combinación interna (INNER JOIN)

La operación más habitual a la hora de cruzar tablas es la combinación interna en la que se crea una nueva tabla únicamente con los registros relacionados de ambas. Para ello Julia cuenta con la función innerjoin() en la que se deben indicar dos DataFrames y el criterio de unión que se indica en la propiedad on. En los datos de ejemplo el identificador del cliente es id en la tabla clients, mientras que el identificador del cliente en la factura es client_id. Así para unir a los clientes con las facturas se puede hacer.

innerjoin(clients, invoices, on=:id=>:client_id)
9×6 DataFrame
│ Row │ id    │ first_name │ last_name │ age   │ invoice_id │ amount  │
│     │ Int64 │ String     │ String    │ Int64 │ Int64      │ Float64 │
├─────┼───────┼────────────┼───────────┼───────┼────────────┼─────────┤
│ 1   │ 1     │ Oralie     │ Fidgeon   │ 30    │ 7          │ 45.77   │
│ 2   │ 2     │ Imojean    │ Benet     │ 21    │ 2          │ 24.36   │
│ 3   │ 2     │ Imojean    │ Benet     │ 21    │ 4          │ 19.75   │
│ 4   │ 2     │ Imojean    │ Benet     │ 21    │ 9          │ 14.41   │
│ 5   │ 2     │ Imojean    │ Benet     │ 21    │ 12         │ 12.78   │
│ 6   │ 3     │ Michele    │ Woodlands │ 29    │ 1          │ 77.91   │
│ 7   │ 3     │ Michele    │ Woodlands │ 29    │ 6          │ 17.13   │
│ 8   │ 3     │ Michele    │ Woodlands │ 29    │ 10         │ 52.69   │
│ 9   │ 4     │ Ailbert    │ Risdale   │ 22    │ 8          │ 81.7    │

Al ser una combinación interna solamente se representan los clientes que tiene al menos una factura y las facturas que tienen un cliente en la otra tabla. Ignorando el resto de los registros. En este caso se obtienen los nueve registros de las doce facturas.

Combinaciones externas izquierda y derecha (LEFT JOIN y RIGHT JOIN)

Puede ser que necesitemos una tabla con todos los clientes, aunque estos no hayan comprado nada o todas las facturas, aunque no tengamos los datos de los clientes. En este caso la combinación interna no es válida ya que omite los casos en los que no existe relación. Por lo que es necesario usar las combinaciones externas izquierda y derecha. Las cuales se pueden implementar mediante las funciones leftjoin() o rightjoin() respectivamente. Ambas con una sintaxis similar a innerjoin().

En las combinaciones externas izquierdas se incluyen todos los registros del primer DataFrame (el situado a la izquierda), incluso si no existen valores coincidentes para registros en el segundo DataFrame (el situado a la izquierda). Siendo al revés en el caso de las combinaciones externas derecha. Así para obtener todos los clientes, aunque no compren nada se puede realizar mediante una combinación externa izquierda.

leftjoin(clients, invoices, on=:id=>:client_id)
10×6 DataFrame
│ Row │ id    │ first_name │ last_name │ age   │ invoice_id │ amount   │
│     │ Int64 │ String     │ String    │ Int64 │ Int64?     │ Float64? │
├─────┼───────┼────────────┼───────────┼───────┼────────────┼──────────┤
│ 1   │ 1     │ Oralie     │ Fidgeon   │ 30    │ 7          │ 45.77    │
│ 2   │ 2     │ Imojean    │ Benet     │ 21    │ 2          │ 24.36    │
│ 3   │ 2     │ Imojean    │ Benet     │ 21    │ 4          │ 19.75    │
│ 4   │ 2     │ Imojean    │ Benet     │ 21    │ 9          │ 14.41    │
│ 5   │ 2     │ Imojean    │ Benet     │ 21    │ 12         │ 12.78    │
│ 6   │ 3     │ Michele    │ Woodlands │ 29    │ 1          │ 77.91    │
│ 7   │ 3     │ Michele    │ Woodlands │ 29    │ 6          │ 17.13    │
│ 8   │ 3     │ Michele    │ Woodlands │ 29    │ 10         │ 52.69    │
│ 9   │ 4     │ Ailbert    │ Risdale   │ 22    │ 8          │ 81.7     │
│ 10  │ 5     │ Stevy      │ MacGorman │ 24    │ missing    │ missing  │

Por otro lado, una lista de todas las facturas, aunque no exista un registro con los datos del cliente se puede obtener con combinación externa derecha (o izquierda cambiando el orden de los DataFrames).

rightjoin(clients, invoices, on=:id=>:client_id)
12×6 DataFrame
│ Row │ id    │ first_name │ last_name │ age     │ invoice_id │ amount  │
│     │ Int64 │ String?    │ String?   │ Int64?  │ Int64      │ Float64 │
├─────┼───────┼────────────┼───────────┼─────────┼────────────┼─────────┤
│ 1   │ 3     │ Michele    │ Woodlands │ 29      │ 1          │ 77.91   │
│ 2   │ 2     │ Imojean    │ Benet     │ 21      │ 2          │ 24.36   │
│ 3   │ 2     │ Imojean    │ Benet     │ 21      │ 4          │ 19.75   │
│ 4   │ 3     │ Michele    │ Woodlands │ 29      │ 6          │ 17.13   │
│ 5   │ 1     │ Oralie     │ Fidgeon   │ 30      │ 7          │ 45.77   │
│ 6   │ 4     │ Ailbert    │ Risdale   │ 22      │ 8          │ 81.7    │
│ 7   │ 2     │ Imojean    │ Benet     │ 21      │ 9          │ 14.41   │
│ 8   │ 3     │ Michele    │ Woodlands │ 29      │ 10         │ 52.69   │
│ 9   │ 2     │ Imojean    │ Benet     │ 21      │ 12         │ 12.78   │
│ 10  │ 7     │ missing    │ missing   │ missing │ 3          │ 74.65   │
│ 11  │ 7     │ missing    │ missing   │ missing │ 5          │ 27.46   │
│ 12  │ 6     │ missing    │ missing   │ missing │ 11         │ 32.03   │

Nótese que en estos casos los registros que faltan son reemplazados por missing. Cambiando el tipo de dato de la columna del original a una unión de con el tipo Missing. Lo que se denota por el signo de interrogación al final del tipo de dato.

Combinaciones externas (OUTER JOIN)

outerjoin(clients, invoices, on=:id=>:client_id)
13×6 DataFrame
│ Row │ id    │ first_name │ last_name │ age     │ invoice_id │ amount   │
│     │ Int64 │ String?    │ String?   │ Int64?  │ Int64?     │ Float64? │
├─────┼───────┼────────────┼───────────┼─────────┼────────────┼──────────┤
│ 1   │ 1     │ Oralie     │ Fidgeon   │ 30      │ 7          │ 45.77    │
│ 2   │ 2     │ Imojean    │ Benet     │ 21      │ 2          │ 24.36    │
│ 3   │ 2     │ Imojean    │ Benet     │ 21      │ 4          │ 19.75    │
│ 4   │ 2     │ Imojean    │ Benet     │ 21      │ 9          │ 14.41    │
│ 5   │ 2     │ Imojean    │ Benet     │ 21      │ 12         │ 12.78    │
│ 6   │ 3     │ Michele    │ Woodlands │ 29      │ 1          │ 77.91    │
│ 7   │ 3     │ Michele    │ Woodlands │ 29      │ 6          │ 17.13    │
│ 8   │ 3     │ Michele    │ Woodlands │ 29      │ 10         │ 52.69    │
│ 9   │ 4     │ Ailbert    │ Risdale   │ 22      │ 8          │ 81.7     │
│ 10  │ 5     │ Stevy      │ MacGorman │ 24      │ missing    │ missing  │
│ 11  │ 7     │ missing    │ missing   │ missing │ 3          │ 74.65    │
│ 12  │ 7     │ missing    │ missing   │ missing │ 5          │ 27.46    │
│ 13  │ 6     │ missing    │ missing   │ missing │ 11         │ 32.03    │

Otra opción a la hora de combinar dos DataFrames es realizar una unión externa en la que se recogerán todos los registros de las dos tablas, aunque no exista relación con uno de la otra. Algo que se puede realizar mediante el uso de la función outerjoin().

Semicombinaciones

Hasta ahora el resultado de las funciones es un nuevo DataFrame con los datos de los combinar los dos anteriores. Pero puede que no sean necesarios los datos de una de las dos tablas. Ya que podemos querer solamente los clientes que han comprado y los que no. Esto se puede hacer con la función semijoin(), en los que el resultado serán los registros del primer DataFrame que estén relacionados con el segundo. La forma de llamar la función es exactamente igual a las funciones vistas anteriormente.

semijoin(clients, invoices, on=:id=>:client_id)
4×4 DataFrame
│ Row │ id    │ first_name │ last_name │ age   │
│     │ Int64 │ String     │ String    │ Int64 │
├─────┼───────┼────────────┼───────────┼───────┤
│ 1   │ 1     │ Oralie     │ Fidgeon   │ 30    │
│ 2   │ 2     │ Imojean    │ Benet     │ 21    │
│ 3   │ 3     │ Michele    │ Woodlands │ 29    │
│ 4   │ 4     │ Ailbert    │ Risdale   │ 22    │

Alternativamente se pueden obtener la lista de registros del primer DataFrame que no tiene relación mediante antijoin().

antijoin(clients, invoices, on=:id=>:client_id)
1×4 DataFrame
│ Row │ id    │ first_name │ last_name │ age   │
│     │ Int64 │ String     │ String    │ Int64 │
├─────┼───────┼────────────┼───────────┼───────┤
│ 1   │ 5     │ Stevy      │ MacGorman │ 24    │

Combinar con múltiples criterios

Ahora nos puede surgir un problema, ¿qué pasa si tenemos más de un criterio para la combinación de los datos de dos DataFrames? Por ejemplo, tenemos varias tiendas y en cada una de ellas el cliente tiene un identificador diferente. En este caso solo hay que pasar en un vector los criterios de unión. Así se puede crear un nuevo ejemplo en que se añade un identificador de tiene tanto al cliente como a la factura. Por lo que lo que hace único un registro es la combinación de ambos.

clients = DataFrame(store_id=[1, 1, 1, 2, 2, 2],
    client_id=[1, 2, 3, 1, 2, 3],
    first_name=["Oralie" ,"Imojean" ,"Michele", "Ailbert", "Stevy", "Imojean"],
    last_name=["Fidgeon" ,"Benet" ,"Woodlands", "Risdale", "MacGorman", "Benet"],
    age=[30 ,21 ,29 ,22, 24, 22])

invoices = DataFrame(invoice_id=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
    store_id=[1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2],
    client_id=[3, 2, 7, 2, 7, 3, 1, 4 ,2, 3, 6, 2],
    amount=[77.91, 24.36, 74.65, 19.75, 27.46, 17.13, 45.77, 81.7, 14.41, 52.69, 32.03, 12.78])
6×5 DataFrame
│ Row │ store_id │ client_id │ first_name │ last_name │ age   │
│     │ Int64    │ Int64     │ String     │ String    │ Int64 │
├─────┼──────────┼───────────┼────────────┼───────────┼───────┤
│ 1   │ 1        │ 1         │ Oralie     │ Fidgeon   │ 30    │
│ 2   │ 1        │ 2         │ Imojean    │ Benet     │ 21    │
│ 3   │ 1        │ 3         │ Michele    │ Woodlands │ 29    │
│ 4   │ 2        │ 1         │ Ailbert    │ Risdale   │ 22    │
│ 5   │ 2        │ 2         │ Stevy      │ MacGorman │ 24    │
│ 6   │ 2        │ 3         │ Imojean    │ Benet     │ 22    │

12×4 DataFrame
│ Row │ invoice_id │ store_id │ client_id │ amount  │
│     │ Int64      │ Int64    │ Int64     │ Float64 │
├─────┼────────────┼──────────┼───────────┼─────────┤
│ 1   │ 1          │ 1        │ 3         │ 77.91   │
│ 2   │ 2          │ 1        │ 2         │ 24.36   │
│ 3   │ 3          │ 1        │ 7         │ 74.65   │
│ 4   │ 4          │ 1        │ 2         │ 19.75   │
│ 5   │ 5          │ 1        │ 7         │ 27.46   │
│ 6   │ 6          │ 1        │ 3         │ 17.13   │
│ 7   │ 7          │ 2        │ 1         │ 45.77   │
│ 8   │ 8          │ 2        │ 4         │ 81.7    │
│ 9   │ 9          │ 2        │ 2         │ 14.41   │
│ 10  │ 10         │ 2        │ 3         │ 52.69   │
│ 11  │ 11         │ 2        │ 6         │ 32.03   │
│ 12  │ 12         │ 2        │ 2         │ 12.78   │

Así para obtener una combinación interna de ambas solo tenemos que modificar el criterio como se muestra a continuación.

innerjoin(clients, invoices, on=[:client_id, :store_id])
8×7 DataFrame
│ Row │ store_id │ client_id │ first_name │ last_name │ age   │ invoice_id │ amount  │
│     │ Int64    │ Int64     │ String     │ String    │ Int64 │ Int64      │ Float64 │
├─────┼──────────┼───────────┼────────────┼───────────┼───────┼────────────┼─────────┤
│ 1   │ 1        │ 2         │ Imojean    │ Benet     │ 21    │ 2          │ 24.36   │
│ 2   │ 1        │ 2         │ Imojean    │ Benet     │ 21    │ 4          │ 19.75   │
│ 3   │ 1        │ 3         │ Michele    │ Woodlands │ 29    │ 1          │ 77.91   │
│ 4   │ 1        │ 3         │ Michele    │ Woodlands │ 29    │ 6          │ 17.13   │
│ 5   │ 2        │ 1         │ Ailbert    │ Risdale   │ 22    │ 7          │ 45.77   │
│ 6   │ 2        │ 2         │ Stevy      │ MacGorman │ 24    │ 9          │ 14.41   │
│ 7   │ 2        │ 2         │ Stevy      │ MacGorman │ 24    │ 12         │ 12.78   │
│ 8   │ 2        │ 3         │ Imojean    │ Benet     │ 22    │ 10         │ 52.69   │

Combinar DataFrames en Julia con la potencia de SQL

Las posibilidades a la hora de combinar DataFrames en Julia son similares a las que nos puede ofrecer SQL. Lo que nos permite importar los datos directamente y realizar las combinaciones directamente en la sesión de Julia. Por lo que, sí necesitamos al estar trabajando vemos que necesitas otro tipo de combinación solo se puede realizar rápidamente sin tener que volver a la base de datos.

Publicidad