45 votes

Comment puis-je accélérer la lecture de plusieurs fichiers et l'insertion des données dans un cadre de données ?

J'ai un certain nombre de fichiers texte, disons 50, que je dois lire dans un cadre de données massif. Pour l'instant, j'utilise les étapes suivantes.

  1. Lisez chaque fichier et vérifiez quelles sont les étiquettes. Les informations dont j'ai besoin sont souvent contenues dans les premières lignes. Les mêmes étiquettes se répètent pour le reste du fichier, avec des types de données différents à chaque fois.
  2. Créez un cadre de données avec ces étiquettes.
  3. Lisez à nouveau le fichier et remplissez le cadre de données avec des valeurs.
  4. Concaténer ce cadre de données avec un cadre de données principal.

Cela fonctionne assez bien pour les fichiers de 100 Ko - quelques minutes, mais pour 50 Mo, cela prend des heures et ce n'est pas pratique.

Comment puis-je optimiser mon code ? En particulier -

  1. Comment puis-je identifier les fonctions qui prennent le plus de temps et que je dois optimiser ? Est-ce la lecture du fichier ? Est-ce l'écriture dans le cadre de données ? Où mon programme passe-t-il son temps ?
  2. Dois-je envisager le multithreading ou le multiprocessing ?
  3. Puis-je améliorer l'algorithme ?
    • Peut-être lire le fichier entier en une seule fois dans une liste, plutôt que ligne par ligne,
    • Analyse les données par morceaux ou par fichier entier, plutôt que ligne par ligne,
    • Affectez les données au cadre de données par morceaux/en une seule fois, plutôt que ligne par ligne.
  4. Y a-t-il autre chose que je puisse faire pour que mon code s'exécute plus rapidement ?

Voici un exemple de code. Mon propre code est un peu plus complexe, car les fichiers texte sont plus complexes, de sorte que je dois utiliser environ 10 expressions régulières et plusieurs boucles while pour lire les données et les affecter au bon endroit dans le bon tableau. Pour que le MWE reste simple, je n'ai pas non plus utilisé d'étiquettes répétitives dans les fichiers d'entrée du MWE, ce qui donne l'impression que je lis le fichier deux fois sans raison. J'espère que cela a un sens !

import re
import pandas as pd

df = pd.DataFrame()
paths = ["../gitignore/test1.txt", "../gitignore/test2.txt"]
reg_ex = re.compile('^(.+) (.+)\n')
# read all files to determine what indices are available
for path in paths:
    file_obj = open(path, 'r')
    print file_obj.readlines()

['a 1\n', 'b 2\n', 'end']
['c 3\n', 'd 4\n', 'end']

indices = []
for path in paths:
    index = []
    with open(path, 'r') as file_obj:
        line = True
        while line:
            try:
                line = file_obj.readline()
                match = reg_ex.match(line)
                index += match.group(1)
            except AttributeError:
                pass
    indices.append(index)
# read files again and put data into a master dataframe
for path, index in zip(paths, indices):
    subset_df = pd.DataFrame(index=index, columns=["Number"])
    with open(path, 'r') as file_obj:
        line = True
        while line:
            try:
                line = file_obj.readline()
                match = reg_ex.match(line)
                subset_df.loc[[match.group(1)]] = match.group(2)
            except AttributeError:
                pass
    df = pd.concat([df, subset_df]).sort_index()
print df

  Number
a      1
b      2
c      3
d      4

Mes fichiers d'entrée :

test1.txt

a 1
b 2
end

test2.txt

c 3
d 4
end

2 votes

Il faut probablement acheter un disque plus rapide :)

3 votes

En attendant, cherchez un bon profileur Python. Il s'agit d'un outil de type général qui vous dira quelle partie du programme est le goulot d'étranglement.

1 votes

Ne pouvez-vous pas lire l'ensemble des 50 fichiers dans un cadre de données et ensuite exécuter des manipulations basées sur des regex ? Ce sera plus rapide car les opérations de filtrage sur pandas sont très rapides.....

21voto

Некто Points 753

Je l'ai utilisé de nombreuses fois car il s'agit d'une implémentation particulièrement facile du multiprocessing.

import pandas as pd
from multiprocessing import Pool

def reader(filename):
    return pd.read_excel(filename)

def main():
    pool = Pool(4) # number of cores you want to use
    file_list = [file1.xlsx, file2.xlsx, file3.xlsx, ...]
    df_list = pool.map(reader, file_list) #creates a list of the loaded df's
    df = pd.concat(df_list) # concatenates all the df's into a single df

if __name__ == '__main__':
    main()

