web-dev-qa-db-fra.com

Un moyen efficace de lire des fichiers csv de 15 M lignes dans python

Pour mon application, je dois lire plusieurs fichiers de 15 M lignes chacun, les stocker dans un DataFrame et enregistrer le DataFrame au format HDFS5.

J'ai déjà essayé différentes approches, notamment pandas.read_csv avec les spécifications chunksize et dtype, et dask.dataframe. Ils prennent tous les deux environ 90 secondes pour traiter 1 fichier, et j'aimerais donc savoir s'il existe un moyen de traiter efficacement ces fichiers de la manière décrite. Dans ce qui suit, je montre un code des tests que j'ai effectués.

import pandas as pd
import dask.dataframe as dd
import numpy as np
import re 

# First approach
store = pd.HDFStore('files_DFs.h5')

chunk_size = 1e6

df_chunk = pd.read_csv(file,
                sep="\t",
                chunksize=chunk_size,
                usecols=['a', 'b'],
                converters={"a": lambda x: np.float32(re.sub(r"[^\d.]", "", x)),\
                            "b": lambda x: np.float32(re.sub(r"[^\d.]", "", x))},
                skiprows=15
           )              
chunk_list = [] 


for chunk in df_chunk:
      chunk_list.append(chunk)


df = pd.concat(chunk_list, ignore_index=True)

store[dfname] = df
store.close()

# Second approach

df = dd.read_csv(
        file,
        sep="\t",
        usecols=['a', 'b'],
        converters={"a": lambda x: np.float32(re.sub(r"[^\d.]", "", x)),\
                    "b": lambda x: np.float32(re.sub(r"[^\d.]", "", x))},
        skiprows=15
     )
store.put(dfname, df.compute())
store.close()

Voici à quoi ressemblent les fichiers (les espaces se composent d'un onglet littéral):

a   b
599.998413  14.142895
599.998413  20.105534
599.998413  6.553850
599.998474  27.116098
599.998474  13.060312
599.998474  13.766775
599.998596  1.826706
599.998596  18.275938
599.998718  20.797491
599.998718  6.132450)
599.998718  41.646194
599.998779  19.145775
16
Gabriel Dante

Tout d'abord, permet de répondre au titre de la question

1- Comment lire efficacement 15M lignes d'un csv contenant des flotteurs

Je vous suggère d'utiliser modin :

Génération d'échantillons de données:

import modin.pandas as mpd
import pandas as pd
import numpy as np

frame_data = np.random.randint(0, 10_000_000, size=(15_000_000, 2)) 
pd.DataFrame(frame_data*0.0001).to_csv('15mil.csv', header=False)
!wc 15mil*.csv ; du -h 15mil*.csv

    15000000   15000000  480696661 15mil.csv
    459M    15mil.csv

Passons maintenant aux repères:

%%timeit -r 3 -n 1 -t
global df1
df1 = pd.read_csv('15mil.csv', header=None)
    9.7 s ± 95.1 ms per loop (mean ± std. dev. of 3 runs, 1 loop each)
%%timeit -r 3 -n 1 -t
global df2
df2 = mpd.read_csv('15mil.csv', header=None)
    3.07 s ± 685 ms per loop (mean ± std. dev. of 3 runs, 1 loop each)
(df2.values == df1.values).all()
    True

Donc, comme nous pouvons le voir, modin était approximativement 3 fois plus rapide sur ma configuration.


Maintenant pour répondre à votre problème spécifique

2- Nettoyer un fichier csv contenant des caractères non numériques, puis le lire

Comme les gens l'ont remarqué, votre goulot d'étranglement est probablement le convertisseur. Vous appelez ces lambdas 30 millions de fois. Même la surcharge de l'appel de fonction devient non triviale à cette échelle.

Attaquons ce problème.

Génération d'un ensemble de données sale:

!sed 's/.\{4\}/&)/g' 15mil.csv > 15mil_dirty.csv

Approches

