FireDucks と Polarsを比較してみた

本記事はFireDucksユーザー記事シリーズの第6弾です.本記事はBell様に執筆して頂きました


はじめに

とある企業でデータサイエンティストをやっています。今回はデータサイエンスでよく使うpandasを高速化するという、FireDucksを触ってみました。通常のpandasと、polarsとの比較をいくつかの処理でやってみました。

FireDucks とは

FireDucksとは、pandasを高速化するためのライブラリで、NECさんが提供しています。import 文を少し変えるだけでほぼコードを書き換える必要がなく、データ分析や前処理を高速に実行することができます。高速化の仕組みとしては、Pythonプログラムを中間言語に変換してから、効率的に実行することができる計算方法に変換することと、マルチスレッド高速化の2つの仕組みによるとのことです。

使い方

pipでインストールできます。公式ページにあるように、ソースコード上で変更箇所は基本的にはimport文のみになっています。

```import pandas as pd```→```import fireducks.pandas as pd```

fireducksではあとでまとめて実行することによって高速化しているのでそのまま使うと各処理単位の実行時間計測ができません。なのでベンチマークのために_evaluate()メソッドを使って即座に実行するようにしています。

ベンチマーク

filter, groupby, sorting, merge, concatのそれぞれの処理について、scikit-learn の fetch_kddcup99データセットでベンチマークしました。applyについても確認したかったですが、未実装とのことなので今回は見送りました。

環境

下記のような環境で実行しました。

ubuntu-22.04.3
CPU: Intel(R) Core(TM) i7-6800K CPU @ 3.40GHz 3.40 GHz
RAM: 64 GB

データセット

今回の検証にあたって、使用したデータセットはsklearn.datasets.fetch_kddcup99 になります。レコード数は 494020 になっています。

Pandas

pandasを使って下記のコードを実行しました。

import numpy as np
import pandas as pd
import time
from sklearn.datasets import fetch_kddcup99


def run_filter(df):
    #print(df.num_failed_logins.drop_duplicates())
    start_time = time.time()
    df_filtered = df[df['num_failed_logins'] >= 3]
    print("pandas filter:", time.time() - start_time)

def run_groupby(df):
    headers = ["protocol_type"]
    headers.extend([h for h in df.columns if 'rate' in h])

    start_time = time.time()
    grouped = df[headers].groupby('protocol_type').mean()
    print("pandas groupby:", time.time() - start_time)

def run_sort(df):
    headers = [h for h in df.columns if 'rate' in h]
    start_time = time.time()
    sorted_df = df.sort_values(by=headers)
    print("pandas sort:", time.time() - start_time)
    
def run_merge(df):
    headers = [h for h in df.columns if 'rate' in h]
    h1 = headers[:int(len(headers)/2)]
    h2 = headers[int(len(headers)/2):]
    df_1 = df[h1].reset_index(drop=False) 
    df_2 = df[h2].reset_index(drop=False).sample(frac=1)
    start_time = time.time()
    df_3 = pd.merge(df_1, df_2, on='index', how='left')
    print("pandas merge:", time.time() - start_time)

def run_concat(df):
    df_1 = df.iloc[:int(len(df)/2)]
    df_2 = df.iloc[int(len(df)/2):]
    start_time = time.time()
    df_3 = pd.concat([df_1, df_2])
    print("pandas concat:", time.time() - start_time)
    
def main():
    df = fetch_kddcup99(as_frame=True).data
    print(df.columns)
    run_filter(df)
    run_groupby(df)
    run_sort(df)
    run_merge(df)
    run_concat(df)

if __name__ == "__main__":
    main()

結果下記のようになりました。

Pandas filter: 0.01571178436279297
pandas groupby: 1.5911147594451904
pandas sorting: 1.3550989627838135
pandas merge: 1.2715435028076172
pandas concat: 0.4150705337524414

polars

poloarを使って下記のコードを実行しました。

import numpy as np
import pandas as pd
import polars as pl
import time
from sklearn.datasets import fetch_kddcup99
import timeit 


def run_filter(df):
    start_time = time.time()
    df_filtered = (df.filter(pl.col('num_failed_logins') >= 3))
    print(df_filtered)
    print("polars filter:", time.time() - start_time)

def run_groupby(df):
    headers = ["protocol_type"]
    headers.extend([h for h in df.columns if 'rate' in h])

    start_time = time.time()
    grouped = df.group_by("protocol_type").agg(pl.col(headers).mean())
    #grouped.collect()
    print(grouped)
    print("polars groupby:", time.time() - start_time)

