はじめに
Python Polars は2024-07-01に、待望の v1.0.0 のメジャーアップデートが公開された。本記事では、恒例の破壊的変更について紹介していきたい。なお、筆者が重要と思うものをpickupしているのでバイアスがあることに注意してもらいたい。気になる方は公式ドキュメントを参照してほしい。
参考: 前回の v0.20系の破壊的変更の記事
Disclaimer
この記事の内容は、私個人の意見や見解であり、私が所属する組織の公式な立場、方針、意見を反映するものではありません。この記事の内容について、組織はいかなる責任も負いません。
(一口メモ)Polarsの破壊的変更の追い方
- 基本は release page を見る
- 💥 Breaking changes の項目がそれ
- 2024-07-04 時点で Python Polars 1.0.0 があるのでそれを見る
- Milestones に、変更に関するissues, pull requestがあるのでそれを眺める
- v1.0.0 に関しては、Official v1 docsに公式まとめがあるので、以下はその中から重要と思われるものを抜粋して説明する
執筆時点(2024-07-05)での実行環境
before
>>> sys.version
'3.11.9 (main, Apr 26 2024, 16:03:42) [Clang 15.0.0 (clang-1500.3.9.4)]'
>>> pl.__version__
'0.20.31'
after
>>> sys.version
'3.11.9 (main, Apr 26 2024, 16:03:42) [Clang 15.0.0 (clang-1500.3.9.4)]'
>>> pl.__version__
'1.0.0'
それではいってみましょう
Series constructorでのtime zoneの取り扱い変更 (danger!!!)
今回の変更で最も影響があるものを挙げろと言われればこれ。公式docsの例は timezoneを Europe/Amsterdam
としているが、日本人に理解しやすいように Asia/Tokyo
での挙動を以下で示している。公式docsと順番は変えてあえて一番目に持ってきています。
before
>>> from datetime import datetime
>>> pl.Series([datetime(2020, 1, 1)], dtype=pl.Datetime('us', 'Asia/Tokyo'))
shape: (1,)
Series: '' [datetime[μs, Asia/Tokyo]]
[
2020-01-01 00:00:00 JST
]
after
>>> pl.Series([datetime(2020, 1, 1)], dtype=pl.Datetime('us', 'Asia/Tokyo'))
shape: (1,)
Series: '' [datetime[μs, Asia/Tokyo]]
[
2020-01-01 09:00:00 JST
]
(続き)
before-afterで変更はないが、aware な datetime を渡しても、pl.Series側でdtypeを指定しないとUTC表示で扱われる
before
>>> from datetime import datetime
>>> from zoneinfo import ZoneInfo
>>> pl.Series([datetime(2020, 1, 1, tzinfo=ZoneInfo("Asia/Tokyo"))])
shape: (1,)
Series: '' [datetime[μs, UTC]]
[
2019-12-31 15:00:00 UTC
]
after
>>> pl.Series([datetime(2020, 1, 1, tzinfo=ZoneInfo("Asia/Tokyo"))])
shape: (1,)
Series: '' [datetime[μs, UTC]]
[
2019-12-31 15:00:00 UTC
]
(続き)
元データ(datetime(2020, 1, 1)
)での tzinfo=ZoneInfo("Asia/Tokyo")
の有無(naive/aware) と
dtype=pl.Datetime("us", "Asia/Tokyo")
の有無の結果
code
pl.Series([datetime(2020, 1, 1)])
pl.Series([datetime(2020, 1, 1)], dtype=pl.Datetime("us", "Asia/Tokyo"))
pl.Series([datetime(2020, 1, 1, tzinfo=ZoneInfo("Asia/Tokyo"))])
pl.Series([datetime(2020, 1, 1, tzinfo=ZoneInfo("Asia/Tokyo"))], dtype=pl.Datetime("us", "Asia/Tokyo"))
before
input | dtype指定 | 結果 |
---|---|---|
naive | 無 | Series: '' [datetime[μs]] [2020-01-01 00:00:00] |
naive | 有 | Series: '' [datetime[μs, Asia/Tokyo]] [2020-01-01 00:00:00 JST] |
aware | 無 | Series: '' [datetime[μs, UTC]] [2019-12-31 15:00:00 UTC] |
aware | 有 | ValueError: time-zone-aware datetimes are converted to UTC |
after
input | dtype指定 | 結果 | 変更 |
---|---|---|---|
naive | 無 | Series: '' [datetime[μs]] [2020-01-01 00:00:00] |
|
naive | 有 | Series: '' [datetime[μs, Asia/Tokyo]] [2020-01-01 09:00:00 JST] |
有 |
aware | 無 | Series: '' [datetime[μs, UTC]] [2019-12-31 15:00:00 UTC] |
|
aware | 有 | Series: '' [datetime[μs, Asia/Tokyo]] [2020-01-01 00:00:00 JST] |
有 |
Series constructorの型の厳格化
実行時の不確実性を減らすために厳格になることは望ましいと個人的に感じる。Hintが表示されるのは厳しさの中に優しさがあるのでは。
before
>>> s = pl.Series([1, 2, 3.5])
shape: (3,)
Series: '' [f64]
[
1.0
2.0
3.5
]
>>> s = pl.Series([1, 2, 3.5], strict=False)
shape: (3,)
Series: '' [i64]
[
1
2
null
]
>>> s = pl.Series([1, 2, 3.5], strict=False, dtype=pl.Int8)
Series: '' [i8]
[
1
2
null
]
after
>>> s = pl.Series([1, 2, 3.5])
Traceback (most recent call last):
...
TypeError: unexpected value while building Series of type Int64;
found value of type Float64: 3.5
Hint: Try setting `strict=False` to allow passing data
with mixed types.
>>> s = pl.Series([1, 2, 3.5], strict=False)
shape: (3,)
Series: '' [f64]
[
1.0
2.0
3.5
]
>>> s = pl.Series([1, 2, 3.5], strict=False, dtype=pl.Int8)
Series: '' [i8]
[
1
2
3
]
read_scan_parquet
Hive パーテションの取り扱い変更
ファイル指定(単一ファイル、glob, list of ファイル)の読み込み時に、Hiveパーテションの自動読み込みはされなくなったので、 同じ挙動のためには hive_partitioning=True
の指定が必要。同一コードのままでも実行時エラーなど出ないので注意が必要。
before
>>> pl.read_parquet("dataset/a=1/foo.parquet")
shape: (2, 2)
┌─────┬─────┐
│ a ┆ x │
│ --- ┆ --- │
│ i64 ┆ f64 │
╞═════╪═════╡
│ 1 ┆ 1.0 │
│ 1 ┆ 2.0 │
└─────┴─────┘
after
>>> pl.read_parquet("dataset/a=1/foo.parquet")
shape: (2, 1)
┌─────┐
│ x │
│ --- │
│ f64 │
╞═════╡
│ 1.0 │
│ 2.0 │
└─────┘
>>> pl.read_parquet("dataset/a=1/foo.parquet", hive_partitioning=True)
shape: (2, 2)
┌─────┬─────┐
│ a ┆ x │
│ --- ┆ --- │
│ i64 ┆ f64 │
╞═════╪═════╡
│ 1 ┆ 1.0 │
│ 1 ┆ 2.0 │
└─────┴─────┘
replace
機能の分離
replace
実行結果の型が変わるケースでエラーとなる。 replace_strict
の新しいmethodに書き換えが必要。
before
>>> s = pl.Series([1, 2, 3])
>>> s.replace(1, "a")
shape: (3,)
Series: '' [str]
[
"a"
"2"
"3"
]
after
>>> s.replace(1, "a")
Traceback (most recent call last):
...
polars.exceptions.InvalidOperationError: conversion from `str` to `i64` failed in column 'literal' for 1 out of 1 values: ["a"]
>>> s.replace_strict(1, "a", default=s)
shape: (3,)
Series: '' [str]
[
"a"
"2"
"3"
]
Expr.clipの null の挙動変更
便利な clip method の範囲指定の None
(null value) について、解釈が変更。個人的には変更後の挙動は嬉しい。(結果をnullにするようなユースケースはほぼないので。)
before
>>> df = pl.DataFrame(
{"a": [0, 1, 2], "min": [1, None, 1]})
>>> df.select(pl.col("a").clip("min"))
shape: (3, 1)
┌──────┐
│ a │
│ --- │
│ i64 │
╞══════╡
│ 1 │
│ null │
│ 2 │
└──────┘
after
>>> df.select(pl.col("a").clip("min"))
shape: (3, 1)
┌──────┐
│ a │
│ --- │
│ i64 │
╞══════╡
│ 1 │
│ 1 │
│ 2 │
└──────┘
str.to_datetimeのformatでの %.f
が nano sec から micro secへ変更
こういう変更が、csv or json等に永続化して別プロセスでそれを読み込む場合、忘れた頃にパースエラーで workflow止まるので要注意。
before
>>> s = pl.Series(["2022-08-31 00:00:00.123456789"])
>>> s.str.to_datetime(format="%Y-%m-%d %H:%M:%S%.f")
shape: (1,)
Series: '' [datetime[ns]]
[
2022-08-31 00:00:00.123456789
]
after
>>> s.str.to_datetime(format="%Y-%m-%d %H:%M:%S%.f")
shape: (1,)
Series: '' [datetime[us]]
[
2022-08-31 00:00:00.123456
]
Series.equals がデフォルトではカラム名の名前を見なくなる
エラーにならないので注意。Seriesを比較するユースケースは私はあまりないかも。
before
>>> s1 = pl.Series("foo", [1, 2, 3])
>>> s2 = pl.Series("bar", [1, 2, 3])
>>> s1.equals(s2)
False
after
>>> s1.equals(s2)
True
>>> s1.equals(s2, check_names=True)
False
df.sql()が廃止し pl.sql()に
DataFrameオブジェクトにメソッドが生えるより、pl.sql()のほうが自然に感じる。
before
>>> df1 = pl.DataFrame({"id1": [1, 2]})
>>> df2 = pl.DataFrame({"id2": [3, 4]})
>>> df1.sql("SELECT * FROM df1 CROSS JOIN df2")
shape: (4, 2)
┌─────┬─────┐
│ id1 ┆ id2 │
│ --- ┆ --- │
│ i64 ┆ i64 │
╞═════╪═════╡
│ 1 ┆ 3 │
│ 1 ┆ 4 │
│ 2 ┆ 3 │
│ 2 ┆ 4 │
└─────┴─────┘
after
>>> df1.sql("SELECT * FROM df1 CROSS JOIN df2")
Traceback (most recent call last):
...
polars.exceptions.SQLInterfaceError: relation 'df1' was
not found
>>> pl.sql("SELECT * FROM df1 CROSS JOIN df2", eager=True)
shape: (4, 2)
┌─────┬─────┐
│ id1 ┆ id2 │
│ --- ┆ --- │
│ i64 ┆ i64 │
╞═════╪═════╡
│ 1 ┆ 3 │
│ 1 ┆ 4 │
│ 2 ┆ 3 │
│ 2 ┆ 4 │
└─────┴─────┘
以上です。
繰り返しますが、全ての破壊的変更については公式ドキュメントを参照してください。
最後に個人の感想ですが、v1.0のメジャーアップデートになったからといえ今後も破壊的変更はカジュアルにおこなわれるのではと懸念(期待)しています。
(このタイミングでのv1化は、某🦆dbが1.0になった時流に乗ったマーケティング戦略上の都合なのか、polars BV(非公開株式会社)として資金調達し組織化した関係から何かプレッシャーがあったのか、いろいろ考えさせられます。)