pandasとpolarsで処理速度・使いやすさを比較！

Python使いであれば間違いなく使っているライブラリの１つがpandasです。
テーブルデータを簡単に処理できる優れものですが、後継を匂わせているライブラリがpolarsです。

特に速度面ではpandasよりアドバンテージがあるということもあり、pandasの痒いところに手が届くというふうにも見られます。

今回、pandasとpolars間で処理速度を測定してみて、使いやすさ等も含めてどちらが良いか検証して行きたいと思います。
最後に以下の３つのポイントで所感をお伝えして行きます。

- **実行速度**
- **ライブラリの利便性**
- **pandasとpolarsで置き換えができるか**

## 使用するデータセット
使用するのはKaggleのHome Credit Default Riskを使用して行きます。
金融系のテーブルデータで処理をいろいろ試すのに良いと思ったので選定しました。

https://www.kaggle.com/competitions/home-credit-default-risk/data

## pandas & polars 処理を比較
ここからはpandasとpolarsを並行して使い、処理速度や書き方等を比較して行きます。
環境はローカルjupyter notebookなので環境によって差が出る可能性があります。

未インストールの場合はインストールしておきましょう。
```
pip install polars pandas
```

import文
```python
import pandas as pd
import polars as pl
```

### 1. データの読み込み
まずはデータの読み込みから試して行きます。
普通にread_csvで２つとも呼び出すことができます。


```python
# pandas
pd_application_train = pd.read_csv('./home-credit-default-risk/application_train.csv')
# 1.17s
```

```python
# polars
pl_application_train = pl.read_csv('./home-credit-default-risk/application_train.csv')
# 966ms
```
処理にかかった時間はpandasが1.17s、polarsが966msなのでpolarsの勝利となります。（約1.2倍の差）

ちなみに統計量の出し方や先頭行の出し方も両者書方が一緒です。
```python
# describe(pandasの方が若干早い)
pd_application_train.describe() # 789ms
pl_application_train.describe() # 939ms

# head（polarsの方が若干早い）
pd_application_train.head() # 151us
pl_application_train.head() # 116us

```


### 2. テーブル操作
ここからは特徴量エンジニアリングに基づくテーブル操作でいろいろ比較をして行きたいと思います。
まずは他のテーブルから特徴量エンジニアリングして結合するところまで進めます。

他のテーブルで過去の履歴が見れるので、IDの出現頻度を抽出します。
ここではpandasとpolarsの書き方が異なるので、同じ処理になりますが見た目が異なる場合があります。

```python
# pandas
previous_loan_counts_pd = bureau_pd.groupby('SK_ID_CURR', as_index=False)['SK_ID_BUREAU'].count().rename(columns={'SK_ID_BUREAU': 'previous_loan_counts'})
# 76.3 ms
```
↑ 約1.18倍の差 ↓

```python
# polars
previous_loan_counts_pl = (
    bureau_pl.groupby('SK_ID_CURR')
    .agg(pl.col('SK_ID_BUREAU').count().alias('previous_loan_counts'))
# 90.3 ms
```
実行時間をコードの下に記載してますが、この処理はpandasが早かったです。

### 3. テーブル結合

上記で作成したテーブルと読み込んだテーブルを結合して行きます。
書き方が微妙に変わります。

```python
# pandas
pd_application_train = pd.merge(pd_application_train, previous_loan_counts_pd, on='SK_ID_CURR', how='left')
# 139ms
```
↑ 約4.74倍の差 ↓
```python
# polars
pl_application_train = pl_application_train.join(
    previous_loan_counts_pl,
    on='SK_ID_CURR',how="left"
)
# 29.3ms
```

こちらは明らかにpolarsの方が速度が出ています。
上記の処理をpolarsで再現するという感じなので全く同じ挙動かと問われると微妙です。

### 4. 欠損値穴埋め
上記のマージしたテーブルから欠損値を0で穴埋めして行きます。
ここも書き方が両者で微妙に変わります。


```python
# pandas
pd_application_train = pd_application_train.fillna(0)
# 557ms
```
↑ 約20.3倍の差 ↓

```python
# polars
pl_application_train = pl_application_train.fill_null(0)
# 27.4ms
```

欠損値穴埋めもpollarsの方が早いですね。
これも内部の処理が同じかと言われると疑問なんですが、単純比較だとかなり速度に差が出ていますね。

ちなみに欠損値を含むレコードを落とした場合は以下のような結果になりました。

