Polars 1.0.0 の破壊的変更の紹介（抜粋）

# はじめに

Python Polars は2024-07-01に、待望の v1.0.0 のメジャーアップデートが公開された。本記事では、恒例の破壊的変更について紹介していきたい。なお、筆者が重要と思うものをpickupしているのでバイアスがあることに注意してもらいたい。気になる方は[公式ドキュメント](https://github.com/pola-rs/polars/blob/main/docs/releases/upgrade/1.md)を参照してほしい。

参考: 前回の [v0.20系の破壊的変更の記事](https://qiita.com/hkzm/items/9da3ecf4024e3798403b)

## Disclaimer

この記事の内容は、私個人の意見や見解であり、私が所属する組織の公式な立場、方針、意見を反映するものではありません。この記事の内容について、組織はいかなる責任も負いません。


## （一口メモ）Polarsの破壊的変更の追い方

- 基本は [release page](https://github.com/pola-rs/polars/releases) を見る
    - 💥 Breaking changes の項目がそれ
    - 2024-07-04 時点で [Python Polars 1.0.0](https://github.com/pola-rs/polars/releases/tag/py-1.0.0) があるのでそれを見る
- [Milestones](https://github.com/pola-rs/polars/milestones) に、変更に関するissues, pull requestがあるのでそれを眺める
- v1.0.0 に関しては、[Official v1 docs](https://github.com/pola-rs/polars/blob/main/docs/releases/upgrade/1.md)に公式まとめがあるので、以下はその中から重要と思われるものを抜粋して説明する


# 執筆時点(2024-07-05)での実行環境

### before
```sh
>>> 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
```sh
>>> 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

```python
>>> 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

```python

>>> 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

```python
>>> 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

```python


>>> 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")` の有無の結果


<details><summary>code</summary>

```python
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"))
```

</details>


### before

| input | dtype指定 | 結果 |
|:-: |:-: |:- |
|  naive | 無 | Series: '' [datetime[μs]]<br>[2020-01-01 00:00:00]|
|  naive | 有 | Series: '' [datetime[μs, Asia/Tokyo]]<br>[2020-01-01 **00:00:00** JST]|
|  aware | 無 | Series: '' [datetime[μs, UTC]]<br>[2019-12-31 15:00:00 UTC]|
|  aware | 有 | **ValueError: time-zone-aware <br>datetimes are converted to UTC** |


### after

| input | dtype指定 | 結果 | 変更 | 
|:-: |:-: |:- | :-:|
|  naive | 無 | Series: '' [datetime[μs]]<br>[2020-01-01 00:00:00]| |
|  naive | 有 | Series: '' [datetime[μs, Asia/Tokyo]]<br>[2020-01-01 **09:00:00** JST]| 有 |
|  aware | 無 | Series: '' [datetime[μs, UTC]]<br>[2019-12-31 15:00:00 UTC]| |
|  aware | 有 | **Series: '' [datetime[μs, Asia/Tokyo]]<br>[2020-01-01 00:00:00 JST]**| 有 |




# Series constructorの型の厳格化

実行時の不確実性を減らすために厳格になることは望ましいと個人的に感じる。Hintが表示されるのは厳しさの中に優しさがあるのでは。

### before

```python
>>> 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

```python
>>> 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

```python
>>> pl.read_parquet("dataset/a=1/foo.parquet")
shape: (2, 2)
┌─────┬─────┐
│ a   ┆ x   │
│ --- ┆ --- │
│ i64 ┆ f64 │
╞═════╪═════╡
│ 1   ┆ 1.0 │
│ 1   ┆ 2.0 │
└─────┴─────┘
```


### after

```python
>>> 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

```python
>>> s = pl.Series([1, 2, 3])
>>> s.replace(1, "a")
shape: (3,)
Series: '' [str]
[
        "a"
        "2"
        "3"
]
```

### after

```python
>>> 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

```python
>>> 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

```python


>>> 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

```python
>>> 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

```python

>>> 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

```python
>>> s1 = pl.Series("foo", [1, 2, 3])
>>> s2 = pl.Series("bar", [1, 2, 3])
>>> s1.equals(s2)
False
```


### after

```python
>>> s1.equals(s2)
True
>>> s1.equals(s2, check_names=True)
False
```



# df.sql()が廃止し pl.sql()に

DataFrameオブジェクトにメソッドが生えるより、pl.sql()のほうが自然に感じる。

### before

```python
>>> 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

```python
>>> df1.sql("SELECT * FROM df1 CROSS JOIN df2")
Traceback (most recent call last):
...
polars.exceptions.SQLInterfaceError: relation 'df1' was
 not found
```

```python
>>> 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   │
└─────┴─────┘
```


以上です。

繰り返しますが、全ての破壊的変更については[公式ドキュメント](https://github.com/pola-rs/polars/blob/main/docs/releases/upgrade/1.md)を参照してください。

最後に個人の感想ですが、v1.0のメジャーアップデートになったからといえ今後も破壊的変更はカジュアルにおこなわれるのではと懸念（期待）しています。

（このタイミングでのv1化は、某🦆dbが1.0になった時流に乗ったマーケティング戦略上の都合なのか、[polars BV](https://pola.rs/about-us/)（非公開株式会社）として[資金調達](https://www.eu-startups.com/2023/08/amsterdam-based-polars-secures-e3-6-million-to-let-data-scientists-focus-more-on-their-code-and-less-on-infrastructure/)し組織化した関係から何かプレッシャーがあったのか、いろいろ考えさせられます。）