Skip to content

Pandas 数据透视表:像 Excel 一样汇总并重塑数据(指南)

Updated on

每个用过 Excel 的分析师都熟悉数据透视表:把一个字段拖到“行”区域、另一个拖到“列”区域,选择一个汇总函数,原本密密麻麻的交易明细表就会变成清爽的汇总表——按类别求总计、按地区算平均值、按月份统计次数。现在你需要在 Python 里做同样的事,但用嵌套的 groupby 再配 unstack 写起来既笨拙又难读。

真正令人抓狂的是:当数据有多个分组层级、你需要小计/总计,或者你想对不同列同时应用多个聚合函数时,把 groupbyaggunstack 一路链下去会产出脆弱的代码——数据形状一变就容易崩。

pandas 的 pivot_table 把 Excel 风格数据透视表的完整能力带到了 Python,并提供了干净、声明式的 API:一次函数调用就能处理分组、聚合、多级索引、小计/总计,以及缺失值处理。本指南会覆盖每个参数,带你走完实战示例,并把 pivot_tablegroupbypivotcrosstab 做对比,让你清楚知道该选哪一个工具。

📚

pd.pivot_table() 是做什么的

pd.pivot_table() 会从 DataFrame 生成一种“电子表格风格”的汇总表。你需要指定:哪些列作为行标签(index)、哪些列作为列标题(columns)、要汇总的数值列(values),以及要应用的聚合函数(aggfunc)。输出是一个新的 DataFrame,其中每个单元格都是一个汇总统计值。

下面是一个简单的前后对比:

原始数据:

regionproductrevenue
NorthWidget1200
NorthGadget800
SouthWidget1500
SouthGadget950
NorthWidget1400
SouthWidget1600

透视表(按 region 和 product 汇总 revenue 的 sum):

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)

参数速查表

参数说明默认值
data要汇总的 DataFrame必填
values要聚合的列None(所有数值列)
index用作行标签的列None
columns用作列标题的列None
aggfunc聚合函数:'mean''sum''count''min''max',或函数/dict/list'mean'
fill_value用于替换结果中 NaN 的值None
margins添加行/列总计(小计/总计)False
margins_namemargins 行/列的名称'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)

基础透视表:按地区汇总收入总和

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

输出:

        revenue
region
East       4320
North      4250
South      4330

这等价于 sales.groupby('region')['revenue'].sum(),但会返回 DataFrame 而不是 Series。

添加列标题

添加 columns 参数,就能按第二个变量进一步拆分汇总结果:

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

输出:

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

现在每个单元格表示某个 region-product 组合的收入总和。

多个聚合函数

把函数列表传给 aggfunc,可以一次计算多个统计指标:

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

输出:

          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

结果列会形成 MultiIndex:顶层是聚合函数,第二层是 product。

对不同列使用不同聚合函数

aggfunc 传入字典,就能对不同的 value 列应用不同的函数:

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

输出:

       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)

输出:

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

All 行显示每个产品的总收入;All 列显示每个地区的总收入;右下角是总计中的总计(Grand Total)。

你也可以用 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,更利于展示,也能避免下游计算出错。

多级索引(行分组)

把列名列表传给 index,即可创建层级式的行标签:

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

输出:

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 下,提供一种“下钻”视图。

多级列

同样地,把列表传给 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。

使用自定义聚合函数

你可以向 aggfunc 传入任何可调用对象,包括 lambda 和 NumPy 函数:

# 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)

输出:

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

输出:

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 里做的月度汇总报表类似的结果:按地区与月份汇总,并包含总计。

pivot_table vs pivot vs groupby vs crosstab

这四个函数的能力有重叠,但使用场景各不相同:

功能pivot_table()pivot()groupby()crosstab()
聚合支持(任意函数)不支持(遇到重复会报错)支持(任意函数)支持(有限)
处理重复键支持(通过 aggfunc不支持支持支持
小计/总计(margins)支持不支持不支持(需手动)支持
填充缺失值支持(fill_value不支持不支持支持(fill_value
输入DataFrameDataFrameDataFrameSeries/arrays
输出DataFrameDataFrameDataFrame/SeriesDataFrame
多级索引支持支持支持支持
最适合带聚合的汇总分析唯一键值数据的重塑灵活的分组分析频数表
默认函数meanN/AN/Acount

何时使用哪个

pivot_table():当你需要 Excel 风格的汇总(带聚合)、小计/总计、或多个聚合函数时。它通常是创建汇总表时最强且最易读的选择。

pivot():当 index 与 columns 的组合是唯一的,你只是想做重塑而不需要聚合时。它比 pivot_table() 更快,因为跳过了聚合步骤。

groupby():当你需要分组计算,但不需要宽表(wide-format)输出时。groupby 默认给你长表(long-format)结果。虽然 groupby().unstack() 能做出类似 pivot_table 的宽表,但 pivot_table 通常更直观。

crosstab():当你在做类别变量的频数表/交叉表时。crosstab() 可直接接收 Series 或数组(不局限于 DataFrame),且默认是计数。

等价示例

下面三段会产生相同结果:

# 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

pivot_table 版本更易读,特别是当你要加 margins、填充缺失值或使用多个聚合函数时。

展平 MultiIndex 列

创建带多个聚合函数的透视表后,经常会得到不太好用的 MultiIndex 列:

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)

输出:

        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,所以性能表现类似。对于大数据集:

数据集规模预期耗时
100K 行,2 个分组列~5 ms
1M 行,2 个分组列~50 ms
10M 行,3 个分组列~500 ms

优化策略:

  1. 在透视前先缩小数据 —— 在调用 pivot_table() 前先过滤行、只保留必要列。
  2. 使用 categorical dtype —— 把字符串列转成 category,分组会更快。
  3. 避免 lambda 聚合 —— 内置字符串函数名(如 '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 的界面:你可以把 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() 只做数据重塑,不做聚合——它要求 index 与 columns 的组合必须唯一;如果存在重复组合会报错。pivot_table() 能通过 summean 等函数对重复项进行聚合。简单重塑用 pivot();需要聚合或小计/总计时用 pivot_table()

如何在 pandas 透视表中添加总计(margins)?

pivot_table() 调用中设置 margins=Truepd.pivot_table(df, values='revenue', index='region', columns='product', aggfunc='sum', margins=True)。这会添加一个名为 All 的行和列来显示小计/总计。也可以用 margins_name='Total' 自定义标签。

透视表可以使用多个聚合函数吗?

可以。向 aggfunc 传入列表:aggfunc=['sum', 'mean', 'count']。这会生成 MultiIndex 列头:一层是函数、一层是 value 列。你也可以传字典,对不同列应用不同函数:aggfunc={'revenue': 'sum', 'units': 'mean'}

pivot_table 和 pandas 的 groupby 有什么不同?

两者都能做聚合,但 pivot_table() 会生成宽表(wide-format)结果(列标题来自某个分组变量),而 groupby() 默认产出长表(long-format)结果。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 中创建汇总表最通用的工具。要点如下:

  • valuesindexcolumns 定义汇总什么、如何分组行、如何分组列。
  • aggfunc 指定聚合函数;传列表实现多函数,传字典实现按列分别聚合。
  • margins=True 添加小计/总计——相当于 Excel 的 “Grand Total”。
  • fill_value 为缺失组合提供默认值(通常是 0)。
  • 当你需要 margins 或 fill values 时,优先 pivot_table() 而不是 groupby().unstack(),可读性更好。
  • 当数据的键组合唯一且不需要聚合时,优先 pivot() 而不是 pivot_table()
  • 做类别变量的简单频数表时,优先 crosstab()

如果你想交互式探索透视表结果,PyGWalker (opens in a new tab) 提供了可视化拖拽界面,让你在 Jupyter Notebook 中获得接近 Excel 数据透视表的体验。

📚