Avec Pandas, mettez au régime vos gros datasets
Les datasets très volumineux sont un calvaire pour les Data Scientists. Fort heureusement les librairies Pandas permettent de les manipuler en optimisant leur poids drastiquement et en rien de temps.
Article écrit par
, , etTout Data Scientist se confronte à des jeux de donnés colossaux. Ceux-ci sont souvent constitués de plusieurs millions de d’entrées . Ces DataFrames Pandas consomment énormément de mémoire et de temps de calcul. Il est dès lors critique de les optimiser pour les visualiser, les manipuler et les utiliser dans des modèles de machine learning. Voici comment y remédier en un minimum de manipulations.
Notre démonstration s’appuie sur une portion du dataset des contrôles automobiles réalisés par la police U.S entre 2000 et 2018 . Les données compilées par l’Université de Stranford recensent environ 50 000 sessions de contrôles routiers, classés par État. A lui seul, le jeu de données de l’État de New York utilisé représente près de 8 millions enregistrements.
À la découverte du dataset
Notre fichier csv pèse 1 Go. Pire, à la lecture avec Pandas, le DataFrame consomme 6.5 Go de ressource mémoire. Notons à ce propos qu’une reconnaissance préliminaire pour l’estimation en profondeur de l’occupation mémoire avec la fonction pandas.DataFrame.info() nécessite le transfert du paramètre “deep”
import pandas as pd
df = pd.read_csv("/content/drive/My Drive/ny_police.csv")
df.info(memory_usage='deep')
En y regardant de plus près, notre DataFrame contient 18 colonnes. La majorité sont composées de chaînes de caractères considérées en tant qu’objets par Pandas.
df.head()
Optimiser ce type de données aboutira à un gain de ressource mémoire substantiel.
Un gain considérable en catégorisant les objets
Pour y parvenir, une méthode efficace consiste à affecter un index à chaque objet fortement redondant. Lors des futures opérations, Pandas utilisera ce dernier, moins coûteux en espace. La méthode n’est viable que si le nombre d’objets uniques représente plus de 50% du nombre total d’objets. Ce qui est le cas dans les données collectées par Stanford.
df_obj = df.select_dtypes(include=['object']).copy()
df_obj.describe()
A titre d’exemple, la colonne “subject_sex” ne comporte que des valeurs “female” et “male”.
df_obj.groupby('subject_sex').size()
Lors du processus d’optimisation, chaque colonne est testée dans une boucle pour vérifier le seuil de valeurs uniques susceptibles d’enclencher ou non l’indexation.
|converted_obj = pd.DataFrame()for col in df_obj.columns:
if len(df_obj[col].unique()) / len(df_obj[col]) < 0.5:
converted_obj.loc[:,col] = df_obj[col].astype('category')
else:
converted_obj.loc[:,col] = df_obj[col]
Ci-après, une méthode pour tester les gains après chaque optimisation.
def mem_usage(pandas_obj):
if isinstance(pandas_obj,pd.DataFrame):
usage_b = pandas_obj.memory_usage(deep=True).sum()
else:
usage_b = pandas_obj.memory_usage(deep=True)
usage_mb = usage_b / 1024 ** 2 # convertir les bytes en megabytes
return "{:03.2f} MB".format(usage_mb) # afficher sous format nombre (min 3 chiffres) et une précision de deux décimales
Utilisons la une première fois sur les objets catégorisés.
print(mem_usage(df_obj))
print(mem_usage(converted_obj))
On observe que la consommation de mémoire a diminuée de 61%.
Pandas bien trop généreux dans l’encodage des valeurs numériques
Enchaînons avec les valeurs numériques. Par défaut Pandas encode les nombres en 64 bits. Dans la majorité des cas, la plage d’encodage est surdimensionnée. En effet, pour les entiers, la norme permet de les encoder dans un intervalle compris entre -9x10¹⁸ et 9x10¹⁸.
De plus par défaut Pandas encode l’ensemble des entiers qu’ils soient positifs ou négatifs. Il en résulte une moitié de la plage d’encodage souvent inutilisée .
Pour y remédier, la librairie propose une fonction pd.to_numeric() pour optimiser l’encodage des valeurs numériques. Celle-ci parcourt le DataFrame, colonne par colonne et sélectionne la plage d’encodage (8, 16, 32, ou 64 bits) la plus efficiente pour chacune d’elle.
Dans notre cas, outre l’absence d’entiers négatifs, nous constatons que l’encodage en 64 bits est excessif.
df_int = df.select_dtypes(include=['int']).copy()
df_int.describe()
df_int.info(memory_usage='deep')
Le paramètre downcast=’unsigned’ permet de définir un encodage exclusif des entiers positifs. La fonction apply() sert quant à elle à itérer le traitement sur toutes les colonnes.
converted_int = df_int.apply(pd.to_numeric, downcast='unsigned')converted_int.info(memory_usage='deep')
Après application de la fonction, les entiers sont maintenant encodés en 32 bits unsigned (sans valeurs négatives). Il en ressort un gain de mémoire de 50%.
print(mem_usage(df_int))
print(mem_usage(converted_int))
La même opération peut être réalisée sur les colonnes de float. A ceci près que l’encodage ne peut être optimisé pour ne prendre en compte que les valeurs positives.
df_float = df.select_dtypes(include=['float']).copy()
df_float.info(memory_usage = 'deep')
converted_float = df_int.apply(pd.to_numeric, downcast='float')
converted_float.info(memory_usage='deep')
A l’image des entiers, le gain est également de 50% sur les float.
print(mem_usage(df_float))
print(mem_usage(converted_float))
L’étape suivante consiste à remplacer les portions de dataset optimisées dans le DataFrame global (optimized_df).
optimized_df[converted_obj.columns] = converted_obj
optimized_df[converted_int.columns] = converted_int
optimized_df[converted_float.columns] = converted_float
Sans surprise, au vu de la proportion de colonnes d’objets, le gain total de mémoire est également de 61%.
print(mem_usage(df))
print(mem_usage(optimized_df))
A ce stade, l’optimisation est achevée. Afin d’automatiser la démarche lors de chaque ouverture du csv, Pandas permet de spécifier, sous forme de dictionnaire, les formats de données à appliquer à chaque colonne.
Des plus, la librairie permet de convertir, à la volée lors de l’ouverture, les colonnes de données temporelles en format datetime. Ce format est plus approprié pour traiter ce type de données. Il est donc judicieux de retirer ces colonnes lors de la construction du dictionnaire.
dtypes = optimized_df.drop(['date', 'time'],axis=1).dtypes
column_types = dict(zip(dtypes.index, [i.name for i in dtypes.values]))
print(column_types)
Voici le dictionnaire une fois constitué.
{'raw_row_number': 'uint32', 'location': 'category', 'county_name': 'category', 'subject_age': 'float32', 'subject_race': 'category', 'subject_sex': 'category', 'type': 'category', 'violation': 'category', 'speed': 'float32', 'posted_speed': 'float32', 'vehicle_color': 'category', 'vehicle_make': 'category', 'vehicle_model': 'category', 'vehicle_type': 'category', 'vehicle_registration_state': 'category', 'vehicle_year': 'float32'}
Celui-ci peut maintenant être réinjecté à l’ouverture du csv comme argument la fonction read_csv(). La conversion en datetime des séries temporelles est obtenue en passant la liste des colonnes à traiter dans l’argument parse_dates.
read_and_optimized = pd.read_csv('/content/drive/My Drive/ny_police.csv',dtype=column_types,parse_dates=[['date', 'time']],infer_datetime_format=True)
A la vérification, Pandas prend bien en compte les types de colonnes spécifiées dans le dictionnaire.
read_and_optimized.info(memory_usage='deep')
L’espace se retrouve donc bien optimisé dès la constitution du dataset.
Au delà des données, l’optimisation passe par le format du fichier
Il est encore possible de rogner un peu d’espace disque et de rendre l’importation des données dans un dataset encore plus rapide en remplaçant le fichier csv par une sauvegarde HDF5. Pandas propose la fonction to_hdf() pout le faire.
from pathlib import Path
path = Path("/content/drive/My Drive")
read_and_optimized.to_hdf(path/'ny_police.h5', key='read_and_optimized', format="table")
La fonction read_hdf() permet d’ouvrir le fichier HDF5.
df_test = pd.read_hdf(path/'ny_police.h5')
En comparaison le fichier csv pèse 1Go alors que le HDF5 n’occupe que 470 Mo.
Mieux, Pandas met 53s pour ouvrir le csv et appliquer les type de données aux colonnes contre seulement 10.3s pour le HDFS.
Pour résumer, Pandas offre des fonctions permettant d’optimiser la majorité des types de données. Seuls les booléens en sont dépourvus, du fait de leur nature. Ces mécanismes peuvent être utilisés en cours de manipulation d’un dataset voire même dès l’importation des données.
Plus que l’espace de stockage économisé, ces améliorations permettent réduire la durée du traitement lors d’applications de data visualisation ou de machine learning.
Qui plus est, leur mise à profit requiert un coût effort / temps optimal.
Retrouvez le notebook de l’article sur Github. Ce dernier a été réalisé avec Google Colab.