4 trucos para mejorar su trabajo con Pandas DataFrame
noviembre 1, 2021Índice
Introducción
Pandas es una de las librerías de Python más usadas para el análisis de datos. Lo usamos tanto para realizar análisis de datos como para prepararlos cuando vamos a entrenar nuestros algoritmos de Machine Learning.
En ocasiones, este trabajo se puede volver muy lento, esto sucede más cuando tenemos muchísimos datos que procesar y analizar.
En esta publicación te enseño 4 pequeños trucos para mejorar su programación con Pandas.
Para esta publicación voy a usar un fichero CSV que pesa aproximadamente 3.4 Gigas. Contiene 3 millones de filas y 102 columnas. Lo he modificado de este conjunto de datos (fichero train): https://www.kaggle.com/c/tabular-playground-series-nov-2021/data.
Empecemos:
Leyendo CSV
¿Cuántas veces hemos perdido mucho tiempo intentando leer un fichero CSV desde pandas? A día de hoy tenemos conjuntos de datos con millones de filas. Este conjunto de datos muchas veces se encuentran almacenados en formato CSV.
Intentemos cargar el fichero en memoria usando la función pd.read_csv
import pandas as pd %%time pandas_df = pd.read_csv(csv_FILENAME)
Podemos ver que ha tardado casi ¡30 segundos!. A lo mejor puedes pensar que no es mucho tiempo, ¿verdad? Ahora imagina que tienes un directorio con muchos archivos de este tamaño y que cada x tiempo se añaden más y más archivos, ¿A que ahora has cambiado de opinión?.
¿Podemos optimizar estos tiempos? La respuesta es: SI. Pero para eso tenemos que usar otras librerías: datatable, dask, vaex (si conocen alguna otra librería, escribirlo en los comentarios por favor). Probemos:
import datatable as dt import dask.dataframe as dd import vaex %%time datatable_df = dt.fread(csv_FILENAME).to_pandas() %%time dask_df = dd.read_csv(csv_FILENAME).compute() %%time vaex_df = vaex.from_csv(csv_FILENAME).to_pandas_df()
De las 3 librerías me ha sorprendido gratamente la librería datatable. Ha reducido de 30 segundos a ¡casi 2 segundos!
Método Apply
Gran parte del trabajo de un analista de datos y un científico de datos es transformar variables a partir de las variables originales. A un analista es necesario para generar reportes y a un científico de datos es necesario para alimentar su modelo de Machine Learning.
Supongamos que queremos generar una nueva columna con la siguiente condición:
import numpy as np def genera_variables(col1, col2, col3, col4): return np.log1p((col1 + col3) ** 2 / np.sqrt(np.abs(col2) + np.abs(col4)))
En general no es recomendable realizar bucles para crear nuevas variables. Solo en ciertos casos podría ser útil, pero no es lo recomendable.
Probemos usando el método apply:
%%time pandas_df['nueva_variable'] = pandas_df.apply( lambda row: genera_variables( row['f1'], row['f2'], row['f3'], row['f4'] ), axis=1 )
Usando el método apply, vemos que el proceso ha tardado aproximadamente 1 minuto. ¿Podemos optimizar estos tiempos? Si:
Probemos usando numpy arrays
%%time pandas_df['nueva_variable_np'] = genera_variables(pandas_df['f1'].values, pandas_df['f2'].values, pandas_df['f3'].values, pandas_df['f4'].values)
Otra opción es usando list comprehension, veamos:
%%time pandas_df['nueva_variable_list'] = [genera_variables(f1, f2, f3, f4) for (f1, f2, f3, f4) in zip(pandas_df['f1'], pandas_df['f2'], pandas_df['f3'], pandas_df['f4'])]
Veamos cuánto tardan:
Vemos que usando numpy arrays la mejora es significativa. La opción list comprehension también es buena, pero de ambas numpy arrays es mucho más rápida.
Algo que he notado al escribir la publicación es que la opción list comprehension devuelve exactamente los mismos resultados que el método apply. La opción numpy arrays es mucho más rápida, pero en poquísimos casos, los resultados no son los mismos (pero estamos hablando del decimo sexto decimal):
Esto no pasa usando list comprehension:
Habrá ocasiones en las que tendrá que aplicar bucles para generar nuevas variables. En estos casos pruebe usando los siguientes métodos de pandas: apply, applymap, itertuples.
Optimizando memoria
Estoy seguro que a todos nos ha aparecido el siguiente mensaje de error cuando trabajamos con pandas dataframes: memory error
Esto puede suceder cuando tenemos variables que tienen reservada más memoria de lo que deberían. Mayormente sucede cuando leemos desde un CSV, ya que todas las variables numéricas se almacenan en memoria en formato float64 o int64 y las variables categóricas o texto en formato object.
La siguiente función optimiza el uso de memoria:
def reduciendo_uso_memoria(df: pd.DataFrame, verbose: bool = True) -> pd.DataFrame: DICT_TIPO_INT = { 'int8': [np.iinfo(np.int8).min, np.iinfo(np.int8).max], 'int16': [np.iinfo(np.int16).min, np.iinfo(np.int16).max], 'int32': [np.iinfo(np.int32).min, np.iinfo(np.int32).max], 'int64': [np.iinfo(np.int64).min, np.iinfo(np.int64).max] } DICT_TIPO_FLOAT = { 'float16': [np.finfo(np.float16).min, np.finfo(np.float16).max], 'float32': [np.finfo(np.float32).min, np.finfo(np.float32).max] } LIST_TYPES = list(DICT_TIPO_INT.keys()) + list(DICT_TIPO_FLOAT.keys()) + ['float64'] memoria_inicial = df.memory_usage().sum() / 1024 ** 2 for col in df.columns: col_type = df[col].dtypes if col_type not in LIST_TYPES: continue c_min, c_max = df[col].min(), df[col].max() if str(col_type)[:3] == 'int': if c_min > DICT_TIPO_INT['int8'][0] and c_max < DICT_TIPO_INT['int8'][1]: df[col] = df[col].astype(np.int8) elif c_min > DICT_TIPO_INT['int16'][0] and c_max < DICT_TIPO_INT['int16'][1]: df[col] = df[col].astype(np.int16) elif c_min > DICT_TIPO_INT['int32'][0] and c_max < DICT_TIPO_INT['int32'][1]: df[col] = df[col].astype(np.int32) elif c_min > DICT_TIPO_INT['int64'][0] and c_max < DICT_TIPO_INT['int64'][1]: df[col] = df[col].astype(np.int64) else: if c_min > DICT_TIPO_FLOAT['float16'][0] and c_max < DICT_TIPO_FLOAT['float16'][1]: df[col] = df[col].astype(np.float16) elif c_min > DICT_TIPO_FLOAT['float32'][0] and c_max < DICT_TIPO_FLOAT['float32'][1]: df[col] = df[col].astype(np.float32) else: df[col] = df[col].astype(np.float64) memoria_final = df.memory_usage().sum() / 1024 ** 2 if verbose: print('Uso de memoria reducido a {:.2f} Mb (reducción {:.1f}%)'.format( memoria_final, 100 * (memoria_inicial - memoria_final) / memoria_inicial ) ) return df
Veamos si realmente obtenemos una reducción de memoria:
Hemos reducido en más de un ¡70%! la memoria de nuestro DataFrame, una mejora significativa. Tenga en cuenta que esta optimización se pierde si guarda el DataFrame en CSV, por lo que es necesario volver a ejecutar la función cuando lea un CSV.
A lo mejor puede pensar que no tiene sentido esta mejora en memoria, pero cuando empiece a ver mensajes como OutOfMemory me agradecerá ;).
Guardando CSV
Hemos visto que leer ficheros CSV es muy lento. Lamentablemente pasa lo mismo si guardamos en CSV. Veamos:
%%time pandas_df.to_csv(FILENAME_SAVE_CSV, index=False)
Más de 3 minutos, no es mucho si quiere ir a por un café ;).
Probemos guardando en otros formatos:
%%time pandas_df.to_feather(FILENAME_SAVE_FEATHER) %%time pandas_df.to_pickle(FILENAME_SAVE_PICKLE)
Podemos ver una mejora, no solo en tiempos sino también en memoria en disco. Otra ventaja de guardar en otros formatos (como pickle) es que se mantiene el formato de las variables, cosa que no pasa si guardamos en CSV. Aunque tenga en cuenta que es importante la versión de las librerías si va a abrir el fichero en otro ordenador.
Conclusión
En esta publicación hemos aprendido 4 trucos para mejorar nuestro trabajo en pandas DataFrame. Tenga en cuenta que estas mejoras se notarán cuando trabajemos con conjuntos de datos muy grandes. Con conjunto de datos pequeños, prácticamente no notará ninguna mejora.
De todas maneras, les recomiendo probar estos trucos.
Espero que la publicación le haya gustado, si es así, le animo a que me pueda invitar un café 🙂
buymeacoffee.com/jahaziel
Por cierto, el código lo puede obtener en el siguiente notebook:
https://github.com/Jazielinho/pandas_trucos/blob/master/Trucos%20Pandas.ipynb
Cualquier comentario es bienvenido.