4 méthodes en Python pour charger de gros fichiers de données

De plus en plus, j’ai à charger et traiter de gros fichiers de données en Python pour de l’analyse de données. J’étais habitué à plusieurs centaines de Mo, mais en passant au Go, il m’a fallut investiguer de nouvelles manières de le faire correctement. Je propose ici un résumé de toutes mes tentatives, fructueuses ou non, pour vous donner des pistes de la méthode la plus appropriée à utiliser dans votre cas.

Notre cas test sera le suivant: votre client favoris vous a envoyé une série temporelle, chaque valeur réelle étant relevée à un temps mesurée en millisecondes depuis « epoch », tout cela dans un joli fichier ASCII. Générons un exemple:

Notre fichier est de 2 millions de lignes sur deux colonnes, toutes les valeurs sont des réels. Sa taille est d’environ 100Mo. Voyons quelle mémoire il occupe.

Cela signifie que chaque réel occupe 8 octets en mémoire et que le tableau complet pèse 32Mo. Le fichier est bien plus gros sur le disque car il est stocké sous format texte via des chaînes de caratères. Les valeurs numériques ne sont pas encodées en binaire, ce qui est significativement plus gros.

file.readlines

La première tentative est de charger notre fichier comme n’importe quel autre fichier, ligne à ligne.

Après l’instruction readlines, le processus python consomme jusqu’à 200Mo de mémoire pour un fichier de moins de 100 Mo ! Et la consommation propre de l’objet (mesurée via getsizeof) est de 166 Mo. Le temps d’exécution moyen est de 0.54 secondes (import exclus).

Cette méthode est très compacte, mais à l’inconvénient majeur de charger tout le fichier en mémoire. Et derrière vos propres objets, de la mémoire additionnelle est utilisée par la méthode readlines. Si vous comptez n’utiliser qu’une partie du fichier seulement, vous êtes obligé de la charger entièrement. C’est pratique pour de petits fichiers, mais inatteignable pour de gros fichiers. Cela est dû au fichier qui est considéré comme du texte. De plus, cela nécessitera de convertir le texte en valeurs numériques, ce qui implique un traitement supplémentaire. Quoique générique, on peut faire mieux.

numpy.loadtxt

Essayons les méthodes fournies par numpy comme loadtxt. Voici un bout de code pour charger un fichier texte.

Après chargement, l’objet obtenu est un bon vieux tableau numpy. Aaaahh… En une seule ligne, nous pouvons charger n’importe quel fichier texte formaté comme un tableau. Et des arguments supplémentaires sont disponibles pour régler le caractère de délimitation de champs, le nombre de lignes d’en tête, … Le processus python occupe 140Mo de mémoire supplémentaire et l’objet lui même occupe 32Mo. Fait intéressant, pendant l’exécution, le processus python occupe jusqu’à 430Mo de mémoire avant de libérer près de la moitié. Cela doit être considéré : loadtxt demande plus de mémoire au chargement que la taille de l’objet finalement obtenu. Le temps d’exécution est de 19.5 secondes. C’est considérable !

Le principal obstacle rencontré dans les deux méthodes précédentes est principalement du au format texte. Nous manipulons des données numériques et la pire des manières de stocker de telles données est de recourir à des chaînes de caractères. Mais cette situation est commune car les fichiers textes sont préférés, car facilement lisibles depuis un terminal. Donc la première étape est de les convertir !

numpy.load

Numpy propose son propre format de stockage binaire pour sérialiser des tableaux numpy. Utilisez numpy.save sur votre tableau initial :

Sur le disque, le nouveau fichier occupe 32Mo, ce qui correspond à la taille du tableau mesurée plus haut. Pour le charger, utiliser la méthode numpy.load:

Le processus Python occupe 32Mo supplémentaire, hors import, avec aucune utilisation temporaire de plus de mémoire au chargement. La seule mémoire utilisée est celle requise par le tableau. Jetons un oeil au temps d’exécution : 0.02 secondes ! La meilleur solution jusqu’à présent, mais qui nécessite la conversion initiale au format propre proposé par Numpy, qui limite l’échange de données aux seuls habitués du Python. Regardons un standard plus largement utilisé.

HDF5 !

HDF est un acronyme anglais signifiant Format de Donnée Hiérarchique, et la version 5 est le standard le plus largement utilisé dans la communauté scientifique. Il est implémenté en Python par le package pytables entre autres. Voyons comment stocker notre tableau numpy dans ce format:

Sur le disque, notre nouveau fichier pèse 32Mo, soit la même taille que le format Numpy. Chargeons le:

A peine 1Mo de mémoire requise au chargement (import exclus), et moins de 0.002 secondes pour exécuter ces lignes. Pourquoi si peu ? En fait, nous n’avons chargé aucune donnée. Nous avons juste ouvert un connecteur vers le disque et la donné ne sera chargé qu’à la demande. Pour comparer avec les chargement précédents, nous forçons le chargement du tableau entier:

Nous retrouvons 32Mo de données en mémoire, et le process occupe environ 32Mo (import exclus), ainsi, aucune mémoire supplémentaire n’est requise au chargement. Et le temps d’exécution est de 0.02 secondes. Nous avons des performances similaires au format npy de Numpy, mais en utilisant un standard qui pourra être partagé avec plusieurs logiciels scientifiques. Nous montrerons dans un post à venir que des fonctionnalités additionnelles permettent de traiter partiellement la donnée sans avoir à charger l’intégralité du fichier en mémoire.

Conclusion

Pour conclure ce benchmark, nous retiendrons qu’au moins 4 méthodes différentes existent quand il s’agit de manipuler de gros fichiers de données en Python. Le format HDF semble un choix raisonnable, avec un minimum de base à assimiler pour le manipuler. Nous verrons plus tard comment tirer plein avantage de ce format. En attendant, sur notre exemple, il est le plus performant en terme d’accès à la donnée et d’occupation mémoire, à égalité avec le format proposé par Numpy, et l’avantage d’être échangeable avec d’autres langages et logiciel de traitement de donnée. Voici un résumé des performances mémoire de chacune des méthodes évoquées :

parsingfileschart3

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *