Polars vs Pandas: Rustの力を活用してDataFrame処理を10倍高速化する

Python tutorial - IT technology blog
Python tutorial - IT technology blog

なぜPandasは大規模データで頻繁に「息切れ」するのか?

Pythonでデータ分析を行っているなら、Memory Error(メモリ不足エラー)にはお馴染みでしょう。Pandasは非常に優れており柔軟ですが、歴史的な制約を抱えています。それは、マルチコアCPUや数十GBのデータセットが一般的ではなかった時代に設計されたということです。

Pandasの致命的な弱点は、シングルスレッド(single-threaded)処理の仕組みにあります。8コア의 CPUを持っていても、5GBのDataFrameでフィルタリング処理を実行すると、実際に働いているのは1コアだけで、残りの7コアは完全に「遊んでいる」状態になります。さらに、Pandasは通常、実際のデータファイルサイズの2〜3倍のRAMを消費します。

私自身、16GBのRAMを搭載したマシンで6GBのCSVファイルをPandasで読み込もうとした際、システムがフリーズした経験があります。その時、より現代的な解決策が必要だと確信しました。Polarsは、まさにこの問題を解決するために登場しました。Rustで書かれたPolarsは、現代のマルチコアCPUのアーキテクチャを最大限に活用します。また、Apache Arrowフォーマットを採用することで、メモリへのアクセスと管理を極めて効率的に最適化しています。

PythonプロジェクトへのPolarsのインストール

PolarsはPyPIで提供されているため、インストールは非常に簡単です。使用するためにRustを別途インストールする必要はありません。

pip install polars

ExcelやParquetなどの形式をスムーズに扱うには、以下のサポートライブラリもインストールすることをお勧めします。

pip install connectorx fsspec openpyxl

私の経験上、常にクリーンな仮想環境(venv)を使用することをお勧めします。Polarsは新機能のアップデートが非常に速いため、環境を分離しておくことで、不要なバージョンの競合を避けることができます。

Polarsのパワーを引き出す:Pandasの考え方をそのまま持ち込まない

Polarsがいかに速いかを実感するには、アプローチを変える必要があります。Polarsを単なるPandasのコピーとして使っているなら、その性能の8割を無駄にしていることになります。

1. Eager評価からLazy(遅延)評価への切り替え

Pandasでは、記述したすべてのコマンドが即座に実行されます(Eager評価)。しかし、Polarsの真の強みはLazyFrameにあります。すべてのデータをメモリにロードする代わりに、Polarsはファイル構造をスキャンし、コマンドが実行されるのを待ちます。

import polars as pl

# Lazy(遅延)アプローチ
q = (
    pl.scan_csv("large_system_log.csv")
    .filter(pl.col("status") == "error")
    .select([
        pl.col("timestamp"),
        pl.col("user_id")
    ])
)

# collect() を呼び出した時に初めて、計算が実際に開始される
df = q.collect()

この仕組みが高速なのは、Polarsがクエリ最適化(Query Optimizer)エンジンを備えているからです。列を選択する前にデータをフィルタリングすると、ディスクから必要な行と列だけを読み取ります。約1億行のデータセットを処理する際、この方法により通常の70%のメモリを節約できました。

2. インデックス指定の代わりにExpression APIを使用する

Pandasは.loc.ilocに大きく依存していますが、これらは時として混乱を招きます。Polarsでは、非常に明快で保守しやすいコードが書けるExpressionsの使用を推奨しています。

# Pandas: df[df['price'] > 500]
# Polars:
df.filter(pl.col("price") > 500)

# PolarsのGroupByは非常に強力です
result = df.group_by("region").agg([
    pl.col("sales").sum().alias("total_revenue"),
    pl.col("customer_id").n_unique().alias("unique_users")
])

大きな利点は、Polarsがagg内の計算を自動的に並列実行することです。合計、カウント、平均など10個の計算がある場合、Polarsはそれらを異なるスレッドに割り当て、複数のCPUコアで同時に処理します。

3. 厳格なデータ型チェック

Polarsは厳格な型付け(Strict typing)を採用しており、文字列と数値が混在することで発生するような「初歩的な」エラーを防ぐことができます。ファイルを読み込む際に欠損値の処理方法を定義することも可能です:

df = pl.read_csv("data.csv", null_values=["N/A", "?", "NULL"])

パフォーマンスの検証とリソースの監視

広告の言葉を鵜呑みにせず、数値を信じましょう。私はよくtimeitモジュールを使用して、2つのライブラリ間の速度を直接比較しています。

実際のプロジェクトで、1,000万行を処理するスクリプトをPandasからPolarsに移行したところ、驚くべき結果が得られました。実行時間が45秒からわずか3.5秒に短縮されたのです。スクリプトが終わるのをコーヒーを飲みながら待つ必要はなく、結果はほぼ瞬時に表示されます。

Linuxを使用しているなら、Polarsがcollect()を実行している間にhtopを開いてみてください。すべてのCPUメーターが100%まで跳ね上がるのが見えるはずです。これは、Polarsがハードウェアの性能を限界まで引き出して処理を行っている証拠です。

RAMに対してデータが大きすぎる場合は、collect(streaming=True)を使用してください。この機能により、Polarsはデータを小さなバッチ(一塊)ごとに処理できます。これは、DaskやRayのような外部ライブラリなしではPandasが実現できないことです。

結論として、データセットが数百MBを超えている場合や、Pandasの「カメのような」遅さに飽き飽きしている場合は、今すぐPolarsを試してみてください。構文を少し学び直すだけで、これほどの圧倒的なパフォーマンスが手に入るなら、非常に安い投資と言えるでしょう。

Share: