【AWS Lambdaで検証】Polarsって本当に速いの？

## 概要

2022年12月頃から[Polars](https://www.pola.rs/)が話題です。ベンチマーク[h2oai's db-benchmark](https://github.com/h2oai/db-benchmark)によるとPandasで1081秒かかる処理が57秒で実行できるそうです。

ベンチマークはビッグデータでの比較ですが、PandasやPolarsを最大限活用するような **ビッグデータをゴリゴリ処理していく業務** はそこまで多くないと思います。それでもPolarsを使う価値があるのかということが気になりました。

例えばデータベースからクエリしてきた数千から数万件程度のデータを処理するときであっても同じようなパフォーマンス差がでるのでしょうか。

何かと使う機会が多いAWS Lambdaを検証環境にしてPandas, Polarsそれぞれを使った場合でどれぐらいパフォーマンスに差がでるのか検証してみました。



## Polarsとは

PolarsはRust製の`DataFrame`ライブラリで基本の書き方はPandasと似ています。

```polars_vs_pandas.py
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とか地味に便利なものもありますし。

```python
# 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の売りである軽量さと高速な処理ですがマシン上で利用可能なすべてのコアを使用したり、クエリを最適化し、不要な作業やメモリ割り当てを削減するといった様々な工夫によって実現しているようです。

https://pola-rs.github.io/polars-book/user-guide/index.html

## AWS Lambdaとは


2020年にAWS Lambda(以下、Lambda)のメモリ上限が10GBに拡張されたことによってLambdaでできることが増えました。

https://aws.amazon.com/jp/blogs/aws/new-for-aws-lambda-functions-with-up-to-10-gb-of-memory-and-6-vcpus/

Amazon EventBridgeと組み合わせることで定期実行したりできるためバッチ処理をさくっと実装したい場合には便利なサービスです。

とは言ってもストレージは最大10GBでタイムアウトは最大15分ですので大規模なデータ処理などにはLambdaは不向きといえます。


ここまでの話からもわかるようにそもそもLambdaでPandasやPolarsを使ってデータ処理ロジックを組むのは滅多にないような気がしますが、それでも使えそうなケースがないわけではありません。

例えばDyanmoDBに保存されているデータに対してクエリして何らかの集計を行うようなケースには有効な可能性があります。

Pythonではboto3でDynamoDBの項目をクエリすることができますが、あくまでもデータ取得や簡単なフィルタリング機能しか提供していないため必要に応じてクエリ後にデータ処理が必要になります。

というわけで2倍でも早くなれば嬉しいなという軽い気持ちで検証してみました。

:::note warn
注意
厳密な比較検証ではないので温かい目で見守っていただけると助かります。
:::

## 検証環境

#### ライブラリバージョン

* Python3.8
* polars-0.16.8
* pandas-1.5.3

#### Lambdaの設定

* タイムアウト 10分
* メモリ 1000MB
* エフェメラルストレージ 2000MB

## 検証結果


### データフレームの作成 

本来はDynamoDBからデータをクエリするべきですがめんどくさかったのでLambda起動時に1000000件の乱数データを発生させてPandasとPolarsそれぞれで`DataFrame`を作成して検証しました。


```python
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倍ぐらいはやくなってます🙌

### 分散の計算

次に分散の計算をしてみます。

```python
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倍ぐらいですね。そもそも実行時間が短いのであまり差がでてないです。

### フィルタリング

次にフィルタリングです。

```python

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を使っておけば良さそうです。
時間があれば追加検証しようと思います。

## 参考文献

https://pola-rs.github.io/polars-book/user-guide/index.html