第 4.1 节 核心知识点#
预计学习时长: 45 分钟
本节目标:
- 知道
groupby()到底做了什么(拆 → 算 → 拼);- 会用
sum/mean/count/size/max/min做统计;- 会用
agg()给不同列指定不同的聚合函数;- 分得清
agg()(每组出一个值)和transform()(每组出一个值再广播回每一行);- 会用
pivot_table()做二维汇总、crosstab()做交叉频数;- 会按多个字段做多级分组。
先说清楚一件事:groupby 到底做了什么#
一句话:拆 → 算 → 拼。
准备数据:
import pandas as pd
data = {
"订单号": [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008],
"下单日期": ["2024-01-05", "2024-01-08", "2024-01-12", "2024-02-03",
"2024-02-15", "2024-03-02", "2024-03-20", "2024-04-08"],
"城市": ["北京", "上海", "北京", "广州", "上海", "北京", "深圳", "广州"],
"商品": ["手机", "电脑", "耳机", "手机", "键盘", "显示器", "耳机", "手机"],
"销量": [2, 1, 3, 2, 5, 1, 4, 2],
"单价": [3999.0, 7999.0, 599.0, 3999.0, 299.0, 1299.0, 599.0, 3999.0],
"金额": [7998.0, 7999.0, 1797.0, 7998.0, 1495.0, 1299.0, 2396.0, 7998.0],
"客户评分": [4.5, 4.0, 4.8, 3.9, 4.2, 4.6, 4.1, 4.9],
}
df = pd.DataFrame(data)我们想知道「每个城市一共卖了多少钱」。
Excel 里的做法:插入 → 数据透视表 → 城市拖到行标签,金额拖到值。
Pandas 里的做法:
result = df.groupby("城市")["金额"].sum()
print(result)输出:
城市
上海 9494.0
北京 11094.0
广州 15996.0
深圳 2396.0
Name: 金额, dtype: float64背后的三个步骤:
- 拆——按「城市」把 8 行拆成 4 组:北京 3 行、上海 2 行、广州 2 行、深圳 1 行;
- 算——每一组单独把「金额」加起来;
- 拼——把 4 个和拼成一张表,「城市」变成了索引。
注意结果的两点变化
- 结果是 Series,不是 DataFrame(因为你只统计了一列);
- 「城市」不再是普通列,变成了索引。
这两点在第 2 章讲过,现在你会知道它们是 groupby 的必然结果。想让「城市」变回列,后面会讲
reset_index()。
分组键可以是一列,也可以是多列#
df.groupby("城市") # 按一列分
df.groupby(["城市", "商品"]) # 按两列分分组键可以是函数、字典,甚至数组#
# 按金额高低分两档
df.groupby(df["金额"] > 5000)
# 用字典做映射分组
df.groupby({"城市": "区域", "商品": "品类"}.get)这些比较进阶,先记住有这回事就行。
知识点 1:GroupBy Basics(分组操作基础)#
三种写法,结果完全一样#
df.groupby("城市")["金额"].sum() # 推荐:先分组再选列
df.groupby("城市").sum()["金额"] # 先全表聚合再选列(会算所有数值列)
df["金额"].groupby(df["城市"]).sum() # 老写法,不推荐用第一种
df.groupby("列")["要算的列"].聚合()。 第二种会把「销量」「单价」「客户评分」全都算一遍,慢而且结果里一堆你不需要的列。
看每个组有几行:size()#
print(df.groupby("城市").size())输出:
城市
上海 2
北京 3
广州 2
深圳 1
dtype: int64为什么顺序是「上海、北京、广州、深圳」?#
groupby() 默认会按分组键排序(参数 sort=True)。而中文的排序是按 Unicode 码点来的,「上」的码点比「北」小,所以「上海」排第一。
想保持原始出现顺序,加 sort=False:
df.groupby("城市", sort=False).size()
# 北京 3, 上海 2, 广州 2, 深圳 1和
value_counts()对比一下df["城市"].value_counts() # 北京 3, 上海 2, 广州 2, 深圳 1
value_counts()是按数量从多到少排的,而且结果里分组键是普通列不是索引。 要计数的话,两种都行;但记住它们的排序规则不一样,别被绕晕。
知识点 2:Aggregation Functions(聚合函数)#
常用的六个#
g = df.groupby("城市")["金额"]
g.sum() # 总和
g.mean() # 平均值
g.count() # 计数(跳过缺失值)
g.size() # 分组大小(不跳缺失值)
g.max() # 最大值
g.min() # 最小值一次看全:
print(df.groupby("城市")["金额"].agg(["count", "mean", "sum", "max", "min"]))输出:
count mean sum max min
城市
上海 2 4747.0 9494.0 7999.0 1495.0
北京 3 3698.0 11094.0 7998.0 1299.0
广州 2 7998.0 15996.0 7998.0 7998.0
深圳 1 2396.0 2396.0 2396.0 2396.0传一个列表进去,就能一次算多个统计量——这是最常用的写法。
count() 和 size() 有什么区别?#
这是个高频问题。假设「金额」里有一个缺失值:
import numpy as np
d = df.copy()
d.loc[1, "金额"] = np.nan
print(d.groupby("城市")["金额"].count()) # 上海少了一个
print(d.groupby("城市").size()) # 全部照常输出:
城市
上海 1
北京 3
广州 2
深圳 1
dtype: int64城市
上海 2
北京 3
广州 2
深圳 1
dtype: int64原因:
count()统计的是「这一列里有多少个非空值」,NaN 不算数;size()统计的是「这个组有几行」,不管单元格里有没有值。
选择原则
- 想统计「每组有几条记录」→ 用
size()(或者count()但要确保没缺失值);- 想统计「每组有几家客户填了表」→ 用
count()。一旦你关心的是「笔数」,
size()更安全,因为它不会被缺失值干扰。
换别的聚合函数#
除了内置的,还可以传自己的函数:
# 求最大值和最小值的差(极差)
df.groupby("城市")["金额"].agg(lambda x: x.max() - x.min())输出:
城市
上海 6504.0
北京 6699.0
广州 0.0
深圳 0.0
dtype: float64agg():给不同列指定不同的聚合函数#
当你要一次算好几列、而且每列的算法不一样时,用 agg() 传字典:
print(df.groupby("城市").agg({"金额": "sum", "销量": "mean"}))输出:
金额 销量
城市
上海 9494.0 3.0
北京 11094.0 2.0
广州 15996.0 2.0
深圳 2396.0 4.0同一列要多个统计量,传元组列表:
print(df.groupby("城市").agg({"金额": ["sum", "mean"], "销量": "sum"}))输出:
金额 销量
sum mean sum
城市
上海 9494.0 4747.0 6
北京 11094.0 3698.0 6
广州 15996.0 7998.0 4
深圳 2396.0 2396.0 4想让输出列名更好看,用命名聚合(注意值是元组):
print(df.groupby("城市").agg(总金额=("金额", "sum"), 订单数=("订单号", "count")))输出:
总金额 订单数
城市
上海 9494.0 2
北京 11094.0 3
广州 15996.0 2
深圳 2396.0 1命名聚合的写法是
agg(新列名=("原列名", "函数"))——值必须是元组。 写成agg(总金额="sum")会报TypeError: Must provide 'func' or tuples of '(column, aggfunc)'。
agg() 和 transform() 有什么区别?#
这是本章第二大坑。
agg():每组算出一个值,组里有几行就出几行结果;transform():每组算出一个值,然后广播回组里的每一行,结果长度和原表一样。
先看 agg():
print(df.groupby("城市")["金额"].agg("sum"))城市
上海 9494.0
北京 11094.0
广州 15996.0
深圳 2396.04 行(4 个城市)。
再看 transform():
print(df.groupby("城市")["金额"].transform("sum"))0 11094.0
1 9494.0
2 11094.0
3 15996.0
4 9494.0
5 11094.0
6 2396.0
7 15996.08 行,和原表一样长。第 0 行是北京,值是北京的总金额 11094;第 1 行是上海,值 9494……
transform() 最大的用途是「造新列」:
df = df.copy()
df["城市总金额"] = df.groupby("城市")["金额"].transform("sum")
print(df[["城市", "金额", "城市总金额"]]) 城市 金额 城市总金额
0 北京 7998.0 11094.0
1 上海 7999.0 9494.0
2 北京 1797.0 11094.0
3 广州 7998.0 15996.0
4 上海 1495.0 9494.0
5 北京 1299.0 11094.0
6 深圳 2396.0 2396.0
7 广州 7998.0 15996.0现在你可以直接算「这笔订单占本城市总金额的比例」:
df["占比"] = df["金额"] / df["城市总金额"]
print(df[["城市", "金额", "占比"]].round(3)) 城市 金额 占比
0 北京 7998.0 0.721
1 上海 7999.0 0.843
2 北京 1797.0 0.162
3 广州 7998.0 0.500
4 上海 1495.0 0.157
5 北京 1299.0 0.117
6 深圳 2396.0 1.000
7 广州 7998.0 0.500一句话选型
- 要汇总成一张小表(每个组一行)→
agg()- 要保留原表每一行,只是加上组级别的信息 →
transform()
知识点 3:Pivot Tables(透视表)#
groupby() 是「一维」分组——只按一个维度分。透视表是「二维」——同时按行和列分。
print(pd.pivot_table(df, index="城市", columns="商品", values="金额", aggfunc="sum"))输出:
商品 手机 显示器 电脑 耳机 键盘
城市
上海 NaN NaN 7999.0 NaN 1495.0
北京 7998.0 1299.0 NaN 1797.0 NaN
广州 15996.0 NaN NaN NaN NaN
深圳 NaN NaN NaN 2396.0 NaN读法:行是城市,列是商品,格子里的数字是金额之和。
NaN 是怎么回事?#
北京没有卖过「电脑」,所以那个格子是空的。这是透视表的正常行为——没有数据的位置就是 NaN。
想让空格子显示成 0,加 fill_value=0:
print(pd.pivot_table(df, index="城市", columns="商品", values="金额",
aggfunc="sum", fill_value=0))商品 手机 显示器 电脑 耳机 键盘
城市
上海 0.0 0.0 7999.0 0.0 1495.0
北京 7998.0 1299.0 0.0 1797.0 0.0
广州 15996.0 0.0 0.0 0.0 0.0
深圳 0.0 0.0 0.0 2396.0 0.0四个核心参数#
pd.pivot_table(
df, # 数据源
index="城市", # 行:按谁分行
columns="商品", # 列:按谁分列
values="金额", # 值:算什么
aggfunc="sum" # 用什么函数算
)| 参数 | 作用 |
|---|---|
index | 行的分组依据 |
columns | 列的分组依据 |
values | 要被聚合的列 |
aggfunc | 聚合方式,默认是 "mean" |
aggfunc的默认值是「平均」不是「求和」!pd.pivot_table(df, index="城市", values="金额") # 默认 mean pd.pivot_table(df, index="城市", values="金额", aggfunc="sum") # 求和这个默认值经常坑到人。写透视表时,养成习惯把
aggfunc明确写出来。
加一行合计#
print(pd.pivot_table(df, index="城市", values="金额", aggfunc="sum",
margins=True, margins_name="合计")) 金额
城市
上海 9494.0
北京 11094.0
广州 15996.0
深圳 2396.0
合计 38980.0margins=True 加合计,margins_name="合计" 给它起个中文名(默认叫 "All")。
一次算多列#
print(pd.pivot_table(df, index="城市", values=["金额", "销量"], aggfunc="sum")) 金额 销量
城市
上海 9494.0 6
北京 11094.0 6
广州 15996.0 4
深圳 2396.0 4知识点 4:Cross Tabulation(交叉表)#
如果只关心「频数」(各组合出现了多少次),crosstab() 更直接:
print(pd.crosstab(df["城市"], df["商品"]))输出:
商品 手机 显示器 电脑 耳机 键盘
城市
上海 0 0 1 0 1
北京 1 1 0 1 0
广州 2 0 0 0 0
深圳 0 0 0 1 0读法:上海有 1 笔电脑、1 笔键盘;广州有 2 笔手机。
对照一下 pivot_table 做的「金额求和」,crosstab 做的是「数个数」。
crosstab 还能算比例#
print(pd.crosstab(df["城市"], df["商品"], normalize="index").round(2))商品 手机 显示器 电脑 耳机 键盘
城市
上海 0.00 0.00 0.50 0.00 0.50
北京 0.33 0.33 0.00 0.33 0.00
广州 1.00 0.00 0.00 0.00 0.00
深圳 0.00 0.00 0.00 1.00 0.00normalize="index" 是「按行归一化」——每一行加起来等于 1。上面表示:广州的订单 100% 是手机,北京的订单里手机、显示器、耳机各占三分之一。
还可以 normalize="columns"(按列归一化)或 normalize="all"(按总计归一化)。
crosstab 和 pivot_table 怎么选?#
crosstab() | pivot_table() | |
|---|---|---|
| 默认算什么 | 频数 | 平均值(aggfunc="mean") |
| 主要用途 | 看两个分类变量的组合分布 | 做带数值汇总的报表 |
| 参数风格 | crosstab(行, 列) | pivot_table(df, index=, columns=, values=, aggfunc=) |
速记
- 只想知道「有多少笔」→
crosstab()- 想知道「每格是多少钱、卖了多少件」→
pivot_table()
知识点 5:Multi-Level GroupBy(多级分组)#
按多个字段分组,就是「先按城市分,组内再按商品分」。
print(df.groupby(["城市", "商品"])["金额"].agg(["count", "sum"]))输出:
count sum
城市 商品
上海 电脑 1 7999.0
键盘 1 1495.0
北京 手机 2 7998.0
显示器 1 1299.0
耳机 1 1797.0
广州 手机 2 15996.0
深圳 耳机 1 2396.0注意结果索引是两层的:外层是城市,内层是商品。层级之间有一个空行,那是为了让你看清分组边界。
层级索引怎么取用?#
g = df.groupby(["城市", "商品"])["金额"].sum()
g.loc["北京"] # 取北京这一组(内层索引被保留)
g.loc["北京", "手机"] # 取北京-手机这一组
g.loc["北京"].sum() # 汇总北京下面所有商品
g.index.levels[0] # 第 0 层:城市列表换回普通列#
print(df.groupby(["城市", "商品"])["金额"].sum().reset_index()) 城市 商品 金额
0 上海 电脑 7999.0
1 上海 键盘 1495.0
2 北京 手机 7998.0
3 北京 显示器 1299.0
4 北京 耳机 1797.0
5 广州 手机 15996.0
6 深圳 耳机 2396.0reset_index() 把两层索引都变回普通列——第 2 章讲过,这里是它的主要用途之一。
分组结果里的缺失值#
分组统计时,某组某个值缺失会让这一组的统计变 NaN。这是 Pandas 聚合函数的默认行为(skipna=True 表示跳过缺失值再算)。
比如「金额」里有一格是 NaN,北京组的 mean 只会拿剩下的值算:
d = df.copy()
d.loc[1, "金额"] = np.nan
print(d.groupby("城市")["金额"].mean())城市
上海 1495.0
北京 3698.0
广州 7998.0
深圳 2396.0
dtype: float64上海的金额本来有 7999.0 和 1495.0 两个,现在一个是 NaN。mean() 跳过 NaN,只拿 1495.0 一个来算,平均值就是 1495.0。
mean()默认跳过缺失值(参数skipna=True),所以有缺失时它的分母会变小——这通常正是你想要的。 但如果你希望「把缺失当 0 一起算」,得先fillna(0)再分组;如果你想看的是「几家客户填了表」,用count()而不是mean()。
本节小结#
groupby()= 拆 → 算 → 拼,结果里分组键变成索引。- 常用聚合:
summeancountsizemaxmin;传列表可以一次算多个。 count()数的是「非空值个数」,size()数的是「行数」——要数笔数就用size()。agg()给不同列不同算法:传字典;命名聚合写成agg(新名=("原列名", "函数"))。agg()每组出一个值(结果变短),transform()每组出一个值再广播回每行(结果不变长)。pivot_table()是二维透视表,aggfunc默认是mean不是sum,记得显式写。crosstab()专门数频数,支持normalize算比例。- 多级分组用
groupby(["A", "B"]),结果是两层索引,reset_index()可以摊平。
下一节预告 现在你能从一张表做出汇总报表了。但如果报表要「合上另一张表」的数据——比如订单表要带上客户表里的客户名——就需要第 5 章的
merge()。而当你要把「每个季度一列」的长表变成「每行一个季度」的明细时,melt()就派上用场了。