形状変更#

データの形状を変更する操作は、データ分析や加工において重要なステップです。Polarsでは効率的に形状を変更するための様々な方法を提供しています。この章では、その中でも特に重要な操作について説明します。

import polars as pl
from helper.jupyter import row

ピボットテーブル#

ピボットテーブルは、データの集計や再構成を行うための強力なツールです。特に、複数の次元(列)を指定してデータを再配置し、表形式で集計結果を分かりやすく整理するのに役立ちます。Polarsでは、pivotメソッドを使用してピボットテーブルを簡単に作成できます。一般的にピボットテーブルに変換すると、元々縦長いテーブルは横長いテーブルになります。また、unpivotメソッドはpivotの逆演算で、横長いテーブルを縦長いテーブルに変換します。

pivot#

pivotメソッドを使用すると、データフレームをピボットテーブルの形式に変換できます。この操作では、指定した列を基準にしてデータを集計し、行列形式で再構築します。

以下の例では、name列を新しい列ラベルとして使用し、index列を行ラベル(インデックス)として設定しています。また、value列の値を新しいテーブルのセルに挿入し、同じ行と列の組み合わせが複数存在する場合にはaggregate_functionで指定した方法(この場合はfirst)で値を集約します。

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],
    )
)

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引数を使用して、同じ行列の組み合わせに対応する複数の値をどのように集約するかを指定できます。Polarsはこの引数に以下のような標準的な関数を受け付けます:

  1. sum: 値を合計します。

  2. mean: 値の平均を計算します。

  3. カスタム演算式: ユーザー定義の集約演算式を適用可能です。ここではpl.element()ですべての値をリストに集約します。

以下の例を通して、異なるaggregate_functionを使用したピボットの結果を確認します。

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引数に複数の列を渡すことで、指定した複数の列がピボットテーブルの中で展開されます。この場合、結果のテーブルでは、列ラベルがname列の値に基づき、各ラベルに対応するサブ列として指定された値の列が表示されます。又、separator引数を指定することで、列名と値の結合文字を自由に変更できます。

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#

DataFrame.unpivot()pivot()の逆操作で、横長のデータフレームを縦長の形式に変換します。このメソッドを使うことで、複数の列を1つの列に変換し、元の形式に戻すことができます。unpivotでは、指定した列を一つの列にまとめ、その他の列をインデックスとして保持します。これにより、カテゴリごとにデータを整形することができます。unpivot()メソッドには以下の引数があります:

  • on: unpivotする対象となる列を指定します。列名や列名のリスト、あるいは列を選択するセレクターを使用できます。通常、cs.exclude()を使ってインデックス列や不要な列を除外することが一般的です。列を選択する演算式(pl.col()pl.exclude()など)は使用できません。

  • index: 新しい縦長データフレームで、インデックスとして使用する列を指定します。index列は、元のデータフレームでの位置を保持するための役割を果たします。

  • variable_name: on引数で指定した複数の列名から一つの列を作成します。この引数で、この新しい列の名前を指定します。例えば、元の列名をまとめた新しい列を作成できます。

  • value_name: onで指定した列の値から作成した列の名前を指定します。これにより、onで指定された各列の値が新しい列に変換され、結果として縦長のデータフレームに格納されます。

次の例では、index列を除くすべての列をunpivotし、name列に元の列名を、value列に対応する値を格納します。index列はそのまま保持され、結果的に縦長のデータフレームが作成されます。このように、unpivotを使うと、ピボットで横長に変換されたデータを再び縦長に戻すことができます。

from polars import selectors as cs
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

df6のような複数の列をpivot()values引数に渡して作成したデータフレームを元の形に戻すには少し複雑な処理が必要です。以下のコードでは、unpivot()を使って縦長のデータフレームに戻し、その後、列名と元の名前を分割してpivot()を使って再構成しています。

  1. unpivot(on=cs.exclude('index'), index='index'):

    • df6のピボットされたデータフレームを縦長のデータフレームに変換します。

    • index='index'で、index列はそのまま保持され、on=cs.exclude('index')により、index列以外の全ての列が縦長になります。variable列には元々の列名(例えば、value:A, score:Bなど)が格納されます。

  2. pl.col('variable').str.split(":"):

    • variable列(元々の列名)を:で分割し、リスト(2つの要素)にします。例えば、value:A['value', 'A']に分割されます。

  3. .list.to_struct(fields=['colname', 'name']):

    • 分割されたリストを構造体に変換します。この構造体は2つのフィールド(colnamename)を持ちます。

  4. .struct.unnest():

    • 構造体を展開し、2つの新しい列(colnamename)を作成します。これにより、colname列には元の列名(valuescore)が、name列には元々のデータフレームのこの二つ列のデータが格納されます。

  5. pivot('colname', index=['index', 'name'], values='value'):

    • 最後に、colname列を使って元のデータをピボットします。indexには元々のindex列とname列を使い、valuesには元々の値が入っていたvalue列を指定します。

(
df6.unpivot(on=cs.exclude('index'), index='index')
.with_columns(
    pl.col('variable')
      .str.split(":")
      .list.to_struct(fields=['colname', 'name'])
      .struct.unnest()
)
.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に似ており、一つの列の要素を複数の列に展開するために使用します。how引数で並び順を指定でき、'vertical'では縦方向(行優先)、'horizontal'では横方向(列優先)に展開されます。また、fill_values引数を使うことで、欠損部分に埋める値を指定できます。

以下の例では、indexnamevaluescoreの4つの列を持つデータフレームを作成し、unstack()を使って縦方向および横方向に展開します。

df_vertical = df.unstack(4, how='vertical', columns=['index', 'name'], fill_values=[-1, '?'])
df_horizontal = df.unstack(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() は、カテゴリカルデータをダミー変数(one-hotエンコーディング) に変換するためのメソッドです。指定した列に含まれる各カテゴリが新しい列として展開され、その列には 0 または 1 の値が割り当てられます。

  • 0: そのカテゴリに該当しない場合

  • 1: そのカテゴリに該当する場合

複数の列を指定した場合、それぞれの列に対してダミー変数を生成します。

  • drop_first=False(デフォルト): すべてのカテゴリをダミー変数として作成します。

  • drop_first=True: 基準となるカテゴリ(最初のカテゴリ)を除外し、残りのカテゴリのみをダミー変数として作成します。これは多重共線性を防ぐために使われます(回帰分析などの統計モデルで役立ちます)。

df_original = df.filter(pl.col('index') < 2).select('index', 'name', 'value')
df_dummies1 = df_original.to_dummies(['index', 'name'], drop_first=False)
df_dummies2 = df_original.to_dummies(['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