LoginSignup
28
25

Import を変更するだけで高速化!? Pandas 互換ライブラリ FireDucks を検証する

Posted at

1. はじめに

 本記事では、Import を書き換えるだけで高速化できる、Pandas 互換のライブラリ FireDucks が公開されたので、実行速度やメモリ消費量等を検証してみたいと思います!

(NECさんが提供されているので日本語のドキュメントが豊富です。)

2. 環境準備

 FireDucks のライブラリは現在 Linux のみで提供されているようなので、Windows PC上の WSL2+Docker 環境で実行を行いました。

使用した Dockerfile はこちらです。

FROM python:3.10-slim

# OSのパッケージ更新およびtimeのインストール
RUN apt-get update && \
    apt-get install -y time && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# pipのアップグレードおよび必要なPythonパッケージのインストール
RUN pip install --upgrade pip && \
    pip install --no-cache-dir pandas fireducks scikit-learn
:pushpin: 詳しい Docker 環境での実行方法

以下の docker-compose.yml ファイルを準備し,

docker-compose.yml
version: '3'
services:
  playground:                 
    container_name: 'fireducks-python'  # コンテナ名
    build:
      dockerfile: ./Dockerfile

    restart: always           
    working_dir: '/workspace' 
    tty: true                 
    volumes:
      - ./workspace:/workspace

以下を実行してください。

  1. docker-composeのビルド&起動
    docker-compose up -d --build
    
  2. コンテナの中に入る (Pythonファイルはworkspaceディレクトリに置いてください)。
    docker exec -it fireducks-python /bin/bash
    

3. 実行時間を比較する

 ローカル環境で実行している場合は、以下のコマンドから fireducks ライブラリのインストールをお願いします。

pip install fireducks

3.1. 検証用のコード

 実行速度の計算用に、テーブルデータの様々な操作を行うコードを作成しました。このコードで、使用するライブラリを Pandas から FireDucks に変更した場合に、どのくらい速くなるのかを検証します。

まず Pandas 用のコードはこちらです。

speed_test.py
import pandas as pd
import numpy as np
import time

def create_random_dataframe(N):
    return pd.DataFrame({
        'A': np.random.randn(N),
        'B': np.random.randn(N),
        'C': np.random.choice(['X', 'Y', 'Z'], N),
    })

def perform_filtering(df):
    start_time = time.time()
    df_filtered = df[df['A'] > 0.5]
    print("pandas filtering:", time.time() - start_time)

def perform_groupby(df):
    start_time = time.time()
    grouped = df.groupby('C').mean()
    print("pandas groupby:", time.time() - start_time)

def perform_sorting(df):
    start_time = time.time()
    sorted_df = df.sort_values(by='B')
    print("pandas sorting:", time.time() - start_time)

def perform_join(df):
    start_time = time.time()
    df1 = df[['A', 'B']].copy()
    df2 = df[['C']].copy()
    joined_df = df1.join(df2)
    print("pandas join:", time.time() - start_time)

def perform_fillna(df):
    start_time = time.time()
    df_na = df.copy()
    df_na.loc[df_na.sample(frac=0.1).index, 'A'] = np.nan
    filled_df = df_na.fillna(0)
    print("pandas fillna:", time.time() - start_time)

def perform_dropna(df):
    start_time = time.time()
    df_na = df.copy()
    df_na.loc[df_na.sample(frac=0.1).index, 'A'] = np.nan
    dropped_df = df_na.dropna()
    print("pandas dropna:", time.time() - start_time)

def perform_calculation(df):
    start_time = time.time()
    df['D'] = df['A'] + df['B']
    print("pandas calculation:", time.time() - start_time)

def main():
    N = 50000000
    df = create_random_dataframe(N)
    perform_filtering(df)
    perform_groupby(df)
    perform_sorting(df)
    perform_join(df)
    perform_fillna(df)
    perform_dropna(df)
    perform_calculation(df)

if __name__ == "__main__":
    main()

FireDucks 用のコードはこちらです。

:page_facing_up: コード(長くなるので折りたたんでいます)
speed_test_fd.py
import fireducks.pandas as pd
import numpy as np
import time

def create_random_dataframe(N):
    return pd.DataFrame({
        'A': np.random.randn(N),
        'B': np.random.randn(N),
        'C': np.random.choice(['X', 'Y', 'Z'], N),
    })._evaluate()

def perform_filtering(df):
    start_time = time.time()
    df_filtered = df[df['A'] > 0.5]._evaluate()
    print("pandas filtering:", time.time() - start_time)

