概要
2022年12月頃からPolarsが話題です。ベンチマークh2oai's db-benchmarkによるとPandasで1081秒かかる処理が57秒で実行できるそうです。
ベンチマークはビッグデータでの比較ですが、PandasやPolarsを最大限活用するような ビッグデータをゴリゴリ処理していく業務 はそこまで多くないと思います。それでもPolarsを使う価値があるのかということが気になりました。
例えばデータベースからクエリしてきた数千から数万件程度のデータを処理するときであっても同じようなパフォーマンス差がでるのでしょうか。
何かと使う機会が多いAWS Lambdaを検証環境にしてPandas, Polarsそれぞれを使った場合でどれぐらいパフォーマンスに差がでるのか検証してみました。
Polarsとは
PolarsはRust製のDataFrame
ライブラリで基本の書き方はPandasと似ています。
import polars as pl
import pandas as pd
# csvファイルの読み込み
pl_df_csv = pl.read_csv('hogehoge.csv') # Polars
pd_df_csv = pd.read_csv('hogehoge.csv') # Pandas
# DataFrameの作成
# Polars
pl_dataframe = pl.DataFrame(
{
"integer": [1, 2, 3],
"date": [
(datetime(2022, 1, 1)),
(datetime(2022, 1, 2)),
(datetime(2022, 1, 3))
],
"float":[4.0, 5.0, 6.0]
})
# Pandas
pd_dataframe = pd.DataFrame(
data = {
"integer": [1, 2, 3],
"date": [
(datetime(2022, 1, 1)),
(datetime(2022, 1, 2)),
(datetime(2022, 1, 3))
],
"float":[4.0, 5.0, 6.0]
})
ほぼほぼ同じではあるものも個人的にはPolarsの方が書きやすいと思います。
when~thenとか地味に便利なものもありますし。
# whenの中で条件を書いてthenでTrue, otherwiseでFalseのときの挙動を書く。
df.with_columns(
pl.when(pl.col("foo") > 2).then(pl.lit(1)).otherwise(pl.lit(-1))
)
# 複数のwhen, thenを連鎖させることもできる。
df.with_columns(
pl.when(pl.col("foo") > 2)
.then(1)
.when(pl.col("bar") > 2)
.then(4)
.otherwise(-1)
)
Polarsの売りである軽量さと高速な処理ですがマシン上で利用可能なすべてのコアを使用したり、クエリを最適化し、不要な作業やメモリ割り当てを削減するといった様々な工夫によって実現しているようです。
AWS Lambdaとは
2020年にAWS Lambda(以下、Lambda)のメモリ上限が10GBに拡張されたことによってLambdaでできることが増えました。
Amazon EventBridgeと組み合わせることで定期実行したりできるためバッチ処理をさくっと実装したい場合には便利なサービスです。
とは言ってもストレージは最大10GBでタイムアウトは最大15分ですので大規模なデータ処理などにはLambdaは不向きといえます。
ここまでの話からもわかるようにそもそもLambdaでPandasやPolarsを使ってデータ処理ロジックを組むのは滅多にないような気がしますが、それでも使えそうなケースがないわけではありません。
例えばDyanmoDBに保存されているデータに対してクエリして何らかの集計を行うようなケースには有効な可能性があります。
Pythonではboto3でDynamoDBの項目をクエリすることができますが、あくまでもデータ取得や簡単なフィルタリング機能しか提供していないため必要に応じてクエリ後にデータ処理が必要になります。
というわけで2倍でも早くなれば嬉しいなという軽い気持ちで検証してみました。
注意
厳密な比較検証ではないので温かい目で見守っていただけると助かります。
検証環境
ライブラリバージョン
- Python3.8
- polars-0.16.8
- pandas-1.5.3
Lambdaの設定
- タイムアウト 10分
- メモリ 1000MB
- エフェメラルストレージ 2000MB
検証結果
データフレームの作成
本来はDynamoDBからデータをクエリするべきですがめんどくさかったのでLambda起動時に1000000件の乱数データを発生させてPandasとPolarsそれぞれでDataFrame
を作成して検証しました。
import json
import random
import polars as pl
import pandas as pd
import time
# 乱数を発生させてデータを作成
item1 = [random.random() for i in range(1000000)]
item2 = [random.random() for i in range(1000000)]
item3 = [random.random() for i in range(1000000)]
item4 = [random.random() for i in range(1000000)]
item5 = [random.random() for i in range(1000000)]
item6 = [random.random() for i in range(1000000)]
# Pandas 計測開始
start = time.time()
df_pd = pd.DataFrame(
data = {
"item1": item1,
"item2": item2,
"item3": item3,
"item4": item4,
"item5": item5,
"item6": item6,
}
)
print("pandas", time.time() - start)
# Polars 計測開始
start = time.time()
df_pl = pl.DataFrame(
{
"item1": item1,
"item2": item2,
"item3": item3,
"item4": item4,
"item5": item5,
"item6": item6,
}
)
print("polars", time.time() - start)
実行結果
pandas 1.2423439025878906
polars 0.4033346176147461
何回か実行してみましたが大体3倍ぐらいはやくなってます🙌
分散の計算
次に分散の計算をしてみます。
start = time.time()
df_pd.var()
print("pandas var", time.time() - start)
start = time.time()
df_pl.var()
print("polars var", time.time() - start)
polars var 0.05542445182800293
pandas var 0.08168625831604004
こちらは1.5倍ぐらいですね。そもそも実行時間が短いのであまり差がでてないです。
フィルタリング
次にフィルタリングです。
start = time.time()
pd_result = df_pd[
(df_pd["item1"]< 0.5) & (df_pd["item2"] < 0.5) & (df_pd["item3"] < 0.5)
& (df_pd["item4"] < 0.5) &(df_pd["item5"] < 0.5) & (df_pd["item6"] < 0.5)
]
print("pandas filter", time.time() - start)
start = time.time()
pl_result = df_pl.filter(
(pl.col("item1") < 0.5) & (pl.col("item2") < 0.5) & (pl.col("item3") < 0.5)
& (pl.col("item4") < 0.5) &(pl.col("item5") < 0.5) & (pl.col("item6") < 0.5)
)
print("polars filter", time.time() - start)
polars filter 0.016983985900878906
pandas filter 0.08617496490478516
乱数の影響なのか全然安定しませんでしたが、平均で2倍弱、最大で5.4倍ほどでした。
さいごに
少なくともPandasより遅くなるということはなさそうですので迷ったらPolarsを使っておけば良さそうです。
時間があれば追加検証しようと思います。
参考文献