Pandas Guide to Big Data Analysis

Cuando se usa la biblioteca de pandas para analizar conjuntos de datos pequeños, cuyo tamaño no supera los 100 megabytes, el rendimiento rara vez se convierte en un problema. Pero cuando se trata del estudio de conjuntos de datos, cuyos tamaños pueden alcanzar varios gigabytes, los problemas de rendimiento pueden conducir a un aumento significativo en la duración del análisis de datos e incluso pueden conducir a la imposibilidad de realizar análisis debido a la falta de memoria.

Si bien las herramientas como Spark pueden procesar de manera eficiente grandes conjuntos de datos (desde cientos de gigabytes hasta varios terabytes), para utilizar plenamente sus capacidades, generalmente necesita un hardware bastante potente y costoso. Y, en comparación con los pandas, no difieren en un rico conjunto de herramientas para la limpieza, la investigación y el análisis de datos de alta calidad. Para conjuntos de datos de tamaño mediano, es mejor intentar usar pandas de manera más eficiente, en lugar de cambiar a otras herramientas.



En el artículo, cuya traducción publicamos hoy, hablaremos sobre las peculiaridades de trabajar con memoria cuando se usan pandas, y cómo simplemente reducir el consumo de memoria en casi un 90% simplemente seleccionando los tipos de datos apropiados almacenados en las columnas de las estructuras de datos de la tabla del DataFrame .

Trabajando con datos sobre juegos de beisbol


Trabajaremos con datos sobre juegos de béisbol de Grandes Ligas recopilados durante 130 años y tomados de Retrosheet .

Inicialmente, estos datos se presentaron como 127 archivos CSV, pero los combinamos en un conjunto de datos usando csvkit y agregamos, como la primera fila de la tabla resultante, una fila con nombres de columna. Si lo desea, puede descargar nuestra versión de estos datos y experimentar con ellos, leyendo el artículo.

Comencemos importando un conjunto de datos y echemos un vistazo a sus primeras cinco líneas. Puede encontrarlos en esta tabla, en el hoja del de .

 import pandas as pd gl = pd.read_csv('game_logs.csv') gl.head() 

A continuación se muestra información sobre las columnas más importantes de la tabla con estos datos. Si desea leer las explicaciones para todas las columnas, aquí puede encontrar un diccionario de datos para todo el conjunto de datos.

  • date - Fecha del juego.
  • v_name : el nombre del equipo invitado.
  • v_league - Liga del equipo visitante.
  • h_name : el nombre del equipo local.
  • h_league - La liga del equipo local.
  • v_score : puntos del equipo visitante.
  • h_score : puntos del equipo local.
  • v_line_score : un resumen de los puntos del equipo invitado, por ejemplo, 010000(10)00 .
  • h_line_score : un resumen de los puntos del equipo local, por ejemplo, 010000(10)0X .
  • park_id : el identificador del campo en el que se jugó el juego.
  • attendance : la cantidad de espectadores.

Para encontrar información general sobre el objeto DataFrame , puede usar el método DataFrame.info () . Gracias a este método, puede aprender sobre el tamaño de un objeto, sobre los tipos de datos y sobre el uso de la memoria.

Por defecto, los pandas, en aras del ahorro de tiempo, DataFrame información aproximada sobre el uso de memoria de un DataFrame . Estamos interesados ​​en información precisa, por lo que estableceremos el parámetro memory_usage en 'deep' .

 gl.info(memory_usage='deep') 

Aquí está la información que logramos obtener:

 <class 'pandas.core.frame.DataFrame'> RangeIndex: 171907 entries, 0 to 171906 Columns: 161 entries, date to acquisition_info dtypes: float64(77), int64(6), object(78) memory usage: 861.6 MB 

Al final resultó que, tenemos 171,907 filas y 161 columnas. La biblioteca de pandas detectó automáticamente los tipos de datos. Hay 83 columnas con datos numéricos y 78 columnas con objetos. Las columnas de objeto se usan para almacenar datos de cadena y en los casos en que la columna contiene datos de diferentes tipos.

