Combinando estructuras de datos con pandas

| | ,

La librería pandas tiene bastantes funcionalidades para combinar distintas Series o DataFrames: la función concat, el método append, merge, join… El problema de tener tantas opciones es que, muchas veces, ¡es difícil saber cuándo usar unas y otras! Vamos a intentar poner un poco de orden.

pd.concat(): concatenación de objetos

pd.concat() permite concatenar objetos a lo largo del eje X (“pegando” unas filas debajo de otras) o a lo largo del eje Y (“pegando” unas columnas al lado de otras). Por supuesto, en el caso de las series solo hay un posible eje a lo largo del cual concatenar.

Inciso: de ahora en adelante, me referiré tanto a las series como a los dataframes con ambos nombres en minúsculas, aunque en la documentación oficial de pandas los verás como Serie y DataFrame.

Los parámetros más relevantes son los siguientes:

  • axis indica el eje a lo largo del cual se quieren concatenar los objetos. Por defecto es 0 (para los dataframes, este es el eje de las filas, mientras que para las series este es el único eje posible), aunque puede ser 0,1…
  • join permite indicar “qué hacer con los otros ejes”:
    • Si se quiere llevar a cabo una unión, hay que fijar el parámetro join=”outer”. Esta es la opción por defecto, ya que no conlleva ninguna pérdida de información.
    • Para llevar a cabo una intersección, hay que fijar join=”inner”. Esta opción, en muchos casos, provocará una pérdida de información.
  • ignore_index permite indicar si se desea ignorar los índices originales de las estructuras de datos que se están combinando. Si es “True”, entonces creará un nuevo índice automático de números consecutivos, empezando en 0.

Imagina que partimos de este dataframe:

uno = pd.DataFrame({"col1": [1, 2, 3],
                   "col2": [4, 5, 6],
                   "col3": [7, 8, 9]},
                  index = ["id1", "id2", "id3"])

Si pruebas a ejecutar el código anterior en un editor, verás que la salida es como la siguiente:

Ahora, vamos a crear otro dataframe distinto:

dos = pd.DataFrame({"col1": [10, 11, 12, 13],
                   "col2": [14, 15, 16, 17],
                   "col3": [18, 19, 20, 21]})

¿Cuál sería el resultado si combinásemos las dos estructuras de datos anteriores, utilizando esta combinación de parámetros para la función concat?:

pd.concat([uno, dos],
         axis = 0,
         join = "outer",
         ignore_index = False)

Obtendríamos este nuevo dataframe:

Fíjate en que, al fijar el parámetro axis en 0, estamos “pegando” unas filas debajo de otras, es decir, estamos concatenando a lo largo del eje de las filas. Además, ignore_index=False hace que se conserven los índices originales (id1, id2, id3, 0, 1, 2, 3).

Intenta anticipar cuál sería el resultado si ahora utilizamos esta otra combinación de parámetros:

pd.concat([uno, dos],
         axis = 1,
         join = "outer",
         ignore_index = False)

Esta vez, el dataframe resultante no queda tan compacto:

¿Qué ha pasado? Estamos concatenando a lo largo del eje de las columnas, es decir, pegando unas columnas al lado de otras. Sin embargo, al fijar el parámetro join en “outer”, aunque no coincidan los índices de ambos dataframes (es decir, los “nombres” de las filas) y, por tanto, no sea posible pegar las columnas del dataframe de nombre “dos” a la derecha de las columnas del dataframe “uno”, le estamos diciendo a pandas que se las apañe como sea para combinarlas, y el resultado es que se añaden columnas extra pero con índices distintos, y se rellena con valores en blanco o NaN.

¿Sabes qué ocurriría si fijásemos el parámetro join en “inner“, en lugar de “outer“?

pd.concat([uno, dos],
         axis = 1,
         join = "inner",
         ignore_index = False)

Pues que pandas ahora solo pegaría aquellas columnas para las cuales el índice (o nombre de fila) coincidiese. Es decir, en este ejemplo, ninguna:

Como resultado de la operación anterior, nos quedamos con un dataframe vacío.

Ya ves que puedes modificar los parámetros de la manera que necesites, para obtener el resultado que te haga falta. Además, ten en cuenta que también puedes combinar series entre sí, o una serie con un dataframe. Veamos un ejemplo de esto último. Para eso, definimos una nueva serie, llamada serie1:

serie1 = pd.Series(["s1", "s2", "s3"],
                  name = "col5",
                  index = ["n1", "n2", "n3"])

Vamos a combinarla con uno de los dataframes que hemos usado en los ejemplos anteriores:

pd.concat([uno, serie1],
         axis = 1,
         join = "outer",
         ignore_index = False)

El resultado es el siguiente:

La serie se añade como una nueva columna (debido a que hemos fijado axis=1) y, como no coincide el índice de la serie (n1, n2, n3) con el del dataframe (id1, id2, id3), no queda más remedio que ampliar el dataframe resultante y rellenar con valores NaN (esto se debe a join=”outer”).

