Pandasを待つのはもうやめよう:大規模データセットのためのPolars実用ガイド

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

私がPandasからPolarsに乗り換えた理由

以前、16GBのRAMを搭載したノートPCで10GBのCSVファイルをPandasのDataFrameに読み込もうとしたことがありました。数秒もしないうちに、ファンがジェットエンジンのような音を立て始めました。簡単なhead()を実行する間もなく、ターミナルにはMemoryErrorが表示されて停止しました。Pythonでデータを扱っているなら、おそらく同様の限界に突き当たったことがあるでしょう。Pandasが業界標準であるのには理由がありますが、現代のデータエンジニアリングの規模を想定して作られたものではありません。

Polarsはその状況を変えます。Rustで書かれ、Apache Arrowのメモリフォーマットをベースに構築されたPolarsは、Pandasが処理しきれないデータセットも扱えます。私の経験では、Sparkクラスターのようなオーバーヘッドなしでスケーラブルなパイプラインを構築するには、Polarsへの移行が最も効果的です。これは単なるマイナーアップデートではなく、Pythonによるデータ処理の根本的な転換なのです。

クイックスタート:数分で使い始める

移行は思ったよりも簡単です。構文はPandasよりも厳格ですが、メソッドチェーンを活用した論理的で読みやすい構造になっています。

インストール

pip install polars

最初のDataFrame

基本的なフィルタリングと集計操作を見てみましょう。直接的なインデックス操作ではなく、式(Expression)を使用している点に注目してください。

import polars as pl

# DataFrameの作成
df = pl.DataFrame({
    "id": [1, 2, 3, 4, 5],
    "category": ["A", "B", "A", "C", "B"],
    "value": [10.5, 20.0, 15.2, 7.8, 12.1]
})

# メソッドチェーンを使用したフィルタリング、グループ化、合計
result = (df.filter(pl.col("value") > 10)
          .group_by("category")
          .agg(pl.col("value").sum())
          .sort("value", descending=True))

print(result)

pl.col()関数はPolarsの心臓部です。これらの「式(Expressions)」により、エンジンは実際にデータが移動する前にクエリを最適化できるため、コードの効率が大幅に向上します。

エンジン:なぜPolarsはPandasより優れているのか

Polarsは単なるラッパーではなく、アーキテクチャを完全に見直したものです。3つの具体的な機能が、従来のライブラリに対して圧倒的な優位性をもたらしています。

1. RustとApache Arrow

PolarsはRustで記述されており、厳格なメモリ安全性を備えたC言語レベルのパフォーマンスを提供します。内部のメモリレイアウトにはApache Arrowを採用しています。Arrowは列指向(カラムナ)であるため、CPUは隣接する列から不要なデータを読み取る無駄なサイクルを消費しません。このレイアウトにより、非常に高速なベクトル演算が可能になります。

2. デフォルトでの並列処理

Pandasは主にシングルスレッドで動作します。CPUに12個のコアがあっても、Pandasは通常そのうちの11個をアイドル状態のままにします。Polarsは異なります。利用可能なすべてのコアにワークロードを自動的に分散します。複雑なmultiprocessingコードを一行も書くことなく、マルチスレッドのパフォーマンスを享受できます。

3. クエリ最適化(Query Optimizer)

Lazy APIを使用する場合、Polarsはコードを一行ずつ実行するのではなく、まずスクリプト全体を分析します。スクリプトの最後で5,000万行のデータセットをフィルタリングする場合、Polarsはそのフィルタリング処理をプロセスの最初に移動させます。この「述語プッシュダウン(predicate pushdown)」により、エンジンはディスクから必要な特定の行と列だけを読み取ります。

遅延評価による100GBファイルの処理

データセットが利用可能なRAMよりも大きい場合は、read_csvの使用をやめましょう。代わりにscan_csvを使用してLazy APIを起動します。

# ファイルを読み込まずにクエリプランを作成する
lazy_query = (pl.scan_csv("massive_dataset.csv")
              .filter(pl.col("status") == "active")
              .select([
                  pl.col("user_id"),
                  (pl.col("revenue") * 0.8).alias("net_revenue")
              ]))

# collect()を呼び出したときのみ、エンジンがプランを最適化して実行する
df_final = lazy_query.collect()

.collect()を呼び出すと、Polarsにプランの実行を指示します。100列のうち2列しか必要ない場合、エンジンはディスクからその2列だけを抽出します。本番環境で、メモリ使用量が40GBから2GBに削減されるのを実際に目にしたことがあります。

複雑な集計の整理

Polarsのウィンドウ関数は非常にすっきりしています。例えば、ユーザーごとの最新3件のトランザクションを取得するのは一行で済みます:

df.group_by("user_id").agg([
    pl.col("transaction_amount").tail(3).alias("last_3_tx")
])

本番環境で得た教訓

データパイプラインでPolarsを1年間使用した結果、一般的なボトルネックを防ぐためのいくつかのアドバイスを見つけました。

1. .apply() のパフォーマンスの罠

Pandasでは.apply(lambda x: ...)は標準的なツールですが、Polarsではパフォーマンスキラーになります。Pythonのラムダ式を使用すると、データが高速なRustコアから低速なPythonインタープリタに強制的に戻されてしまいます。applyに頼る前に、必ずpl.when().then().otherwise()のようなネイティブの式を探してください。

2. スキーマを尊重する

Polarsは型に対して厳格です。Int32の列とInt64の列を結合しようとすると、Polarsは推測して合わせるのではなく、クラッシュします。最初は面倒に感じるかもしれませんが、これによりPandasプロジェクトを悩ませる「静かなデータの破損」バグを防ぐことができます。クリーニングプロセスの早い段階で.cast(pl.Int64)を使用することに慣れましょう。

3. CSVよりもParquetを選択する

可能な限り、データはParquet形式で保存してください。PolarsはParquetのメタデータを読み取って、不要なデータのチャンク全体をスキップできます。scan_parquetと適切なファイルフォーマットを組み合わせることで、標準的なワークステーションでも数百ギガバイトのデータを処理できるようになります。

4. シームレスな統合

コードベース全体を書き直す必要はありません。Pandasを必要とする特定のライブラリがある場合は、単にdf.to_pandas()を使用してください。どちらのライブラリもArrow互換であるため、この変換は通常ほぼ瞬時に行われ、メモリ効率も非常に高いです。

Polarsへの切り替えには、マインドセットの転換が必要です。コンピュータに「どのように」行うかを指示するのではなく、「何を」したいかを伝えるようになります。結果は一目瞭然です。Polarsのオプティマイザに任せるだけで、処理時間が20分から40秒未満に短縮されるのを私は見てきました。

Share: