[Python] Importation de données & Optimisation

Bonjour la zone,

Je viens vous voir pour un problème récurrent concernant l’importation de données avec Python.

Contexte : Je dois traiter une quantité assez importante de données avec un script Python, mais j’ai remarqué que l’importation des données peut prendre un temps fou. Dans l’objectif de réduire le temps de calcul, j’aimerais votre avis sur deux façon différentes d’importer des données. Peut être qu’il n’y a pas de réponse tranchante et qu’il faut tout simplement essayer, mais j’aimerais quand même savoir pourquoi choisir une méthode plutôt que l’autre.

Les données à importer sont sous forme de matrice.

Méthode 1 : J’importe un grand fichier au format CSV avec 200 colonnes et 8 000 000 de lignes et je subdivise cette énorme matrice à l’intérieur du script Python.

Méthode 2 : Je partage mon fichier CSV en 40 000 fichiers plus petits (avec la commande split en bash), et j’importe donc 40 000 petits fichiers de 200 lignes et 200 colonnes avec une boucle.

Dans tous les cas, les fonctions qui tournent dérrière sont les mêmes.

La méthode 2 est celle que j’effectue pour l’instant.

Merci d’avance,

lhooq

Bonjour,

  • La méthode 1 a l’avantage de ne pas perdre de temps en ouverture fermeture des 40000 fichiers.

  • La méthode 2 a l’avantage de simuler des transactions sans tout recommencer si ça plante. Et éventuellement de vider la mémoire utilisée.

Mais bon sans en savoir plus c’est difficile de se prononcer. D’autant que tu ne dis pas qu’est-ce que tu fais de ces données.

  • Tu les mets dans une base de données ? Dans ce cas pourquoi ne pas utiliser les utilitaires d’importation de SGBD ?
  • Tu génères d’autres csv d’un volume similaire ?
  • Tu fais des stats ?
  • Tu ne fais qu’une liste à partir de 10% d’infos utiles ?
  • Tu t’en sers pour faire de la transposition de données ? (comme certains fichiers comptables ou étatiques qui génèrent dans une seule ligne, plusieurs lignes concaténées, avec un indicateur de nombre de ligne au début). Et dans cas, et son contraire, un outil de script comme python est parfait.
  • Tu fais du datamining ?

Merci pour ta réponse !

En effet, j’ai pas précisé : j’utilise ces données seulement en lecture afin de calculer des statistiques de génétique (fréquences, etc.) dessus (les données représentent des génotypes d’individus). Au bout du compte, je génère d’autre CSV de taille à peu près comparable.

La différence entre les deux méthodes résident surtout dans l’accès disque, qui prend du temps quand il y a pas mal de fichier à aller chercher, ouvrir et fermer.

Je ne vois sincèrement pas l’intérêt de la méthode 2. Elle risque d’augmenter les coûts en ajoutant des ouvertures/fermetures de fichiers. On parle de quelle volumétrie (en taille) ?

Dans tous les cas, optimiser les traitements de lecture (comment est parcouru le fichier) me semble être le premier axe à étudier: lire en avançant dans le fichier, en remplissant un buffer par ligne, et en nettoyant un maximum de données avant de stocker en mémoire. Si la ram n’est pas un problème, c’est je pense la solution la plus rapide. Si il y a des contraintes de ram, alors il faut optimiser l’organisation des données en mémoire.

1 « J'aime »

Hello,

Il y a pas mal de façons assez efficaces de gérer ce problème, mais il faut en savoir un peu plus pour choisir la bonne.

  1. quel est ton facteur limitant, la ram, le cpu, les I/O ?
  2. les opérations sur tes lignes sont-elles indépendantes ou conditionnées les unes aux autres ? Vu que ta méthode 2 consiste à spliter l’input, j’imagine que les lignes sont indépendantes.
  3. comment stockes tu les valeurs de sortie ? Dans une liste au cul de laquelle tu append (très, très, TRES lent) ou directement dans un fichier ? Le mieux ici, c’est de coller ca dans un fichier et ensuite de le relire si tu en as re-besoin. C’est littéralement 3 ordres de grandeur plus rapide.

Commence par voir le 3). Si ca ne règle pas le problème, je vais faire l’educated guess que la réponse à 1) est le CPU, et la réponse à 2) est que les lignes sont indépendantes.

Dans ce cas ce que tu veux faire c’est utiliser un pool (multiprocessing.Pool(nb_de_process)) et de maper ton fichier à ce pool de manière à distribuer le fichier en stream à des workers qui pourront utiliser tous les coeurs de ta machine. Sur une machine moyenne, ca veut dire que tu fais *8 juste là dessus.
Ce que tu peux faire c’est alors d’avoir un fichier ouvert avant le pull dans lequel tu vas dump les résultats de chaque worker. Typiquement, si tu as un worker à qui tu envoies 10000 lignes, il va return 10000 résultats différents dans une liste de résultats, que tu vas append au cul du fichier ouvert avant le pool (et donc thread-protected par le pool.imap() ou pool.map()

Si c’est pas clair ou que tu galères poke moi et j’essaierai d’être plus explicite et de poster des snippets.

JT

Perso j’aurais tendance a utiliser l’orm de django pour cela parce que en plus tu peux l’utiliser dans Jupyter tu as des traces des operations et tu as l’interface d’admin de django.

De plus apres tu peux faire un cache avec redis :slight_smile:

Ci-dessous un snippet qui devrait répondre au soucis. J’utilise ce genre de structure au quotidien sur des fichiers qui font des dizaines de Go et ca marche nickel,

  • pas de problème de mémoire (puisque le fichier est traité en stream),
  • rapide parce que parallèle
  • thread safe sans se prendre la tête
  • aucune librairie exotique en plus de python3.5 dans ce cas

Le code pourrait être un poil plus simple, mais comme souvent les gens ont envie de passer à chaque worker une/des variables en argument, j’ai fait en sorte que ce soit possible ici.
Ce script fait deux trucs très cons, mais qui devraient être un bon exemple. Le fichier en input est une liste de mots, un par ligne dont la taille peut faire des dizaines de millions de lignes sans aucune difficulté. Rien n’est en mémoire. Le script :

  1. écrit dans un fichier toutes les lignes qui contiennent un « e »
  2. compte le nombre de mots contenant un « a »

tu peux jouer avec group_size suivant ton application et ton hardware pour optimiser la vitesse.

J’espère que c’est assez clair :wink: Dis moi si y a un truc qui l’est pas.

import multiprocessing
import itertools

def worker(args):
words, some_variable = args

out = []
count = 0
for word in words:
    if 'e' in word:
        out.append(word.replace('\n',''))
    if 'a' in word:
        count += 1
return out,count

def main():

input_filename = 'many_words.txt'
output_filename = 'only_with_e.txt'
group_size = 1000
some_variable = 'if you want to pass a variable or a tuple in addition to the file pass it here'
#number of process in parallel = number of cores on the cpu
cpus = multiprocessing.cpu_count()
p = multiprocessing.Pool(cpus)
count = 0 #init a counter of words containing the letter "a"


with open(input_filename, 'r') as f, open(output_filename, 'w') as f_out:
    for results in p.imap(worker,
                          ((list((x for _, x in items)), some_variable) for _, items in itertools.groupby(enumerate(f), lambda x: x[0] // group_size))):
        
        words_from_worker,count_from_worker = results #unpack the result
        
        for word in words_from_worker: #loop on the rows of the result list of each worker
            f_out.write(word+'\n') # write the result to the file (thread safe)
        count += count_from_worker # add the count of words with 'a' from each worker to the general counter (thread safe too)
print(count)

if name == ‹ main ›:
main()

Je suis de loin d’accord a ce qui a été dit au dessus, à ceci près que je découperai le csv avec un générateur pour le ventiler sur les workers.

def read_csv_lines(path):
    with open(path, 'r') as f:
        reader = csv.reader(f) 
        yield next(reader)

Dans Pool, j’utiliserai map_async avec un callback pour traiter les résultats, histoire de pas tout garder en ram en attendant que le garbage collector fasse son job.

Tout cumulé ça devrait améliorer les perfs de monsieur.
L’étape d’après c’est de ne pas le faire en python. :slight_smile:

En effet ca marche aussi avec map_async. Après dans la pratique j’ai jamais eu de soucis de ram qui s’accumule avec imap. Si tu veux avoir une idée de la progression (si c’est un super lent process), ca peut être intéressant d’utiliser tqdm, mais a priori tu es obligé de passer sur map_unordered pour que ca marche.

Pour ce qui est de découper le fichier, c’est ce que je fais ici mais par paquet pas ligne à ligne. Dans mes applications au quotidien c’est en général plus adapté du point de vue perf, mais ca peut varier assez largement, à tester :slight_smile:

En étape d’après sinon si c’est un truc un peu crucial et amené à être utilisé souvent, je pousserais la logique du map et je regarderais comment scale avec Spark, par exemple, qui a une bonne API Python si le monsieur veut pas se coller au C++ ou au Scala.

Si tu veux le faire en distribué tu as celery bon la solution spark aussi ou sinon le faire sur une db distribuée aussi.

Alors merci à tous pour votre aide !

Pour commencer j’effectue mes simulations sur le calculateur du labo qui est blindé en RAM (125Go) et qui à un bon processeur du style Xeon E7 (une vingtaine de coeurs) ou alors directement sur le cluster de l’Ifremer. Donc il n’y a pas vraiment de limitation physique.

C’est pas énorme, des fichiers CSV d’environ 10Go pour les plus gros (pour l’instant mais ça peut évoluer).

Oui c’est exactement ça le soucis, mais c’est dur à quantifier.

Cette partie est déjà optimisée mais en effet la manière de parcourir les données à un impact monstreux sur le temps de calcul (d’où l’optimisation :slight_smile: )

  1. Pas vraiment de facteur limitant au niveau du matériel : calculateur puissant ou cluster.

2)Oui les lignes sont indépendantes. C’est assez simple comme opération : mes lignes sont composées de 0 et de 1 (génotypes d’individus) et je teste des associations d’allèles (0 ou 1) sur deux colonnes distinctes (et sur la même lignes) colonnes : est-ce que le génotypes est 00, 11, 01 ou 10.

  1. Je stocke mes sortie sous la même forme qu’en entrée : un fichier texte en format CSV (grosse matrice). Je calcule plusieurs matrices que j’ajoute au fur et à mesure dans un même fichier.

Oui c’est exactement ça !

Merci beaucoup, je vais étudier ça. J’ai l’impression de faire à peu près la même chose mais avec le package PP (Parallel Python), mais je maîtrise vachement moins ce genre de chose. Je metterais ce que j’ai fait ici, pour que tu puisses regarder :slight_smile:

Ça à l’air sexy, merci !

Oui, mon directeur de recherche pense que ça pourrait être pas mal de passer en Fortran (vachement plus rapide du coup), mais le Fortran c’est pas ma tasse :confounded:

J’ai récemment changer la méthode d’importation de mes données, je vous dirais si c’est effectivement plus rapide !

En tou cas, je pense que je reviendrais vers vous pour la question de la parallélisation quand je commencerais à utiliser le cluster de l’Ifremer !