Polars, 旬の13のお役立ち機能

某kaggleコンペでPolarsの日本語投稿があってから日本での認知度が急速に広まったPolars. 使い方は以下の良質な記事にお任せして、マニアックな機能を紹介しよう。にしてもVaexは普及せずだった。。

- [超高速DataFrameライブラリー「Polars」について](https://zenn.dev/hiro_torii/articles/06d7e845e146ee)
- [pandasから移行する人向け polars使用ガイド](https://qiita.com/nkay/items/9cfb2776156dc7e054c8)
- [テーブルデータ処理に悩むあなたに朗報！Polarsの使い方を徹底解説　その１：基本編](https://dev.classmethod.jp/articles/polars-tutorial-001-standard/)
- [Polarsでデータサイエンス100本ノックを解く（前編）](https://qiita.com/_jinta/items/28442d25bba067c13bb7)

2023年2月18日現在での実行環境
```python
import polars as pl
pl.show_versions()
---Version info---
Polars: 0.16.6
Index type: UInt32
Platform: Linux-5.15.79.1-microsoft-standard-WSL2-x86_64-with-glibc2.31
Python: 3.10.9 (main, Dec 27 2022, 12:45:52) [Clang 15.0.6 ]
---Optional dependencies---
pyarrow: 10.0.1
pandas: 1.5.3
numpy: 1.24.2
fsspec: <not installed>
connectorx: <not installed>
xlsx2csv: <not installed>
deltalake: <not installed>
matplotlib: <not installed>
```

## 0. `.shrink_dtype()` 便利

（2023-02-18 13:00+09:00 追記）

https://pola-rs.github.io/polars/py-polars/html/reference/expressions/api/polars.Expr.shrink_dtype.html

Pandasでは定石であったメモリ節約のための数値型の縮減変換は `shrink_dtype()` が用意されているのでそれを使うのが吉。

```python
df = pl.DataFrame(
    {
        "a": [1, 2, 3],
        "b": [1, 2, 2 << 32],
        "c": [-1, 2, 1 << 30],
        "d": [-112, 2, 112],
        "e": [-112, 2, 129],
        "f": ["a", "b", "c"],
        "g": [0.1, 1.32, 0.12],
        "h": [True, None, False],
    }
)
df
```
```
shape: (3, 8)
┌─────┬────────────┬────────────┬──────┬──────┬─────┬──────┬───────┐
│ a   ┆ b          ┆ c          ┆ d    ┆ e    ┆ f   ┆ g    ┆ h     │
│ --- ┆ ---        ┆ ---        ┆ ---  ┆ ---  ┆ --- ┆ ---  ┆ ---   │
│ i64 ┆ i64        ┆ i64        ┆ i64  ┆ i64  ┆ str ┆ f64  ┆ bool  │
╞═════╪════════════╪════════════╪══════╪══════╪═════╪══════╪═══════╡
│ 1   ┆ 1          ┆ -1         ┆ -112 ┆ -112 ┆ a   ┆ 0.1  ┆ true  │
│ 2   ┆ 2          ┆ 2          ┆ 2    ┆ 2    ┆ b   ┆ 1.32 ┆ null  │
│ 3   ┆ 8589934592 ┆ 1073741824 ┆ 112  ┆ 129  ┆ c   ┆ 0.12 ┆ false │
└─────┴────────────┴────────────┴──────┴──────┴─────┴──────┴───────┘
```

```python
df.select(pl.all().shrink_dtype())
```

```
shape: (3, 8)
┌─────┬────────────┬────────────┬──────┬──────┬─────┬──────┬───────┐
│ a   ┆ b          ┆ c          ┆ d    ┆ e    ┆ f   ┆ g    ┆ h     │
│ --- ┆ ---        ┆ ---        ┆ ---  ┆ ---  ┆ --- ┆ ---  ┆ ---   │
│ i8  ┆ i64        ┆ i32        ┆ i8   ┆ i16  ┆ str ┆ f32  ┆ bool  │
╞═════╪════════════╪════════════╪══════╪══════╪═════╪══════╪═══════╡
│ 1   ┆ 1          ┆ -1         ┆ -112 ┆ -112 ┆ a   ┆ 0.1  ┆ true  │
│ 2   ┆ 2          ┆ 2          ┆ 2    ┆ 2    ┆ b   ┆ 1.32 ┆ null  │
│ 3   ┆ 8589934592 ┆ 1073741824 ┆ 112  ┆ 129  ┆ c   ┆ 0.12 ┆ false │
└─────┴────────────┴────────────┴──────┴──────┴─────┴──────┴───────┘
```




## 1. 単一な値は pl.lit(...) 

pl.selectなどの戻り値は行数が1か元のFrameの長さnと同一でないとエラーとなる。行数が1の場合は自動でnに拡張されるが、固定値等の場合はpl.litを使う。というか単一のリテラル値の列の作成であればpl.litすら不要

```python
df = pl.DataFrame({
    "a": [1,2,3],
    "b": list("abc"),
})
# df.with_columns(pl.lit(99).alias("c")) より記述短くて済む。
df.with_columns(c=99)
```
```
shape: (3, 3)
┌─────┬─────┬─────┐
│ a   ┆ b   ┆ c   │
│ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ i32 │
╞═════╪═════╪═════╡
│ 1   ┆ a   ┆ 99  │
│ 2   ┆ b   ┆ 99  │
│ 3   ┆ c   ┆ 99  │
└─────┴─────┴─────┘
```


## 2. `df.with_columns` での列名の指定は名前付き引数でも渡せる。

https://pola-rs.github.io/polars/py-polars/html/reference/dataframe/api/polars.DataFrame.with_columns.html

てことで一つ上で紹介したが名前付き引数のほうが思考の流れが邪魔されない（という個人的感想）。aliasを適用するのにカッコが余分に必要となるので。

```python
df1 = pl.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
print(df1.with_columns(baz=pl.col("foo") * 2, qux=pl.col("bar") + 1))
print(df1.with_columns([(pl.col("foo") * 2).alias("baz"), (pl.col("bar") + 1).alias("qux")]))
```

```
shape: (3, 4)
┌─────┬─────┬─────┬─────┐
│ foo ┆ bar ┆ baz ┆ qux │
│ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ i64 ┆ i64 ┆ i64 │
╞═════╪═════╪═════╪═════╡
│ 1   ┆ 4   ┆ 2   ┆ 5   │
│ 2   ┆ 5   ┆ 4   ┆ 6   │
│ 3   ┆ 6   ┆ 6   ┆ 7   │
└─────┴─────┴─────┴─────┘
shape: (3, 4)
┌─────┬─────┬─────┬─────┐
│ foo ┆ bar ┆ baz ┆ qux │
│ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ i64 ┆ i64 ┆ i64 │
╞═════╪═════╪═════╪═════╡
│ 1   ┆ 4   ┆ 2   ┆ 5   │
│ 2   ┆ 5   ┆ 4   ┆ 6   │
│ 3   ┆ 6   ┆ 6   ┆ 7   │
└─────┴─────┴─────┴─────┘
```


## 3. pandas DataFrame変換は arrow で渡せばゼロコピー

https://pola-rs.github.io/polars/py-polars/html/reference/dataframe/api/polars.DataFrame.to_pandas.html

`df.to_pandas(use_pyarrow_extension_array=True)` の引数を指定すると瞬速。ただし Pandas は 1.5.0 以上の必要ありです。

```python
df = pl.DataFrame({
    "a": pl.arange(0, 5e7, eager=True),
}).with_columns(
    b=pl.col("a") // 10000,
    c=pl.col("a").shuffle(seed=1)
)
df
```
```
shape: (50000000, 3)
┌──────────┬──────┬──────────┐
│ a        ┆ b    ┆ c        │
│ ---      ┆ ---  ┆ ---      │
│ i64      ┆ i64  ┆ i64      │
╞══════════╪══════╪══════════╡
│ 0        ┆ 0    ┆ 27454110 │
│ 1        ┆ 0    ┆ 2309916  │
│ 2        ┆ 0    ┆ 15065100 │
│ 3        ┆ 0    ┆ 12766444 │
│ ...      ┆ ...  ┆ ...      │
│ 49999996 ┆ 4999 ┆ 17167194 │
│ 49999997 ┆ 4999 ┆ 9092583  │
│ 49999998 ┆ 4999 ┆ 1929693  │
│ 49999999 ┆ 4999 ┆ 35668469 │
└──────────┴──────┴──────────┘
```

```python
%time pd_df1 = df.to_pandas()
CPU times: user 478 ms, sys: 968 ms, total: 1.45 s
Wall time: 943 ms
```

```python
%time pd_df2 = df.to_pandas(use_pyarrow_extension_array=True)
CPU times: user 1.88 ms, sys: 0 ns, total: 1.88 ms
Wall time: 2.01 ms
```

実に 943/2.01=469倍だ。（っていう風に書く人よくいるけど、まったく意味ないです）

`to_pandas(...)` のその他引数は [pyarrow.Table.to_pandas](https://arrow.apache.org/docs/python/generated/pyarrow.Table.html#pyarrow.Table.to_pandas) を参照するとよいです。 `categories=[...]` の引数指定はそこそこ出番ありそう。

## 3.5 map_dict で一括変換

(2023-02-19 10:30+09:00 追加）

https://pola-rs.github.io/polars/py-polars/html/reference/expressions/api/polars.Expr.map_dict.html

v0.16.3 （この記事の１週間前なので出来立て）から `.map_dict(...)` メソッドが追加された。Pandasの `.map()` は単純なdictを渡しても見つからない場合のデフォルト値の設定はできなかった（ただし `defaultdict` を使えばできた）が、 `.map_dict` の引数 `default=` を指定できる。この指定にはリテラルだけでなく `pl.Expr` すなわち `pl.col(...)` の別の列の値も指定可能。（当然型には気を付ける）



## 4. vstack concat どちらを使うかは後続でのクエリ次第

https://pola-rs.github.io/polars/py-polars/html/reference/api/polars.concat.html#polars.concat

https://pola-rs.github.io/polars/py-polars/html/reference/dataframe/api/polars.DataFrame.vstack.html#polars.DataFrame.vstack

https://pola-rs.github.io/polars/py-polars/html/reference/dataframe/api/polars.DataFrame.extend.html#polars.DataFrame.extend

縦の連結は三種類あるが、メモリ再配置あるなしで速度が変わる。 .extend は破壊的変更となることに注意。

ここより引用 https://www.rhosignal.com/posts/polars-extend-vstack/

- `pl.concat([df_1,df_2])` 新しい単一のロケーションにコピー
- `df_1.vstack(df_2)` コピーなしでリンク的に接続（メモリ上は分離のまま）
- `df_1.extend(df_2)` df_2をコピーしてdf_1にappend

```python
df_1 = pl.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
df_2 = pl.DataFrame({"foo": [10, 20, 30], "bar": [40, 50, 60]})

pl.concat([df_1, df_2]) # immutabble
df_1.vstack(df_2) # immutable
df_1.extend(df_2) # in-place change for df_1
```


## 5. pl.DataFrame.apply が遅い？そりゃそうだ。

（追記 2023-10-31)
***Deprecated since version 0.19.0: This method has been renamed to DataFrame.map_rows().***

ということでメソッド名は `map_rows()` に変更となるようです。

なお Rust で [Expression plugins](https://pola-rs.github.io/polars/user-guide/expressions/plugins/) を書くとメチャ速でいけるそうです。私は試していませんが。

---


https://pola-rs.github.io/polars/py-polars/html/reference/dataframe/api/polars.DataFrame.apply.html#polars.DataFrame.apply

UDF (User Defined Function) は Python の実行、 Expr は Rustの実行（並列化もあり）のため、UDFはめちゃ遅い。

## 6. 巨大csvをparquetにしたい？ pl.scan_csv と pl.LazyFrame.sink_parquet の活用だ。

https://pola-rs.github.io/polars/py-polars/html/reference/lazyframe/api/polars.LazyFrame.sink_parquet.html

collect(stream=True)しなくてよいよ。最後に sink_parquetでOK

https://twitter.com/DataPolars/status/1625230356520509447

## 7. 危険な pl.Expr.set_sorted

https://pola-rs.github.io/polars/py-polars/html/reference/series/api/polars.Series.set_sorted.html

ソート済みフラグを立てると、その後のjoinや集約関数等での激速を得られる。しかしソート済みでなくてもフラグを立てられるので、劇薬でしかない。上級者向け設定。

## 8. 非asciiの文字列のカウントは str.length ちゃうで。 str.n_char や

https://pola-rs.github.io/polars/py-polars/html/reference/expressions/api/polars.Expr.str.n_chars.html

```python
df = pl.DataFrame({"s": ["Café", None, "345", "東京"]}).with_columns(
    [
        pl.col("s").str.n_chars().alias("nchars"),
        pl.col("s").str.lengths().alias("length"),
    ]
)
df
```
```
shape: (4, 3)
┌──────┬────────┬────────┐
│ s    ┆ nchars ┆ length │
│ ---  ┆ ---    ┆ ---    │
│ str  ┆ u32    ┆ u32    │
╞══════╪════════╪════════╡
│ Café ┆ 4      ┆ 5      │
│ null ┆ null   ┆ null   │
│ 345  ┆ 3      ┆ 3      │
│ 東京 ┆ 2      ┆ 6      │
└──────┴────────┴────────┘
```


## 9. とはいってもSQLで操作したくなる時あるのは否めない

https://duckdb.org/docs/guides/python/polars


そんな時はduckdbです。arrowを通じてゼロコピー操作。（ただし duckdb ver 7.0 以上。つまり数日前からできるようになった。）
以下はサンプル丸コピーです。

```sh
python -m pip install duckdb[polars]
```

```python
import duckdb
import polars as pl

df = pl.DataFrame(
    {
        "A": [1, 2, 3, 4, 5],
        "fruits": ["banana", "banana", "apple", "apple", "banana"],
        "B": [5, 4, 3, 2, 1],
        "cars": ["beetle", "audi", "beetle", "beetle", "beetle"],
    }
)
duckdb.sql('SELECT * FROM df').show()

df2 = df.lazy()
duckdb.sql('SELECT * FROM df2').show()
```

`duckdb.sql('SELECT * FROM df')` の結果は `duckdb.DuckDBPyRelation` 型なので、pl.DataFrameに戻すには `.pl()` メソッドを追加して `duckdb.sql('SELECT * FROM df').pl()` とする必要がある。

## 10. テーブル表示は全部見せてね

https://pola-rs.github.io/polars/py-polars/html/reference/api/polars.Config.set_tbl_cols.html

一時的な変更は下記の例のように `with pl.Config() as cfg:` を活用

```python
df = pl.DataFrame({str(i): [i] for i in range(20)})
print(df)
with pl.Config() as cfg:
    cfg.set_tbl_cols(-1)  
    print(df)
```

```
shape: (1, 20)
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ 0   ┆ 1   ┆ 2   ┆ 3   ┆ ... ┆ 16  ┆ 17  ┆ 18  ┆ 19  │
│ --- ┆ --- ┆ --- ┆ --- ┆     ┆ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ i64 ┆ i64 ┆ i64 ┆     ┆ i64 ┆ i64 ┆ i64 ┆ i64 │
╞═════╪═════╪═════╪═════╪═════╪═════╪═════╪═════╪═════╡
│ 0   ┆ 1   ┆ 2   ┆ 3   ┆ ... ┆ 16  ┆ 17  ┆ 18  ┆ 19  │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
shape: (1, 20)
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ 0   ┆ 1   ┆ 2   ┆ 3   ┆ 4   ┆ 5   ┆ 6   ┆ 7   ┆ 8   ┆ 9   ┆ 10  ┆ 11  ┆ 12  ┆ 13  ┆ 14  ┆ 15  ┆ 16  ┆ 17  ┆ 18  ┆ 19  │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 │
╞═════╪═════╪═════╪═════╪═════╪═════╪═════╪═════╪═════╪═════╪═════╪═════╪═════╪═════╪═════╪═════╪═════╪═════╪═════╪═════╡
│ 0   ┆ 1   ┆ 2   ┆ 3   ┆ 4   ┆ 5   ┆ 6   ┆ 7   ┆ 8   ┆ 9   ┆ 10  ┆ 11  ┆ 12  ┆ 13  ┆ 14  ┆ 15  ┆ 16  ┆ 17  ┆ 18  ┆ 19  │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
```

もしくは `df.glimpse()` でざっくり確認とか。



## 11. stringの中身も省略せずに表示させたい。

https://pola-rs.github.io/polars/py-polars/html/reference/api/polars.Config.set_fmt_str_lengths.html

上記リンク先をどうぞ


## 12. pl.Config は保存しておくと便利。

https://pola-rs.github.io/polars/py-polars/html/reference/config.html

以下のように保存できる。

```python
pl.Config.state() # check current status
pl.Config.restore_defaults() # reset to default
str = pl.Config.save()
pl.Config.load(str)

```

## その他お役立ち twitter

https://twitter.com/RitchieVink
https://twitter.com/braaannigan
をフォローすれば万全です。

## 最後に

てことで Polars, Duckdb, ibis あたりのPython library と dbt をデータハンドリング（ELTとかETLとかあえて言わない）をメインにしていく1年となっていくと思います。そしてschemaの存在しないcsvファイルはExcelしかつかえないビジネスパーソンの戯れの存在となり、zstd圧縮のparquetフォーマットが当然となる時代がやってきます（願望）