Pandas Guide to Big Data Analysis

Lorsque vous utilisez la bibliothèque pandas pour analyser de petits ensembles de données, dont la taille ne dépasse pas 100 mégaoctets, les performances deviennent rarement un problème. Mais en ce qui concerne l'étude des ensembles de données, dont la taille peut atteindre plusieurs gigaoctets, les problèmes de performances peuvent entraîner une augmentation significative de la durée de l'analyse des données et peuvent même entraîner l'impossibilité d'effectuer une analyse en raison d'un manque de mémoire.

Alors que des outils comme Spark peuvent traiter efficacement des ensembles de données volumineux (de centaines de gigaoctets à plusieurs téraoctets), afin d'utiliser pleinement leurs capacités, vous avez généralement besoin d'un matériel assez puissant et coûteux. Et, par rapport aux pandas, ils ne diffèrent pas par de riches ensembles d'outils pour le nettoyage, la recherche et l'analyse des données de haute qualité. Pour les ensembles de données de taille moyenne, il est préférable d'essayer d'utiliser les pandas plus efficacement, plutôt que de passer à d'autres outils.



Dans l'article, dont nous publions la traduction aujourd'hui, nous parlerons des particularités du travail avec la mémoire lors de l'utilisation de pandas, et comment réduire simplement la consommation de mémoire de près de 90% en sélectionnant simplement les types de données appropriés stockés dans les colonnes des structures de données de table du DataFrame .

Travailler avec des données sur des jeux de baseball


Nous travaillerons avec des données sur les matchs de baseball de la Major League collectées sur 130 ans et extraites de Retrosheet .

Initialement, ces données étaient présentées sous la forme de 127 fichiers CSV, mais nous les avons combinées en un seul ensemble de données à l'aide de csvkit et ajouté, comme première ligne du tableau résultant, une ligne avec les noms des colonnes. Si vous le souhaitez, vous pouvez télécharger notre version de ces données et l'expérimenter en lisant l'article.

Commençons par importer un jeu de données et jetons un œil à ses cinq premières lignes. Vous pouvez les trouver dans ce tableau, sur le feuille de .

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

Vous trouverez ci-dessous des informations sur les colonnes les plus importantes du tableau contenant ces données. Si vous souhaitez lire les explications de toutes les colonnes, vous trouverez ici un dictionnaire de données pour l'ensemble des données.

  • date - Date de la partie.
  • v_name - Le nom de l'équipe invitée.
  • v_league - Ligue de l'équipe visiteuse.
  • h_name - Le nom de l'équipe locale.
  • h_league - La ligue de l'équipe à domicile.
  • v_score - Points de l'équipe à l'extérieur.
  • h_score - Points de l'équipe à domicile.
  • v_line_score - Un résumé des points de l'équipe invitée, par exemple - 010000(10)00 .
  • h_line_score - Un résumé des points de l'équipe à domicile, par exemple - 010000(10)0X .
  • park_id - L'identifiant du champ sur lequel le jeu a été joué.
  • attendance - Le nombre de téléspectateurs.

Afin de trouver des informations générales sur l'objet DataFrame , vous pouvez utiliser la méthode DataFrame.info () . Grâce à cette méthode, vous pouvez en apprendre davantage sur la taille d'un objet, sur les types de données et sur l'utilisation de la mémoire.

Par défaut, les pandas, pour gagner du temps, DataFrame informations approximatives sur l'utilisation de la mémoire d'un DataFrame . Nous souhaitons des informations précises, nous allons donc définir le paramètre memory_usage sur 'deep' .

 gl.info(memory_usage='deep') 

Voici les informations que nous avons réussi à obtenir:

 <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 

Il s'est avéré que nous avons 171 907 lignes et 161 colonnes. La bibliothèque pandas a détecté automatiquement les types de données. Il y a 83 colonnes avec des données numériques et 78 colonnes avec des objets. Les colonnes d'objets sont utilisées pour stocker des données de chaîne et dans les cas où la colonne contient des données de différents types.

Maintenant, afin de mieux comprendre comment vous pouvez optimiser l'utilisation de la mémoire avec ce DataFrame , parlons de la façon dont les pandas stockent les données en mémoire.

Vue interne d'un DataFrame


À l'intérieur des pandas, les colonnes de données sont regroupées en blocs avec des valeurs du même type. Voici un exemple de la façon dont les 12 premières colonnes d'un DataFrame sont stockées dans des pandas.


Représentation interne de différents types de données dans les pandas

