web-dev-qa-db-ja.com

pythonで15 M行のcsvファイルを読み取る効率的な方法

私のアプリケーションでは、それぞれ15 M行の複数のファイルを読み取ってDataFrameに保存し、DataFrameをHDFS5形式で保存する必要があります。

私はすでにさまざまなアプローチを試しました。特に、チャンクサイズとdtypeを指定したpandas.read_csvとdask.dataframeです。どちらも1つのファイルを処理するのに約90秒かかるので、これらのファイルを上記の方法で効率的に処理する方法があるかどうか知りたいのですが。以下では、実行したテストのコードをいくつか示します。

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()

ファイルは次のようになります(空白はリテラルタブで構成されます)。

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

まず、質問のタイトルに答えましょう

1-floatを含むcsvの15M行を効率的に読み取る方法

modin を使用することをお勧めします:

サンプルデータの生成:

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

ベンチマークへ:

%%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

ご覧のように、私のセットアップではmodinが約3倍高速でした


今あなたの特定の問題に答えるために

2-数値以外の文字を含むcsvファイルをクリーンアップしてから読み取る

人々が指摘しているように、あなたのボトルネックはおそらくコンバーターです。あなたはそれらのラムダを3000万回呼び出しています。その規模では、関数呼び出しのオーバーヘッドでさえ重要になります。

この問題を攻撃してみましょう。

ダーティデータセットを生成しています:

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

アプローチ

最初に、私はmodinをconverters引数とともに使用してみました。次に、正規表現を呼び出す回数を減らす別のアプローチを試しました。

まず、正規表現ですべてをフィルタリングするFile-likeオブジェクトを作成します。

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

次に、それをread = csvの最初の引数としてpandasに渡します。

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

ベンチマーク:

%%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)

Modinが再び勝利したようです!残念ながら、modinはまだバッファーからの読み取りを実装していないため、最終的なアプローチを考案しました。

究極のアプローチ:

%%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)

これはre.subよりもかなり速いtranslateを使用します。また、ubuntu(および他のLinux)が通常提供するインメモリファイルシステムである/dev/shmも使用します。そこに書き込まれたファイルはディスクに保存されないため、高速です。最後に、modinを使用してファイルを読み取り、modinのバッファー制限を回避します。このアプローチは、あなたのアプローチよりも約30倍高速です。ま​​た、それもかなり単純です。

8
polvoazul

よく私の調査結果はパンダにあまり関連していないが、むしろいくつかの一般的な落とし穴です。

Your code: 
(genel_deneme) ➜  derp time python a.py
python a.py  38.62s user 0.69s system 100% cpu 39.008 total
  1. 正規表現をプリコンパイルする
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. Np.float32を直接使用してから、より良い方法を見つけてください。これは、予想よりも6〜10倍遅いためです。以下はあなたが望むものではありませんが、私はここで問題を表示したいだけです。
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

フロートで結果を達成する別の方法を見つけます。この問題の詳細 https://stackoverflow.com/a/6053175/37491

  1. 可能であれば、ファイルと作業をサブプロセスに分割します。すでに一定サイズの個別のチャンクで作業しています。したがって、基本的には、ファイルを分割して、マルチプロセッシングまたはスレッドを使用して別々のプロセスでジョブを処理できます。
2
altunyurt