En utilisant cette méthode, vous devriez être en mesure d'augmenter considérablement la vitesse de votre programme sans trop de travail. Si vous ne savez pas combien de processeurs vous avez, vous pouvez vérifier en lançant votre shell et en tapant

echo %NUMBER_OF_PROCESSORS%

EDIT : Pour rendre cette opération encore plus rapide, pensez à changer vos fichiers en csvs et à utiliser la fonction pandas. pandas.read_csv

0 votes

Le module CSV natif de Python permet de spécifier ' ' comme séparateur.

17voto

Mahmud Points 16

Avant de sortir le marteau du multiprocesseur, votre première étape devrait être de faire un peu de profilage. Utilisez cProfile pour identifier rapidement les fonctions qui prennent beaucoup de temps. Malheureusement, si vos lignes sont toutes dans un seul appel de fonction, elles apparaîtront comme des appels de bibliothèque. line_profiler est meilleur mais prend un peu plus de temps de configuration.

NOTE. Si vous utilisez ipython, vous pouvez utiliser %timeit (commande magique pour le module timeit) et %prun (commande magique pour le module profile) pour chronométrer vos instructions et vos fonctions. Une recherche sur Google vous montrera quelques guides.

Pandas est une bibliothèque merveilleuse, mais j'ai été occasionnellement victime d'une mauvaise utilisation avec des résultats atroces. En particulier, méfiez-vous des opérations append()/concat(). Cela pourrait être votre goulot d'étranglement, mais vous devriez profiler pour être sûr. Habituellement, les opérations numpy.vstack() et numpy.hstack() sont plus rapides si vous n'avez pas besoin d'effectuer l'alignement des index/colonnes. Dans votre cas, il semble que vous puissiez vous en sortir avec des séries ou des ndarrays numpy 1-D qui peuvent vous faire gagner du temps.

BTW, un try Le blocage en python est beaucoup plus lent, souvent 10x ou plus, que la vérification d'une condition invalide, donc assurez-vous d'en avoir absolument besoin avant de le coller dans une boucle pour chaque ligne. C'est probablement l'autre facteur de perte de temps ; j'imagine que vous avez collé le bloc try pour vérifier AttributeError en cas d'échec de match.group(1). Je vérifierais d'abord si la correspondance est valide.

Même ces petites modifications devraient suffire pour que votre programme s'exécute beaucoup plus rapidement avant d'essayer quelque chose de radical comme le multitraitement. Ces bibliothèques Python sont géniales, mais elles apportent un nouvel ensemble de défis à relever.

1 votes

Il est assez évident, en regardant son script, que la lecture ligne par ligne d'un fichier de 50 Mo est à l'origine du goulot d'étranglement. Même l'exécution d'un pandas.read_excel sur un fichier de 50 Mo prend quelques minutes.

3voto

Tout d'abord, si vous lisez le fichier plusieurs fois, il semble que ce soit le goulot d'étranglement. Essayez de lire le fichier dans un seul objet de type chaîne de caractères, puis utilisez la commande cStringIO sur elle plusieurs fois.

Deuxièmement, vous n'avez pas vraiment montré de raison de construire les indices avant de lire tous les fichiers. Même si c'est le cas, pourquoi utiliser Pandas pour IO ? Il semble que vous puissiez les construire dans des structures de données python ordinaires (en utilisant peut-être __slots__ ), puis de le placer dans le cadre de données principal. Si vous n'avez pas besoin de l'index du fichier X avant de lire le fichier Y (comme votre deuxième boucle semble le suggérer), il vous suffit de parcourir les fichiers en boucle une seule fois.

Troisièmement, vous pouvez soit utiliser de simples split / strip sur les chaînes de caractères pour extraire les éléments séparés par des espaces, ou si c'est plus compliqué (il y a des guillemets et autres), utilisez la fonction CSV de la bibliothèque standard de Python. Tant que vous ne montrerez pas comment vous construisez réellement vos données, il est difficile de suggérer un correctif lié à cela.

Ce que vous avez montré jusqu'à présent peut être fait assez rapidement avec le simple

for path in paths:
    data = []
    with open(path, 'r') as file_obj:
        for line in file_obj:
            try:
                d1, d2 = line.strip().split()
            except ValueError:
                pass
            data.append(d1, int(d2)))
    index, values = zip(*data)
    subset_df = pd.DataFrame({"Number": pd.Series(values, index=index)})

Voici la différence de temps d'exécution sur une machine virtuelle dont l'espace disque n'est pas pré-alloué (les fichiers générés font environ 24 Mo) :

import pandas as pd
from random import randint
from itertools import combinations
from posix import fsync

outfile = "indexValueInput"