Vous pouvez remarquer que les blocs ne stockent pas les informations de nom de colonne. Cela est dû au fait que les blocs sont optimisés pour stocker les valeurs disponibles dans les cellules du tableau de l'objet DataFrame . La classe BlockManager est chargée de stocker des informations sur la correspondance entre les index de ligne et de colonne de l'ensemble de données et ce qui est stocké dans des blocs du même type de données. Il joue le rôle d'une API qui donne accès aux données de base. Lorsque nous lisons, DataFrame valeurs, la classe DataFrame interagit avec la classe BlockManager pour convertir nos requêtes en appels de fonction et de méthode.

Chaque type de données a une classe spécialisée dans le module pandas.core.internals . Par exemple, pandas utilise la classe ObjectBlock pour représenter des blocs contenant des colonnes de chaînes et la classe FloatBlock pour représenter des blocs contenant des colonnes contenant FloatBlock nombres à virgule flottante. Pour les blocs représentant des valeurs numériques qui ressemblent à des entiers ou à des nombres à virgule flottante, pandas combine les colonnes et les stocke en tant que ndarray données ndarray la bibliothèque NumPy. Cette structure de données est basée sur le tableau C, les valeurs sont stockées dans un bloc de mémoire continu. Grâce à ce schéma de stockage des données, l'accès aux fragments de données est très rapide.

Étant donné que les données de différents types sont stockées séparément, nous examinons l'utilisation de la mémoire de différents types de données. Commençons par l'utilisation moyenne de la mémoire pour différents types de données.

 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)) 

En conséquence, il s'avère que les indicateurs moyens d'utilisation de la mémoire pour des données de différents types ressemblent à ceci:

 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 

Ces informations nous font comprendre que la majeure partie de la mémoire est consacrée à 78 colonnes stockant des valeurs d'objet. Nous en parlerons plus tard, mais réfléchissons maintenant à l'amélioration de l'utilisation de la mémoire avec des colonnes qui stockent des données numériques.

Sous-types


Comme nous l'avons déjà dit, les pandas représentent des valeurs numériques sous ndarray structures de données ndarray NumPy et les stockent dans des blocs de mémoire contigus. Ce modèle de stockage de données vous permet d'économiser de la mémoire et d'accéder rapidement aux valeurs. Étant donné que les pandas représentent chaque valeur du même type en utilisant le même nombre d'octets et ndarray structures ndarray stockent des informations sur le nombre de valeurs, les pandas peuvent ndarray rapidement et avec précision la quantité de mémoire consommée par les colonnes stockant des valeurs numériques.

De nombreux types de données dans les pandas ont de nombreux sous-types qui peuvent utiliser moins d'octets pour représenter chaque valeur. Par exemple, le type float a les sous-types float16 , float32 et float64 . Le nombre dans le nom du type indique le nombre de bits que le sous-type utilise pour représenter les valeurs. Par exemple, dans les sous-types juste énumérés, 2, 4, 8 et 16 octets sont utilisés respectivement pour le stockage des données. Le tableau suivant montre les sous-types des types de données les plus couramment utilisés chez les pandas.
Utilisation de la mémoire, octets
Numéro à virgule flottante
Entier
Entier non signé
Date et heure
Valeur booléenne
Objet
1
int8
uint8
bool
2
float16
int16
uint16
4
float32
int32
uint32
8
float64
int64
uint64
datetime64
Capacité de mémoire variable
objet

Une valeur de type int8 utilise 1 octet (8 bits) pour stocker un nombre et peut représenter 256 valeurs binaires (puissance de 2 à 8). Cela signifie que ce sous-type peut être utilisé pour stocker des valeurs dans la plage de -128 à 127 (dont 0).

Pour vérifier les valeurs minimales et maximales appropriées pour le stockage à l'aide de chaque sous-type entier, vous pouvez utiliser la méthode numpy.iinfo() . Prenons un exemple:

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

En exécutant ce code, nous obtenons les données suivantes:

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

Ici, vous pouvez faire attention à la différence entre les types uint (entier non signé) et int (entier signé). Les deux types ont la même capacité, mais lorsqu'ils stockent uniquement des valeurs positives dans des colonnes, les types non signés permettent une utilisation plus efficace de la mémoire.

Optimisation du stockage des données numériques à l'aide de sous-types


La fonction pd.to_numeric() peut être utilisée pour convertir des types numériques. Pour sélectionner des colonnes entières, nous utilisons la méthode DataFrame.select_dtypes() , puis nous les optimisons et comparons l'utilisation de la mémoire avant et après l'optimisation.

 #     ,   , #   ,      . 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) 

