【Python系列课程】Pandas(五):分组聚合与函数应用——groupby、apply
📊 阅读时长:20分钟 | 关键词:Pandas、groupby分组聚合、apply函数应用、DataFrame运算
引言
如果说前几篇文章是"数据整理基本功",那 groupby() 就是 Pandas 的"生产力引擎"。在真实数据分析中,几乎永远在回答这类问题:“每个部门的平均工资是多少?”“各产品线的月销量趋势?”“不同用户分层的消费行为?”——本质上都是"分组,然后聚合"。这篇文章带你彻底掌握它。
一、groupby() 分组聚合(核心!)
1.1 基本用法
groupby() 的核心逻辑是 split-apply-combine(分割-应用-合并):
原始数据 按 company 分组 聚合(求平均) 合并结果
+-------+------+ +---+----+------+ +---+------+-----+ +---+------+-----+
|company|salary| | A | 8 | 10|15| | A | 11.0 |29.0| | A | 11.0 |29.0|
| A | 8 | --> | | | | | --> | B | ? | ? | -> | B | 21.5 |29.5|
| B | 15 | +---+----+---+--+ | C | ? | ? | | C | 22.5 |36.7|
| A | 10 | +---+----+------+ +---+------+-----+ +---+------+-----+
| C | 15 | | B | 15 | 28 |
| C | NaN | +---+----+----+
| B | 28 | +---+----+------+
| C | 30 | | C | 15 | NaN|30|
| A | 15 | +---+----+---+--+
+-------+------+
代码实现:
import pandas as pd
import numpy as np
d = {
'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]
}
df = pd.DataFrame(data=d)
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
1.2 分组迭代
# groupby 返回 DataFrameGroupBy 对象
df_gb = df.groupby(by='company', as_index=False)
# 可以迭代查看每个分组
for g, data in df_gb:
print(g)
print(data)
输出:
A
company salary age
0 A 8.0 26
2 A 10.0 26
7 A 15.0 35
B
company salary age
1 B 15.0 29
5 B 28.0 30
C
company salary age
3 C 15.0 30
4 C NaN 50
6 C 30.0 30
1.3 分组属性
print(df_gb.ngroups) # 分成了几组:3
print(df_gb.groups) # 各分组的索引
print(df_gb.indices) # 各分组的索引(与 groups 类似)
1.4 获取指定分组
print(df_gb.get_group('A'))
print(df_gb.get_group('B'))
print(df_gb.get_group('C'))
1.5 聚合操作(agg)
这是 groupby() 最核心的能力:
# 对每个分组做聚合
print(df_gb.agg('mean')) # 平均值
print(df_gb.agg('sum')) # 求和
print(df_gb.agg('max')) # 最大值
print(df_gb.agg('min')) # 最小值
print(df_gb.agg('median')) # 中位数
print(df_gb.agg('std')) # 标准差
print(df_gb.agg('var')) # 方差
print(df_gb.agg('count')) # 计数
# 也可以直接用 np 函数
print(df_gb.agg(np.mean))
mean() 结果:
company salary age
0 A 11.000000 29.000000
1 B 21.500000 29.500000
2 C 22.500000 36.666667
📊 一眼看出:C 公司平均薪资最高(22.5),A 公司平均年龄最低(29)。
1.6 变换操作(transform)
agg() 把多行压成一行,transform() 保持原始行数,把聚合结果广播回去:
# transform 保持原始形状
print(df_gb.transform('mean'))
salary age
0 11.000000 29.000000
1 21.500000 29.500000
2 11.000000 29.000000
3 22.500000 36.666667
4 22.500000 36.666667
5 21.500000 29.500000
6 22.500000 36.666667
7 11.000000 29.000000
这个特性的妙用——给每行新增"所属分组的平均值"列:
# 一次性新增两列:各组平均薪资、平均年龄
df[['avg_salary', 'avg_age']] = df_gb.transform('mean')
print(df)
company salary age avg_salary avg_age
0 A 8.0 26 11.000000 29.000000
1 B 15.0 29 21.500000 29.500000
2 A 10.0 26 11.000000 29.000000
3 C 15.0 30 22.500000 36.666667
4 C NaN 50 22.500000 36.666667
5 B 28.0 30 21.500000 29.500000
6 C 30.0 30 22.500000 36.666667
7 A 15.0 35 11.000000 29.000000
1.7 参数详解
# as_index=False:分组列不作为索引,而是保留为普通列
df_gb = df.groupby(by='company', as_index=False)
# sort=False:结果不按分组标签排序(性能更高)
df_gb = df.groupby(by='salary', sort=False)
# dropna=False:分组列中的 NaN 也保留为一个分组
df_gb = df.groupby(by='salary', dropna=False)
# 按多列分组
df_gb = df.groupby(by=['age', 'company'])
| 参数 | 说明 |
|---|---|
by |
按哪个(哪些)列分组 |
as_index |
True 分组列作为索引(默认);False 保留为普通列 |
sort |
True 结果按分组标签排序(默认);False 不排序 |
dropna |
True 忽略 NaN 分组(默认);False 保留 NaN 分组 |
二、apply() 函数应用
apply() 比 agg() 更灵活——你可以传入任意自定义函数,对每一行或每一列做处理。
d = [[1, 2, 0], [4, 1, 9], [2, 5, 7], [4, 3, 6]]
df = pd.DataFrame(d, columns=['A', 'B', 'C'])
print(df)
A B C
0 1 2 0
1 4 1 9
2 2 5 7
3 4 3 6
# axis=0:对每一列应用函数(默认)
print(df.apply(np.sum))
# axis=1:对每一行应用函数
print(df.apply(np.sum, axis=1))
输出:
# axis=0(每列求和)
A 11
B 11
C 22
# axis=1(每行求和)
0 3
1 14
2 14
3 13
agg() vs apply() 区别:
agg() |
apply() |
|
|---|---|---|
| 适用场景 | groupby 后的聚合操作 | 任意自定义函数 |
| 灵活性 | 只能用内置聚合函数或 np 函数 | 可以传任意自定义函数 |
| 典型用途 | df_gb.agg('mean') |
df.apply(custom_func, axis=1) |
💡 简单说:
agg()做"分组统计",apply()做"自定义处理"。
三、DataFrame 运算
DataFrame 保留了 NumPy 的数组运算能力,且运算时索引自动对齐。
3.1 与标量运算
d = np.arange(9).reshape((3, 3))
df1 = pd.DataFrame(data=d, columns=list('abc'), index=['n1', 'n2', 'n3'])
print(df1)
print(df1 + 1) # 每个元素 +1
print(df1 - 1) # 每个元素 -1
print(df1 * 2) # 每个元素 ×2
print(df1 / 2) # 每个元素 ÷2
3.2 DataFrame 之间的运算(索引对齐)
这是 NumPy 没有的特性——两个 DataFrame 运算时,相同标签的值做运算,不同标签的做并集并填 NaN。
d = np.arange(16).reshape((4, 4))
df2 = pd.DataFrame(data=d, columns=list('dacf'), index=['n1', 'n2', 'n3', 'n4'])
print(df2)
d a c f
n1 0 1 2 3
n2 4 5 6 7
n3 8 9 10 11
n4 12 13 14 15
# df1 和 df2 做运算,相同标签的值运算,不同标签的填 NaN
print(df1 + df2)
print(df1 - df2)
print(df1 * df2)
print(df1 / df2)
df1 + df2 的结果:
a b c d f
n1 1.0 NaN 4.0 NaN NaN
n2 9.0 NaN 12.0 NaN NaN
n3 17.0 NaN 18.0 NaN NaN
n4 NaN NaN NaN NaN NaN
df1 列索引是 a, b, c,行索引是 n1, n2, n3。df2 列索引是 d, a, c, f,行索引是 n1, n2, n3, n4。只有 a 和 c 两列在两者中都存在(共有的标签),它们的值被成功运算。b, d, f 在不同表中各有缺失,以及行 n4 只在 df2 存在,所以结果为 NaN。
⚠️ 核心规则:相同行索引名 + 相同列索引名的值进行运算,其余做并集填充 NaN。
小结
| 序号 | 知识点 | 一句话总结 |
|---|---|---|
| 1 | groupby() |
split-apply-combine:先分组、再聚合、最后合并 |
| 2 | agg() |
分组后做统计(mean/sum/count 等),多行聚合为一行 |
| 3 | transform() |
保持行数不变,把聚合结果广播回每行 |
| 4 | apply() |
对每列或每行应用自定义函数,比 agg 更灵活 |
| 5 | DataFrame 运算 | 保留 NumPy 数组运算能力,索引自动对齐,不同标签填 NaN |
groupby() 是 Pandas 的精髓——掌握了它,你就从"数据处理"迈入了"数据分析"的门槛。
下一篇文章,我们将学 Pandas 的"最后一公里"——CSV 和 Excel 文件读写。数据最终要存下来、交给别人看,文件操作是必经之路。
本文是「Python从入门到数据分析」系列的第 17 篇。关注我,不错过后续更新。
更多推荐
所有评论(0)