機械学習データ前処理入門

機械学習データ前処理:複数の切り口で集計!Pandasでピボットテーブル(クロス集計)を作る方法【Pandas入門】

Tags: Pandas, データ集計, ピボットテーブル, クロス集計, データ前処理

機械学習モデルの構築において、データを理解し、適切な形に整えるデータ前処理は非常に重要なステップです。データには様々な情報が詰まっていますが、そのままの状態では見えにくい傾向やパターンが多く存在します。これらの隠れた情報を引き出すための強力な手法の一つに、「集計」があります。

特に、データを複数の異なる「切り口」からまとめて眺めることができる集計方法は、データの構造や特性を理解する上で非常に役立ちます。ビジネスの現場でExcelのピボットテーブルを使った経験がある方もいらっしゃるかもしれません。Pandasライブラリでも、これと同様の、あるいはさらに柔軟な集計を行うことができます。

この記事では、Pandasを使ってデータを複数の軸で集計し、多角的な視点から分析するための「ピボットテーブル」と「クロス集計」の作成方法について、初心者の方にも分かりやすく解説します。

ピボットテーブル(クロス集計)とは?

まず、ピボットテーブルやクロス集計がどのようなものか、簡単に理解しましょう。

一般的なデータテーブルは、各行が個別の観測データ(例:商品の販売記録1件)、各列がその観測データに関する属性(例:販売日、商品名、価格、販売数量)となっています。

ピボットテーブルやクロス集計では、このような元のデータを、特定の列を「行の軸」に、別の列を「列の軸」にして再構成し、それぞれの交差する部分(セル)に指定した値(別の列の値)を集計(合計、平均、カウントなど)した結果を表示します。

例えば、以下のような架空の販売データがあったとします。

| 日付 | エリア | 商品名 | 販売数量 | 売上(円) | | :--------- | :------- | :------- | :------- | :--------- | | 2023-01-01 | 東京 | りんご | 3 | 450 | | 2023-01-01 | 大阪 | みかん | 5 | 500 | | 2023-01-02 | 東京 | みかん | 2 | 200 | | 2023-01-02 | 大阪 | りんご | 4 | 600 | | 2023-01-03 | 東京 | りんご | 1 | 150 | | 2023-01-03 | 大阪 | みかん | 6 | 600 |

このデータを、「行の軸」をエリア、「列の軸」を商品名として、「売上(円)」の合計値を集計するピボットテーブルを作成すると、以下のような表が得られます。

| エリア | りんご | みかん | | :----- | :----- | :----- | | 大阪 | 600 | 1100 | | 東京 | 600 | 200 |

このように、元のデータでは1件ずつバラバラに記録されていた販売実績を、「エリアごと」かつ「商品名ごと」という複数の切り口でまとめて見ることができます。これにより、「大阪ではみかんの売上が高い」「東京ではりんごの方が売れている」といった傾向を一目で把握することが可能になります。

この多角的な集計は、機械学習モデルの訓練において、データセットの理解を深めたり、新たな特徴量を作成したりする際に非常に有効です。

Pandasでのピボットテーブル・クロス集計

Pandasでは、主に以下の2つの関数を使ってピボットテーブルやクロス集計を作成します。

  1. DataFrame.pivot_table(): より汎用的で高機能なピボットテーブルを作成するための関数です。欠損値の処理や複数の集計方法の指定など、多くのオプションを持っています。
  2. pd.crosstab(): 2つ以上の要素間の「度数」(出現頻度)を集計することに特化した関数です。カテゴリデータの関連性を調べたい場合などに便利です。

まずは、より汎用的なpivot_table()から見ていきましょう。

DataFrame.pivot_table() の使い方

pivot_table()関数は、元のDataFrameに対してメソッドとして使用します。基本的な引数は以下の通りです。

それでは、先ほどの販売データを使って実際にピボットテーブルを作成してみましょう。

まず、サンプルデータを含むDataFrameを作成します。

import pandas as pd

# サンプルデータの作成
data = {
    '日付': ['2023-01-01', '2023-01-01', '2023-01-02', '2023-01-02', '2023-01-03', '2023-01-03'],
    'エリア': ['東京', '大阪', '東京', '大阪', '東京', '大阪'],
    '商品名': ['りんご', 'みかん', 'みかん', 'りんご', 'りんご', 'みかん'],
    '販売数量': [3, 5, 2, 4, 1, 6],
    '売上(円)': [450, 500, 200, 600, 150, 600]
}
df = pd.DataFrame(data)