for suffix in ('1', '2'):
    with open(outfile+"_" + suffix, 'w') as f:
        for i, label in enumerate(combinations([chr(i) for i in range(ord('a'), ord('z')+1)], 8)) :
            val = randint(1, 1000000)
            print >>f, "%s %d" % (''.join(label), val)
            if i > 3999999:
                break
        print >>f, "end"
        fsync(f.fileno())

def readWithPandas():
    data = []
    with open(outfile + "_2", 'r') as file_obj:
        for line in file_obj:
            try:
                d1, d2 = str.split(line.strip())
            except ValueError:
                pass
            data.append((d1, int(d2)))
    index, values = zip(*data)
    subset_df = pd.DataFrame({"Numbers": pd.Series(values, index=index)})

def readWithoutPandas():
    data = []
    with open(outfile+"_1", 'r') as file_obj:
        for line in file_obj:
            try:
                d1, d2 = str.split(line.strip())
            except ValueError:
                pass
            data.append((d1, int(d2)))
    index, values = zip(*data)

def time_func(func, *args):
    import time
    print "timing function", str(func.func_name)
    tStart = time.clock()
    func(*args)
    tEnd = time.clock()
    print "%f seconds " % (tEnd - tStart)

time_func(readWithoutPandas)
time_func(readWithPandas)

Les temps qui en résultent sont :

timing function readWithoutPandas
4.616853 seconds 
timing function readWithPandas
4.931765 seconds 

Vous pouvez essayer ces fonctions avec votre construction d'index et voir quelle serait la différence de temps. Il est presque certain que le ralentissement provient des lectures multiples sur le disque. Et comme Pandas ne prendra pas le temps de construire votre cadre de données à partir d'un dictionnaire, il est préférable de trouver comment construire votre index en Python pur avant de transmettre les données à Pandas. Mais faites à la fois la lecture des données et la construction de l'index en une seule lecture de disque.

Si vous imprimez à l'intérieur de votre code, attendez-vous à ce que cela prenne énormément de temps. Le temps qu'il faut pour écrire du texte brut sur un tty est bien plus long que le temps qu'il faut pour lire/écrire sur le disque.

2voto

cgte Points 350

Considérations générales sur python :

Tout d'abord, pour la mesure du temps, vous pouvez utiliser un tel extrait :

from time import time, sleep

class Timer(object):
    def __init__(self):
        self.last = time()

    def __call__(self):
        old = self.last
        self.last = time()
        return self.last - old

    @property
    def elapsed(self):
        return time() - self.last

timer = Timer()

sleep(2)
print timer.elapsed
print timer()
sleep(1)
print timer()

Vous pourriez alors faire un benchmark du code exécuté plusieurs fois, et vérifier la différence.

A ce sujet, je commente en ligne :

with open(path, 'r') as file_obj:
    line = True
    while line: #iterate on realdines instead.
        try:
            line = file_obj.readline()
            match = reg_ex.match(line)
            index += match.group(1)
            #if match:
            #    index.extend(match.group(1)) # or extend

        except AttributeError:
            pass

Votre code précédent n'est pas vraiment pythonique, vous pourriez vouloir essayer/excepter. Ensuite, essayez seulement de faire dans le minimum de lignes possibles.

Les mêmes avis s'appliquent au deuxième bloc de code.

Si vous avez besoin de lire les mêmes fichiers plusieurs fois, vous pouvez les stocker en RAM en utilisant StringIO ou, plus simplement, conserver un dict {path : content} que vous ne lisez qu'une fois.

Les regex Python sont connus pour être lents, vos données semblent assez simples, vous pouvez envisager d'utiliser les méthodes split et strip sur vos lignes d'entrée.

 striped=[l.split() for l in [c.strip() for c in file_desc.readlines()] if l] 

Je vous recommande de lire ceci : https://gist.github.com/JeffPaine/6213790 la vidéo correspondante est ici https://www.youtube.com/watch?v=OSGv2VnC0go

0voto

Ron Distante Points 11

Vous pouvez importer le modèle de multitraitement et utiliser un pool de processus travailleurs pour ouvrir simultanément plusieurs fichiers en tant qu'objets de fichier, ce qui accélère la partie chargement de votre code. Pour tester le temps, importez la fonction datetime et utilisez le code suivant :

import datetime
start=datetime.datetime.now()

#part of your code goes here

execTime1=datetime.datetime.now()
print(execTime1-start)

#the next part of your code goes here

execTime2=datetime.datetime.now()
print(execTime2-execTime1)

En ce qui concerne la lecture de chaque fichier une seule fois, envisagez d'utiliser un autre script multiprocesseur pour construire une liste de lignes dans chaque fichier, de sorte que vous puissiez vérifier une correspondance sans opération d'E/S de fichier.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X