形状変更#

ワイドな表とロングな表は、別のデータではなく同じデータの 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)
indexnamevaluescore
i64stri64i64
0"A"101
1"A"203
2"A"305
0"B"404
1"B"503
2"B"602
0"C"701
1"C"802
2"C"903
1"A"1004
shape: (3, 4)
indexABC
i64i64i64i64
0104070
1205080
2306090

aggregate_function には、重複したセルをどうまとめるかを渡します。よく使うのは次の 3つです。

  1. sum — 合計します。同じセルに複数回記録があり、足し合わせたい場面に向きます。

  2. mean — 平均をとります。重複記録の影響を弱めたい場面に向きます。

  3. 任意の式 — 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)
indexABC
i64i64i64i64
0104070
11205080
2306090
shape: (3, 4)
indexABC
i64f64f64f64
010.040.070.0
160.050.080.0
230.060.090.0
shape: (3, 4)
indexABC
i64list[i64]list[i64]list[i64]
0[10][40][70]
1[20, 100][50][80]
2[30][60][90]

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)
indexvalue_Avalue_Bvalue_Cscore_Ascore_Bscore_C
i64list[i64]list[i64]list[i64]list[i64]list[i64]list[i64]
0[10][40][70][1][4][1]
1[20, 100][50][80][3, 4][3][2]
2[30][60][90][5][2][3]
shape: (3, 7)
indexvalue:Avalue:Bvalue:Cscore:Ascore:Bscore:C
i64i64i64i64i64i64i64
0104070141
11005080432
2306090523

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)
indexABC
i64i64i64i64
0104070
1205080
2306090
shape: (9, 3)
indexnamevalue
i64stri64
0"A"10
1"A"20
2"A"30
0"B"40
1"B"50
2"B"60
0"C"70
1"C"80
2"C"90

values に複数の列を渡して作った df6 を元に戻すには、列名を分解してから pivot し直します。手順は次のとおりです。

  1. unpivot(on=cs.exclude('index'), index='index') — まず縦長に戻します。variable 列には value:A、score:B のような元の列名が入ります。

  2. pl.col('variable').str.split(':') — 列名を : で分割し、['value', 'A'] のようなリストにします。

  3. .list.to_struct(fields=['colname', 'name']) — リストを 2つのフィールドを持つ構造体に変換します。

  4. .struct.unnest() — 構造体を展開し、colname と name の 2列に分けます。

  5. 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")
)
shape: (9, 4)
indexnamevaluescore
i64stri64i64
0"A"101
1"A"1004
2"A"305
0"B"404
1"B"503
2"B"602
0"C"701
1"C"802
2"C"903

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)
indexnamevaluescore
i64stri64i64
0"A"101
1"A"203
2"A"305
0"B"404
1"B"503
2"B"602
0"C"701
1"C"802
2"C"903
1"A"1004
shape: (4, 6)
index_0index_1index_2name_0name_1name_2
i64i64i64strstrstr
012"A""B""C"
121"A""B""A"
20-1"A""C""?"
01-1"B""C""?"
shape: (4, 3)
index_0index_1index_2
i64i64i64
012
012
012
1nullnull

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)
indexnamevalue
i64stri64
0"A"10
1"A"20
0"B"40
1"B"50
0"C"70
1"C"80
1"A"100
shape: (7, 6)
index_0index_1name_Aname_Bname_Cvalue
u8u8u8u8u8i64
1010010
0110020
1001040
0101050
1000170
0100180
01100100
shape: (7, 4)
index_1name_Bname_Cvalue
u8u8u8i64
00010
10020
01040
11050
00170
10180
100100