Maîtriser le chargement de données efficace avec Pandas : Techniques pratiques et exemples de code.
Introduction
Le traitement efficace des données est la pierre angulaire du travail avec Pandas, en particulier lorsqu’il s’agit d’ensembles de données volumineux.
Dans ce guide, nous allons nous concentrer sur l’optimisation du processus de chargement des données. Nous couvrirons des stratégies clés telles que l’optimisation des types de données et l’utilisation du chunking, et nous approfondirons des méthodes supplémentaires telles que le chargement sélectif de colonnes, la spécification de colonnes de date, l’utilisation de convertisseurs, le saut de lignes, le mapping de la mémoire et le choix de formats de fichiers efficaces.
Chaque méthode est accompagnée d’un exemple de code pratique, ce qui facilite l’intégration de ces conseils dans tes flux de travail.
Optimisation des types de données
Le choix d’un type de données efficace est crucial pour réduire l’utilisation de la mémoire et accélérer le chargement des données.
Tu trouveras ci-dessous un tableau comparant les types de données les plus courants et leurs alternatives moins gourmandes en mémoire :
| Costly Data TypeMemory | Efficient Alternative | | ---------------------- | ----------------------------------- | | int64 | int32 or int16 | | float64 | float32 | | object | category (for limited unique values)|
Bien que ces conversions permettent d’optimiser considérablement les performances, il convient d’être prudent. La modification des types de données peut parfois avoir des conséquences inattendues.
- Limitation de la plage de valeurs : La conversion de
int64
enint16
peut provoquer un débordement si les valeurs dépassent la plage deint16
.
import pandas as pd import numpy as np import logging # DataFrame d'origine avec int64 df = pd.DataFrame({'int_column': [np.iinfo(np.int64).max, 100, 200]}) logging.warning(f"Le DataFrame d'origine est \n{df}") # Tentative de conversion en int16 df['int_column'] = df['int_column'].astype('int16') logging.warning(f'Le DataFrame converti est \n{df}')
Ce qui nous donne :
WARNING:root:Le DataFrame d'origine est int_column 0 9223372036854775807 1 100 2 200 WARNING:root:Le DataFrame converti est int_column 0 -1 1 100 2 200
La première entrée de la colonne int_column
est passée d’un très grand nombre à -1 en raison d’un dépassement de valeur lors de la conversion du type.
- Précision en virgule flottante : La réduction de la précision de
float64
àfloat32
peut entraîner des erreurs d’arrondi. Bien que l’arrondi puisse sembler mineur, il est important dans les contextes où une grande précision numérique est requise, comme les calculs scientifiques ou financiers.
# DataFrame d'origine avec float64 df = pd.DataFrame({'float_column': [1.23456789012345, 2.34567890123456]}) # Conversion en float32 df['float_column'] = df['float_column'].astype('float32') print(df)
- Conversions catégorielles : La conversion d’
object
encategorical
peut entraîner des problèmes liés à des valeurs uniques inattendues.
df = pd.DataFrame({'object_column': ['pomme', 'banane', 'pomme', 'orange']}) # Conversion en categorical df['object_column'] = df['object_column'].astype('category') # Ajout d'une nouvelle ligne avec une catégorie inattendue df = pd.concat([df, pd.DataFrame([{'object_column': 'kiwi'}])], ignore_index=True) print(df)
Après l’ajout de la nouvelle valeur « kiwi », qui ne faisait pas partie de l’ensemble d’origine des catégories, le type de données de la colonne « object_column » est devenu dtype('O')
, ce qui indique un type de données « object ». Cela signifie que la colonne n’est plus de type catégoriel.
Tu peux te demander quel est l’intérêt de convertir le type d’objet d’origine en type catégorique dès le départ. La décision d’utiliser des types catégoriels plutôt que des objets est souvent motivée par la nécessité de réduire l’utilisation de la mémoire et d’améliorer les performances, comme nous l’avons vu plus haut. Si tu as une colonne avec un grand nombre de valeurs répétées (comme le sexe, les noms de pays, les catégories de produits), la convertir en type catégoriel peut réduire de manière significative l’empreinte mémoire et accélérer les calculs.
- Gestion des valeurs null : Les colonnes d’entiers ne supportent pas
np.nan
par défaut et nécessitent un type d’entier nullable. Pour résoudre ces problèmes, Pandas a introduit des types d’entiers nullable ("Int8"
,"Int16"
,"Int32"
,"Int64"
, etc.). Ces types sont spécialement conçus pour gérer les données manquantes tout en préservant le type de données entier.
df = pd.DataFrame({'int_column': [1, np.nan, 3]}) # La tentative de conversion en int normal provoquera une erreur # Utiliser plutôt un type d'entier nullable df['int_column'] = df['int_column'].astype('Int32') print(df)
Nous donne sans aucune erreur :
int_column 0 1 1 <NA> 2 3
Mais nous devons être prudents lorsque nos DataFrames pandas doivent interagir avec d’autres bibliothèques qui requièrent int64
int32
. Mais les majuscules "Int16"
, "Int32"
, etc… sont des types de données spécifiques à Pandas, qui peuvent ne pas être compatibles avec d’autres bibliothèques.
Chacun de ces exemples illustre le type de problèmes qui peuvent survenir lors d’un changement de type de données. Être conscient de ces pièges potentiels te permet de prendre des décisions plus éclairées lors de l’optimisation de tes données.
Utiliser le découpage en morceaux pour améliorer les performances
Pour les très grands ensembles de données, il n’est pas toujours possible de charger l’ensemble des données en mémoire. Pandas te permet de charger les données en plus petits morceaux, qui peuvent être traités un par un.
chunk_size = 10000 # Nombre de lignes par morceau/chunk for chunk in pd.read_csv('huge_dataset.csv', chunksize = chunk_size): process(chunk)
Dans l’exemple du chargement par morceaux, process(chunk)
représente un espace réservé pour toute opération de traitement des données que tu pourrais effectuer sur chaque morceau. La principale différence en termes de performances provient de l’utilisation réduite de la mémoire et de la possibilité de commencer à traiter les données avant que l’ensemble du jeu de données ne soit chargé. Cela peut s’avérer particulièrement avantageux lorsqu’il s’agit d’ensembles de données trop volumineux pour être stockés en une seule fois dans la mémoire.
Tu peux tester le code ci-dessus en remplaçant "huge_dataset.csv"
par ton propre jeu de données et le comparer au chargement normal dans Pandas. Tu peux observer les gains d’efficacité du chargement par morceaux, en particulier lorsque la taille de l’ensemble de données augmente.
Méthodes supplémentaires pour optimiser le chargement des données
- Chargement sélectif des colonnes : Lors du chargement des données, tu peux spécifier les colonnes à charger. Cette fonction est particulièrement utile si ton jeu de données comporte de nombreuses colonnes, mais que tu n’as besoin que d’un sous-ensemble pour ton analyse.
# Charger uniquement des colonnes spécifiques df = pd.read_csv('data.csv', usecols=['Column1', 'Column2'])
- Spécification des colonnes de date : Si ton jeu de données contient des champs de date, le fait de les spécifier lors du chargement peut faire gagner du temps, car Pandas les analysera immédiatement en tant que dates, au lieu d’exiger une étape de conversion distincte ultérieurement.
# Chargement des colonnes et analyse des dates df = pd.read_csv('data.csv', parse_dates=['DateColumn'])
- Utilisation des convertisseurs : Les convertisseurs nous permettent d’appliquer une fonction aux données pendant le processus de chargement. L’utilisation d’une fonction de conversion lors du chargement de données avec Pandas peut en effet contribuer à l’optimisation de la mémoire, mais son objectif premier est souvent le nettoyage ou la transformation des données plutôt que l’économie de mémoire. Cependant, dans certains scénarios, elle peut contribuer indirectement à l’efficacité de la mémoire, en t’évitant de créer des structures de données intermédiaires supplémentaires pour le post-traitement après le chargement.
# Définir une fonction de convertisseur def converter_function(value): return value.strip() # Utiliser le convertisseur pendant le chargement df = pd.read_csv('data.csv', converters={'Column': converter_function})
- Sauter des lignes : Si ton jeu de données comprend des en-têtes, des pieds de page ou d’autres lignes non essentielles, tu peux les ignorer lors du chargement afin d’économiser du temps et de la mémoire.
# Sauter les 10 premières lignes du fichier df = pd.read_csv('data.csv', skiprows=10)
- Mapping de la mémoire : Pour les ensembles de données extrêmement volumineux, le mapping de la mémoire peut être un moyen efficace d’accéder à des fichiers volumineux sans les charger entièrement dans la mémoire.
# Utiliser le mapping de la mémoire pour les fichiers volumineux df = pd.read_csv('large_data.csv', memory_map=True)
Il ressemble au découpage en morceaux, mais ils ont en fait des objectifs différents. Le mapping de la mémoire est plus adapté aux scénarios dans lesquels tu as besoin d’un accès aléatoire à différentes parties d’un fichier, sans avoir besoin de transformations complexes. Il est particulièrement utile pour les opérations de lecture lourdes sur des fichiers volumineux. Le traitement par morceaux est plus approprié lorsque tu dois effectuer un traitement de données complexe ou des transformations qui ne peuvent être réalisées en un seul chargement en raison de contraintes de mémoire. Il est idéal pour les scénarios dans lesquels le traitement séquentiel des données est acceptable ou nécessaire.
- Utiliser des formats de fichiers efficaces : Les différents formats de fichiers peuvent avoir un impact significatif sur le temps de chargement. Les formats tels que Parquet ou HDF5 sont souvent plus efficaces que les fichiers CSV ou Excel traditionnels.
# Charger un fichier Parquet df = pd.read_parquet('data.parquet')
Les formats tels que Parquet et HDF5 prennent en charge la compression, ce qui réduit la taille des fichiers et la quantité de données à charger en mémoire.
De même que pour le chargement sélectif des colonnes, Parquet utilise un format de stockage en colonnes, qui est plus efficace pour certains types de requêtes et d’opérations, en particulier celles qui impliquent un sous-ensemble de colonnes. Cela peut réduire l’empreinte mémoire lors du chargement de colonnes spécifiques.
En outre, les nombres entiers et les dates peuvent être stockés plus efficacement, au lieu d’être représentés sous forme de texte dans les fichiers CSV.
Ainsi, si nous pouvons déposer les données dans des formats plus efficaces, tels que Parquet et HDF5, dès le début, bon nombre des méthodes susmentionnées ne sont plus nécessaires.
Pour créer des fichiers dans des formats tels que Parquet ou HDF5, tu peux utiliser Pandas avec des bibliothèques supplémentaires.
- Parquet : Nécessite la bibliothèque
pyarrow
oufastparquet
.
df.to_parquet('filename.parquet', engine='pyarrow')
- HDF5 : utilise la bibliothèque
h5py
ouPyTables
.
df.to_hdf('filename.h5', key='data', format='table')
Principaux enseignements
- Diverses techniques d’optimisation : Nous avons exploré plusieurs méthodes pour optimiser le chargement des données avec Pandas, ce qui inclut l’optimisation des types de données, le découpage en morceaux, le chargement sélectif des colonnes, l’analyse des dates, etc.
- Comprendre les compromis : Chaque méthode a ses avantages et ses inconvénients. Les comprendre peut t’aider à prendre des décisions éclairées en fonction de tess données et de tes besoins spécifiques.
- Résoudre les problèmes à la source : La mise à disposition de tes données dans des formats efficaces, tels que parquet et hdf5, résout ton problème dès le départ.