Ahora, para comprender mejor cómo puede optimizar el uso de la memoria con este DataFrame , hablemos sobre cómo los pandas almacenan datos en la memoria.

Vista interna de un marco de datos


Dentro de los pandas, las columnas de datos se agrupan en bloques con valores del mismo tipo. Aquí hay un ejemplo de cómo las primeras 12 columnas de un DataFrame se almacenan en pandas.


Representación interna de diferentes tipos de datos en pandas

Puede notar que los bloques no almacenan información de nombre de columna. Esto se debe al hecho de que los bloques están optimizados para almacenar los valores disponibles en las celdas de la DataFrame objeto DataFrame . La clase BlockManager es responsable de almacenar información sobre la correspondencia entre los índices de fila y columna del conjunto de datos y lo que se almacena en bloques del mismo tipo de datos. Desempeña el papel de una API que proporciona acceso a datos básicos. Cuando leemos, editamos o DataFrame valores, la clase DataFrame interactúa con la clase BlockManager para convertir nuestras solicitudes en llamadas a funciones y métodos.

Cada tipo de datos tiene una clase especializada en el módulo pandas.core.internals . Por ejemplo, pandas usa la clase ObjectBlock para representar bloques que contienen columnas de cadena y la clase FloatBlock para representar bloques que contienen columnas que FloatBlock números de punto flotante. Para los bloques que representan valores numéricos que parecen números enteros o números de coma flotante, pandas combina las columnas y las almacena como la ndarray datos ndarray la biblioteca NumPy. Esta estructura de datos se basa en la matriz C, los valores se almacenan en un bloque continuo de memoria. Gracias a este esquema de almacenamiento de datos, el acceso a los fragmentos de datos es muy rápido.

Dado que los datos de diferentes tipos se almacenan por separado, examinamos el uso de la memoria de diferentes tipos de datos. Comencemos con el uso promedio de memoria para diferentes tipos de datos.

 for dtype in ['float','int','object']:   selected_dtype = gl.select_dtypes(include=[dtype])   mean_usage_b = selected_dtype.memory_usage(deep=True).mean()   mean_usage_mb = mean_usage_b / 1024 ** 2   print("Average memory usage for {} columns: {:03.2f} MB".format(dtype,mean_usage_mb)) 

Como resultado, resulta que los indicadores promedio de uso de memoria para datos de diferentes tipos se ven así:

 Average memory usage for float columns: 1.29 MB Average memory usage for int columns: 1.12 MB Average memory usage for object columns: 9.53 MB 

Esta información nos hace comprender que la mayor parte de la memoria se gasta en 78 columnas que almacenan valores de objetos. Hablaremos más sobre esto más adelante, pero ahora pensemos si podemos mejorar el uso de la memoria con columnas que almacenan datos numéricos.

Subtipos


Como ya dijimos, los pandas representan valores numéricos como estructuras de datos ndarray NumPy y los almacenan en bloques contiguos de memoria. Este modelo de almacenamiento de datos le permite ahorrar memoria y acceder rápidamente a los valores. Dado que los pandas representan cada valor del mismo tipo utilizando el mismo número de bytes, y ndarray estructuras de ndarray almacenan información sobre el número de valores, los pandas pueden ndarray rápida y precisa la cantidad de memoria consumida por las columnas que almacenan valores numéricos.

Muchos tipos de datos en pandas tienen muchos subtipos que pueden usar menos bytes para representar cada valor. Por ejemplo, el tipo float tiene subtipos float16 , float32 y float64 . El número en el nombre del tipo indica el número de bits que utiliza el subtipo para representar los valores. Por ejemplo, en los subtipos recién enumerados, se utilizan 2, 4, 8 y 16 bytes respectivamente para el almacenamiento de datos. La siguiente tabla muestra los subtipos de los tipos de datos más utilizados en pandas.
Uso de memoria, bytes
Número de coma flotante
Entero
Entero sin signo
Fecha y hora
Valor booleano
Objeto
1
int8
uint8
bool
2
float16
int16
uint16
4 4
float32
int32
uint32
8
float64
int64
uint64
datetime64
Capacidad de memoria variable
objeto

