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 pandasVous 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înesIci, 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 int8Afin 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 . - , — .