Tout d'abord, j'ai essayé d'utiliser modin avec l'argument converters. Ensuite, j'ai essayé une approche différente qui appelle moins souvent l'expression rationnelle:

Je vais d'abord créer un objet de type fichier qui filtre tout à travers votre expression régulière:

class FilterFile():
    def __init__(self, file):
        self.file = file
    def read(self, n):
        return re.sub(r"[^\d.,\n]", "", self.file.read(n))
    def write(self, *a): return self.file.write(*a) # needed to trick pandas
    def __iter__(self, *a): return self.file.__iter__(*a) # needed

Ensuite, nous le passons à pandas comme premier argument dans read_csv:

with open('15mil_dirty.csv') as file:
    df2 = pd.read_csv(FilterFile(file))

Repères:

%%timeit -r 1 -n 1 -t
global df1
df1 = pd.read_csv('15mil_dirty.csv', header=None,
        converters={0: lambda x: np.float32(re.sub(r"[^\d.]", "", x)),
                    1: lambda x: np.float32(re.sub(r"[^\d.]", "", x))}
           )
    2min 28s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)
%%timeit -r 1 -n 1 -t
global df2
df2 = mpd.read_csv('15mil_dirty.csv', header=None,
        converters={0: lambda x: np.float32(re.sub(r"[^\d.]", "", x)),
                    1: lambda x: np.float32(re.sub(r"[^\d.]", "", x))}
           )
    38.8 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)
%%timeit -r 1 -n 1 -t
global df3
df3 = pd.read_csv(FilterFile(open('15mil_dirty.csv')), header=None,)
    1min ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)

On dirait que Modin gagne à nouveau! Malheureusement, Modin n'a pas encore implémenté la lecture à partir des tampons, j'ai donc conçu l'approche ultime.

L'approche ultime:

%%timeit -r 1 -n 1 -t
with open('15mil_dirty.csv') as f, open('/dev/shm/tmp_file', 'w') as tmp:
    tmp.write(f.read().translate({ord(i):None for i in '()'}))
df4 = mpd.read_csv('/dev/shm/tmp_file', header=None)
    5.68 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)

Cela utilise translate qui est considérablement plus rapide que re.sub, et utilise également /dev/shm qui est un système de fichiers en mémoire qu'ubuntu (et d'autres linux) fournissent habituellement. Tout fichier qui y est écrit n'ira jamais sur le disque, il est donc rapide. Enfin, il utilise modin pour lire le fichier, en contournant la limitation de tampon de modin. Cette approche est environ 30 fois plus rapide que votre approche, et elle est également assez simple.

8
polvoazul

Eh bien, mes découvertes ne sont pas très liées aux pandas, mais plutôt à certains pièges courants.

Your code: 
(genel_deneme) ➜  derp time python a.py
python a.py  38.62s user 0.69s system 100% cpu 39.008 total
  1. précompiler votre regex
Replace re.sub(r"[^\d.]", "", x) with precompiled version and use it in your lambdas
Result : 
(genel_deneme) ➜  derp time python a.py 
python a.py  26.42s user 0.69s system 100% cpu 26.843 total
  1. Essayez de trouver un meilleur moyen que d'utiliser directement np.float32, car il est 6 à 10 fois plus lent que je ne le pense. Ce n'est pas ce que vous voulez, mais je veux juste montrer le problème ici.
replace np.float32 with float and run your code. 
My Result:  
(genel_deneme) ➜  derp time python a.py
python a.py  14.79s user 0.60s system 102% cpu 15.066 total

Trouvez une autre façon d'obtenir le résultat avec les flotteurs. Plus d'informations sur ce problème https://stackoverflow.com/a/6053175/37491

  1. Si possible, divisez votre fichier et le travail en sous-processus. Vous travaillez déjà sur des morceaux distincts de taille constante. Donc, fondamentalement, vous pouvez diviser le fichier et gérer le travail dans des processus séparés en utilisant le multi-traitement ou les threads.
2
altunyurt