超高速…だけじゃない！Pandasに代えてPolarsを使いたい理由


**PolarsというPandasを100倍くらい高性能にしたライブラリ**がとても良いので布教します[^100]。PolarsはRustベースのDataFrameライブラリですが、本記事ではPythonでのそれについて語ります。

ちなみにpolarsは白熊の意です。そりゃあまあ、**白熊と大熊猫比べたら白熊のほうが速いし強いよね**ってことです[^pronounce]。

[^100]: 数字は個人の見解です。なお、本記事を書いている人とPolarsとの関係は、1ヶ月くらい趣味でそこそこな規模のデータを触る際に使ってた程度です。逆にその短期間で信者になるくらい最高なライブラリだとも言えます。

[^pronounce]: 読み方は「ぽらーす」ではなく「ぽーらす」寄りで、私の耳が正しければ「ぽぅらーす」みたいに聞こえます ( https://youtu.be/iwGIuGk5nCE?t=66 ) 。音は「暴発」ではなく「ボーナス」っぽい感じです（このあたりの説明下手くそですいません笑）。極を意味するpole（ぽーる）とか極値を意味するpolar（ぽーらー）から来てるはずなので、それに近い発音をしておけば恥はかかないはずです。

<img width=30%><img width=40% src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/665792/b27fb564-2109-463f-519e-cde65fbdb544.png>

# 何がいいの？
推しポイントは３つあります

- 高速！
- お手軽！
- 書きやすい！


### 1. 高速
<figure>
  <img width=80% src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/665792/59473692-2665-e4a2-7b9c-ff50b996ada6.png>
</figure>

画像はTPCHのBenchmark（紫がPolars）[^tpch]。

日本語でも色々記事があるので割愛しますが、**Rust**や**Apach Arrow**などにお世話になっており、非常に速いです。MemoryErrorに悩まされる問題も解決されます。開発者のRitchieがしゃれおつなツイートをしてるので、そちらも参考にどうぞ ↓ [^tw]。
<font color="grey">
抄訳：
（ひとつ目）Pandasは黄色くした部分でDataFrameをフルコピーしてて、イケてないよ！
（ふたつ目）一方Polarsでフルコピーしてるのは、2枚目の黄色い部分だよ！
</font>

https://twitter.com/RitchieVink/status/1532067902954029057


[^tpch]: https://www.pola.rs/benchmarks.html Polarsは比較的最近のライブラリなので、こういった評価で対象になってないことが多々あります。。
[^tw]: https://twitter.com/RitchieVink/status/1532067902954029057 。ちなみに私も速い理由をちゃんと分かってはないですが、このあたりにRithcieによる説明があります → https://www.pola.rs/posts/i-wrote-one-of-the-fastest-dataframe-libraries/



### 2. お手軽
```
pip install polars
```
だけでスタートできます。高速なデータフレーム処理ライブラリとして有名なcuDF（GPUを使う）とかpyspark（sparkを使う）とかと比べてお手軽です。Google Colabだとデフォルトでインストールされてるので、`import polars as pl` するだけで普通に使えます。

また、Pandasと似た書き方でわりと動くのも嬉しいところかもしれません[^おてがる]。

[^おてがる]: が、この後で述べるようにもっとスマートな書き方があるため、推奨はしません。高速化の恩恵を受けにくいなどの理由から、公式にもpandas-likeなAPIは非推奨になる流れで、warningが出たりエラー吐くように変わってきております。


### 3. 書きやすい

実はこれが一番の推しポイントですが、あまり語られることが多くない気がします。ということで、**この記事ではPolarsの書きやすさの話をメインでしていきます！！**

……の前にまず、概要的なところを簡単に紹介しましょう。


# Polars入門
## 10行で把握するPolars
irisデータセットを例に適当な処理を書いてみましょう。
```python
import polars as pl

df = pl.read_csv("https://j.mp/iriscsv")                         # データ読み込み
df_agg = (
    df
    .select([pl.col("^sepal_.*$"), pl.col("species")])           # 列の選択
    .with_columns((pl.col("sepal_width") * 2).alias("new_col"))  # 列の追加
    .filter(pl.col("sepal_length") > 5)                          # 行の選択
    .group_by("species")                                         # グループ化
    .agg(pl.all().mean())                                        # 全列に対して平均を集計
)
```
Polarsは上のコードように、処理をメソッドチェーンを繋いで記述することが多いです。Rのdplyrのパイプみたいなもんです。基本的には書いた順に処理が走るので、何も考えずにつなげていけばOKです。

上記で行っているのは、

* `read_csv`：データの読み込み
* `select`：列の選択
* `with_columns`：列の追加
* `filter`：行の選択
* `gorupby`：グループ化
* `agg`：集計

になります。`pl.col` で列を選択し、その列に対する処理を続けます。`alias` は列名の変更、`pl.all` は全列の選択です。

## 参考になるページ
Polarsについて学ぶときは一番上、公式のUser Guideが分かりやすいです。3つ目のページではpandasの同じ処理と並べて比較できます。4つ目はチートシートです。
- [Polars User Guide](https://pola-rs.github.io/polars-book/)
- [API reference — Polars documentation](https://pola-rs.github.io/polars/py-polars/html/reference/)
- [Modern Polars](https://kevinheavey.github.io/modern-polars/)
- [Polars_cheat_sheet.pdf](https://franzdiebold.github.io/polars-cheat-sheet/Polars_cheat_sheet.pdf?utm_source=pocket_saves)

日本語だとこのあたりがまとまっていて参考になると思います。１，２個目は使い方の初歩から網羅的に書いているようなもの、３個目はクエリ最適化の中身の解説も含んでおり、４個目は最近（23/2/18時点）のバージョンまで含めたTipsが説明されています。
- [pandasから移行する人向け polars使用ガイド - Qiita](https://qiita.com/nkay/items/9cfb2776156dc7e054c8)
- [テーブルデータ処理に悩むあなたに朗報！Polarsの使い方を徹底解説　その１：基本編 | DevelopersIO](https://dev.classmethod.jp/articles/polars-tutorial-001-standard/)
- [超高速DataFrameライブラリー「Polars」について](https://zenn.dev/hiro_torii/articles/06d7e845e146ee)
- [Polars, 旬の13のお役立ち機能 - Qiita](https://qiita.com/hkzm/items/8427829f6aa7853e6ad8)

問題を解きながら身に付けたい場合は以下があります。

- [Polarsでデータサイエンス100本ノックを解く（前編） - Qiita](https://qiita.com/_jinta/items/28442d25bba067c13bb7)
- [Polarsでデータサイエンス100本ノックを解く（後編） - Qiita](https://qiita.com/_jinta/items/394b1682893e5991d592)
- [Python初学者のためのPolars100本ノック - Qiita](https://qiita.com/kunishou/items/1386d14a136f585e504e)

上二つは私が書いた、[データサイエンス100本ノック（構造化データ編）](https://github.com/The-Japan-DataScientist-Society/100knocks-preprocess) を解いたものです。ひたすら例が見たい人はこれらをぜひ！問題としては3つ目に記載されているもののほうが基礎的なところから網羅していて良い気がします[^100k]。

[^100k]: 私自身は解いてないのでなんとも言えませんが。。違ったらご指摘ください。


# pl.Exprを使う
Polarsの書きやすさの中心にあるのが、**polars.Expression**というやつです。これは何かというと、

> a mapping from a series to a series

と説明されてます[^expl]。つまり、**ある列から他の列への加工処理の方法**を記述したものです。複数列からのmappingもできます。データフレームに対する主な処理は `select`, `with_columns`, `agg`, `filter` などですが[^context]、**任意の場所でpl.Exprを使うことが出来ます**。どういうことか。

[^expl]: https://pola-rs.github.io/polars-book/user-guide/expressions/operators/
[^context]: contextなどと呼ばれます。 https://pola-rs.github.io/polars-book/user-guide/concepts/contexts/

たとえば、文字列で入ってる `cost` 列を整数に変換する処理は
```python
pl.col("cost").str.extract("\$(.*)").cast(pl.Int64)
```
という pl.Expr で書けます（まず `pl.col` でcost列を指定し、それに対し `str.extract` で `$` に続く部分を抽出する文字列処理を行い、最後に `cast` でInt型に変換しています）。これを色んな所で使いまわせます。

```python
# 整数に変換した列の追加
df.with_columns(pl.col("cost").str.extract("\$(.*)").cast(pl.Int64).alias("cost_int"))

# コストが100ドル以上の行を選択
df.filter(pl.col("cost").str.extract("\$(.*)").cast(pl.Int64) > 100)

# 店舗ごとのコストの合計を計算
df.group_by("store").agg(pl.col("cost").str.extract("\$(.*)").cast(pl.Int64).sum())
```

ここで強調したいのは、同じ表現を繰り返し使えることではありません。ポイントは、**どんな処理も同じ頭の働かせ方で済む**ことです。書き方に悩むことがなくなり、統一感も出しやすいです。

このようにpl.Expressionを用いると柔軟な記述が実現できるため、公式でもExpression APIを推奨しています。



# Pandasとの比較
Pythonでのデータフレーム操作のデファクトスタンダードはPandasでしょう。が、正直Pandas好みじゃない人、いますよね。自分はdplyrからPandasに移った人なので「あれ？」となることが多々ありました[^ill-iden]。

[^ill-iden]: 例えばこの辺りに書いてあります。こちら頷くことが多い記事で、大変参考にしています。 → https://ill-identified.hatenablog.com/entry/2021/09/18/130716

多くの高速化ライブラリはPandasっぽく書けることを推してます。が、それを超えた良さをPolarsは持っているので、そのあたりを比較していきましょう。


### 概念とかの違い
**１．Indexがない**
個人的にあれはバグの温床だと思ってます。あって嬉しい場面はあるものの、まあなくても困りません。

**２．同じ列に色んな型が混在できるという謎仕様がない**
`pd.read_csv` の `low_memory` とかの引数がこれに関連します。途中で気づいたとき絶望するやつですね。
Polarsだと当然そんなことはないです。

**３．遅延評価ができる**
これについては[次節](https://qiita.com/_jinta/private/fac13f09e8e8a5769b79#%E9%81%85%E5%BB%B6%E8%A9%95%E4%BE%A1
)で解説します。

**４．列の指定が容易**
Pandasは（書き方によっては）一つの処理で何回もデータフレーム名を書く必要があります。例えば、以下のようなコードはよく目にします。
```python
df_customer[(df_customer["A"] > 0) & (df_customer["B"] == "XXX")]
```
まあ別にいいのですが、処理の途中で行数が変わったり列を追加したりすると、対処が面倒になります。Pandasで書かれたコードに、同じデータフレームに繰り返し再代入する記述スタイルが多いのはこのあたりが理由なのだろうと思います。
一方Polarsですが、 `pl.col` を用いると操作しているデータフレームに対して素直に列を指定できます。

このあたり具体例で見ていきましょう。




### 具体例①：複雑な集計処理
具体的にPandasとPolarsで大きく違う例を見ていきます。
まずはやや複雑な集計処理です。列Gをグループとして

- 列Aの最大値
- 列Aの最小値
- 「列Aと列Bの差」の平均値

を計算し、いい感じの列名を付けたいとしましょう。

▼ Pandasの場合
```python
df["A_B_diff"] = df["A"] - df["B"]
df_agg = df.groupby("G").agg({"A": ["min", "max"], "A_B_diff": "mean"})
df_agg.columns = ["A_min", "A_max", "A_B_diff_mean"]
```
などですかね。。他の書き方もありますが、たぶんどれで書いても若干もっちゃりすると思います。

▼ Polarsの場合
```python
df.group_by("G").agg([
    pl.col("A").min().alias("A_min"),
    pl.col("A").max().alias("A_max"),
    (pl.col("A") - pl.col("B")).mean().alias("A_B_diff_mean"),
])
```
です。スッキリ書けます。集計処理に pl.Expr を渡せるので、複数列の指定が特に容易です。
列名を `alias` でその場で変えられるのもありがたいです。


### 具体例②：apply処理
Pandasだと複雑な列を作成したいときどうしても `apply` が必要になります。例として、以下を満たすような列Cを作成しましょう。

- 列A + 列B が偶数のとき、または 列Aが3のとき：`"い"`
- 列Aが偶数のとき：`"ろ"`
- それ以外：`"は"`


▼ Pandasの場合
```python
def make_col_c(a, b):
    if (a + b) % 2 == 0 or a == 3:
        return "い"
    elif a % 2 == 0:
        return "ろ"
    else:
        return "は"

df["C"] = df.apply(lambda x: make_col_c(x["A"], x["B"]), axis=1)
```
Pandasだとapplyを使って書くことが多くなるかと思います。が、DataFrameに対する**apply処理は非常に遅い**です[^apply]。このくらいなら頑張ればapplyなしでも書けるでしょうが、書きやすさ・読みやすさとのトレードオフがあります。

[^apply]: https://shinyorke.hatenablog.com/entry/pandas-tips#apply%E3%81%AF%E3%81%95%E3%81%BB%E3%81%A9%E9%80%9F%E3%81%8F%E3%81%AA%E3%81%84 などいろんなところで言われてるやつですね。加えて、内部でデータフレームをコピーする際の無駄が大きいです（冒頭のツイートと似た話ですね）。


▼ Polarsの場合
```python
df.with_columns(
    pl.when(((pl.col("A") + pl.col("B")) % 2 == 0) | (pl.col("A") == 3))
    .then("い")
    .when(pl.col("A") % 2 == 0)
    .then("ろ")
    .otherwise("は")
    .alias("C")
)
```
`pl.when(...).then(...).otherwise(...)` は見ての通り、pythonのif...else文に相当する処理です。applyなしで（かつ自然な書き方で）書け、処理も高速です。


ということで、パッと見だとそんなに違いなさげですが、込み入った操作を行うときにPolarsの良さが出ます。

:::note warn
polarsの `apply` は、version0.19から `map_elements` などに変更されました。

https://pola-rs.github.io/polars/releases/upgrade/0.19/#groupby-renamed-to-group_by
:::



# 遅延評価

### 遅延評価とは何か
明示的にPolarsに計算の実行を指示するまでは計算が走らず、指示した段階で溜まった**一連の処理をいい感じにまとめて実行**してくれるものです。「いい感じ」とは、Polarsが内部で**クエリの最適化**や**並列実行**を行ってくれることを指します[^lazy]。

Pandasにこの機能はありません。Daskに近いですが、Daskはクエリの最適化は行ってくれないみたいです。

[^lazy]: クエリに対して `.show_graph(optimized=True)` を呼ぶことで実行グラフを可視化できます。何がどうなって速くなっているかはこちらの記事などが参考になりそうです。その1 → https://pola-rs.github.io/polars-book/user-guide/lazy/optimizations/ , その2 → https://towardsdatascience.com/understanding-lazy-evaluation-in-polars-b85ccb864d0c


### 遅延評価のやりかた
`lazy()` を挟んで、 `collect()`（あるいは `fetch()`）で実行するだけで、かなりシンプルです。

```python
(df.
    .lazy()            # <= 以降の処理を遅延評価にまわす
    .select(...)
    .with_columns(...)
    .group_by(...)
    .agg(...)
    .with_columns([..., ...])
    .collect()         # <= 遅延していた処理をまとめて実行
)
```

デバッグ用途で限られた行数だけ実行したいときは、`collect` の代わりに `fetch` が使えます。
また、データを読み込むところから遅延で評価したい場合、read_csvの代わりに `scan_csv` を使います。

```python
df = (
    scan_csv("path/to/your/data.csv")  # <= データの読み込みから遅延評価にまわす
    .with_columns(...)
    .group_by(...)
    .agg(...)
    .collect()         # <= 遅延していた処理をまとめて実行
)
```

当然どのくらい速くなるかはケースバイケースですが、自分が試したときは半分くらいの実行時間になりました。このような遅延評価の恩恵も、メソッドチェーンを使って一連の処理を一続きで記述できることが前提にあるような気がします。


# 注意点など
## a. matplotlibやsklearnで使う

### matplotlib/seaborn
普通に使えます。
```python
import seaborn as sns
import matplotlib.pyplot as plt
df = pl.read_csv("https://j.mp/iriscsv")

# matplotlibの場合
plt.scatter(df["sepal_length"], df["petal_length"])

# seabornの場合
sns.scatterplot(data=df, x="sepal_length", y="petal_length");
```

### plotly
~~微妙にクセがあります。~~
version 5.16以降、そのまま使えるようになってました[^plotly]。
```python
import plotly.express as px

# どちらの書き方でもOK
px.scatter(x=df["sepal_length"], y=df["petal_length"])
px.scatter(df, x="sepal_length", y="petal_length")
```

[^plotly]: https://github.com/plotly/plotly.py/blob/master/doc/python/px-arguments.md#input-data-as-non-pandas-dataframes

### scikit-learn
~~現状だと、`to_numpy()` で変換したものを渡す必要があります。もちろん `to_pandas()` でも大丈夫です。~~
**そのまま使えるようになってました**（sklearnの全てで可能かは試していません）。
```python
from sklearn.linear_model import LinearRegression

model = LinearRegression()
model.fit(df.select(pl.all().exclude(["petal_width", "species"])), df["petal_width"])
```

なお、**LightGBM**はsklearn-APIだと大丈夫だけど、Training-APIだとyについては `to_numpy()` する必要ありそうです。Xはそのままでいけました。

statsmodelsはまだ `add_constant` など一部しか対応していないようです。


## b. with_columnsの注意点
並列処理が走る関係で、同じ `with_columns` の中で作成した列を別の列の作成時に使うことはできません。以下のように別途 `with_columns` を入れる必要があります。

```python
# これならOK
(df
    .with_columns([
        pl.col("A").cast(pl.Float64).alias("hoge"),
        pl.col("B").mean().over("G").alias("fuga")
    ])
    .with_columns((pl.col("hoge") + pl.col("fuga")).alias("piyo"))
)

# これだとダメ（with_columnsを分けて書く必要がある）
(df
    .with_columns([
        pl.col("A").cast(pl.Float64).alias("hoge"),
        pl.col("B").mean().over("G").alias("fuga")
        (pl.col("hoge") + pl.col("fuga")).alias("piyo"))
    ])
)
```

また、`.alias()`を使う代わりに名前付きの引数を渡すこともできます[^wc_name]。

[^wc_name]: こちらで知りました➞ https://qiita.com/hkzm/items/8427829f6aa7853e6ad8#2-dfwith_columns-%E3%81%A7%E3%81%AE%E5%88%97%E5%90%8D%E3%81%AE%E6%8C%87%E5%AE%9A%E3%81%AF%E5%90%8D%E5%89%8D%E4%BB%98%E3%81%8D%E5%BC%95%E6%95%B0%E3%81%A7%E3%82%82%E6%B8%A1%E3%81%9B%E3%82%8B

```python
df.with_columns([
    hoge=pl.col("A").cast(pl.Float64),
    fuga=pl.col("B").mean().over("G")
])
```

:::note warn
`with_column` は `with_columns` に統一され、使えなくました。
:::


## c. よく出会うエラー

- `Duplicate("Column with name: 'col' has more than one occurrences")`
列名を重複させられないよ！って言っているだけなので、`alias()` で名前変えてあげればOKです。


- ちょいちょいクラッシュする？
おそらく、大きいデータを処理する際に特定のエラーが出るとクラッシュすると思われます。クラッシュしたらエラーも出ず原因が分からないので、書いてる段階だとサイズ小さくしたり `fetch` を使って確認していくのがとりあえずの対処法になるかと思います。
なお、私の場合はデータフレームの行数と一致しないものを `with_column` しようとする場合に出会った印象があります。


## d. その他、雑多に
網羅性も何もないですが、使ってみて引っ掛かったところなど書いておきます。

#### pandasのtransformに該当する処理
```python
def mean_by_department(col: str) -> pl.Expre:
    return pl.col(col).mean().over(pl.col("department"))

(df
    .with_columns(mean_by_department("salary").alias("avg_salary_by_department")
    .filter(mean_by_department("overtime_hours") > 100)
)
```
pl.Exprに `over` を続ければOKです。SQLっぽいですね。
（上のように関数でpl.Exprを返す書き方は、あまり見ないけど結構便利だと思ってます）

#### nanとnullを区別する
https://pola-rs.github.io/polars-book/user-guide/expressions/null/

#### 行方向の処理
pl.DataFrameに対する処理としては `axis=1` で実現できます。
```python
df.select(pl.col("^sepal_.*$")).sum(axis=1)
```

pl.Expr を用いる場合、`sum_horizonal` など行方向用のメソッドがv0.18.8以降追加されました[^hor]。
他にList型 や `fold` を使うこともできます。

[^hor]: https://pola-rs.github.io/polars/py-polars/html/reference/expressions/api/polars.sum_horizontal.html#polars.sum_horizontal

https://pola-rs.github.io/polars-book/user-guide/expressions/lists/

https://pola-rs.github.io/polars-book/user-guide/expressions/folds/

#### グループ化しない集計
`agg` ではなく、`select` を使います。
```python
df.select([
    pl.col("sepal_length").min().alias("length_min"),
    pl.col("sepal_length").median().alias("length_med"),
])
```


#### 時系列での集計
時系列での集計操作は `group_by_dynamic` を使えます。なお、先に `sort` してから実行しないと意図通りの挙動をしないのには注意です。

```python
df.group_by_dynamic("Date", every="1y").agg(pl.col("A").mean())
```

https://pola-rs.github.io/polars-book/user-guide/transformations/time-series/rolling/

:::note warn
`groupby` は `group_by` に、 `groupby_dynamic` は `group_by_dynamic` になりました。
英語の単語ごとに _ で区切るように統一しましょうという流れなのだと思います。
:::


#### 定数やnumpyのarrayを列として追加する
定数の場合は `pl.lit` が、np.arrayやリストなどは `pl.Series` が使えます。
```python
df.with_columns([
    pl.lit(3.14).alias("pie"),
    pl.lit("B").alias("const"),
    pl.Series(np.random.randn(4)).alias('np_random'),
    pl.Series([4,3,2,1]).alias('from_list'),
])
```

#### 特定の要素を変更する

pandasの `loc` を用いた代入や `where` に相当する処理は、when..then..otherwiseで記述するほか、`map_dicts` などを用いる方法があります。

https://qiita.com/_jinta/items/f6382463175031093881


#### 微妙にpandasと違う名前のやつ
目についたものだけ備忘のため表にしておきます（当然これがすべてではないです）。
|pandas |polars|
|-|-|
|isnull|is_null| 
|notnull|is_not_null| 
|nunique|n_unique| 
|groupby|group_by| 
|astype|cast|
 
polarsでは英語の単語ごとに_で区切ると思っておけば分かりやすいです。


# 最後に
Polarsはその高速さを売りに紹介されることが多いのですが、書きやすさとか使いやすさの面でもいいぞ！という主張でした。

実は「ほらね書きやすいでしょ」の多くはpandasでも似たような感じで書けるのですが、どうしてもパフォーマンスが落ちたり、よく流通している書き方がイケてなかったりで、どう書くか悩むことが結構あります。Polarsだと悩まずに最適なコードにたどり着けるのが一番嬉しいポイントだよなあとか思ったりする訳です。

たぶん2020年くらいに出た新しめのライブラリですが、開発は盛んだし、Ritchieはいいやつだし（別に話したことないけど）、今後も勢力を強めていくでしょう。「データが大規模でPandasだと難しいからPolarsを使う」ではなく、デフォでPolarsという選択肢をとるケースも増えてくるのではないかと妄想してます。