Un valor de tipo int8 usa 1 byte (8 bits) para almacenar un número y puede representar 256 valores binarios (potencia de 2 a 8). Esto significa que este subtipo se puede usar para almacenar valores en el rango de -128 a 127 (incluido 0).

Para verificar los valores mínimos y máximos adecuados para el almacenamiento usando cada subtipo de entero, puede usar el método numpy.iinfo() . Considere un ejemplo:

 import numpy as np int_types = ["uint8", "int8", "int16"] for it in int_types:   print(np.iinfo(it)) 

Al ejecutar este código, obtenemos los siguientes datos:

 Machine parameters for uint8 --------------------------------------------------------------- min = 0 max = 255 --------------------------------------------------------------- Machine parameters for int8 --------------------------------------------------------------- min = -128 max = 127 --------------------------------------------------------------- Machine parameters for int16 --------------------------------------------------------------- min = -32768 max = 32767 --------------------------------------------------------------- 

Aquí puede prestar atención a la diferencia entre los tipos uint (entero sin signo) e int (entero con signo). Ambos tipos tienen la misma capacidad, pero cuando se almacenan solo valores positivos en columnas, los tipos sin signo permiten un uso más eficiente de la memoria.

Optimización del almacenamiento de datos numéricos utilizando subtipos.


La función pd.to_numeric() se puede usar para convertir tipos numéricos a la baja. Para seleccionar columnas enteras, utilizamos el método DataFrame.select_dtypes() , luego las optimizamos y comparamos el uso de memoria antes y después de la optimización.

 #     ,   , #   ,      . def mem_usage(pandas_obj):   if isinstance(pandas_obj,pd.DataFrame):       usage_b = pandas_obj.memory_usage(deep=True).sum()   else: #     ,     DataFrame,   Series       usage_b = pandas_obj.memory_usage(deep=True)   usage_mb = usage_b / 1024 ** 2 #       return "{:03.2f} MB".format(usage_mb) gl_int = gl.select_dtypes(include=['int']) converted_int = gl_int.apply(pd.to_numeric,downcast='unsigned') print(mem_usage(gl_int)) print(mem_usage(converted_int)) compare_ints = pd.concat([gl_int.dtypes,converted_int.dtypes],axis=1) compare_ints.columns = ['before','after'] compare_ints.apply(pd.Series.value_counts) 

Aquí está el resultado de un estudio del consumo de memoria:

7.87 MB
1.48 MB

A
Despues
uint8
NaN
5.0
uint32
NaN
1.0
int64
6.0
NaN

Como resultado, puede ver una caída en el uso de memoria de 7.9 a 1.5 megabytes, es decir, redujimos el consumo de memoria en más del 80%. Sin embargo, el impacto general de esta optimización en el DataFrame original no es particularmente fuerte, ya que tiene muy pocas columnas enteras.

Hagamos lo mismo con las columnas que contienen números de coma flotante.

 gl_float = gl.select_dtypes(include=['float']) converted_float = gl_float.apply(pd.to_numeric,downcast='float') print(mem_usage(gl_float)) print(mem_usage(converted_float)) compare_floats = pd.concat([gl_float.dtypes,converted_float.dtypes],axis=1) compare_floats.columns = ['before','after'] compare_floats.apply(pd.Series.value_counts) 

El resultado es el siguiente:

100.99 MB
50.49 MB

A
Despues
float32
NaN
77,0
float64
77,0
NaN

Como resultado, todas las columnas que almacenaban números de coma flotante con tipo de datos float64 ahora almacenan números de tipo float32 , lo que nos dio una reducción del 50% en el uso de memoria.

