形状変更#
ワイドな表とロングな表は、別のデータではなく同じデータの 2通りの並べ方です。ヒートマップを描くときや機械学習モデルに渡すときはワイドな形(カテゴリごとに 1列)が都合よく、グループ集計や時系列のグラフではロングな形(カテゴリ名を 1列に持つ)が都合よくなります。Polars には、その相互変換のためのメソッドがそろっています。
この章では pivot / unpivot、unstack、to_dummies を順に見ます。最初の 2つはロングとワイドの間を行き来し、unstack は一定のステップで値を複数列に展開し、to_dummies はカテゴリを 0/1 の列に広げます。
import polars as pl
from helper.jupyter import row
ピボットテーブル#
ピボットテーブルは、行と列を入れ替えて集計結果を整理した表です。縦長(long)の表を横長(wide)の表に変換すると考えてください。pivot() がその変換で、unpivot() は逆に横長の表を縦長に戻します。
pivot#
pivot() は、指定した列の値を新しい列名に、別の列を行のキーにして、残りの列をセルの中身として並べ直します。
次の例では name を列ラベル、index を行ラベル、value をセルの値にしています。同じ行と列の組み合わせが複数ある場合は、aggregate_function で指定した方法(ここでは first)で 1つにまとめます。
df = pl.DataFrame(
dict(
index=[0, 1, 2, 0, 1, 2, 0, 1, 2, 1],
name=["A", "A", "A", "B", "B", "B", "C", "C", "C", "A"],
value=[10, 20, 30, 40, 50, 60, 70, 80, 90, 100],
score=[1, 3, 5, 4, 3, 2, 1, 2, 3, 4],
)
)
# name を列、index を行にして value を並べる(重複は最初の値)
df1 = df.pivot("name", index="index", values="value", aggregate_function="first")
row(df, df1)
shape: (10, 4)
|
shape: (3, 4)
|
aggregate_function には、重複したセルをどうまとめるかを渡します。よく使うのは次の 3つです。
sum— 合計します。同じセルに複数回記録があり、足し合わせたい場面に向きます。mean— 平均をとります。重複記録の影響を弱めたい場面に向きます。任意の式 —
pl.element()を渡すと、該当する値をすべてリストにまとめ、そのまま残します。
次の例で、3通りの結果を並べて比べます。結果列の型も変わる点に注意してください。sum と mean は数値、pl.element() はリストになります。
# 同じ組み合わせの値を「合計」「平均」「リスト」でまとめる
df2 = df.pivot("name", index="index", values="value", aggregate_function="sum")
df3 = df.pivot("name", index="index", values="value", aggregate_function="mean")
df4 = df.pivot("name", index="index", values="value", aggregate_function=pl.element())
row(df2, df3, df4)
shape: (3, 4)
|
shape: (3, 4)
|
shape: (3, 4)
|
values に複数の列を渡すと、列ごとに値が展開されます。結果の列名は <値の列名><separator><ピボットした列の値> の形になり、separator で区切り文字を変えられます。
# value と score の 2 列を同時に展開する
df5 = df.pivot("name", index="index", values=["value", "score"], aggregate_function=pl.element())
df6 = df.pivot("name", index="index", values=["value", "score"], aggregate_function="max", separator=":")
row(df5, df6)
shape: (3, 7)
|
shape: (3, 7)
|
unpivot#
unpivot() は pivot() の逆で、横長の表を縦長に戻します。on で指定した列を 1つの列にまとめ、index で指定した列はそのまま残します。
on— 縦長にたたむ列。列名、列名のリスト、セレクターのいずれかを渡します。キー列を除くときはcs.exclude()を使うのが定番です。式(pl.col()など)は渡せません。index— そのまま残す列。variable_name— たたんだ列名を格納する新しい列の名前。value_name— 値を格納する新しい列の名前。
次の例は index 以外をすべてたたみ、元の列名を name、値を value に入れています。
from polars import selectors as cs
# index 以外の列を縦長にたたむ
df_u1 = df1.unpivot(on=cs.exclude("index"), index="index", variable_name="name", value_name="value")
row(df1, df_u1)
shape: (3, 4)
|
shape: (9, 3)
|
values に複数の列を渡して作った df6 を元に戻すには、列名を分解してから pivot し直します。手順は次のとおりです。
unpivot(on=cs.exclude('index'), index='index')— まず縦長に戻します。variable列にはvalue:A、score:Bのような元の列名が入ります。pl.col('variable').str.split(':')— 列名を:で分割し、['value', 'A']のようなリストにします。.list.to_struct(fields=['colname', 'name'])— リストを 2つのフィールドを持つ構造体に変換します。.struct.unnest()— 構造体を展開し、colnameとnameの 2列に分けます。pivot('colname', index=['index', 'name'], values='value')— 元の列名で pivot し直せば、元の形に戻ります。
(
df6.unpivot(on=cs.exclude("index"), index="index")
.with_columns(
pl.col("variable")
.str.split(":") # value:A → ['value', 'A']
.list.to_struct(fields=["colname", "name"])
.struct.unnest() # 2 列に分ける
)
.pivot("colname", index=["index", "name"], values="value")
)
| index | name | value | score |
|---|---|---|---|
| i64 | str | i64 | i64 |
| 0 | "A" | 10 | 1 |
| 1 | "A" | 100 | 4 |
| 2 | "A" | 30 | 5 |
| 0 | "B" | 40 | 4 |
| 1 | "B" | 50 | 3 |
| 2 | "B" | 60 | 2 |
| 0 | "C" | 70 | 1 |
| 1 | "C" | 80 | 2 |
| 2 | "C" | 90 | 3 |
unstack#
unstack() は NumPy の reshape に近い操作で、列の値を複数の列に展開します。step に行のまとまりの大きさ、how に展開の向き('vertical' は行優先、'horizontal' は列優先)を指定します。足りない部分は fill_values で埋めます。
Polars 2.0 では引数がすべてキーワード専用になったので、df.unstack(step=4, ...) の形で呼びます。
# 4 行ずつ縦方向に展開する(足りないセルは -1 と '?' で埋める)
df_vertical = df.unstack(step=4, how="vertical", columns=["index", "name"], fill_values=[-1, "?"])
# 3 行ずつ横方向に展開する
df_horizontal = df.unstack(step=3, how="horizontal", columns="index")
row(df, df_vertical, df_horizontal)
shape: (10, 4)
|
shape: (4, 6)
|
shape: (4, 3)
|
to_dummies#
to_dummies() はカテゴリ列を 0/1 のダミー変数(one-hot エンコーディング)に展開します。該当すれば 1、しなければ 0です。複数の列を指定すれば、列ごとにダミーを作ります。
drop_first=False(既定)— すべてのカテゴリを残します。drop_first=True— 最初のカテゴリを基準として落とします。回帰分析で多重共線性を避けたいときに使います。
# index と name をダミー変数に展開する
df_original = df.filter(pl.col("index") < 2).select("index", "name", "value")
df_dummies1 = df_original.to_dummies(columns=["index", "name"], drop_first=False)
df_dummies2 = df_original.to_dummies(columns=["index", "name"], drop_first=True)
row(df_original, df_dummies1, df_dummies2)
shape: (7, 3)
|
shape: (7, 6)
|
shape: (7, 4)
|