前言
透视表(pivot table)是把"长表"转成"交叉表"的经典操作: 用一列做行标签、一列做列标签、一列做取值,交叉的格子里放聚合结果。 Excel 里的透视表人人会点,pandas 里对应的是pivot_table。
初学者最容易犯的两个错:一是把pivot和pivot_table当成一回事; 二是以为不传聚合函数就是求和。实际情况是:pivot只做重塑、不聚合, 遇到重复的"行×列"组合会直接报错;而pivot_table默认的聚合函数是平均值"mean",不是求和。
需要说明:pandas 是第三方库,本机没有 Python 解释器也没有 pandas, 示例无法运行验证,只能逐行人工推演。参数细节以 Pandas 官方文档为准。
一、pivot_table 的参数
官方签名(以当前文档为准):
pandas.pivot_table(data, values=None, index=None, columns=None,
aggfunc='mean', fill_value=None, margins=False,
dropna=True, margins_name='All', observed=True,
sort=True, **kwargs)| 参数 | 作用 |
|---|
data | 输入DataFrame |
values | 要聚合的数值列 |
index | 放到行上的分组键 |
columns | 放到列上的分组键 |
aggfunc | 聚合函数,默认"mean" |
fill_value | 结果里缺失填什么 |
margins | 是否加合计行列 |
理解方式:index和columns是分组维度,values是被聚合的度量,aggfunc决定怎么聚。它和groupby是一体的——pivot_table基本可以理解为 "按index和columns分组后,对values做aggfunc,再把结果摊开"。 参数里dropna=True指的是分组键含缺失的行会被丢弃, 和后面讲的"结果表里的空位"不是一回事,别混淆。
二、最基础的例子
# pandas 1.0+
import pandas as pd
df = pd.DataFrame(
{
"region": ["东", "东", "南", "南", "东"],
"month": ["1月", "2月", "1月", "2月", "1月"],
"amount": [10, 20, 30, 40, 50],
}
)
table = pd.pivot_table(
df,
values="amount",
index="region",
columns="month",
aggfunc="sum",
fill_value=0,
margins=True, # 加 All 合计行列
)
print(table)注意这里显式写了aggfunc="sum"。如果不写,结果会是均值, 这是与直觉最不一致的地方,务必记住。
三、多个聚合函数、多列取值
aggfunc可以传列表,values也可以传列表:
# pandas 1.0+
table = pd.pivot_table(
df,
values="amount",
index="region",
columns="month",
aggfunc=["sum", "count"],
)这样会产生"外层是聚合函数、内层是月份"的列层级(MultiIndex的列)。 当values和aggfunc都是列表时,列层级会更多一层, 取值时要用元组逐级定位,例如table[("sum", "1月")]。
四、fill_value 与 margins
fill_value填的是结果表里的空位。因为不是每种"维度组合"都有数据,
交叉后必然出现空格,填 0 或填其他默认值能让表更整齐。
margins=True会增加All行和All列,值是对应的汇总。
汇总用的函数由aggfunc决定;多函数时行为要按文档确认。
margins_name可改合计行列的名字,默认是"All"。
五、pivot 与 pivot_table 的区别
| 对比 | pivot | pivot_table |
|---|
| 是否聚合 | 不聚合,只重塑 | 会聚合 |
| 重复组合 | 报错 | 用aggfunc合并 |
| 默认聚合 | 无 | "mean" |
| 适用 | 已经是唯一组合的整洁数据 | 有重复、需要汇总的数据 |
pivot的语义很直接:index+columns必须唯一确定一个values, 否则就抛出ValueError: Index contains duplicate entries。 遇到这个报错,第一反应应该是改用pivot_table并指定aggfunc。
六、crosstab:另一条做交叉表的路
如果只是想数"两个类别交叉出现多少次",pd.crosstab更顺手:
# pandas 1.0+
freq = pd.crosstab(df["region"], df["month"])
print(freq)crosstab默认做的是计数(频数),它接受的是类别的序列而不是整张表, 语义上和透视表互补。它还支持normalize参数,用来把频数换算成比例或百分比 (例如按行、按列或整体归一),做占比分析时很方便。
七、把结果整理成"平表"
透视表的结果通常带着行索引和列索引(可能是多层),直接写 Excel 会挤在一起, 不适合再加工。常见的整理方式是"压平列名 + 重置索引":
# pandas 1.0+
flat = table.reset_index()
flat.columns = ["_".join(map(str, col)) if isinstance(col, tuple) else str(col)
for col in flat.columns]
print(flat)reset_index()把行索引变回普通列,列名若是多层元组就拼成一个字符串, 得到的就是一张规规矩矩的平表,可以直接交给to_excel或下游系统。
如果只是想快速看结构,table.stack()/table.unstack()能在 "宽表"和"长表"之间切换;但stack后缺失值默认会被丢掉, 需要保留请显式传dropna=False。
八、完整示例
# pandas 1.0+
import pandas as pd
def report(rows: pd.DataFrame) -> pd.DataFrame:
table = pd.pivot_table(
rows,
values="amount",
index="region",
columns="month",
aggfunc="sum",
fill_value=0,
margins=True,
margins_name="合计",
)
return table.sort_values("合计", ascending=False)
if __name__ == "__main__":
data = pd.DataFrame(
{
"region": ["东", "南", "东"],
"month": ["1月", "1月", "2月"],
"amount": [10, 30, 20],
}
)
print(report(data))常见坑点
- 以为
pivot_table默认求和
❌ 不写aggfunc直接调用,得到的是平均值,和报表预期不符 ✅ 求和就写aggfunc="sum",求计数写"count"或"size"
- 用
pivot处理有重复组合的数据
❌df.pivot(index="a", columns="b", values="c"),抛"重复条目"错误 ✅ 改pivot_table(..., aggfunc="sum")让重复组合被聚合掉
- 用
==之类的判等去找透视表里的空值
❌ 结果表里的空位当成普通缺失用==比,比不出来 ✅ 生成时就传fill_value=0,或事后用isna()判定
fill_value和原始数据的填充搞混
❌ 以为fill_value会改原始DataFrame✅fill_value只作用于生成的透视表,原表不变
- 多函数聚合后按列名直接取列
❌table["1月"]报 KeyError,因为列已经变成多层索引 ✅ 用元组定位table[("sum", "1月")],或先table.columns看层级
aggfunc="count"与"size"混用
❌ 想数行数却用"count",结果NaN没被算进去,数目偏小 ✅"count"数非缺失值,"size"数行数(含缺失),按需求选
- 分组维度里有分类类型时列爆炸
❌ 某个键是category类型且基数很大,透视表出几百列且大量为空 ✅ 注意observed参数:为True时只显示出现过的组合,避免空组合撑爆列
总结
| 需求 | 该怎么做 |
|---|
| 交叉汇总 | pivot_table(values=, index=, columns=, aggfunc=) |
| 默认行为 | aggfunc是"mean",不是"sum" |
| 填空白 | fill_value= |
| 加合计 | margins=True |
| 只重塑不聚合 | pivot(要求组合唯一) |
| 只数频数 | pd.crosstab |
透视表的本质是"分组 + 摊开"。只要记住index/columns是维度、values是度量、aggfunc默认是均值,绝大多数困惑都能避免。 真正动手前,建议先在数据上跑一次groupby确认口径,再换pivot_table摊平。