Cree una copia del DataFrame original, use estas columnas numéricas optimizadas en lugar de las que originalmente estaban presentes en él, y observe el uso general de la memoria después de la optimización.

 optimized_gl = gl.copy() optimized_gl[converted_int.columns] = converted_int optimized_gl[converted_float.columns] = converted_float print(mem_usage(gl)) print(mem_usage(optimized_gl)) 

Esto es lo que tenemos:

861.57 MB
804.69 MB


Aunque redujimos significativamente el consumo de memoria mediante columnas que almacenan datos numéricos, en general, en todo el DataFrame , el consumo de memoria disminuyó solo un 7%. La optimización del almacenamiento de tipos de objetos puede convertirse en una fuente de mejora mucho más grave de una situación.

Antes de hacer esta optimización, veremos más de cerca cómo se almacenan las cadenas en los pandas, y compararemos esto con cómo se almacenan los números aquí.

Comparación de mecanismos para almacenar números y cadenas


El tipo de object representa valores utilizando objetos de cadena Python. Esto se debe en parte a que NumPy no admite la representación de valores de cadena faltantes. Dado que Python es un lenguaje interpretado de alto nivel, no proporciona al programador herramientas para ajustar cómo se almacenan los datos en la memoria.

Esta limitación lleva al hecho de que las cadenas no se almacenan en fragmentos contiguos de memoria; su representación en la memoria está fragmentada. Esto conduce a un aumento en el consumo de memoria y a una desaceleración en la velocidad de trabajo con valores de cadena. Cada elemento en la columna que almacena el tipo de datos del objeto, de hecho, es un puntero que contiene la "dirección" en la que se encuentra el valor real en la memoria.

El siguiente es un diagrama basado en este material que compara el almacenamiento de datos numéricos con los tipos de datos NumPy y el almacenamiento de cadenas con los tipos de datos integrados de Python.


Almacenar datos numéricos y de cadena

Aquí puede recordar que en una de las tablas anteriores se mostró que se utiliza una cantidad variable de memoria para almacenar datos de tipos de objetos. Aunque cada puntero ocupa 1 byte de memoria, cada valor de cadena particular ocupa la misma cantidad de memoria que se usaría para almacenar una sola cadena en Python. Para confirmar esto, utilizaremos el método sys.getsizeof() . Primero, eche un vistazo a las líneas individuales y luego al objeto de pandas de la Series que almacena los datos de la cadena.

Entonces, primero examinamos las líneas habituales:

 from sys import getsizeof s1 = 'working out' s2 = 'memory usage for' s3 = 'strings in python is fun!' s4 = 'strings in python is fun!' for s in [s1, s2, s3, s4]:   print(getsizeof(s)) 

Aquí, los datos de uso de memoria se ven así:

60
65
74
74


Ahora veamos cómo se ve el uso de cadenas en el objeto Series :

 obj_series = pd.Series(['working out',                         'memory usage for',                         'strings in python is fun!',                         'strings in python is fun!']) obj_series.apply(getsizeof) 

Aquí obtenemos lo siguiente:

 0    60 1    65 2    74 3    74 dtype: int64 

Aquí puede ver que los tamaños de las líneas almacenadas en los objetos de pandas de la Series son similares a sus tamaños cuando se trabaja con ellos en Python y cuando se representan como entidades separadas.

Optimización del almacenamiento de datos de tipo de objeto utilizando variables categóricas.


Las variables categóricas aparecieron en pandas versión 0.15. El tipo correspondiente, category , utiliza valores enteros en sus mecanismos internos, en lugar de los valores originales almacenados en las columnas de la tabla. Pandas usa un diccionario separado que establece la correspondencia de valores enteros e iniciales. Este enfoque es útil cuando las columnas contienen valores de un conjunto limitado. Cuando los datos almacenados en una columna se convierten al tipo de category , pandas usa el subtipo int , que permite el uso más eficiente de la memoria y puede representar todos los valores únicos encontrados en la columna.