```python
# pandas
pd_application_train.dropna(subset=["previous_loan_counts"])
# 93.5ms
```
↑ 約10.2倍の差 ↓

```python
# polars
pl_application_train.drop_nulls(subset=["previous_loan_counts"])
# 9.13ms
```

### Matplotlibでの可視化
データの可視化も見て行きたいと思います。
Matplotlibで目的変数の可視化をしたときに変化はあるか、また書き方や見え方が変わってしまうのかを試します。

```python
# pandas
plt.figure(figsize=(8, 5))
sns.countplot(x='TARGET', data=pd_application_train )
plt.title('Distribution of TARGET Variable')
plt.show()
# 1.07s
```
↑ 約6.0倍の差 ↓

```python
# polars
plt.figure(figsize=(8, 5))
sns.countplot(x=pl_application_train['TARGET'].to_numpy())
plt.title('Distribution of TARGET Variable')
plt.show()
# 178ms
```

速度としてはpolarsなんですが、一度numpy変換しなければいけないようです。
それも早さの要因かもしれませんが、可視化する時の書き方が変わるのも置き換える時の注意ポイントの１つです。
表示された図に差は特にありません。（そうでなくては困りますが...）

![pandaspolars.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/707936/78cb2f0c-703c-d9bb-1b37-606290a80896.png)

### One-Hot Encoding
特定のカテゴリデータをワンホットエンコーディングする方法も違いを見て行きます。
CODE_GENDERという性別をラベルで表現しているカラムがあるので、それぞれダミー変数に変換して変換後にカラムを落とすところまでやってみます。


```python
# pandas
gender_dummies = pd.get_dummies(pd_application_train['CODE_GENDER'])
pd_application_train = pd.concat([pd_application_train.drop('CODE_GENDER', axis=1), gender_dummies], axis=1)
# 119ms
```
↑ 約6.18倍の差 ↓

```python
# polars
gender_dummies = pl_application_train.select(pl.col("CODE_GENDER")).to_dummies()
pl_application_train = pl.concat([pl_application_train.drop("CODE_GENDER"),gender_dummies],how="horizontal")
# 19.3ms
```

速度はやはりpolars。カラム１つだけをエンコーディングする分には書き方も大きく違いませんでした。
ただし、コード1〜２行で複数のカテゴリ変数を同時にエンコーディングするという方法はpolarsでは難しいようです。
その点はpandasの方が便利です。

## まとめ

前述した通り、pandasとpolarsを３つのポイントで比較して所感を述べて行きます。

### 実行速度について
速度に関してはpolarsに明らかにアドバンテージがあります。
１部の処理ではpandasが上回る可能性がありそうですが、全体的に見て単純な処理速度ではpolarsが上という結果になりました。速度を重視するのであればpolarsの導入は良いかもしれません。

### ライブラリの利便性について
次にライブラリの利便性に関してはpandasの方が高い印象です。
特に、自分でわざわざ指定しなくてもよしなにライブラリがやってくれる部分が多いので便利です。
polarsに関してはpandasで当たり前にできることが結構面倒だったり、そもそも１行じゃ再現できないみたいな部分が大きいです。

コードの複雑さでいうと、polarsの方が面倒くさい書き方をするところも多い。
テーブルの加工をかなり複雑にした場合、pandasはそこまで難しくないですが、polarsでやろうとするととにかく検索をして調べる時間が増えそうです。調べた先も書き方が微妙に変わっていることもあってpandasの安定性を実感しました。

### 置き換えができるかどうか
pandasで処理に時間がかかるものを速度改善でpolarsをというケースもあるでしょう。
この置き換えがスムーズに行くかどうかについてです。

結論から言うと、置き換えをするのは簡単ではなさそうです。
まず、共通してできる書き方もありますが、ちょっと処理が複雑になるとpandasの処理を再現するのは難しそうです。

さらにバージョン違いで、「以前誰かが共有してくれた書き方が今はできない」と言うこともありました。安定性という意味でもプロダクトに組み込むような使い方をするのは注意が必要そうです。さらに、pandasからpolarsに書き換える工数を考慮すると、複雑な処理であればあるほどコード作成に時間がかかるかもしれません。


よって、まだまだpandasを使うことになりそうです。
ただ、本当に簡単なテーブル操作しかしないのであれば、速度改善に使うことはコスパが良さそうです。


<h3>参考</h3>

[Polars公式ドキュメント](https://pola-rs.github.io/polars/py-polars/html/reference)

[Pandas → Polars 早見表](https://zenn.dev/bee2/articles/e8623a603752ff)