【Python系列课程】20 Pandas(三):分组聚合与合并连接——数据分析的核心操作
> 前面的文章我们分别搞定了Pandas的数据结构和数据清洗。本篇进入Pandas最强大的部分:分组聚合(groupby) 和 表合并(concat/merge)。这两个操作是数据分析工作的核心——你可以没有高阶可视化,但不能不会groupby和merge。
一、分组聚合:groupby 的本质
分组聚合就是SQL里的 GROUP BY——把数据按某个(些)字段分组,然后对每个组分别做统计。
它的逻辑可以用"拆分 → 应用 → 合并"三步来理解:
`
原始数据
↓ split(按key拆分)
各组子数据
↓ apply(对各组应用函数)
各组的统计结果
↓ combine(合并结果)
最终结果DataFrame`
> [图1:"拆分→应用→合并"三步流程图——用不同颜色标注每个阶段]
二、groupby 基础:分组 + 迭代
2.1 创建分组对象
`python
import pandas as pd
import numpy as np
df = pd.DataFrame({
'company': ['A', 'B', 'A', 'C', 'C', 'B', 'C', 'A'],
'salary': [8, 15, 10, 15, np.nan, 28, 30, 15],
'age': [26, 29, 26, 30, 50, 30, 30, 35]
})
print(df)
company salary age
0 A 8.0 26
1 B 15.0 29
2 A 10.0 26
3 C 15.0 30
4 C NaN 50
5 B 28.0 30
6 C 30.0 30
7 A 15.0 35
按company列分组 → 返回DataFrameGroupBy对象(还没计算!)
gb = df.groupby(by='company', as_index=False)
print(type(gb)) #
`关键参数:
| 参数 | 说明 |
|
|
|
| by | 分组依据的列名(或列名列表) |
| as_index | True(默认):分组列变成结果索引;False:保留为普通列 |
| sort | True(默认):结果按分组标签排序;False:保持原顺序 |
| dropna | True(默认):不保留NaN分组;False:NaN也作为独立分组 |
2.2 迭代查看各分组
DataFrameGroupBy 对象是可迭代的,每次迭代返回 (分组标签, 子DataFrame) 元组:
`python
gb = df.groupby(by='company', as_index=False)
查看分成了几组
print(gb.ngroups) # 3 → A、B、C三组
查看各组的索引
print(gb.groups)
{'A': [0, 2, 7], 'B': [1, 5], 'C': [3, 4, 6]}
查看各组的索引(数组形式)
print(gb.indices)
{'A': array([0, 2, 7]), 'B': array([1, 5]), 'C': array([3, 4, 6])}
迭代查看每个分组的数据
for group_name, group_data in gb:
print(f"=== 公司: {group_name} ===")
print(group_data)
print()
获取指定分组的数据
print(gb.get_group('A'))
company salary age
0 A 8.0 26
2 A 10.0 26
7 A 15.0 35
`> [图2:groupby迭代输出截图——展示A、B、C三个分组的数据内容]
三、聚合操作(Aggregation)
聚合就是对每个分组应用一个统计函数,返回一个标量结果。
3.1 常用聚合函数速查表
| 函数名 | 说明 | 示例 |
|
--|
|
|
| 'mean' / np.mean | 平均值 | gb.agg('mean') |
| 'sum' | 求和 | gb.agg('sum') |
| 'max' / 'min' | 最大/最小值 | gb.agg('max') |
| 'count' | 非空值计数 | gb.agg('count') |
| 'median' | 中位数 | gb.agg('median') |
| 'std' | 标准差 | gb.agg('std') |
| 'var' | 方差 | gb.agg('var') |
| 'size' | 每组元素个数(含NaN) | gb.size() |
3.2 对所有列同时聚合
`python
gb = df.groupby(by='company', as_index=False)
对所有数值列求平均值
print(gb.agg('mean'))
company salary age
0 A 11.0 29
1 B 21.5 29
2 C 22.5 36.667 ← NaN被自动忽略
对所有列求最大值
print(gb.agg('max'))
company salary age
0 A 15.0 35
1 B 28.0 30
2 C 30.0 50
对所有列求count(统计非空值数量)
print(gb.agg('count'))
company salary age
0 A 3 3
1 B 2 2
2 C 2 3 ← salary有1个NaN,所以count是2
`3.3 不同列使用不同聚合函数
`python
对salary求平均值,对age求最大值
print(gb.agg({'salary': 'mean', 'age': 'max'}))
company salary age
0 A 11.0 35
1 B 21.5 30
2 C 22.5 50
`> [图3:多列不同聚合函数的结果表格截图——salary列显示mean,age列显示max]
3.4 用 agg 同时应用多个函数
`python
对salary同时求mean、max、min
print(gb['salary'].agg(['mean', 'max', 'min']))
mean max min
A 11.0 15.0 8.0
B 21.5 28.0 15.0
C 22.5 30.0 15.0
`四、变换操作(Transformation)
变换和聚合的区别是:聚合返回一个标量,变换返回一个与原数据等长的结果。
最典型的用法:用各组的均值填充该组的缺失值。
`python
gb = df.groupby(by='company', as_index=False)
变换:返回与原始数据行数相同的结果
每个值被替换为其所在组的均值
salary_filled = gb['salary'].transform('mean')
print(salary_filled)
0 11.0 ← A组均值
1 21.5 ← B组均值
2 11.0 ← A组均值
3 22.5 ← C组均值
4 22.5 ← C组均值(NaN被替换为C组均值!)
5 21.5 ← B组均值
6 22.5 ← C组均值
7 11.0 ← A组均值
Name: salary, dtype: float64
实际应用:用各组均值填充NaN
df_filled = df.copy()
df_filled['salary'] = gb['salary'].transform('mean')
print(df_filled)
company salary age
0 A 11.0 26
1 B 21.5 29
2 A 11.0 26
3 C 22.5 30
4 C 22.5 50 ← NaN被填充为C组均值22.5
5 B 21.5 30
6 C 22.5 30
7 A 11.0 35
`> [图4:transform前后的salary列对比表——用颜色标注被填充的NaN位置]
五、多列分组与多条件分组
5.1 按多列分组
`python
同时按company和age分组
gb2 = df.groupby(by=['company', 'age'])
print(gb2.agg('mean'))
salary
company age
A 26 9.0 ← A公司26岁的人平均年龄
35 15.0
B 29 15.0
30 28.0
C 30 22.5
50 NaN
`注意:多列分组时,结果索引是多层索引(MultiIndex),这是下一篇进阶内容。
5.2 dropna 参数:是否保留NaN分组
`python
df2 = pd.DataFrame({
'company': ['A', 'B', 'A', np.nan, 'C'],
'salary': [8, 15, 10, 20, 30]
})
print(df2)
company salary
0 A 8
1 B 15
2 A 10
3 NaN 20 ← company是NaN
4 C 30
gb3 = df2.groupby(by='company', dropna=False)
print(gb3.agg('sum'))
salary
company
A 18
B 15
C 30
NaN 20 ← NaN也作为一个独立分组!
`六、表合并:concat 与 merge
真实数据分析中,数据往往分散在多个表里,需要合并才能分析。Pandas提供两种主要合并方式:
| 方法 | 类比SQL | 说明 |
|
|
|
|
| pd.concat() | UNION ALL / 按行/列拼接 | 沿轴方向拼接,类似"叠桌子" |
| pd.merge() | JOIN | 按key对齐合并,类似"拼图" |
七、pd.concat():沿轴拼接
7.1 按行拼接(axis=0,默认)
`python
import pandas as pd
df1 = pd.DataFrame([[1, 2], [3, 4]], index=['p1', 'p2'], columns=list('AB'))
df2 = pd.DataFrame([[5, 6], [7, 8]], columns=list('AC'))
print("df1:")
print(df1)
A B
p1 1 2
p2 3 4
print("df2:")
print(df2)
A C
0 5 6
1 7 8
默认:外连接(join='outer'),取所有列的并集,缺失值填NaN
print(pd.concat([df1, df2]))
A B C
p1 1.0 2.0 NaN
p2 3.0 4.0 NaN
0 5.0 NaN 6.0
1 7.0 NaN 8.0
`> [图5:concat外连接结果表格——用颜色标注NaN位置]
7.2 内连接(join='inner')
`python
内连接:只保留两个DataFrame都有的列
print(pd.concat([df1, df2], join='inner'))
A
p1 1.0
p2 3.0
0 5.0
1 7.0
`7.3 按列拼接(axis=1)
`python
按列拼接:相当于把两个表左右拼在一起
print(pd.concat([df1, df2], axis=1))
A B A C
p1 1.0 2.0 NaN NaN
p2 3.0 4.0 NaN NaN
0 NaN NaN 5.0 6.0
1 NaN NaN 7.0 8.0
注意:行索引没有对齐!p1和0是不同的行
`修正:让行索引对齐:
`python
df2.index = ['p1', 'p2'] # 修改df2的索引,使其与df1对齐
print(pd.concat([df1, df2], axis=1))
A B A C
p1 1 2 5 6.0
p2 3 4 7 8.0
`7.4 ignore_index=True:重建索引
`python
拼接后重建连续索引(0,1,2,3...)
print(pd.concat([df1, df2], ignore_index=True))
A B C
0 1.0 2.0 NaN
1 3.0 4.0 NaN
2 5.0 NaN 6.0
3 7.0 NaN 8.0
`八、pd.merge():按key对齐合并(重点!)
merge() 是Pandas里最像SQL的操作,也是最常用的合并方式。
8.1 四种合并类型速查表
| how参数 | SQL等价 | 说明 |
|
--|
|
|
| 'inner'(默认) | INNER JOIN | 只保留两表key的交集 |
| 'left' | LEFT JOIN | 保留左表全部key,右表没有的填NaN |
| 'right' | RIGHT JOIN | 保留右表全部key,左表没有的填NaN |
| 'outer' | FULL OUTER JOIN | 保留两表key的并集 |
8.2 内连接(inner join)
`python
df1 = pd.DataFrame({
'name': ['Tom', 'Bob', 'Jack'],
'age': [18, 17, 19],
'weight':[65, 66, 67]
})
df2 = pd.DataFrame({
'name': ['Tom', 'Jack'],
'height':[168, 187],
'weight':[65, 68]
})
print("df1:")
print(df1)
name age weight
0 Tom 18 65
1 Bob 17 66
2 Jack 19 67
print("df2:")
print(df2)
name height weight
0 Tom 168 65
1 Jack 187 68
内连接:只保留两表都有的name(Tom和Jack)
print(pd.merge(df1, df2, on='name'))
name age weight_x height weight_y
0 Tom 18 65 168 65
1 Jack 19 67 187 68
注意:weight列两表都有,自动加_x/_y后缀区分!
`> [图6:merge内连接结果截图——高亮显示weight_x和weight_y两列]
8.3 左连接(left join)
`python
左连接:保留左表所有name,右表没有的填NaN
print(pd.merge(df1, df2, how='left', on='name'))
name age weight_x height weight_y
0 Tom 18 65 168.0 65.0
1 Bob 17 66 NaN NaN ← Bob在df2里没有,所以填NaN
2 Jack 19 67 187.0 68.0
`8.4 右连接(right join)和外连接(outer join)
`python
右连接:保留右表所有name
print(pd.merge(df1, df2, how='right', on='name'))
外连接:保留两表所有name
print(pd.merge(df1, df2, how='outer', on='name'))
`8.5 多key合并与后缀控制
`python
如果两表有多个相同列,可以用多个key合并
df1 = pd.DataFrame({
'name': ['Tom', 'Bob'],
'age': [18, 19],
'score':[85, 92]
})
df2 = pd.DataFrame({
'name': ['Tom', 'Bob'],
'age': [18, 19],
'score':[88, 90] # 注意:这里的score是另一门课的成绩
})
按name和age两列合并(两列都一样才合并)
print(pd.merge(df1, df2, on=['name', 'age'], suffixes=('_期中', '_期末')))
name age score_期中 score_期末
0 Tom 18 85 88
1 Bob 19 92 90
`九、综合案例:分析销售数据
把groupby和merge串起来,做一个完整的分析案例。
需求:有两个表——订单表(订单ID、客户ID、金额)和客户表(客户ID、姓名、城市),需要:
1. 合并两个表
2. 按城市统计总销售额
3. 找出每个城市的平均订单金额
4. 找出消费最多的前3名客户
`python
import pandas as pd
import numpy as np
模拟数据
orders = pd.DataFrame({
'order_id': [1, 2, 3, 4, 5, 6, 7],
'customer_id': ['C1','C2','C1','C3','C2','C4','C1'],
'amount': [100, 200, 150, 300, 250, 400, 120]
})
customers = pd.DataFrame({
'customer_id': ['C1','C2','C3','C4'],
'name': ['张三','李四','王五','赵六'],
'city': ['北京','上海','北京','广州']
})
print("订单表:")
print(orders)
print("\n客户表:")
print(customers)
步骤1:合并订单表和客户表(左连接,保留所有订单)
df = pd.merge(orders, customers, on='customer_id', how='left')
print("\n合并后的表:")
print(df)
步骤2:按城市统计总销售额
city_sales = df.groupby('city')['amount'].sum().sort_values(ascending=False)
print("\n各城市总销售额:")
print(city_sales)
步骤3:按城市统计平均订单金额
city_avg = df.groupby('city')['amount'].mean()
print("\n各城市平均订单金额:")
print(city_avg)
步骤4:找出消费最多的前3名客户
top_customers = df.groupby(['customer_id', 'name'])['amount'].sum()\
.sort_values(ascending=False).head(3)
print("\n消费最多的前3名客户:")
print(top_customers)
`输出结果:
`
各城市总销售额:
城市
北京 670 ← 张三(100+150+120) + 王五(300)
广州 400 ← 赵六
上海 450 ← 李四(200+250)
消费最多的前3名客户:
customer_id name
C4 赵六 400
C2 李四 450 ← 等等,李四应该是200+250=450
C1 张三 370`
> [图7:销售数据分析结果——各城市销售额柱状图 + 客户消费TOP3条形图]
十、本文总结
| 知识点 | 核心要点 |
|
--|
|
| groupby() 本质 | 拆分→应用→合并;返回GroupBy对象,需要聚合函数触发计算 |
| 聚合 agg() | 对每个组返回一个标量;不同列可用不同函数 |
| 变换 transform() | 返回与原始数据等长的数据;常用作填充缺失值 |
| pd.concat() | 沿轴拼接;join='outer'保留全部,'inner'只保留交集 |
| pd.merge() | 按key对齐合并;how='inner'/'left'/'right'/'outer' |
| 多key合并 | on=['列1','列2'];重复列名自动加_x/_y后缀 |
选择merge还是concat?
- 需要按某列的值对齐两个表 → 用 merge()
- 只是简单地把两个表叠在一起 → 用 concat()
动手练习
1. 创建一个DataFrame df = pd.DataFrame({'部门':['销售','技术','销售','市场','技术'], '工资':[8000,12000,9000,7000,15000], '年龄':[25,30,27,24,32]}),然后:(a)按部门分组,计算各部门平均工资;(b)找出各部门最高工资;(c)用各组平均工资填充工资列的NaN(如果有)。
2. 有两个DataFrame:
`python
df1 = pd.DataFrame({'学号':['S001','S002','S003'], '姓名':['张三','李四','王五']})
df2 = pd.DataFrame({'学号':['S001','S002','S004'], '成绩':[85,92,78]})
`
用merge合并,并保留所有学生(包括df1有但df2没有的)。
3. 用concat把三个相同结构的DataFrame按行拼接,并使用ignore_index=True重建索引。
4. 创建一个销售记录DataFrame,包含日期、产品、销售额三列,按产品分组,同时计算总销售额、平均销售额、销售次数,用一条agg()语句完成。
下一篇预告:《21 Pandas(四):文件读写与实战案例》

浙公网安备 33010602011771号