Polars 2.0.0でMap型が追加されましたので紹介します。
PolarsでJSONのような辞書型データを扱うには、pl.Structとpl.Mapの2つが候補になります。
この2つは似ていますが、明確な違いがあります。
比較
どちらの要素も、複数の「キー(名前)と値」で構成されます。
決定的な違いは決まった構造があるかどうかです。
| 比較項目 | pl.Struct |
pl.Map |
|---|---|---|
| 要素の識別子 | フィールド名(固定) | キー(行ごと・動的に変更可能) |
| キー/フィールド名の型 | String(固定) | 任意の型(1つのキー型を指定) |
| 値の型 | フィールドごとに異なる型が可能 | 1つの値型を指定 |
| 主な用途 | スキーマが固定されたオブジェクト(レコード) | 任意のキーバリュー(タグ、属性データなど) |
pl.Struct
pl.Structは、構造体のようなデータを扱えます。
import polars as pl
df_struct = pl.DataFrame(
{
"user": [
{"id": None, "name": "Alice", "age": 30},
{"id": 2, "name": "Bob", "age": None},
]
},
schema={"user": pl.Struct({"id": pl.Int64, "name": pl.String, "age": pl.Int64})},
)
print(df_struct)
print(
df_struct.select(
id=pl.col("user").struct.field("id"),
name=pl.col("user").struct.field("name"),
age=pl.col("user").struct.field("age"),
)
)
出力
shape: (2, 1)
┌───────────────────┐
│ user │
│ --- │
│ struct[3] │
╞═══════════════════╡
│ {null,"Alice",30} │
│ {2,"Bob",null} │
└───────────────────┘
shape: (2, 3)
┌──────┬───────┬──────┐
│ id ┆ name ┆ age │
│ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ i64 │
╞══════╪═══════╪══════╡
│ null ┆ Alice ┆ 30 │
│ 2 ┆ Bob ┆ null │
└──────┴───────┴──────┘
pl.Map
pl.Mapは、Pythonの辞書に似たデータ構造ですが、キーと値にそれぞれ型が指定されます。キーの種類や数を固定せず、行ごとに異なるキーを持たせたい場合に適しています。
ただし、値の型は同一である必要があります(ここではidやageを文字列として揃えています)。
また、DataFrame作成時はPythonの通常の辞書のほかに、内部表現である[{"key": ..., "value": ...}]形式でも記述可能です。
map.get()で指定したキーの値を取得できます。存在しないキーの場合はnullになります。
df_map = pl.DataFrame(
{
"user": [
{"name": "Alice", "age": "30"},
{"id": "2", "name": "Bob"},
],
},
schema={"user": pl.Map(pl.String, pl.String)},
)
print(df_map)
print(
df_map.select(
id=pl.col("user").map.get("id"),
name=pl.col("user").map.get("name"),
age=pl.col("user").map.get("age"),
)
)
出力
shape: (2, 1)
┌────────────────────────────────┐
│ user │
│ --- │
│ map[str, str] │
╞════════════════════════════════╡
│ {"name": "Alice", "age": "30"} │
│ {"id": "2", "name": "Bob"} │
└────────────────────────────────┘
shape: (2, 3)
┌──────┬───────┬──────┐
│ id ┆ name ┆ age │
│ --- ┆ --- ┆ --- │
│ str ┆ str ┆ str │
╞══════╪═══════╪══════╡
│ null ┆ Alice ┆ 30 │
│ 2 ┆ Bob ┆ null │
└──────┴───────┴──────┘
使い分けの基準
キー(フィールド)が固定されている場合はpl.Structのほうが適しています。フィールドがあらかじめ決まっているため、Polarsの列指向の処理を活かしやすくなります。
キーが行ごとに変化する場合はpl.Mapを使いましょう。ただしpl.Mapでは以下の制約があります。
- キーと値はそれぞれ一定の型である
- 1行の中でキーは一意である
- キーにnullを指定できない
mapネームスペース
pl.Mapの列のExprはmapネームスペースを持ちます。
mapネームスペースには以下のメソッドがあります。
-
keys: キーのリスト -
values: 値のリスト -
len: 要素数 -
get: 特定のキーの値 -
contains_key: 特定のキーがあるか -
entries: keyとvalueを持つ構造体のリスト
df_map2 = pl.DataFrame(
{
"tag": [{5: "e", 10: "j"}, {20: "t"}, {10: "c"}],
},
schema={"tag": pl.Map(pl.Int64, pl.String)},
)
print(
df_map2.select(
keys=pl.col("tag").map.keys(),
values=pl.col("tag").map.values(),
len=pl.col("tag").map.len(),
get_10=pl.col("tag").map.get(10),
contains_key_10=pl.col("tag").map.contains_key(10),
entries=pl.col("tag").map.entries(),
)
)
出力
shape: (3, 6)
┌───────────┬────────────┬─────┬────────┬─────────────────┬─────────────────────┐
│ keys ┆ values ┆ len ┆ get_10 ┆ contains_key_10 ┆ entries │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ list[i64] ┆ list[str] ┆ u32 ┆ str ┆ bool ┆ list[struct[2]] │
╞═══════════╪════════════╪═════╪════════╪═════════════════╪═════════════════════╡
│ [5, 10] ┆ ["e", "j"] ┆ 2 ┆ j ┆ true ┆ [{5,"e"}, {10,"j"}] │
│ [20] ┆ ["t"] ┆ 1 ┆ null ┆ false ┆ [{20,"t"}] │
│ [10] ┆ ["c"] ┆ 1 ┆ c ┆ true ┆ [{10,"c"}] │
└───────────┴────────────┴─────┴────────┴─────────────────┴─────────────────────┘
map.get(key)やmap.contains_key(key)は、keyにExprを指定できます。
つまり、「別の列の値をキーとして動的に検索する」ことが可能です。
some_df.select(
val=pl.col("tag").map.get(pl.col("target_key"))
)
map.entries()の逆変換としてlist.to_map()があります。
まとめ
Polars 2.0.0で追加されたpl.Map型について、pl.Struct型との違いや使い分けを解説しました。
ポイントは以下の2点です。
- キーが固定されている場合:
pl.Structを使う - キーが動的に変わる場合: 柔軟な
pl.Mapを使う
従来、可変キーのJSONデータやParquet/Delta Lake/Icebergに保存されたMap型データを扱う際、PolarsではList[Struct]として扱うか文字列化する必要がありました。
Polars 2.0.0でネイティブなpl.Map型が導入されたことで、Arrow/Parquetのエコシステムとの相互運用性が大幅に向上し、直感的なクエリ操作が可能になりました。
参考