def run_sort(df):
    headers = [h for h in df.columns if 'rate' in h]
    start_time = time.time()
    df_sorted = df.sort(by=headers)
    print(df_sorted)
    print("polars sort:", time.time() - start_time)

def run_merge(df):
    headers = [h for h in df.columns if 'rate' in h]
    h1 = headers[:int(len(headers)/2)]
    h2 = headers[int(len(headers)/2):]
    df_1 = df[h1].with_row_count()
    df_2 = df[h2].with_row_count()
    start_time = time.time()
    df_3 = df_1.join(df_2, on='row_nr', how='left')
    print(df_3)
    print("polars merge:", time.time() - start_time)

def run_concat(df):
    df_1 = df[:int(len(df)/2)]
    df_2 = df[int(len(df)/2):]
    print(df_1, df_2)
    start_time = time.time()
    df_3 = pl.concat([df_1, df_2])
    print(df_3.shape)
    print("polars concat:", time.time() - start_time)

def main():
    df = pl.from_pandas(fetch_kddcup99(as_frame=True).data)
    run_filter(df)
    run_groupby(df)
    run_sort(df)
    run_merge(df)
    run_concat(df)


if __name__ == "__main__":
    main()

結果下記のようになりました。

polars filter: 0.007210969924926758
polars groupby: 0.05714058876037598
polars sorting: 0.08912086486816406
polars merge: 0.014672994613647461
polars concat: 0.18430423736572266

FireDucks

FireDucksを使って下記のコードを実行しました。

import numpy as np
import fireducks.pandas as pd
import time
from sklearn.datasets import fetch_kddcup99


def run_filter(df):
    start_time = time.time()
    df_filtered = df[df['num_failed_logins'] >= 3]._evaluate()
    print("FireDucks filter:", time.time() - start_time)

def run_groupby(df):
    headers = ["protocol_type"]
    headers.extend([h for h in df.columns if 'rate' in h])
    start_time = time.time()
    grouped = df[headers].groupby('protocol_type').mean()._evaluate()
    print("FireDucks groupby:", time.time() - start_time)

def run_sort(df):
    headers = [h for h in df.columns if 'rate' in h]
    start_time = time.time()
    sorted_df = df.sort_values(by=headers)._evaluate()
    print("FireDucks sort:", time.time() - start_time)

def run_merge(df):
    headers = [h for h in df.columns if 'rate' in h]
    h1 = headers[:int(len(headers)/2)]
    h2 = headers[int(len(headers)/2):]
    df_1 = df[h1].reset_index(drop=False)._evaluate()
    df_2 = df[h2].reset_index(drop=False)._evaluate()
    start_time = time.time()
    df_3 = pd.merge(df_1, df_2, on='index', how='left')._evaluate()
    print("FireDucks merge:", time.time() - start_time)

def run_concat(df):
    df_1 = df.iloc[:int(len(df)/2)]._evaluate()
    df_2 = df.iloc[int(len(df)/2):]._evaluate()
    start_time = time.time()
    df_3 = pd.concat([df_1, df_2])._evaluate()
    print(df_3.shape)
    print("FireDucks concat:", time.time() - start_time)

def main():
    df = pd.DataFrame(fetch_kddcup99(as_frame=True).data)
    run_filter(df)
    run_groupby(df)
    run_sort(df)
    run_merge(df)
    run_concat(df)

if __name__ == "__main__":
    main()

FireDucks

FireDucks filter: 0.0036001205444335938
FireDucks groupby: 0.0333254337310791
FireDucks sort: 0.4092569351196289
FireDucks merge: 0.17314743995666504
FireDucks concat: 0.0047304630279541016

結果

結果まとめると下記表のようになり、Pandasよりはどの関数も数倍の高速化ができています。また、Polarsには3勝2敗となりました。

|処理   | Pandas      | Polars      | FireDucks    |
|:-----:|:-----------:|------------:|:------------:|
|filter | 0.0157| 0.00721秒    | 0.00360|
|groupby| 1.59秒      | 0.0571| 0.0333秒     |
|sort   | 1.36| 0.0891秒     | 0.409|
|merge  | 1.27秒      | 0.0147| 0.173秒      |
|concat | 0.415| 0.184秒      | 0.00473|

polarsと比較すると、filter、groupby、concatでfireducksが速くなっています。sortとmergeでfireducksが遅くなっている。

まとめ

importを少しかえるだけでPandasよりも数倍の高速化が実現され、
Polarsにも、3勝2敗でかなり高速な処理が可能ということがわかりました。
さらに、Polarsと比べて変換も圧倒的に楽なので、今後どんどん使っていきたいですね。

この記事が気に入ったらサポートをしてみませんか?