import pandas as pd
df = pd.DataFrame({"teams": [["SF", "NYG"] for _ in range(7)]})
print(df) teams
0 [SF, NYG]
1 [SF, NYG]
2 [SF, NYG]
3 [SF, NYG]
4 [SF, NYG]
5 [SF, NYG]
6 [SF, NYG]
Ryo Nakagami
2026-03-06
2026-04-27
Exercise 1
各行が長さ固定のリストを持つカラムを,要素ごとに別カラムへ展開したい. 入力例は次のようなレコード形式とします.
teams
0 [SF, NYG]
1 [SF, NYG]
2 [SF, NYG]
3 [SF, NYG]
4 [SF, NYG]
5 [SF, NYG]
6 [SF, NYG]
期待する出力
| team1 | team2 |
|---|---|
| SF | NYG |
| SF | NYG |
| … | … |
ポイント
[SF, NYG, OAK] と [SF] の混在) は別問題で,欠損部分を NaN/null で埋める方針が必要最も高速かつ素直なのは tolist() で 2D 配列に戻してから DataFrame を再構築する方法.
| teams | team1 | team2 | |
|---|---|---|---|
| 0 | [SF, NYG] | SF | NYG |
| 1 | [SF, NYG] | SF | NYG |
| 2 | [SF, NYG] | SF | NYG |
| 3 | [SF, NYG] | SF | NYG |
| 4 | [SF, NYG] | SF | NYG |
| 5 | [SF, NYG] | SF | NYG |
| 6 | [SF, NYG] | SF | NYG |
index=df.index を渡すのが落とし穴回避のポイント.元 DataFrame の index が連番でない場合,これを忘れると代入時に NaN が並ぶ.
apply(pd.Series) 版 (短いが遅い)
| teams | team1 | team2 | |
|---|---|---|---|
| 0 | [SF, NYG] | SF | NYG |
| 1 | [SF, NYG] | SF | NYG |
| 2 | [SF, NYG] | SF | NYG |
| 3 | [SF, NYG] | SF | NYG |
| 4 | [SF, NYG] | SF | NYG |
| 5 | [SF, NYG] | SF | NYG |
| 6 | [SF, NYG] | SF | NYG |
文字列カラムの場合は str.split(expand=True)
入力が "SF,NYG" のような区切り文字列ならリスト化を経由せず一発で展開できる.
| teams | team1 | team2 | |
|---|---|---|---|
| 0 | SF,NYG | SF | NYG |
| 1 | SF,NYG | SF | NYG |
| 2 | SF,NYG | SF | NYG |
注意点 !
apply(pd.Series) は各行で Series を新しく作るため遅い.数十万行を超える場合は tolist() 経由に切り替えるべきpd.DataFrame(df["teams"].tolist()) は最大長に合わせて足りない部分を NaN で埋める.これは便利な一方で,型が object に落ちる点に注意df = df.drop(columns="teams") を続けるpolars はリスト型がファーストクラスなので,list.to_struct() で構造体に変換してから unnest する
| team1 | team2 |
|---|---|
| str | str |
| "SF" | "NYG" |
| "SF" | "NYG" |
| "SF" | "NYG" |
| "SF" | "NYG" |
| "SF" | "NYG" |
| "SF" | "NYG" |
| "SF" | "NYG" |
個別に list.get で取り出す書き方
要素数が少なく,特定インデックスだけ欲しい場合は list.get の方が読みやすい.
| team1 | team2 |
|---|---|
| str | str |
| "SF" | "NYG" |
| "SF" | "NYG" |
| "SF" | "NYG" |
| "SF" | "NYG" |
| "SF" | "NYG" |
| "SF" | "NYG" |
| "SF" | "NYG" |
文字列カラムの場合は str.split + list.to_struct
| team1 | team2 |
|---|---|
| str | str |
| "SF" | "NYG" |
| "SF" | "NYG" |
| "SF" | "NYG" |
pandas との対応関係
| 操作 | pandas | polars |
|---|---|---|
| リスト型を複数カラムへ | pd.DataFrame(s.tolist(), index=df.index) |
pl.col(c).list.to_struct(fields=[...]) + unnest |
| 特定要素だけ取り出す | s.str[i] |
pl.col(c).list.get(i) |
| 区切り文字列を展開 | s.str.split(sep, expand=True) |
pl.col(c).str.split(sep).list.to_struct(...) + unnest |
| 元カラムを落とす | .drop(columns=c) |
.drop(c) または unnest 自体が元カラムを置き換える |
注意点 !
list.to_struct() の fields= は要素数より少ない名前を渡すと残りの要素が捨てられる.意図しない切り捨てを防ぐため,upper_bound= で最大要素数を明示しておくと安全null になる.逆に最大長を超える要素は to_struct のフィールド数に揃えて切り捨てられるunnest は対象カラムを新しいカラム群に置き換えるので,元の teams カラムは結果に残らないBigQuery では ARRAY<...> 型を OFFSET(i) または [i]で要素アクセスするのが定石.固定長なら一行で展開できる.
ARRAY 型の固定長アクセス
SAFE_OFFSET(i) を使うと要素数不足時にエラーではなく NULLを返す.行ごとにリスト長が揃わない場合の標準パターン.
文字列カラムから展開: SPLIT
入力が "SF,NYG" の形なら SPLIT で一旦 ARRAY にしてから OFFSET アクセスする.
同じ SPLIT を2回書きたくない場合はサブクエリで一度展開するか LATERAL 相当の ,UNNEST(...) AS arr WITH OFFSET を使う.
OFFSET(i) で要素数が足りないとクエリ全体がエラーになる.本番運用では SAFE_OFFSET(i) を使うのが基本OFFSET は0-indexed,ORDINAL は1-indexed.混在させるとバグの温床になるため,どちらかに統一するPIVOT 句を検討する.固定長 ARRAY なら本記事の OFFSET アクセスの方が高速かつ可読| 表現 | 例 | 意味 |
|---|---|---|
| list / array | ["SF", "NYG"] |
同型の値を順序で並べた列 |
| tuple | ("SF", "NYG") |
異型でもよいが,アクセスは位置参照 |
| struct | {team1: "SF", team2: "NYG"} |
フィールド名でアクセスする record |
list.to_struct(fields=["team1", "team2"]) は,list を struct にラベル付けしている操作.各ライブラリ/エンジンでの呼び名と表記
| 言語/エンジン | 型名 | リテラル例 | アクセス方法 |
|---|---|---|---|
| polars | Struct |
pl.struct([pl.col("a"), pl.col("b")]) |
pl.col("s").struct.field("a") |
| BigQuery | STRUCT |
STRUCT('SF' AS team1, 'NYG' AS team2) |
s.team1 |
| Spark SQL | StructType |
struct(col("a"), col("b")) |
s.a |
| Apache Arrow | struct<...> |
(型定義のみ) | child array |
| Python | dict / NamedTuple / dataclass |
{"team1": "SF", "team2": "NYG"} |
d["team1"] / d.team1 |
unnest との関係
unnest は struct をフィールドごとに別カラムへ展開する操作. 本記事の polars の流れは次の3段階に分解できる.
pl.col("teams") … list 型カラム (["SF", "NYG"]).list.to_struct(fields=["team1", "team2"]) … list を struct ({team1: "SF", team2: "NYG"}) に変換.unnest("teams") … struct を team1 / team2 の2カラムに展開BigQuery の SELECT s.* FROM ... AS s も同等の効果で,struct から個々のフィールドをワイドに開く.
なぜ list → struct を経由するのか
list を直接複数カラムに割るには,「i 番目の要素は何という名前のカラムにすべきか」という情報が必要になる. struct はこの位置 → 名前のマッピングを型として持つので,
list ──[fields=...]──▶ struct ──[unnest]──▶ multi-columns
という素直な責務分離が成立する.polars の API はこの分離を正面から表現しており,pandas の pd.DataFrame(s.tolist()) よりも何が起きているかが追いやすい.
STRUCT(1, 'foo', TRUE) のようにフィールドごとに型が違ってもよい.これが list (同型限定) との最大の違いSTRUCT はフィールド名を省略すると _field_1, _field_2… が自動で付くが,後段の参照可読性が落ちるので原則 AS field で名前付けするStruct 型カラムは Parquet / Arrow にそのまま書き出せるため,展開せずに保存するのが効率的な場面もある (中間ファイルの段階では struct のまま,可視化や JOIN 直前で unnest)