Skip to content

Pandas ピボットテーブル:Excelのようにデータを要約・整形する(ガイド)

Updated on

Excel を使ったことがあるアナリストなら、ピボットテーブルはおなじみです。フィールドを行エリアにドラッグし、別のフィールドを列エリアに置き、集計関数を選ぶだけで、生の取引データが「カテゴリ別の合計」「地域別の平均」「月別の件数」といった、見やすい要約に変わります。いま同じことを Python でやりたいのに、unstack を伴うネストした groupby 呼び出しを書くのは不格好で読みにくく感じるでしょう。

本当のストレスは、複数レベルのグルーピングが必要だったり、小計(合計行/列)が欲しかったり、複数の集約関数を複数列に同時適用したいときにやってきます。groupbyaggunstack をつなぎ合わせる書き方は壊れやすく、データの形が少し変わるだけで動かなくなりがちです。

pandas pivot_table 関数を使うと、Excel 風ピボットテーブルの強力さを、読みやすい宣言的 API として Python で扱えます。1 回の呼び出しで、グルーピング、集約、マルチレベルのインデックス、小計、欠損値処理までまとめて行えます。このガイドでは各パラメータをすべて説明し、実用例を通して使い方を確認し、pivot_tablegroupbypivotcrosstab と比較して「どれを選ぶべきか」を明確にします。

📚

pd.pivot_table() がすること

pd.pivot_table() は、DataFrame からスプレッドシート風の要約テーブルを作成します。行ラベル(index)に使う列、列見出し(columns)に使う列、集約する値(values)、適用する集約関数(aggfunc)を指定します。結果は新しい DataFrame で、各セルに要約統計量が入ります。

簡単な before/after は次のとおりです。

生データ:

regionproductrevenue
NorthWidget1200
NorthGadget800
SouthWidget1500
SouthGadget950
NorthWidget1400
SouthWidget1600

ピボットテーブル(region と product 別の revenue 合計):

regionGadgetWidget
North8002600
South9503100

各セルには、region-product の組み合わせに対する revenue の合計が入っています。

pd.pivot_table() の構文とパラメータ

pd.pivot_table(data, values=None, index=None, columns=None, aggfunc='mean',
               fill_value=None, margins=False, dropna=True, margins_name='All',
               observed=False, sort=True)

パラメータ一覧

ParameterDescriptionDefault
data要約する DataFrameRequired
values集約する列None(数値列すべて)
index行ラベルに使う列None
columns列見出しに使う列None
aggfunc集約関数:'mean', 'sum', 'count', 'min', 'max'、または function/dict/list'mean'
fill_value結果の NaN を置換する値None
margins行/列の合計(小計)を追加False
margins_namemargins 行/列の名前'All'
dropnaすべてが NaN の列を除外True
observedcategorical 列で、観測されたカテゴリのみ表示False
sort結果をソートTrue

全例で使うサンプルデータ

以下の例はすべて、この売上データセットを使用します。

import pandas as pd
import numpy as np
 
sales = pd.DataFrame({
    'date': pd.to_datetime(['2025-01-15', '2025-01-20', '2025-02-10', '2025-02-18',
                            '2025-01-12', '2025-02-22', '2025-01-25', '2025-02-14',
                            '2025-01-30', '2025-02-05', '2025-01-18', '2025-02-28']),
    'region': ['North', 'North', 'North', 'North', 'South', 'South',
               'South', 'South', 'East', 'East', 'East', 'East'],
    'product': ['Widget', 'Gadget', 'Widget', 'Gadget',
                'Widget', 'Gadget', 'Widget', 'Gadget',
                'Widget', 'Gadget', 'Widget', 'Gadget'],
    'salesperson': ['Alice', 'Alice', 'Bob', 'Bob',
                    'Charlie', 'Charlie', 'Diana', 'Diana',
                    'Eve', 'Eve', 'Frank', 'Frank'],
    'revenue': [1200, 800, 1400, 850, 1500, 950, 1100, 780, 1300, 900, 1250, 870],
    'units': [10, 8, 12, 9, 15, 10, 11, 8, 13, 9, 12, 9]
})
 
print(sales)

基本のピボットテーブル:region 別の revenue 合計

table = pd.pivot_table(sales, values='revenue', index='region', aggfunc='sum')
print(table)

Output:

        revenue
region
East       4320
North      4250
South      4330

これは sales.groupby('region')['revenue'].sum() と同等ですが、Series ではなく DataFrame を返します。

列見出しを追加する

columns パラメータを追加して、2 つ目の変数で内訳を分けます。

table = pd.pivot_table(sales, values='revenue', index='region',
                       columns='product', aggfunc='sum')
print(table)

Output:

product  Gadget  Widget
region
East       1770    2550
North      1650    2600
South      1730    2600

これで各セルは、特定の region-product ペアに対する revenue 合計を示します。

複数の集約関数

aggfunc に関数のリストを渡すと、複数の統計量を一度に計算できます。

table = pd.pivot_table(sales, values='revenue', index='region',
                       columns='product', aggfunc=['sum', 'mean', 'count'])
print(table)

Output:

          sum          mean                count
product Gadget Widget Gadget  Widget      Gadget Widget
region
East      1770   2550  885.0  1275.0           2      2
North     1650   2600  825.0  1300.0           2      2
South     1730   2600  865.0  1300.0           2      2

結果の columns は MultiIndex になり、上位レベルが集約関数、下位レベルが product になります。

列ごとに異なる集約関数を使う

aggfunc に辞書を使うと、値の列ごとに異なる関数を適用できます。

table = pd.pivot_table(sales, values=['revenue', 'units'], index='region',
                       columns='product',
                       aggfunc={'revenue': 'sum', 'units': 'mean'})
print(table)

Output:

       revenue         units
product Gadget Widget Gadget Widget
region
East      1770   2550    9.0   12.5
North     1650   2600    8.5   11.0
South     1730   2600    9.0   13.0

revenue は合計、units は平均——Excel のピボットテーブルで「フィールドごとに要約方法を変える」のと同じです。

margins で小計(合計)を追加する

margins パラメータは、総計を示す行と列を追加します。

table = pd.pivot_table(sales, values='revenue', index='region',
                       columns='product', aggfunc='sum', margins=True)
print(table)

Output:

product  Gadget  Widget    All
region
East       1770    2550   4320
North      1650    2600   4250
South      1730    2600   4330
All        5150    7750  12900

All 行は product ごとの総 revenue を示します。All 列は region ごとの総 revenue を示します。右下のセルは全体の総計です。

margins_name でラベルをカスタマイズできます。

table = pd.pivot_table(sales, values='revenue', index='region',
                       columns='product', aggfunc='sum',
                       margins=True, margins_name='Total')
print(table)

fill_value で欠損値を扱う

データに存在しない組み合わせがあると、ピボットテーブルには NaN が入ります。fill_value で置換できます。

# Remove one row to create a missing combination
sales_missing = sales.drop(index=0)
 
table = pd.pivot_table(sales_missing, values='revenue', index='region',
                       columns='product', aggfunc='sum', fill_value=0)
print(table)

NaN を 0 に置き換えると表示が見やすくなり、後続の計算でも扱いやすくなります。

Multi-Level Index(行の階層グルーピング)

index に列名のリストを渡すと、階層的な行ラベルを作れます。

table = pd.pivot_table(sales, values='revenue',
                       index=['region', 'salesperson'],
                       columns='product', aggfunc='sum')
print(table)

Output:

product                Gadget  Widget
region salesperson
East   Eve              900.0  1300.0
       Frank            870.0  1250.0
North  Alice            800.0  1200.0
       Bob              850.0  1400.0
South  Charlie          950.0  1500.0
       Diana            780.0  1100.0

salesperson が region の下にネストされ、データをドリルダウンして見られます。

Multi-Level Columns

同様に、columns にリストを渡すと階層的な列見出しを作れます。

sales['month'] = sales['date'].dt.month_name()
 
table = pd.pivot_table(sales, values='revenue', index='region',
                       columns=['product', 'month'], aggfunc='sum', fill_value=0)
print(table)

product を上位、month を下位とする 2 階層の列見出しになります。

カスタム集約関数を使う

aggfunc には、lambda や NumPy 関数など任意の callable を渡せます。

# Range (max - min) of revenue by region
table = pd.pivot_table(sales, values='revenue', index='region',
                       columns='product', aggfunc=lambda x: x.max() - x.min())
print(table)

Output:

product  Gadget  Widget
region
East         30      50
North        50     200
South       170     400

その他の便利なカスタム集約例:

# Coefficient of variation
table = pd.pivot_table(sales, values='revenue', index='region',
                       aggfunc=lambda x: x.std() / x.mean() * 100)

実例:学生の成績分析

students = pd.DataFrame({
    'student': ['Alice', 'Alice', 'Alice', 'Bob', 'Bob', 'Bob',
                'Charlie', 'Charlie', 'Charlie', 'Diana', 'Diana', 'Diana'],
    'subject': ['Math', 'Science', 'English'] * 4,
    'semester': ['Fall', 'Fall', 'Fall', 'Fall', 'Fall', 'Fall',
                 'Spring', 'Spring', 'Spring', 'Spring', 'Spring', 'Spring'],
    'score': [92, 88, 95, 78, 85, 72, 90, 93, 88, 85, 79, 91]
})
 
# Average score by subject and semester
table = pd.pivot_table(students, values='score', index='subject',
                       columns='semester', aggfunc='mean', margins=True)
print(table.round(1))

Output:

semester   Fall  Spring   All
subject
English    83.5    89.5  86.5
Math       85.0    87.5  86.2
Science    86.5    86.0  86.2
All        85.0    87.7  86.3

Spring の平均が全体的に少し高く、特に English の伸びが大きいことが一目でわかります。

実例:月次売上レポート

# Create a monthly sales summary
sales['month'] = sales['date'].dt.strftime('%Y-%m')
 
report = pd.pivot_table(sales, values=['revenue', 'units'],
                        index='region', columns='month',
                        aggfunc={'revenue': 'sum', 'units': 'sum'},
                        margins=True, fill_value=0)
print(report)

Excel で作るのと同種の月次サマリーレポート(region と month 別の合計)を作成できます。

pivot_table vs pivot vs groupby vs crosstab

この 4 つの関数は機能が一部重なりますが、それぞれ得意分野が異なります。

Featurepivot_table()pivot()groupby()crosstab()
AggregationYes (any function)No (raises on duplicates)Yes (any function)Yes (limited)
Handles duplicatesYes, via aggfuncNoYesYes
Subtotals (margins)YesNoNo (manual)Yes
Fill missing valuesYes (fill_value)NoNoYes (fill_value)
InputDataFrameDataFrameDataFrameSeries/arrays
OutputDataFrameDataFrameDataFrame/SeriesDataFrame
Multi-level indexYesYesYesYes
Best forSummarizing data with aggregationReshaping unique key-value dataFlexible grouped analysisFrequency tables
Default functionmeanN/AN/Acount

それぞれを使うべき場面

pivot_table() を使うのは、集約つきの Excel 風サマリー、小計(margins)、複数の集約関数が必要なときです。要約テーブルを作るうえで最も強力で読みやすい選択肢です。

pivot() を使うのは、index と columns の組み合わせが一意で、集約せずに単に整形(reshape)したいときです。集約ステップをスキップするため pivot_table() より高速です。

groupby() を使うのは、グループ単位の計算が必要だが、ワイド形式の出力が不要なときです。groupby はデフォルトでロング形式を返します。groupby().unstack()pivot_table 風にもできますが、pivot_table の方が読みやすいことが多いです。

crosstab() を使うのは、カテゴリ変数の度数表やクロス集計を作るときです。crosstab() は Series や配列を直接受け取れ(DataFrame に限定されない)、デフォルトはカウントです。

等価性の例

次の 3 行は同じ結果になります。

# pivot_table approach
result1 = pd.pivot_table(sales, values='revenue', index='region',
                         columns='product', aggfunc='sum')
 
# groupby + unstack approach
result2 = sales.groupby(['region', 'product'])['revenue'].sum().unstack()
 
# Both produce the same table
print(result1.equals(result2))  # True

特に margins、fill_value、複数集約などを追加していくと、pivot_table の方が読みやすくなります。

MultiIndex columns をフラット化する

複数の集約関数を使うピボットテーブルを作ると、操作しづらい MultiIndex columns になることがあります。

table = pd.pivot_table(sales, values='revenue', index='region',
                       columns='product', aggfunc=['sum', 'mean'])
 
# Flatten the column MultiIndex
table.columns = ['_'.join(col).strip() for col in table.columns.values]
print(table)

Output:

        sum_Gadget  sum_Widget  mean_Gadget  mean_Widget
region
East          1770        2550        885.0       1275.0
North         1650        2600        825.0       1300.0
South         1730        2600        865.0       1300.0

列名がフラットな文字列になり、参照しやすくなります。

ピボットテーブルのソートとフィルタ

ピボットテーブルは通常の DataFrame なので、ソートやフィルタが可能です。

table = pd.pivot_table(sales, values='revenue', index='region',
                       columns='product', aggfunc='sum', margins=True)
 
# Sort by total revenue (All column), descending
sorted_table = table.sort_values('All', ascending=False)
print(sorted_table)
 
# Filter to show only regions with Widget revenue > 2500
filtered = table[table['Widget'] > 2500]
print(filtered)

ピボットテーブルのエクスポート

ステークホルダーが期待する Excel や、CSV に保存できます。

table = pd.pivot_table(sales, values='revenue', index='region',
                       columns='product', aggfunc='sum', margins=True)
 
# Export to Excel
table.to_excel('sales_pivot.xlsx', sheet_name='Revenue Summary')
 
# Export to CSV
table.to_csv('sales_pivot.csv')

パフォーマンスのヒント

pivot_table() は内部的に groupby を呼ぶため、性能は概ね同程度です。大規模データセットでは次の目安になります。

Dataset SizeExpected Time
100K rows, 2 group cols~5 ms
1M rows, 2 group cols~50 ms
10M rows, 3 group cols~500 ms

最適化戦略:

  1. ピボット前にデータを減らすpivot_table() の前に行をフィルタし、必要列だけを選択する。
  2. categorical dtypes を使う — 文字列列を category dtype にするとグルーピングが高速になる。
  3. lambda の aggfunc を避ける — 組み込みの文字列指定('sum', 'mean')は最適化された C コードを使う。lambda は遅い Python ループにフォールバックしやすい。
# Faster: use categorical dtypes
sales['region'] = sales['region'].astype('category')
sales['product'] = sales['product'].astype('category')
 
# Faster: use string name instead of lambda
table = pd.pivot_table(sales, values='revenue', index='region',
                       columns='product', aggfunc='sum')  # 'sum' is optimized

PyGWalker でピボットテーブルを可視化する

pd.pivot_table() は数値の要約に優れていますが、データのパターンをインタラクティブに可視化したい場合もあります。PyGWalker (opens in a new tab) はオープンソースの Python ライブラリで、ビジュアルなドラッグ&ドロップ操作でピボットテーブル、棒グラフ、ヒートマップなどを作成できます。初期セットアップ後は、追加コードなしで操作できます。

import pandas as pd
import pygwalker as pyg
 
sales = pd.DataFrame({
    'region': ['North', 'North', 'South', 'South', 'East', 'East'] * 2,
    'product': ['Widget', 'Gadget'] * 6,
    'revenue': [1200, 800, 1500, 950, 1300, 900, 1400, 850, 1100, 780, 1250, 870],
    'units': [10, 8, 15, 10, 13, 9, 12, 9, 11, 8, 12, 9]
})
 
# Launch interactive pivot table and visualization
walker = pyg.walk(sales)

PyGWalker では Tableau のような UI で、region を rows、product を columns、revenue を values にドラッグして視覚的にピボットテーブルを作れます。テーブル表示とチャート表示を即座に切り替えられ、集約関数を試したり、結果をエクスポートしたりできます。追加のコードを書く必要はありません。

Google Colab (opens in a new tab)Kaggle (opens in a new tab) で PyGWalker を試すか、pip install pygwalker でインストールできます。

FAQ

pandas の pivot と pivot_table の違いは何ですか?

pivot() は集約なしでデータを整形(reshape)します。index と columns の組み合わせが一意である必要があり、重複があるとエラーになります。pivot_table()summean などの関数で集約することで重複を扱えます。単純な整形には pivot()、集約や小計が必要なら pivot_table() を使ってください。

pandas のピボットテーブルに合計(margins)を追加するには?

pivot_table() 呼び出しで margins=True を設定します:pd.pivot_table(df, values='revenue', index='region', columns='product', aggfunc='sum', margins=True)。これにより小計の All 行と All 列が追加されます。ラベルは margins_name='Total' で変更できます。

ピボットテーブルで複数の集約関数を使えますか?

はい。aggfunc にリストを渡します:aggfunc=['sum', 'mean', 'count']。これにより、関数用のレベルと値列(/分類)のレベルを持つ MultiIndex の列見出しが作られます。また、辞書で列ごとに関数を変えることもできます:aggfunc={'revenue': 'sum', 'units': 'mean'}

pandas の pivot_table は groupby とどう違いますか?

どちらも集約できますが、pivot_table() は(グルーピング変数の一つを列見出しにして)ワイド形式の結果を作ります。一方 groupby() はデフォルトでロング形式を返します。pivot_table() は margins(小計)や fill_value(欠損補完)も標準でサポートします。内部的には pivot_table()groupby() を利用しています。

ピボットテーブルの NaN はどう扱いますか?

fill_value パラメータで NaN を特定の値に置換します:pd.pivot_table(df, ..., fill_value=0)。また dropna=True(デフォルト)により、すべてが NaN の列は除外されます。

割合(パーセンテージ)を持つピボットテーブルは作れますか?

はい。まず件数や合計でピボットテーブルを作成し、その後に合計で割って割合にします。割合ベースのクロス集計なら pd.crosstab()normalize も使えます。ピボットテーブルでは手動で計算します:table = table.div(table.sum(axis=1), axis=0) * 100

まとめ

pandas の pivot_table() は、Python でサマリーテーブルを作るための最も汎用的なツールです。要点は次のとおりです。

  • values, index, columns で「何を要約するか」「行のグルーピング」「列のグルーピング」を定義する。
  • aggfunc で集約関数を指定する。複数関数ならリスト、列ごとに変えるなら辞書。
  • margins=True で小計(Excel の「総計」)を追加する。
  • fill_value で存在しない組み合わせの欠損を(通常 0 で)埋める。
  • 可読性のために groupby().unstack() より pivot_table() を優先(特に margins や fill_value が必要な場合)。
  • 集約が不要でキー組み合わせが一意なら pivot() を優先
  • カテゴリの度数表なら crosstab() を優先

ピボットテーブルをインタラクティブに探索したいなら、PyGWalker (opens in a new tab) が Jupyter Notebook 内で Excel のピボットテーブル体験に近い、ドラッグ&ドロップの可視化インターフェースを提供します。

📚