PolarsのList型とArray型を比較してみた

Polarsに格納できるデータタイプ型にList型とArray型があります。
ドキュメントで一覧見てると集合に対して同じ結果が出る処理も多そう？
今回はList型とArray型のメソッド種類の比較をして各々利用できそうなものをピックアップと同じような処理をしているのであればどっちが早いかを検証していきたいと思います。

# List型とArray型のメソッド比較
PolarsのAPIドキュメントのList型とArray型の対比は以下です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3605750/5f1a9eb3-9dd0-e27c-fbba-5a52116ca18c.png)

前半はほとんど同じ処理ですね。List型でもarg_max取れたりしていてArray型と違いないのでは？というのが最初の印象でした。正確に差分を知るためにList型とArray型のメソッドの差を確認してみます。せっかくなのでPorlasで。


```python
all_df = (
    pl.DataFrame(
        {
            "list_text": list_text,
            "array_text": array_text,
        }
    )
    .with_columns(
        pl.col("list_text").str.extract_all(r"Series\.list\.(\w+)\(.*\)").alias("list_method_all"),
        pl.col("array_text").str.extract_all(r"Series\.arr\.(\w+)\(.*\)").alias("arr_method_all"),
    )
)
```

list_text, array_textには先ほどキャプチャで貼り付けたAPIドキュメントをブラウザからざっとコピーした文字列が格納されています。これを必要箇所を`extract_all`メソッドで取り出して別カラムへ格納しています。返り値はリスト型です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3605750/12f161cc-5239-979d-46d0-2800d16e0a94.png)

続いて各々のリストを縦に展開してlistもしくはarr以降のメソッドを取り出します。
```python
list_df = (
    all_df
    .select(
        pl.col("list_method_all").explode(),
    )
    .with_columns(
        pl.col("list_method_all").str.extract(r"Series\.list\.(\w+)").alias("list_method"),
    )
)


arr_df = (
    all_df
    .select(
        pl.col("arr_method_all").explode(),
    )
    .with_columns(
        pl.col("arr_method_all").str.extract(r"Series\.arr\.(\w+)").alias("arr_method"),
    )
)
```
`explode`メソッドにてlistで格納していたメソッド名を縦に展開しています。その後に正規表現でlistもしくはarr以降のメソッドのみを取り出しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3605750/000c8f67-3542-626f-0d4f-63db5a136d24.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3605750/52d3e825-7fcf-360f-70c2-28e0c62baaf0.png)

あとは抽出した列で差集合と積集合をとります。
```python
list_methods = set(list_df.get_column("list_method").to_list())
arr_methods = set(arr_df.get_column("arr_method").to_list())

list_only_methods = list_methods - arr_methods
arr_only_methods = arr_methods - list_methods
common_methods = list_methods & arr_methods

print(f"list_only_methods: {list_only_methods}")
print(f"arr_only_methods: {arr_only_methods}")
print(f"common_methods: {common_methods}")
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3605750/d7b69370-7da6-19b5-eb17-de3a32f2bbd0.png)

これでお互いの差分と共通部分が出ました。list型のメソッドがarr型のメソッドをほぼ含有していますね。arr側のみのメソッドはarr->listにキャストする`to_list`メソッドのみなので実質全てを含有しているといえそうです。
（集計メソッドのうち`mean`だけlistにしかないのはなんでだろう...）

# list型のみメソッド
調べてみると@_jintaさんがlist型のメソッドについて紹介している良記事が。
https://qiita.com/_jinta/items/62777087b4807b3cb301

本記事ではlist型のみのメソッドかつ@_jintaさん記事で紹介されてないメソッドの一部をピックアップしたいと思います。

## set_difference, set_intersection
リスト型で格納しているもの同士の差集合と積集合を取ってくれるメソッドがありました。先ほどのlistとarrの比較でset型を別で作成していましたがPolarsでもそれが可能なようです。
```python
list_s = pl.Series("list_method", [list_df.get_column("list_method").to_list()])
arr_s = pl.Series("arr_method", [arr_df.get_column("arr_method").to_list()])

display(list_s.list.set_difference(arr_s).alias("list_only_methods"))
display(arr_s.list.set_difference(list_s).alias("arr_only_methods"))

display(arr_s.list.set_intersection(list_s).alias("common_methods"))
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3605750/0e800d36-a917-6b98-61d6-54e84eceae68.png)

結果も一致してますね。


## set_union, set_symmetric_difference
setの処理は他にも`set_union`, `set_symmentric_difference`もありました。list型なんだけどset型の集合演算も含めてPolars内で完結できるということですね。
```python
list_s = pl.Series("list_method", [list_df.get_column("list_method").to_list()])
arr_s = pl.Series("arr_method", [arr_df.get_column("arr_method").to_list()])
```

## gather, gather_every
メソッド名から処理が想像できなかったのでピックアップ。
`gather`メソッドではリストで指定したindexに応じてその要素を抽出したリストを返します。
null_on_oob(out of bound)をTrueにするとoobのものは全てnullを返すようになります。

`gather_every`メソッドではリストでなくてintで数値を指定してその数値ごとに要素を最後まで抽出してリストを返すメソッドです。

```python
list_s = pl.Series("list_method", [list_df.get_column("list_method").to_list()])
arr_s = pl.Series("arr_method", [arr_df.get_column("arr_method").to_list()])

concat_s = pl.concat([list_s, arr_s])

display(concat_s)
display(concat_s.list.gather([0, 2]))
display(concat_s.list.gather([0, 2, 24], null_on_oob=True))

display(concat_s.list.gather_every(2), offset=0)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3605750/e6b48060-cc88-dd1b-5f45-589b7d600e2b.png)

# listとarrの速度評価
両方ともに存在したメソッドで速度評価をしてみます。
```python
speed_list_df = pl.Series("random", [np.random.rand(100_000_000).tolist()], dtype=pl.List(pl.Float64))
speed_arr_df = pl.Series("random", [np.random.rand(100_000_000)], dtype=pl.Array(pl.Float64, 1))

display(speed_list_df)
display(speed_arr_df)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3605750/9c42dabe-c8b5-5b40-8013-dc02f6f95fb1.png)

各々1億個のデータを作成しました。
各々sumメソッドで比較してみたらやってみたら以下の通り。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3605750/375eec78-08ec-b100-886d-f03bac757596.png)

速度の差はなさそうでした。。。


# まとめ
PolarsのList型とArray型を比較してみました。
結論、実質的に機能が含有されているかつ速度にも大きな影響はないというところからとりあえずList型を使えば良いかという印象です。またPolars内でデータ加工かできる幅が広がりました。