Voici le résultat d'une étude de la consommation de mémoire:

7.87 MB
1.48 MB

À
Après
uint8
NaN
5,0
uint32
NaN
1.0
int64
6.0
NaN

En conséquence, vous pouvez constater une baisse de l'utilisation de la mémoire de 7,9 à 1,5 mégaoctets, c'est-à-dire que nous avons réduit la consommation de mémoire de plus de 80%. L'impact global de cette optimisation sur le DataFrame origine, cependant, n'est pas particulièrement fort car il a très peu de colonnes entières.

Faisons de même avec les colonnes contenant des nombres à virgule flottante.

 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) 

Le résultat est le suivant:

100.99 MB
50.49 MB

À
Après
float32
NaN
77,0
float64
77,0
NaN

Par conséquent, toutes les colonnes qui stockaient des nombres à virgule flottante avec le type de données float64 stockent désormais des nombres de type float32 , ce qui nous a permis de réduire de 50% l'utilisation de la mémoire.

Créez une copie du DataFrame origine, utilisez ces colonnes numériques optimisées au lieu de celles qui y étaient initialement présentes et examinez l'utilisation globale de la mémoire après l'optimisation.

 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)) 

Voici ce que nous avons obtenu:

861.57 MB
804.69 MB


Bien que nous ayons considérablement réduit la consommation de mémoire par les colonnes stockant des données numériques, en général, sur l'ensemble du DataFrame , la consommation de mémoire n'a diminué que de 7%. L'optimisation du stockage des types d'objets peut devenir une source d'amélioration beaucoup plus sérieuse d'une situation.

Avant de procéder à cette optimisation, nous allons examiner de plus près comment les chaînes sont stockées dans les pandas et comparer cela avec la façon dont les nombres sont stockés ici.

Comparaison des mécanismes de stockage des nombres et des chaînes


Le type d' object représente des valeurs à l'aide d'objets chaîne Python. Cela est dû en partie au fait que NumPy ne prend pas en charge la représentation des valeurs de chaîne manquantes. Étant donné que Python est un langage interprété de haut niveau, il ne fournit pas au programmeur d'outils pour affiner la façon dont les données sont stockées en mémoire.

Cette limitation conduit au fait que les chaînes ne sont pas stockées dans des fragments de mémoire contigus; leur représentation en mémoire est fragmentée. Cela entraîne une augmentation de la consommation de mémoire et un ralentissement de la vitesse de travail avec les valeurs de chaîne. Chaque élément de la colonne stockant le type de données d'objet est en fait un pointeur qui contient «l'adresse» à laquelle la valeur réelle est située en mémoire.

Ce qui suit est un diagramme basé sur ce matériel qui compare le stockage de données numériques en utilisant les types de données NumPy et le stockage de chaînes en utilisant les types de données intégrés de Python.


Stockage de données numériques et de chaînes

Ici, vous vous souvenez que dans l'un des tableaux ci-dessus, il a été montré qu'une quantité variable de mémoire est utilisée pour stocker des données de types d'objets. Bien que chaque pointeur occupe 1 octet de mémoire, chaque valeur de chaîne particulière occupe la même quantité de mémoire qui serait utilisée pour stocker une seule chaîne en Python. Afin de confirmer cela, nous utiliserons la méthode sys.getsizeof() . Tout d'abord, jetez un œil aux lignes individuelles, puis à l'objet pandas de Series qui stocke les données de chaîne.

Donc, nous examinons d'abord les lignes habituelles:

 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)) 

Ici, les données d'utilisation de la mémoire ressemblent à ceci:

60
65
74
74


Voyons maintenant à quoi ressemble l'utilisation de chaînes dans l'objet Series :

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

Ici, nous obtenons ce qui suit:

 0    60 1    65 2    74 3    74 dtype: int64 

Ici, vous pouvez voir que les tailles des lignes stockées dans les objets pandas Series sont similaires à leurs tailles lorsque vous travaillez avec eux en Python et lorsque vous les représentez en tant qu'entités distinctes.

Optimisation du stockage des données de type objet à l'aide de variables catégorielles


Les variables catégorielles sont apparues dans la version 0.15 de pandas. Le type correspondant, category , utilise des valeurs entières dans ses mécanismes internes, au lieu des valeurs d'origine stockées dans les colonnes du tableau. Pandas utilise un dictionnaire séparé qui définit la correspondance des valeurs entières et initiales. Cette approche est utile lorsque les colonnes contiennent des valeurs d'un ensemble limité. Lorsque les données stockées dans une colonne sont converties en type de category , pandas utilise le sous-type int , qui permet l'utilisation la plus efficace de la mémoire et est capable de représenter toutes les valeurs uniques trouvées dans la colonne.