def perform_groupby(df):
    start_time = time.time()
    grouped = df.groupby('C').mean()._evaluate()
    print("pandas groupby:", time.time() - start_time)

def perform_sorting(df):
    start_time = time.time()
    sorted_df = df.sort_values(by='B')._evaluate()
    print("pandas sorting:", time.time() - start_time)

def perform_join(df):
    start_time = time.time()
    df1 = df[['A', 'B']].copy()
    df2 = df[['C']].copy()
    joined_df = df1.join(df2)._evaluate()
    print("pandas join:", time.time() - start_time)

def perform_fillna(df):
    start_time = time.time()
    df_na = df.copy()
    df_na.loc[df_na.sample(frac=0.1).index, 'A'] = np.nan
    filled_df = df_na.fillna(0)._evaluate()
    print("pandas fillna:", time.time() - start_time)

def perform_dropna(df):
    start_time = time.time()
    df_na = df.copy()
    df_na.loc[df_na.sample(frac=0.1).index, 'A'] = np.nan
    dropped_df = df_na.dropna()._evaluate()
    print("pandas dropna:", time.time() - start_time)

def perform_calculation(df):
    start_time = time.time()
    df['D'] = (df['A'] + df['B'])._evaluate()
    print("pandas calculation:", time.time() - start_time)

def main():
    N = 50000000
    df = create_random_dataframe(N)
    perform_filtering(df)
    perform_groupby(df)
    perform_sorting(df)
    perform_join(df)
    perform_fillna(df)
    perform_dropna(df)
    perform_calculation(df)

if __name__ == "__main__":
    main()

FireDucksを使用する場合の変更点

  • import文を import pandas as pd から import fireducks.pandas as pd に変更してください。
    • (使い方はこれだけでOKなので,すごく楽です。)
  • ユーザガイドの 実行モデル に説明があるのですが、FireDucksは複数の処理をあとからまとめて実行するような機能となっているようです。そのためメソッドの処理時間を計測するには、 pd.read_csv("data.csv")._evaluate() のように、_evaluate()をつけて即時実行するように変更する必要があります。

3.2. テーブルデータの操作時間の比較

 先ほどの2種類のコードを以下のコマンドから実行し、PandasとFireDucksの各テーブル操作の処理時間を比較したのが以下の表になります。

/usr/bin/time -v python speed_test.py

上記の Linux関数 time は、メモリ使用量等を計るために使用しています。

処理・統計情報 pandas fireducks 比較結果
Filtering 0.426秒 0.075秒 fireducksの方が約5.7倍高速
Groupby 1.409秒 0.170秒 fireducksの方が約8.3倍高速
Sorting 15.506秒 2.101秒 fireducksの方が約7.4倍高速
Join 0.938秒 2.469秒 pandasの方が約2.6倍高速
Fillna 6.245秒 5.155秒 fireducksの方が約1.2倍高速
Dropna 6.779秒 5.146秒 fireducksの方が約1.3倍高速
Calculation 0.111秒 0.029秒 fireducksの方が約3.8倍高速
ファイル全体の実行時間 0:34.64 0:19.31 fireducksの方が約1.8倍高速
最大メモリ使用量 4,678,272KB (4.47GB) 13,576,508KB (13GB) fireducksの方が約2.9倍メモリを使用

結果

  1. Joinを除く各機能で、FireDucksの方が大幅に実行速度が速くなりました。また、コード全体の実行時間も 34s → 19sと FireDucks の方が1.8倍速い 結果になりました。
    • CPUの使用量をモニタしたところ、Pandasは実行時に1コアしか使用していないですが、FireDucksは複数コアを使用していて CPU 使用率が100%になることがありました。FireDucksは複数コアを効率よく使用することにより、高速化しているのだと思います。
  2. 一方で、メモリ使用量は FireDucks の方が3倍近く多く消費 しています (実際に、FireDucksの方だけメモリ不足でプログラムが落ちたので、一度データサイズ N の数を減らしました)。

FireDucks を使う場合はメモリ使用量が増えるため、良いスペックのPCやサーバを用意する必要がありますが、データサイエンティストの給料の方がメモリより高いと思うので、メモリを増設してでも、時間が半分に短縮できればすごく有用ではないかと思います。

上記の結果は、当方のPC環境およびサンプルコードによるものです。FireDucks の性能を十分に引き出せていない可能性があります。あくまで参考でお願いします。

3.3. ファイル全体の実行結果

 先ほどは各関数の処理結果を計測するために、FireDucks のコードの方で _evaluate() により細切れに関数を実行していました。それにより遅くなっている可能性があるため、計測用のコードをすべて除いて、ファイル全体での実行時間を比較しました。

