pandas
はデータ解析やデータ加工に非常に便利なPythonライブラリですが、並列化されている処理とされていない処理があり、注意が必要です。例えば pd.Sereis.__add__
のようなAPI(つまり df['a'] + df['b']
のような処理です)は処理が numpy
に移譲されているためPythonのGILの影響を受けずに並列化されますが、 padas.DataFrame.apply
などのメソッドはPythonのみで実装されているので並列化されません。
処理によってはそこがボトルネックになるケースもあります。今回は「ほぼimportするだけ」で pandas
の並列化されていない処理を並列化し高速化できる2つのライブラリを紹介します。同時に2つのライブラリのベンチマークをしてみて性能を確かめました。
pandarallel
pandaralell
はPythonの multiprocessing
moduleを使って計算処理を並列化するライブラリです。
インストール
pip install pandarallel
使い方
使い方は簡単で、 import
して初期化するだけです。これで pandas.DataFrame
に pandaralell
のAPIが追加されます。
from pandarallel import pandarallel # 初期化 pandarallel.initialize() # df.apply(func) の代わりに↓を使う df.parallel_apply(func)
これだけで apply
がマルチコアを使った並列処理になります。実際にどのように並列化が行われている確認するために、初期化時に pandarallel.initialize(progress_bar=True)
と指定すると下記のように、プログレスバーを使ってマルチコアが処理を行う様子を可視化してくれるようになります。
pandaralell
で拡張される pandas
のAPIは以下の通りです。全て parallel_{method名}
のように接頭語を付けると pandarallell
のAPIを使うことができます。
- pandas.DataFrame.apply → pandas.DataFrame.parallel_apply
- pandas.DataFrame.applymap → pandas.DataFrame.parallel_applymap
- pandas.DataFrame.groupby.apply → pandas.DataFrame.groupby.parallel_apply
- pandas.DataFrame.groupby.rolling.apply → pandas.DataFrame.groupby.rolling.parallel_apply
- pandas.Series.map → pandas.Series.parallel_map
- pandas.Series.apply → pandas.Series.parallel_apply
- pandas.Series.rolling.apply → pandas.Series.rolling.parallel_apply
性能
READMEに記載されていたベンチマークを見ると3-4倍程度 pandas
より高速化されているようです。
注意事項
一般的にマルチコアを使った並列化に言えることですが、並列化時に「コアごとにプロセス立ち上げる」「データをプロセスに受け渡す/受け取る」などのオーバーヘッドが存在します。そのためデータ量が少ない場合には「並列化しない場合より遅くなってしまう」ことがあります。
また現状ではPython3.5/3.6/3.7のみの対応になっています。
swifter
swifter
は並列アウトコアライブラリの Dask
を活用していて計算処理を並列化するライブラリです。通常Dask
を使う場合は Dask
のAPIを ( pandas
とそれなりに互換性はあるにせよ)理解する必要があるのですが swifter
はほとんど pandas
のみを意識して使うことができます。
パフォーマンス面でもメリットがあります。1つは、vectorize化して実行できるかチェックして、出来る場合はvectorize化して実行してくれること。 「vectorize化」とは numpy
で処理が行うということです。 numpy
で実行できれば numpy
が並列処理を行ってくれるので、そちらに任せるということです。もう1つは pandarallel
で書いた通りオーバーヘッドの関係で「並列化した方が早い」場合と「しない方が早い」場合があるのですが、swifter
はその辺りをデータ数に応じてうまく「並列化するか/しないか」をコントロールしてくれます。
インストール
pip install swifter
使い方
こちらはimportし、 pandas.DataFrame.swifter
のように pandas.DataFrame
の swifter
アクセサーを呼び出すだけで使うことができます。
import swifter # df.apply(func) の代わりに↓を使う df.swifter.apply(func)
こちらはデフォルトでどのように並列実行されているかプログレスバーが表示されるので非表示にする場合は pandas.DataFrame.swifter.progress_bar(False).apply
のように設定すると非表示になります。 swifter
で拡張される pandas
のAPIは下記の通りです。
- pandas.DataFrame.apply → pandas.DataFrame.swifter.apply
- pandas.DataFrame.resample.apply → pandas.DataFrame.resample.swifter.apply
- pandas.DataFrame.applymap → pandas.DataFrame.swifter.applymap
- pandas.DataFrame.rolling.apply → pandas.DataFrame.rolling.swifter.apply
- pandas.Series.apply → pandas.Series.swifter.apply
性能
applyに渡す関数がvectorizeできる場合とできない場合でベンチマークをしています。vectorize出来る場合は常に pandas
, Dask
より早く、vectorizeできな場合はデータ数が少ないときには Dask
より早くデータ数が一定数多くなってもDaskとほぼ同等の結果を出しています。
Benchmark: pandarallel & swifter
簡単なものですが、 pandarallel
と swifter
のベンチマークを行ってみました。
実行環境は以下のとおりです。
- MacBook Pro (16-inch, 2019)
- 2.6 GHz 6-Core Intel Core i7
- 16 GB 2667 MHz DDR4
- Python 3.7.4
swifter.__version__
: 0.305pandarallel.__version__
: 1.4.8
nschloe/perfplot を使って下記のように計測・可視化を行いました。
def unvectorizeble(x): return math.sin(x.a**2) + math.sin(x.b**2) out = perfplot.bench( setup=lambda df_size: pd.DataFrame(dict(a=np.random.randint(1, 8, df_size), b=np.random.rand(df_size))), kernels=[ lambda df: df.apply(unvectorizeble, axis=1), lambda df: df.parallel_apply(unvectorizeble, axis=1), lambda df: df.swifter.progress_bar(False).apply(unvectorizeble, axis=1) ], labels=["pandas", "pandarallel", "swifter"], n_range=[2 ** k for k in range(10, 21)], xlabel="len(df)" ) out.show( logy=False # True )
結果は以下のとおりです。
いくつかの事が確認できます。
- ある程度データ数が多くなると、
pandarallel
とswifter
ともにざっくりとpandas
に比べて 3, 4倍程度早くなる - データ数が少ないとき、
pandarallel
はオーバーヘッドの分だけ処理が遅くなり、swifter
の方が早くなる - データ数が増えてきたタイミングで
swifter
が「並列化する/しない」の戦略を変更している swifter
は戦略決定のオーバーヘッドがあるため最終的にはpandarallel
の方が高速になる
考察・まとめ
数行コードを追加・変更するだけでpandas
を高速化できるpandarallel
と swifter
の紹介を行いました。「 pandarallel
と swifter
もほぼ同じことができるけど、どっちを使えばいいの?」というのは最もな疑問ですが、個人的には swifter
の方が設計、実装において優れているような気がしました。またパフォーマンスに関してもデータ数に応じて実行戦略を決定してくれるのはメリットでしょう。
一方、データ数が十分あるときのパフォーマンスは pandarallel
に僅かですが軍配があがります。 また swifter
はBackendに Dask
を使うためインストールするライブラリが結構増えてしまうというデメリットもあり、お手軽に高速化したい場合は pandarallel
を使うのもありだと思います。またpandarallel
の方が対応しているAPI数では勝っているのでそういう観点からも使う機会があるかもしれません。
pandarallel
と swifter
は pandas
の一部を拡張して高速化を行いますが、もっと全体的に高速化・効率化を行いたい場合は、 Dask
や PySpark
、h2o.ai製の GitHub - h2oai/datatable: data.table for Python、あるいは以前このブログでも紹介した Vaex
を検討してみるのが良いかと思います。
また、 pd.read_csv
を高速化したい場合はu++さんが紹介されていた modin
を使ってみてもいいかもしれません。
今後も便利なライブラリやフレームワークなどあったら紹介していきたいと思います。