Datos de origen y datos categóricos utilizando el subtipo int8

Para comprender exactamente dónde podemos usar datos categóricos para reducir el consumo de memoria, descubrimos el número de valores únicos en las columnas que almacenan los valores de los tipos de objetos:

 gl_obj = gl.select_dtypes(include=['object']).copy() gl_obj.describe() 

Puede encontrar lo que tenemos en esta tabla, en la hoja .

Por ejemplo, en la columna day_of_week , que es el día de la semana en que se jugó el juego, hay 171907 valores. Entre ellos, solo 7 son únicos. En general, un solo vistazo a este informe es suficiente para comprender que se utilizan bastantes valores únicos en muchas columnas para representar los datos de aproximadamente 172,000 juegos.

Antes de hacer la optimización a gran escala, seleccionemos una columna que almacene datos de objetos, al menos day_of_week , y veamos qué sucede dentro del programa cuando se convierte a un tipo categórico.

Como ya se mencionó, esta columna contiene solo 7 valores únicos. Para convertirlo a un tipo categórico, utilizamos el método .astype() .

 dow = gl_obj.day_of_week print(dow.head()) dow_cat = dow.astype('category') print(dow_cat.head()) 

Esto es lo que tenemos:

 0    Thu 1    Fri 2    Sat 3    Mon 4    Tue Name: day_of_week, dtype: object 0    Thu 1    Fri 2    Sat 3    Mon 4    Tue Name: day_of_week, dtype: category Categories (7, object): [Fri, Mon, Sat, Sun, Thu, Tue, Wed] 

Como puede ver, aunque el tipo de columna ha cambiado, los datos almacenados en ella se ven igual que antes. Ahora echemos un vistazo a lo que sucede dentro del programa.

En el siguiente código, usamos el atributo Series.cat.codes para averiguar qué valores enteros utiliza el tipo de category para representar cada día de la semana:

 dow_cat.head().cat.codes 

Logramos descubrir lo siguiente:

 0    4 1    0 2    2 3    1 4    5 dtype: int8 

Aquí puede ver que a cada valor único se le asigna un valor entero y que la columna ahora es del tipo int8 . No faltan valores, pero si ese fuera el caso, -1 se usaría para indicar dichos valores.

Ahora comparemos el consumo de memoria antes y después de convertir la columna day_of_week al tipo de category .

 print(mem_usage(dow)) print(mem_usage(dow_cat)) 

Aquí está el resultado:

9.84 MB
0.16 MB


Como puede ver, al principio se consumieron 9.84 megabytes de memoria, y después de la optimización solo 0.16 megabytes, lo que significa una mejora del 98% en este indicador. Tenga en cuenta que trabajar con esta columna probablemente demuestra uno de los escenarios de optimización más rentables cuando solo se usan 7 valores únicos en una columna que contiene aproximadamente 172,000 elementos.

Aunque la idea de convertir todas las columnas a este tipo de datos parece atractiva, antes de hacerlo, considere los efectos secundarios negativos de dicha conversión. Entonces, el inconveniente más grave de esta transformación es la imposibilidad de realizar operaciones aritméticas en datos categóricos. Esto también se aplica a las operaciones aritméticas ordinarias y al uso de métodos como Series.min() y Series.max() sin convertir primero los datos a un tipo de número real.

Deberíamos limitar el uso del tipo de category a columnas principalmente que almacenan datos del tipo de object , en el que menos del 50% de los valores son únicos. Si todos los valores de una columna son únicos, el uso del tipo de category aumentará el nivel de uso de memoria. Esto se debe al hecho de que en la memoria debe almacenar, además de los códigos de categoría numéricos, los valores de cadena originales. Los detalles sobre las restricciones de tipo de category se pueden encontrar en la documentación de los pandas.

