LoginSignup
64
66

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

Last updated at Posted at 2023-02-18

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

2023年2月18日現在での実行環境

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 追記)

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

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 │
└─────┴────────────┴────────────┴──────┴──────┴─────┴──────┴───────┘
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すら不要

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 での列名の指定は名前付き引数でも渡せる。

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

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 で渡せばゼロコピー

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

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 │
└──────────┴──────┴──────────┘
%time pd_df1 = df.to_pandas()
CPU times: user 478 ms, sys: 968 ms, total: 1.45 s
Wall time: 943 ms
%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 を参照するとよいです。 categories=[...] の引数指定はそこそこ出番ありそう。

3.5 map_dict で一括変換

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

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

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

縦の連結は三種類あるが、メモリ再配置あるなしで速度が変わる。 .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
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 を書くとメチャ速でいけるそうです。私は試していませんが。


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

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

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

7. 危険な pl.Expr.set_sorted

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

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

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で操作したくなる時あるのは否めない

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

python -m pip install duckdb[polars]
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. テーブル表示は全部見せてね

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

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の中身も省略せずに表示させたい。

上記リンク先をどうぞ

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

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

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フォーマットが当然となる時代がやってきます(願望)

64
66
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
64
66