Polarsに格納できるデータタイプ型にList型とArray型があります。
ドキュメントで一覧見てると集合に対して同じ結果が出る処理も多そう?
今回はList型とArray型のメソッド種類の比較をして各々利用できそうなものをピックアップと同じような処理をしているのであればどっちが早いかを検証していきたいと思います。
List型とArray型のメソッド比較
PolarsのAPIドキュメントのList型とArray型の対比は以下です
前半はほとんど同じ処理ですね。List型でもarg_max取れたりしていてArray型と違いないのでは?というのが最初の印象でした。正確に差分を知るためにList型とArray型のメソッドの差を確認してみます。せっかくなのでPorlasで。
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
メソッドで取り出して別カラムへ格納しています。返り値はリスト型です。
続いて各々のリストを縦に展開してlistもしくはarr以降のメソッドを取り出します。
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以降のメソッドのみを取り出しています。
あとは抽出した列で差集合と積集合をとります。
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}")
これでお互いの差分と共通部分が出ました。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でもそれが可能なようです。
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"))
結果も一致してますね。
set_union, set_symmetric_difference
setの処理は他にもset_union
, set_symmentric_difference
もありました。list型なんだけどset型の集合演算も含めてPolars内で完結できるということですね。
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で数値を指定してその数値ごとに要素を最後まで抽出してリストを返すメソッドです。
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)
listとarrの速度評価
両方ともに存在したメソッドで速度評価をしてみます。
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)
各々1億個のデータを作成しました。
各々sumメソッドで比較してみたらやってみたら以下の通り。
速度の差はなさそうでした。。。
まとめ
PolarsのList型とArray型を比較してみました。
結論、実質的に機能が含有されているかつ速度にも大きな影響はないというところからとりあえずList型を使えば良いかという印象です。またPolars内でデータ加工かできる幅が広がりました。