【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(四):文件读写与实战案例》

posted @ 2026-06-27 08:45  AI视界尼奥  阅读(7)  评论(0)    收藏  举报