Te animo a hacer más pruebas, inventándote otras estructuras de datos y otras combinaciones de parámetros para ver cómo afectan al resultado.

df.append(): una especie de atajo a pd.concat()

Este método concatena estructuras de datos a lo largo del eje=0, es decir, concatena filas. Las columnas de la segunda estructura que no están en la primera, se agregan como nuevas columnas. Es muy sencillo y tiene muy pocos parámetros, entre los cuales destaca principalmente el siguiente:

  • ignore_index permite indicar si se desea ignorar los índices originales de las estructuras de datos que se están combinando. Si es “True”, entonces creará un nuevo índice automático de números consecutivos, empezando en 0.

En el caso de combinar dataframes, los índices deben ser diferentes, pero las columnas no necesariamente tienen que serlo.

Para ver cómo funciona este método en la práctica, vamos a seguir usando los dataframes de los ejemplos anteriores:

uno.append(dos, ignore_index=False)

Puedes ver el resultado de esta operación a continuación:

Te dejo aquí otro ejemplo:

uno.append(dos, ignore_index=True)

¡Ojo! Es importante tener en cuenta que concat() y append() hacen una copia completa de los datos y que, por tanto, su reutilización constante puede causar un impacto negativo en el rendimiento.

df.merge()

Pandas también cuenta con un join muy similar al que se utiliza en SQL, pero en este caso se llama merge(). Veamos los parámetros más importantes:

  • left es un dataframe o una serie.
  • right es otro dataframe o serie.
  • on son los nombres de columna o niveles de índice para unirse, que deben estar presentes tanto en el dataframe de la izquierda como en el de la derecha.
  • left_on son las columnas o niveles de índice del dataframe o serie de la izquierda para usar como claves.
  • right_on son las columnas o niveles de índice del dataframe o serie de la derecha para usar como claves.
  • how puede ser ‘left’, ‘right’, ‘outer’ o ‘inner’. Por defecto es ‘inner’.

Mira este ejemplo para entender cómo funciona:

uno.merge(dos, 
         how='inner',
         on = "col1")

Aquí tienes otro más:

uno.merge(dos, 
         how='outer',
         on = "col1")

Y en este puedes ver cómo afecta el parámetro suffixes:

uno.merge(dos, 
         how='outer',
         on = "col1",
         suffixes=('_dfizq', '_dfder'))

Te dejo un último ejemplo para terminar de reforzar:

dos.merge(uno, 
         on = "col1",
         how='right')

Visto esto, pasamos al último método de combinación de estructuras de datos de pandas.

df.join()

df.join() permite unir columnas de un dataframe a las de otro. Sus principales parámetros son:

  • on son los nombres de las columnas o nivel de índice de un dataframe sobre los cuales se unirá el otro.
  • how puede ser ‘left’ (por defecto), ‘right’, ‘outer’ o ‘inner’.

Las siguientes operaciones son completamente equivalentes:

left.join(right, on=key_or_keys)

left.merge(right, left_on=key_or_keys, right_index=True, how=’left’, sort=False)

Para entender bien cómo funciona join en la práctica, vamos a definir dos nuevos dataframes:

eventos1 = pd.DataFrame({"Evento": ["Id1", "Id2", "Id3"],
                        "Encargado": ["Roberto C.", "Jorge L.", "Lucía M."]},
                        index = ['2018-01-01 00:00:00', 
                                 '2018-01-01 01:00:00',
                                 '2018-01-01 02:00:00'])
eventos2 = pd.DataFrame({"Ubicación": ["ss_2", "ss_11", "ss_2", "ss_6", "ss_4"],
                        "Repetido": [True, False, False, True, True]},
                        index = ['2018-01-01 00:00:00', 
                                 '2018-01-01 01:00:00',
                                 '2018-01-01 02:00:00',
                                 '2018-01-01 03:00:00',
                                 '2018-01-01 04:00:00'])

Aquí tienes unos cuantos ejemplos:

eventos1.join(eventos2,
            how = "left")
eventos1.join(eventos2,
            how = "right")
eventos1.join(eventos2,
            how = "inner")
eventos1.join(eventos2,
            how = "outer")

Llegados a este punto, seguro que tienes mucho más claros los parecidos y diferencias entre unas técnicas y otras. Aún así, si te apetece seguir profundizando en este y otros temas de Data Science, échale un vistazo a mi curso.

Recuerda que, en Data Science, hay muchísimas maneras distintas de hacer lo mismo, así que te recomiendo que no intentes aprenderte jamás todas las técnicas de memoria. Es mucho mejor dominar una sola, y ser consciente del resto de opciones que puedes utilizar si las necesitas para algún caso puntual.

Anterior

Anaconda vs Miniconda

Stateless vs Stateful

Siguiente

Deja un comentario