もう全部Polarsでいいんじゃないかな
本記事は、某企業アドベントカレンダー2024の21日目の記事です。
KaggleのInstacart Market Basket Analysisデータセットを用いて、Polars (EagerおよびLazyモード) とPandasのパフォーマンスを比較検証します。
何も考えずPandasを使い続ける私。
本記事は、Polarsの魅力に触れることで、自分自身をPolarsへ移行する気にさせることを目的とした記事です。
Pandas VS Polarsはよく取り上げられている話題です。
しかし、私の戦争はまだ終わっていないのです。(?)
実行環境
本記事のコードは、誰でも簡単に実行できるように、Google Colabノートブックとして公開しています。
準備物は一点
- Kaggle API token(kaggle.json)
- データセットは、kaggle-api経由で取得します。そのために必要となるのが、Kaggle API tokenです。
-
Kaggleへログイン後、Settings内のCreate New Tokenより取得してください。(無料です。)
さあ、お前もPolarserにならないか?(Polarserという単語など無い。)
PandasとPolars
Pandas (パンダ)
言わずもがな、Pythonのデータ処理において基準的なライブラリです。
私も大学時代、Pandas以外の選択肢を考えたことがありませんでした..。
恋は盲目ってやつですかね...。
Polars (シロクマ)
PolarはPandasと比較して非常に優れた処理速度を持つとされています。皆さんも速いの好きですよね。
Polarsの日本語ユーザーガイドでは、以下のPlars哲学が紹介されています。
マシン上で利用可能なすべてのコアを活用します。
不要な処理/メモリ割り当てを減らすためにクエリを最適化します。
利用可能な RAM よりもはるかに大きなデータセットを扱うことができます。
一貫性があり予測可能な API を提供します。
厳格なスキーマに準拠しています(クエリ実行前にデータ型が分かっている必要があります)。
「マシン上で利用可能なすべてのコアを活用します。」これはロマンです。
また、PolarsはRustで書かれており、これによりC/C++並みのパフォーマンスが得られるそうです。
競技プロ経験者として、この時点でPolarsの圧勝だろうなという予感がします。
Polarsの2つのモード
Polarsには、データ処理を行うための2つのモードが存在します。
モードと聞くだけで嬉しくなりますね。変形機構はロマンです。
仮面ライダーカ◯トのキャストオフはいつ見ても涎が出ますよね。
さて、なんの話をしてたんでしたっけ...。
Polarsのモードですね。シロクマには2つのモードがあります。
-
Eager モード
Pandas のように、データ操作を即座に実行するモードです。
操作ごとに結果が計算されるため、デバッグがしやすく、直感的に操作を行えます。 -
Lazy モード
遅延実行(Lazy Evaluation)に基づいたモードで、操作を一旦記録し、最適化された形でまとめて実行します。大規模なデータセットを効率的に処理できることが特徴です。
遅延実行とは?
遅延実行とは、指示されたデータ処理を即座に実行するのではなく、操作を「計画」として記録し、必要なタイミングで一括して実行する仕組みのことです。
これによって、最終的に必要となるデータを把握できるため、不要な計算を排除し、処理全体を最適化できます。
やられた嫌なことをネチネチとメモに書き溜め、然るべきタイミングで一気に放出するイメージですかね。こんなイメージでどうですか?Polarsさん。そうですか、駄目ですか。
# 遅延読み込み(scanから始まるscan_parquetやscan_csvで読み込む)
data = pl.scan_csv("example.csv")
# 操作をチェインするが、この時点ではまだ実行はされない
result = (
data
.filter(pl.col("column_a") > 10)
.group_by("column_b")
.agg(pl.sum("column_c"))
.sort("column_b")
)
# .collect() を呼び出すことで最適化され、一括して実行される
final_result = result.collect()
実験概要
データセット
KaggleのInstacart Market Basket Analysisデータセットを使用します。(こちらのコンペの規約に承諾することで、kaggle-api経由でデータセットが取得できます。公開ノートブックを実行する場合、事前にこちらへログインし、規約に承諾してください。)
このデータセットは、20万人以上のInstacartユーザーが何を購入しているのか、時系列ログデータが格納されています。もちろん匿名化されています。
リレーショナルセットのため、複数のcsvに分割され格納されています。
なんとなくとっつきやすそうな内容のデータを探していたら、リレーショナルセットに当たってしまいましたが、前処理が少し面倒なんですよね。
- aisles.csv
- departments.csv
- order_products__*.csv
- orders.csv
- products.csv
ベンチマークタスク
2つのデータ処理タスクをベンチマークとします。
商品データと部門データの集計
商品のデータセット (products) と部門のデータセット (departments) を使用して、各部門に属する商品の数を集計する処理を行います。
- Pandas
products_df = pd.read_csv("products.csv")
departments_df = pd.read_csv("departments.csv")
result_pandas = products_df['department_id'].value_counts().reset_index().merge(
departments_df, on='department_id'
)[['department', 'count']]
result_pandas = result_pandas.sort_values('count', ascending=False)
- Polars (Eager モード)
products_pl_df = pl.read_csv("products.csv")
departments_pl_df = pl.read_csv("departments.csv")
result_polars_eager = products_pl_df.group_by("department_id").agg(
pl.count().alias("count")
).join(departments_pl_df, on="department_id").select(["department", "count"])
result_polars_eager = result_polars_eager.sort("count", descending=True)
- Polars (Lazy モード)
products_pl_df = pl.scan_csv("products.csv")
departments_pl_df = pl.scan_csv("departments.csv")
result_polars_lazy = products_pl_df.group_by("department_id").agg(
pl.count().alias("count")
).join(departments_pl_df, on="department_id").select(["department", "count"]).sort("count", descending=True).collect()
ユーザー行動の分析
顧客の注文データ (orders) を分析し、顧客の行動に関する(と思われる..)指標の算出をします。
- 平均注文回数 (avg_num_orders): 顧客一人当たりの平均注文回数。
- 平均注文間隔 (avg_time_between_orders): 顧客が注文してから次の注文をするまでの平均日数。
- 顧客ごとの平均注文間隔 (user_avg_interval_df): 各顧客の注文間隔の平均値を格納したDF
- Pandas
orders_df = pd.read_csv("orders.csv")
avg_num_orders = orders_df.groupby('user_id')['order_id'].nunique().mean()
orders_without_first_time_df = orders_df[orders_df['days_since_prior_order'].notna()]
avg_time_between_orders = orders_without_first_time_df.groupby('user_id')['days_since_prior_order'].mean().mean()
- Polars (Eager モード)
orders_df = pl.read_csv("orders.csv")
avg_num_orders = orders_df.group_by('user_id').agg(
pl.col('order_id').n_unique()
).select(pl.col('order_id').mean()).item()
orders_without_first_time_df = orders_df.filter(pl.col("days_since_prior_order").is_not_null())
avg_time_between_orders = orders_without_first_time_df.group_by('user_id').agg(
pl.col('days_since_prior_order').mean()
).select(pl.col('days_since_prior_order').mean()).item()
- Polars (Lazy モード)
orders_df = pl.scan_csv("orders.csv")
avg_num_orders = orders_df.group_by('user_id').agg(
pl.col('order_id').n_unique()
).select(pl.col('order_id').mean()).collect().item()
orders_without_first_time_df = orders_df.filter(pl.col("days_since_prior_order").is_not_null())
avg_time_between_orders = orders_without_first_time_df.group_by('user_id').agg(
pl.col('days_since_prior_order').mean()
).select(pl.col('days_since_prior_order').mean()).collect().item()
実験結果
ベンチマークタスクの結果を示します。
出力結果
商品データと部門データの処理:
Pandas 処理時間 (10回試行): 平均 = 0.0870秒, 標準偏差 = 0.0237秒
Polars (Eager) 処理時間 (10回試行): 平均 = 0.0593秒, 標準偏差 = 0.1340秒
Polars (Lazy) 処理時間 (10回試行): 平均 = 0.0080秒, 標準偏差 = 0.0054秒
顧客行動分析:
Pandas 平均注文数: 16.59, 平均注文間隔: 15.45日
Polars (Eager) 平均注文数: 16.59, 平均注文間隔: 15.45日
Polars (Lazy) 平均注文数: 16.59, 平均注文間隔: 15.45日
Pandas 処理時間 (10回試行): 平均 = 4.0994秒, 標準偏差 = 0.7845秒
Polars (Eager) 処理時間 (10回試行): 平均 = 2.7798秒, 標準偏差 = 0.7308秒
Polars (Lazy) 処理時間 (10回試行): 平均 = 2.3896秒, 標準偏差 = 0.5274秒
処理時間の10回平均を確認すると、どちらのタスクも、Pandasが一番遅く、PolarsのLazyモードが最も早い結果になっていることが分かります。
また、平均注文数、平均注文間隔が一致しているため、ライブラリごとに同じデータ処理タスクが正確に実行されている事が確認できます。
matplotlibによるベンチマーク結果の可視化
商品データと部門データの集計
顧客行動分析
考察と今後の展望
Polars「ポラポラポラポラポラポラポラポラポラポラポラポラポラポラポラポラPolars!!」
綾波○イ「ポラポラする。」
matplotlibによって可視化したグラフを眺めていたら、ポラポララッシュが聞こえてきました。
速度(処理速度)と精密さ(型安全)を併せ持つ、まさにスタープ◯チナですな。(?)
商品データと部門データの集計タスクでは、pandasより約10倍の速度。
顧客行動分析タスクでは、pandasより約2倍の速度という結果が得られています。
「もう全部あいつ1人(Polars)でいいんじゃないかな。」
ということで、皆さんも乗り換える気になっていただけましたかね。
私と一緒に、Polarsへ入門する気になってくだされば執筆冥利につきます。
この門をくぐる者は一切の希望を捨てよ。
嫌です。だって、門の先のシロクマは希望なんだもの。
今後の展望として、さらに大きいデータセットでメモリ使用量も含めたベンチマークを行いたいと思います。
ここまでお読みいただきありがとうございました!!
Discussion