Pandas ピボットテーブル:Excelのようにデータを要約・整形する(ガイド)
Updated on
Excel を使ったことがあるアナリストなら、ピボットテーブルはおなじみです。フィールドを行エリアにドラッグし、別のフィールドを列エリアに置き、集計関数を選ぶだけで、生の取引データが「カテゴリ別の合計」「地域別の平均」「月別の件数」といった、見やすい要約に変わります。いま同じことを Python でやりたいのに、unstack を伴うネストした groupby 呼び出しを書くのは不格好で読みにくく感じるでしょう。
本当のストレスは、複数レベルのグルーピングが必要だったり、小計(合計行/列)が欲しかったり、複数の集約関数を複数列に同時適用したいときにやってきます。groupby、agg、unstack をつなぎ合わせる書き方は壊れやすく、データの形が少し変わるだけで動かなくなりがちです。
pandas pivot_table 関数を使うと、Excel 風ピボットテーブルの強力さを、読みやすい宣言的 API として Python で扱えます。1 回の呼び出しで、グルーピング、集約、マルチレベルのインデックス、小計、欠損値処理までまとめて行えます。このガイドでは各パラメータをすべて説明し、実用例を通して使い方を確認し、pivot_table を groupby、pivot、crosstab と比較して「どれを選ぶべきか」を明確にします。
pd.pivot_table() がすること
pd.pivot_table() は、DataFrame からスプレッドシート風の要約テーブルを作成します。行ラベル(index)に使う列、列見出し(columns)に使う列、集約する値(values)、適用する集約関数(aggfunc)を指定します。結果は新しい DataFrame で、各セルに要約統計量が入ります。
簡単な before/after は次のとおりです。
生データ:
| region | product | revenue |
|---|---|---|
| North | Widget | 1200 |
| North | Gadget | 800 |
| South | Widget | 1500 |
| South | Gadget | 950 |
| North | Widget | 1400 |
| South | Widget | 1600 |
ピボットテーブル(region と product 別の revenue 合計):
| region | Gadget | Widget |
|---|---|---|
| North | 800 | 2600 |
| South | 950 | 3100 |
各セルには、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)パラメータ一覧
| Parameter | Description | Default |
|---|---|---|
data | 要約する DataFrame | Required |
values | 集約する列 | None(数値列すべて) |
index | 行ラベルに使う列 | None |
columns | 列見出しに使う列 | None |
aggfunc | 集約関数:'mean', 'sum', 'count', 'min', 'max'、または function/dict/list | 'mean' |
fill_value | 結果の NaN を置換する値 | None |
margins | 行/列の合計(小計)を追加 | False |
margins_name | margins 行/列の名前 | 'All' |
dropna | すべてが NaN の列を除外 | True |
observed | categorical 列で、観測されたカテゴリのみ表示 | 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.0revenue は合計、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 12900All 行は 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.0salesperson が 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.3Spring の平均が全体的に少し高く、特に 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 つの関数は機能が一部重なりますが、それぞれ得意分野が異なります。
| Feature | pivot_table() | pivot() | groupby() | crosstab() |
|---|---|---|---|---|
| Aggregation | Yes (any function) | No (raises on duplicates) | Yes (any function) | Yes (limited) |
| Handles duplicates | Yes, via aggfunc | No | Yes | Yes |
| Subtotals (margins) | Yes | No | No (manual) | Yes |
| Fill missing values | Yes (fill_value) | No | No | Yes (fill_value) |
| Input | DataFrame | DataFrame | DataFrame | Series/arrays |
| Output | DataFrame | DataFrame | DataFrame/Series | DataFrame |
| Multi-level index | Yes | Yes | Yes | Yes |
| Best for | Summarizing data with aggregation | Reshaping unique key-value data | Flexible grouped analysis | Frequency tables |
| Default function | mean | N/A | N/A | count |
それぞれを使うべき場面
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 Size | Expected Time |
|---|---|
| 100K rows, 2 group cols | ~5 ms |
| 1M rows, 2 group cols | ~50 ms |
| 10M rows, 3 group cols | ~500 ms |
最適化戦略:
- ピボット前にデータを減らす —
pivot_table()の前に行をフィルタし、必要列だけを選択する。 - categorical dtypes を使う — 文字列列を
categorydtype にするとグルーピングが高速になる。 - 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 optimizedPyGWalker でピボットテーブルを可視化する
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() は sum や mean などの関数で集約することで重複を扱えます。単純な整形には 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 のピボットテーブル体験に近い、ドラッグ&ドロップの可視化インターフェースを提供します。