:page_facing_up: コード(折りたたんでいます)
import pandas as pd
import numpy as np

def create_random_dataframe(N):
    return pd.DataFrame({
        'A': np.random.randn(N),
        'B': np.random.randn(N),
        'C': np.random.choice(['X', 'Y', 'Z'], N),
    })

def perform_filtering(df):
    df_filtered = df[df['A'] > 0.5]

def perform_groupby(df):
    grouped = df.groupby('C').mean()

def perform_sorting(df):
    sorted_df = df.sort_values(by='B')

def perform_join(df):
    df1 = df[['A', 'B']].copy()
    df2 = df[['C']].copy()
    joined_df = df1.join(df2)

def perform_fillna(df):
    df_na = df.copy()
    df_na.loc[df_na.sample(frac=0.1).index, 'A'] = np.nan
    filled_df = df_na.fillna(0)

def perform_dropna(df):
    df_na = df.copy()
    df_na.loc[df_na.sample(frac=0.1).index, 'A'] = np.nan
    dropped_df = df_na.dropna()

def perform_calculation(df):
    df['D'] = df['A'] + df['B']

def main():
    N = 50000000
    df = create_random_dataframe(N)
    perform_filtering(df)
    perform_groupby(df)
    perform_sorting(df)
    perform_join(df)
    perform_fillna(df)
    perform_dropna(df)
    perform_calculation(df)

if __name__ == "__main__":
    main()

FireDucksの方は1行目を import fireducks.pandas as pd に変更してください。

処理・統計情報 pandas fireducks 比較結果
ファイル全体の実行時間 0:35.19 0:14.15 fireducksの方が約2.5倍高速
最大メモリ使用量 4,679,576KB (4.46GB) 12,816,680KB (12.2GB) fireducksの方が約2.7倍メモリを使用

Pandasは実行時間はあまり変化していませんが、FireDucksはさらに速くなり、Pandas よりも 2.5倍高速 になりました!

3.4. モデルの訓練の実行

 ChatGPTに相談したところ、機械学習のモデル(例えばランダムフォレスト)の訓練には numpy を使っているため、おそらく FireDucks に変更しても速度には影響ないとのことでしたが、念のため比較を実施しました。

 使用したコードはこちらです。データセットには kaggle の Santander Customer Transaction Prediction Dataset を使用しました。

import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score
import time

# 1. データの読み込み
start_time = time.time()
train = pd.read_csv('data/train.csv')
X = train.drop(columns=['ID_code', 'target'])
y = train['target']
print("Data loading:", time.time() - start_time, "seconds")

# 2. モデルの学習
start_time = time.time()
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)
clf = RandomForestClassifier(n_estimators=100, random_state=42)
clf.fit(X_train, y_train)
print("Model training:", time.time() - start_time, "seconds")

# 3. モデルの評価
start_time = time.time()
preds = clf.predict_proba(X_val)[:, 1]
roc_score = roc_auc_score(y_val, preds)
print(f"Validation ROC-AUC: {roc_score}")
print("Model evaluation:", time.time() - start_time, "seconds")

実行結果の比較はこちらです。

処理・統計情報 pandas fireducks 比較結果
データ読み込み時間 2.27秒 0.80秒 fireducksの方が約2.8倍高速
モデル訓練時間 595.72秒 596.69秒 ほぼ同じ
モデル評価時間 1.10秒 1.03秒 ほぼ同じ
ファイル全体の実行時間 9:59.69 9:59.14 ほぼ同じ
最大メモリ使用量 1,572,952KB (1.5GB) 2,331,356KB (2.2GB) fireducksの方が約1.5倍メモリを使用

ChatGPTが言っていたようにモデルの訓練時間の短縮には影響がありませんでした。

メモリ消費量が増えることを考えると、FireDucks は大規模データの前処理や探索的データ分析(EDA)などのフェーズで使用するのがよいと思います。

4. おわりに

 Importをちょっと変えるだけで、倍以上速くなるのはすごく便利だと思います(エラーなどは一切出ませんでした)。ただ私のPCが、ただのゲーミングPCのため、能力不足により十分な真価を引き出せなかったようにも思います。
 ちょっとした計算機で、毎日 Pandas をぶん回している方はぜひ試していただければ!

(今回使ったサンプルコードは、ChatGPTと相談しつつすべて自作しましたが、そういえばどこかの論文に比較用のサンプルコードが公開されているかもと、書き終わってから気づきました。)

28
25
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
25