Creemos un bucle que recorra en iteración todas las columnas que almacenan datos del tipo de object , descubra si el número de valores únicos en las columnas supera el 50% y, de ser así, los convierte en category tipo.

 converted_obj = pd.DataFrame() for col in gl_obj.columns:   num_unique_values = len(gl_obj[col].unique())   num_total_values = len(gl_obj[col])   if num_unique_values / num_total_values < 0.5:       converted_obj.loc[:,col] = gl_obj[col].astype('category')   else:       converted_obj.loc[:,col] = gl_obj[col] 

Ahora compare lo que sucedió después de la optimización con lo que sucedió antes:

 print(mem_usage(gl_obj)) print(mem_usage(converted_obj)) compare_obj = pd.concat([gl_obj.dtypes,converted_obj.dtypes],axis=1) compare_obj.columns = ['before','after'] compare_obj.apply(pd.Series.value_counts) 

Obtenemos lo siguiente:

752.72 MB
51.67 MB

A
Despues
objeto
78,0
NaN
categoría
NaN
78,0

category , , , , , , , , .

, , , object , 752 52 , 93%. , . , , , , 891 .

 optimized_gl[converted_obj.columns] = converted_obj mem_usage(optimized_gl) 

:

'103.64 MB'

. - . , datetime , , , .

 date = optimized_gl.date print(mem_usage(date)) date.head() 

:

0.66 MB

:

 0    18710504 1    18710505 2    18710506 3    18710508 4    18710509 Name: date, dtype: uint32 

, uint32 . - datetime , 64 . datetime , , , .

to_datetime() , format , YYYY-MM-DD .

 optimized_gl['date'] = pd.to_datetime(date,format='%Y%m%d') print(mem_usage(optimized_gl)) optimized_gl.date.head() 

:

104.29 MB

:

 0   1871-05-04 1   1871-05-05 2   1871-05-06 3   1871-05-08 4   1871-05-09 Name: date, dtype: datetime64[ns] 


DataFrame . , , , , , , , . , . , , , . , , DataFrame , .

, . pandas.read_csv() , . , dtype , , , , — NumPy.

, , . , .

 dtypes = optimized_gl.drop('date',axis=1).dtypes dtypes_col = dtypes.index dtypes_type = [i.name for i in dtypes.values] column_types = dict(zip(dtypes_col, dtypes_type)) #    161 ,  #  10  /   #     preview = first2pairs = {key:value for key,value in list(column_types.items())[:10]} import pprint pp = pp = pprint.PrettyPrinter(indent=4) pp.pprint(preview)     : {   'acquisition_info': 'category',   'h_caught_stealing': 'float32',   'h_player_1_name': 'category',   'h_player_9_name': 'category',   'v_assists': 'float32',   'v_first_catcher_interference': 'float32',   'v_grounded_into_double': 'float32',   'v_player_1_id': 'category',   'v_player_3_id': 'category',   'v_player_5_id': 'category'} 

, , .

- :

 read_and_optimized = pd.read_csv('game_logs.csv',dtype=column_types,parse_dates=['date'],infer_datetime_format=True) print(mem_usage(read_and_optimized)) read_and_optimized.head() 

:

104.28 MB

, .

, , , , . pandas 861.6 104.28 , 88% .


, , , . .

 optimized_gl['year'] = optimized_gl.date.dt.year games_per_day = optimized_gl.pivot_table(index='year',columns='day_of_week',values='date',aggfunc=len) games_per_day = games_per_day.divide(games_per_day.sum(axis=1),axis=0) ax = games_per_day.plot(kind='area',stacked='true') ax.legend(loc='upper right') ax.set_ylim(0,1) plt.show() 


,

, 1920- , , 50 , .

, , , 50 , .

, .

 game_lengths = optimized_gl.pivot_table(index='year', values='length_minutes') game_lengths.reset_index().plot.scatter('year','length_minutes') plt.show() 




, 1940- .

Resumen


pandas, , DataFrame , 90%. :

  • , , , , .
  • .

, , , , , , pandas, , .

Estimados lectores! eugene_bb . - , — .

Source: https://habr.com/ru/post/442516/


All Articles