Données source et données catégorielles utilisant le sous-type int8

Afin de comprendre exactement où nous pouvons utiliser des données catégorielles pour réduire la consommation de mémoire, nous trouvons le nombre de valeurs uniques dans les colonnes qui stockent les valeurs des types d'objets:

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

Vous pouvez trouver ce que nous avons dans ce tableau, sur la feuille .

Par exemple, dans la colonne day_of_week , qui est le jour de la semaine où le jeu a été joué, il y a 171907 valeurs. Parmi eux, seuls 7 sont uniques. Dans l'ensemble, un seul coup d'œil sur ce rapport suffit pour comprendre que plusieurs valeurs uniques sont utilisées dans de nombreuses colonnes pour représenter les données d'environ 172 000 jeux.

Avant de procéder à l'optimisation à grande échelle, sélectionnons une colonne qui stocke les données d'objet, au moins day_of_week , et voyons ce qui se passe à l'intérieur du programme lorsqu'il est converti en un type catégorique.

Comme déjà mentionné, cette colonne ne contient que 7 valeurs uniques. Pour le convertir en un type catégoriel, nous utilisons la méthode .astype() .

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

Voici ce que nous avons obtenu:

 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] 

Comme vous pouvez le voir, bien que le type de la colonne ait changé, les données qui y sont stockées ont la même apparence qu'auparavant. Voyons maintenant ce qui se passe à l'intérieur du programme.

Dans le code suivant, nous utilisons l'attribut Series.cat.codes pour déterminer les valeurs entières que le type de category utilise pour représenter chaque jour de la semaine:

 dow_cat.head().cat.codes 

Nous réussissons à découvrir ce qui suit:

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

Ici, vous pouvez voir que chaque valeur unique se voit attribuer une valeur entière et que la colonne est désormais de type int8 . Il n'y a pas de valeurs manquantes, mais si c'était le cas, -1 serait utilisé pour indiquer ces valeurs.

Comparons maintenant la consommation de mémoire avant et après la conversion de la colonne day_of_week en type de category .

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

Voici le résultat:

9.84 MB
0.16 MB


Comme vous pouvez le voir, au début, 9,84 mégaoctets de mémoire ont été consommés, et après optimisation, seulement 0,16 mégaoctets, ce qui signifie une amélioration de 98% de cet indicateur. Veuillez noter que l'utilisation de cette colonne illustre probablement l'un des scénarios d'optimisation les plus rentables lorsque seules 7 valeurs uniques sont utilisées dans une colonne contenant environ 172 000 éléments.

Bien que l'idée de convertir toutes les colonnes en ce type de données semble intéressante, avant de le faire, considérez les effets secondaires négatifs d'une telle conversion. Ainsi, le plus grave inconvénient de cette transformation est l'impossibilité d'effectuer des opérations arithmétiques sur des données catégorielles. Cela s'applique également aux opérations arithmétiques ordinaires et à l'utilisation de méthodes telles que Series.min() et Series.max() sans d'abord convertir les données en un type de nombre réel.

Nous devons limiter l'utilisation du type de category à principalement des colonnes qui stockent des données de type object , dans lesquelles moins de 50% des valeurs sont uniques. Si toutes les valeurs d'une colonne sont uniques, l'utilisation du type de category augmentera le niveau d'utilisation de la mémoire. Cela est dû au fait qu'en mémoire, vous devez stocker, en plus des codes de catégorie numériques, les valeurs de chaîne d'origine. Les détails sur les restrictions de type de category peuvent être trouvés dans la documentation des pandas.

Créons une boucle qui itère sur toutes les colonnes stockant des données d' object type, découvre si le nombre de valeurs uniques dans les colonnes dépasse 50% et, si tel est le cas, les convertit en category type.

 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] 

Comparez maintenant ce qui s'est passé après l'optimisation avec ce qui s'est passé avant:

 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) 

Nous obtenons ce qui suit:

752.72 MB
51.67 MB

À
Après
objet
78,0
NaN
catégorie
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- .

Résumé


pandas, , DataFrame , 90%. :

  • , , , , .
  • .

, , , , , , pandas, , .

Chers lecteurs! eugene_bb . - , — .

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


All Articles