はじめに
全国のPolarsファンのみなさまこんにちは。カジュアルに破壊的変更を行うPolarsのversion 0.20 のリリースが近づいてきました。そこで、今回の破壊的変更を一足先にご紹介いたします。ちなみに map_dict()
が replace()
に rename されていたのは最近知りました。
Disclamers
この内容は本家release noteの pr#12872 について、重要度が大きいと思われる変更点だけを2023年12月16日時点で日本語で紹介しているだけです。おそらく未来の時点で読んでも(また破壊的変更が加わっていて)参考にならない可能性があります。翻訳上のミスや解釈の誤りなどありましたら、コメント欄でご指摘いただけますと助かります。
version 0.20 破壊的変更内容抜粋
join 時の null の扱い
- 0.19まで: join のkeyにnullが存在する場合、null自体もjoinのkeyの対象としていた
- 0.20から: null はデフォルトでは join key の対象としない。
join_nulls=True
で0.19までの挙動と同じになる。
一般のSQLエンジンの挙動に近くなると思う。なお実行速度アップにも貢献するらしい。
Example
Before:
>>> df1 = pl.DataFrame({"a": [1, 2, None], "b": [4, 4, 4]})
>>> df2 = pl.DataFrame({"a": [None, 2, 3], "c": [5, 5, 5]})
>>> df1.join(df2, on="a", how="inner")
shape: (2, 3)
┌──────┬─────┬─────┐
│ a ┆ b ┆ c │
│ --- ┆ --- ┆ --- │
│ i64 ┆ i64 ┆ i64 │
╞══════╪═════╪═════╡
│ null ┆ 4 ┆ 5 │
│ 2 ┆ 4 ┆ 5 │
└──────┴─────┴─────┘
After:
>>> df1.join(df2, on="a", how="inner")
shape: (1, 3)
┌─────┬─────┬─────┐
│ a ┆ b ┆ c │
│ --- ┆ --- ┆ --- │
│ i64 ┆ i64 ┆ i64 │
╞═════╪═════╪═════╡
│ 2 ┆ 4 ┆ 5 │
└─────┴─────┴─────┘
>>> df1.join(df2, on="a", how="inner", join_nulls=True) # Keeps previous behavior
shape: (2, 3)
┌──────┬─────┬─────┐
│ a ┆ b ┆ c │
│ --- ┆ --- ┆ --- │
│ i64 ┆ i64 ┆ i64 │
╞══════╪═════╪═════╡
│ null ┆ 4 ┆ 5 │
│ 2 ┆ 4 ┆ 5 │
└──────┴─────┴─────┘
個人的感想
既存のSQLエンジンの挙動と整合的になるので好ましい変更。
outer join での 左右のkeyの列をキープするようにする
言葉で説明しづらいので、次のexampleを見てください。
Example
Before:
>>> df1 = pl.DataFrame({"L1": ["a", "b", "c"], "L2": [1, 2, 3]})
>>> df2 = pl.DataFrame({"L1": ["a", "c", "d"], "R2": [7, 8, 9]})
>>> df1.join(df2, on="L1", how="outer")
shape: (4, 3)
┌─────┬──────┬──────┐
│ L1 ┆ L2 ┆ R2 │
│ --- ┆ --- ┆ --- │
│ str ┆ i64 ┆ i64 │
╞═════╪══════╪══════╡
│ a ┆ 1 ┆ 7 │
│ c ┆ 3 ┆ 8 │
│ d ┆ null ┆ 9 │
│ b ┆ 2 ┆ null │
└─────┴──────┴──────┘
After:
>>> df1.join(df2, on="L1", how="outer")
shape: (4, 4)
┌──────┬──────┬──────────┬──────┐
│ L1 ┆ L2 ┆ L1_right ┆ R2 │
│ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ i64 ┆ str ┆ i64 │
╞══════╪══════╪══════════╪══════╡
│ a ┆ 1 ┆ a ┆ 7 │
│ b ┆ 2 ┆ null ┆ null │
│ c ┆ 3 ┆ c ┆ 8 │
│ null ┆ null ┆ d ┆ 9 │
└──────┴──────┴──────────┴──────┘
>>> df1.join(df2, on="a", how="outer_coalesce") # Keeps previous behavior
shape: (4, 3)
┌─────┬──────┬──────┐
│ L1 ┆ L2 ┆ R2 │
│ --- ┆ --- ┆ --- │
│ str ┆ i64 ┆ i64 │
╞═════╪══════╪══════╡
│ a ┆ 1 ┆ 7 │
│ c ┆ 3 ┆ 8 │
│ d ┆ null ┆ 9 │
│ b ┆ 2 ┆ null │
└─────┴──────┴──────┘
個人的感想
pandasの merge(..., how="outer")
的な結果が変更となるので要注意。
SQLエンジンでの挙動としてはPostgreSQL系の扱いと同じになる。というかSQLエンジンによって full outer join がkeyを潰す(1列)のか残す(2列)か挙動が違うことを初めて知った。列数が変わるので既存コードが動かなくなるレベルの破壊的変更。ぞくぞくしますね。
count
にて null values を無視するようになる。
Expr
or Series
のメソッドの挙動の変更です。
after: count()
-> null含まない個数。 len()
null含む個数。
Example
Before:
>>> df = pl.DataFrame({"a": [1, 2, None]})
>>> df.select(pl.col("a").count())
shape: (1, 1)
┌─────┐
│ a │
│ --- │
│ u32 │
╞═════╡
│ 3 │
└─────┘
After:
>>> df.select(pl.col("a").count())
shape: (1, 1)
┌─────┐
│ a │
│ --- │
│ u32 │
╞═════╡
│ 2 │
└─────┘
>>> df.select(pl.col("a").len()) # Mirrors previous behavior
shape: (1, 1)
┌─────┐
│ a │
│ --- │
│ u32 │
╞═════╡
│ 3 │
└─────┘
個人的感想
挙動としてはflavorな変更。既存コードで値が結果が変わった場合はこれを疑うべき。
NaN
values が equal あつかいされる
Floating NaN
は、一般的(IEEE754的)には NaN
同士の比較演算子を適用した結果は全て False
となる。その挙動に従わなり equal での比較が True
となる。
Example
Before:
>>> s = pl.Series([1.0, float("nan"), float("inf")])
>>> s == s
shape: (3,)
Series: '' [bool]
[
true
false
true
]
After:
>>> s == s
shape: (3,)
Series: '' [bool]
[
true
true
true
]
個人的感想。
IEEE 754のルールを逸脱する危険な変更。個人的に全く賛同できない。
その他変更
-
replace
メソッドの修正(マッチしない場合はnullとなるなど) - datetime系の要素のIntが効率化(例: dt.month や dt.hour が u32型 から i8型へ)(なぜ u8 でないのであろう?)
-
value_counts()
のカラム名がcounts
からcount
に変更となる - cumk系関数が
cum*
からcum_*
に renameされる
以上です。破壊的変更を楽しむくらいPolars愛に溢れる皆様はゾクゾクできましたでしょうか。