フリーランチ食べたい

No Free Lunch in ML and Life. Pythonや機械学習のことを書きます。

たった数行でpandasを高速化する2つのライブラリ(pandarallel/swifter)

pandas はデータ解析やデータ加工に非常に便利なPythonライブラリですが、並列化されている処理とされていない処理があり、注意が必要です。例えば pd.Sereis.__add__ のようなAPI(つまり df['a'] + df['b'] のような処理です)は処理が numpy に移譲されているためPythonのGILの影響を受けずに並列化されますが、 padas.DataFrame.apply などのメソッドはPythonのみで実装されているので並列化されません。 処理によってはそこがボトルネックになるケースもあります。今回は「ほぼimportするだけ」で pandas の並列化されていない処理を並列化し高速化できる2つのライブラリを紹介します。同時に2つのライブラリのベンチマークをしてみて性能を確かめました。

f:id:mergyi:20200726173057p:plain

pandarallel

pandaralell はPythonの multiprocessing moduleを使って計算処理を並列化するライブラリです。

github.com

インストール

pip install pandarallel

使い方

使い方は簡単で、 import して初期化するだけです。これで pandas.DataFramepandaralell のAPIが追加されます。

from pandarallel import pandarallel

# 初期化
pandarallel.initialize()

# df.apply(func) の代わりに↓を使う
df.parallel_apply(func)

これだけで apply がマルチコアを使った並列処理になります。実際にどのように並列化が行われている確認するために、初期化時に pandarallel.initialize(progress_bar=True) と指定すると下記のように、プログレスバーを使ってマルチコアが処理を行う様子を可視化してくれるようになります。

f:id:mergyi:20200726142038p:plain
READMEより引用

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 より高速化されているようです。

f:id:mergyi:20200726144516p:plain
READMEより引用

注意事項

一般的にマルチコアを使った並列化に言えることですが、並列化時に「コアごとにプロセス立ち上げる」「データをプロセスに受け渡す/受け取る」などのオーバーヘッドが存在します。そのためデータ量が少ない場合には「並列化しない場合より遅くなってしまう」ことがあります。

また現状ではPython3.5/3.6/3.7のみの対応になっています。

swifter

github.com

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.DataFrameswifter アクセサーを呼び出すだけで使うことができます。

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とほぼ同等の結果を出しています。

f:id:mergyi:20200726163954p:plain
READMEより引用

Benchmark: pandarallel & swifter

簡単なものですが、 pandarallelswifter のベンチマークを行ってみました。

実行環境は以下のとおりです。

  • 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.305
  • pandarallel.__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
)

結果は以下のとおりです。

f:id:mergyi:20200726201325p:plain

いくつかの事が確認できます。

  • ある程度データ数が多くなると、 pandarallelswifter ともにざっくりと pandas に比べて 3, 4倍程度早くなる
  • データ数が少ないとき、 pandarallel はオーバーヘッドの分だけ処理が遅くなり、 swifter の方が早くなる
  • データ数が増えてきたタイミングで swifter が「並列化する/しない」の戦略を変更している
  • swifter は戦略決定のオーバーヘッドがあるため最終的には pandarallel の方が高速になる

考察・まとめ

数行コードを追加・変更するだけでpandas を高速化できるpandarallelswifter の紹介を行いました。「 pandarallelswifter もほぼ同じことができるけど、どっちを使えばいいの?」というのは最もな疑問ですが、個人的には swifter の方が設計、実装において優れているような気がしました。またパフォーマンスに関してもデータ数に応じて実行戦略を決定してくれるのはメリットでしょう。

一方、データ数が十分あるときのパフォーマンスは pandarallel に僅かですが軍配があがります。 また swifterはBackendに Dask を使うためインストールするライブラリが結構増えてしまうというデメリットもあり、お手軽に高速化したい場合は pandarallel を使うのもありだと思います。またpandarallel の方が対応しているAPI数では勝っているのでそういう観点からも使う機会があるかもしれません。

pandarallelswifterpandas の一部を拡張して高速化を行いますが、もっと全体的に高速化・効率化を行いたい場合は、 DaskPySpark 、h2o.ai製の GitHub - h2oai/datatable: data.table for Python、あるいは以前このブログでも紹介した Vaex を検討してみるのが良いかと思います。

blog.ikedaosushi.com

また、 pd.read_csv を高速化したい場合はu++さんが紹介されていた modin を使ってみてもいいかもしれません。

upura.hatenablog.com

今後も便利なライブラリやフレームワークなどあったら紹介していきたいと思います。