print("元のDataFrame:")
print(df)

上記のコードを実行すると、以下のようなDataFrameが作成・表示されます。

元のDataFrame:
         日付 エリア  商品名  販売数量  売上(円)
0  2023-01-01   東京   りんご     3     450
1  2023-01-01   大阪  みかん     5     500
2  2023-01-02   東京  みかん     2     200
3  2023-01-02   大阪   りんご     4     600
4  2023-01-03   東京   りんご     1     150
5  2023-01-03   大阪  みかん     6     600

このDataFrameに対して、エリアを行、商品名を列として、売上の合計を計算するピボットテーブルを作成します。

# エリア別・商品名別の売上合計を計算するピボットテーブル
pivot_sales_total = df.pivot_table(
    index='エリア',      # 行の軸
    columns='商品名',    # 列の軸
    values='売上(円)', # 集計対象の値
    aggfunc='sum'        # 集計方法(合計)
)

print("\nエリア別・商品名別の売上合計:")
print(pivot_sales_total)

出力結果は以下のようになります。

エリア別・商品名別の売上合計:
商品名    みかん  りんご
エリア
大阪     1100.0  600.0
東京      200.0  600.0

期待通りのピボットテーブルが作成されました。元のデータに存在しない組み合わせ(例: 東京でみかんの売上がない日付)は、デフォルトで欠損値(NaN)となりますが、今回はたまたま全て存在したためNaNはありませんでした。もしNaNが発生した場合、fill_value=0 のように指定すれば、NaNを0で埋めることができます。

次に、販売数量の合計を計算してみましょう。

# エリア別・商品名別の販売数量合計を計算するピボットテーブル
pivot_quantity_total = df.pivot_table(
    index='エリア',
    columns='商品名',
    values='販売数量',
    aggfunc='sum',
    fill_value=0 # 欠損値を0で埋める
)

print("\nエリア別・商品名別の販売数量合計:")
print(pivot_quantity_total)

出力結果:

エリア別・商品名別の販売数量合計:
商品名  みかん  りんご
エリア
大阪      11      4
東京       2      4

このように、valuesaggfuncを変更することで、様々な切り口で集計することができます。

複数の列を行/列の軸にする

indexcolumnsには、列名のリストを指定することも可能です。これにより、より詳細な階層構造を持つピボットテーブルを作成できます。

例として、「日付」と「エリア」を行の軸、「商品名」を列の軸として、売上合計を集計してみましょう。

# 日付・エリア別・商品名別の売上合計を計算するピボットテーブル
pivot_multi_index = df.pivot_table(
    index=['日付', 'エリア'], # 日付とエリアを組み合わせて行の軸に
    columns='商品名',
    values='売上(円)',
    aggfunc='sum',
    fill_value=0
)

print("\n日付・エリア別・商品名別の売上合計:")
print(pivot_multi_index)

出力結果:

日付・エリア別・商品名別の売上合計:
商品名         みかん  りんご
日付       エリア
2023-01-01 大阪    500    0
           東京      0  450
2023-01-02 大阪      0  600
           東京    200    0
2023-01-03 大阪    600    0
           東京      0  150

このように、複数列を指定することで、より細分化された集計結果を得ることができます。

複数の値を集計する

valuesにも列名のリストを指定し、aggfuncに集計方法のリストまたは辞書を指定することで、複数の値を同時に集計することも可能です。

例として、売上と販売数量の両方を、エリア別・商品名別で合計してみましょう。

# エリア別・商品名別で、売上合計と販売数量合計を同時に集計
pivot_multi_values = df.pivot_table(
    index='エリア',
    columns='商品名',
    values=['売上(円)', '販売数量'], # 複数の集計対象を指定
    aggfunc='sum',             # 全ての集計対象に合計を適用
    fill_value=0
)

print("\nエリア別・商品名別、売上と販売数量の合計:")
print(pivot_multi_values)

出力結果:

エリア別・商品名別、売上と販売数量の合計:
          売上(円)       販売数量
商品名         みかん りんご   みかん りんご
エリア
大阪     1100     600     11      4
東京      200     600      2      4

このように、各列(商品名)の下に集計対象の値(売上、販売数量)が階層的に表示されます。

集計方法を値ごとに変える

aggfuncに辞書形式で { '集計対象列名': '集計方法' } のように指定すると、値ごとに異なる集計方法を適用できます。

# エリア別・商品名別で、売上は合計、販売数量は平均を計算
pivot_diff_aggfunc = df.pivot_table(
    index='エリア',
    columns='商品名',
    values=['売上(円)', '販売数量'],
    aggfunc={'売上(円)': 'sum', '販売数量': 'mean'}, # 値ごとに集計方法を指定
    fill_value=0
)

print("\nエリア別・商品名別、売上合計と販売数量平均:")
print(pivot_diff_aggfunc)

出力結果:

エリア別・商品名別、売上合計と販売数量平均:
          売上(円)       販売数量
商品名         みかん りんご  みかん  りんご
エリア
大阪     1100     600   5.5    4.0
東京      200     600   2.0    2.0

このように、pivot_table()関数は非常に柔軟な集計を行うことができます。

pd.crosstab() の使い方

pd.crosstab()関数は、Pandasのトップレベル関数として使用します。主に、2つ以上の因子(カテゴリカルな列)間の出現頻度(度数)を集計するのに特化しています。これは統計学でいう「クロス集計表」を作成することに相当します。

基本的な引数は以下の通りです。

例として、エリアと商品名の組み合わせの出現頻度(販売実績があった回数)をクロス集計してみましょう。

# エリアと商品名の組み合わせの度数をクロス集計
crosstab_freq = pd.crosstab(
    index=df['エリア'],   # 行の軸
    columns=df['商品名']  # 列の軸
)

print("\nエリア別・商品名別の出現頻度:")
print(crosstab_freq)

出力結果:

エリア別・商品名別の出現頻度:
商品名  みかん  りんご
エリア
大阪      2      1
東京      1      2

この表から、「大阪ではみかんの販売実績が2回、りんごが1回あった」「東京ではみかんが1回、りんごが2回あった」ということが分かります。これはpivot_table(values='任意の列', aggfunc='count') とほぼ同じ結果になりますが、crosstabの方がシンプルに記述できます。

crosstabでもvaluesaggfuncを指定することで、度数以外の集計を行うこともできます。この場合、pivot_tableと機能的に近くなりますが、主にカテゴリデータの集計に焦点を当てたい場合に使い分けられます。

例として、エリア別・商品名別で売上合計をクロス集計してみましょう(pivot_tableと同じ結果になるはずです)。

# エリア別・商品名別の売上合計をクロス集計
crosstab_sales = pd.crosstab(
    index=df['エリア'],
    columns=df['商品名'],
    values=df['売上(円)'], # 集計対象の値
    aggfunc='sum'         # 集計方法(合計)
)

print("\nエリア別・商品名別の売上合計 (crosstab):")
print(crosstab_sales)

出力結果:

エリア別・商品名別の売上合計 (crosstab):
商品名    みかん  りんご
エリア
大阪     1100  600
東京      200  600

また、normalize=True を指定すると、全体の合計に対する割合を表示できます。

# エリアと商品名の組み合わせの割合をクロス集計
crosstab_norm = pd.crosstab(
    index=df['エリア'],
    columns=df['商品名'],
    normalize=True # 全体に対する割合を表示
)

print("\nエリア別・商品名別の割合:")
print(crosstab_norm)

出力結果:

エリア別・商品名別の割合:
商品名      みかん     りんご
エリア
大阪     0.333333  0.166667
東京     0.166667  0.333333

この表は、全6件の販売実績のうち、大阪・みかんの組み合わせが2件(2/6 = 0.333...)、大阪・りんごが1件(1/6 = 0.166...)といった割合を示しています。

どちらの関数を使うべきか?

pivot_table()crosstab()は似た機能を持つため、どちらを使うべきか迷うかもしれません。一般的な使い分けの目安としては以下のようになります。

ほとんどのケースではpivot_table()で対応可能ですが、カテゴリ間の度数集計を素早く行いたい場合はcrosstab()も便利です。

まとめ

この記事では、Pandasを使ってデータを複数の切り口で集計する「ピボットテーブル」と「クロス集計」の作成方法について解説しました。

データ分析や機械学習プロジェクトにおいて、データを様々な角度から眺める集計作業は欠かせません。今回ご紹介したPandasの機能を使って、ご自身のデータをぜひ集計してみてください。次回の記事では、さらに別のデータ前処理手法について解説していきます。