一、为什么要做数据分组
大部分时候,我们手头的数据都是杂乱无章的。比如一个超市的销售记录,里面有日期、商品类别、价格、销量等等。如果直接看原始表格,很难发现什么规律。但要是按商品类别把数据分成几组,再算算每组的总销量或者平均价格,那就一目了然了。这就是分组分析的价值——它能帮我们从整体中找出不同群体之间的差异和共性。
Pandas 里的分组操作非常灵活,它不光是简单的“分组+求和”,还能做很多高级变换。比如你想知道每个销售员每个月的业绩排名,或者想挑出那些销售额连续三个月都在下降的客户,这些都可以用分组方法实现。这篇文章会一步步带你走完这些技巧,让你以后处理数据时不再只会写 groupby().sum()。
二、分组操作的基础:groupby
2.1 groupby 的基本用法
先造一份模拟数据,假设你有一家小公司的员工信息,包括部门、姓名和月薪。
技术栈:Python + Pandas
import pandas as pd
# 构造示例数据
data = {
'部门': ['技术', '技术', '市场', '市场', '技术', '市场'],
'姓名': ['张三', '李四', '王五', '赵六', '陈七', '周八'],
'月薪': [15000, 18000, 12000, 11000, 20000, 13000]
}
df = pd.DataFrame(data)
print("原始数据:")
print(df)
输出:
部门 姓名 月薪
0 技术 张三 15000
1 技术 李四 18000
2 市场 王五 12000
3 市场 赵六 11000
4 技术 陈七 20000
5 市场 周八 13000
现在我们来按“部门”分组,并计算每个部门的平均月薪:
# 按部门分组,取月薪列求均值
result = df.groupby('部门')['月薪'].mean()
print("各部门平均月薪:")
print(result)
输出:
部门
市场 12000.0
技术 17666.7
Name: 月薪, dtype: float64
groupby 返回的是一个 GroupBy 对象,它不会直接给你一个表格,而是等待你后面加上聚合函数(比如 mean、sum、count)。这种设计的好处是你可以链式调用,非常灵活。
2.2 多级分组
有时候我们需要按多个维度分组。比如上面的员工数据,再增加一个“子公司”字段,想看每个子公司每个部门的平均薪酬。
# 增加子公司字段
df['子公司'] = ['北京', '北京', '上海', '上海', '上海', '北京']
print("新增子公司列后:")
print(df)
# 按子公司和部门两列分组
multi_group = df.groupby(['子公司', '部门'])['月薪'].mean()
print("每个子公司各部门平均月薪:")
print(multi_group)
输出:
部门 姓名 月薪 子公司
0 技术 张三 15000 北京
1 技术 李四 18000 北京
2 市场 王五 12000 上海
3 市场 赵六 11000 上海
4 技术 陈七 20000 上海
5 市场 周八 13000 北京
每个子公司各部门平均月薪:
子公司 部门
北京 市场 13000.0
技术 16500.0
上海 市场 11500.0
技术 20000.0
Name: 月薪, dtype: float64
多级分组返回的结果是一个带有多级索引(MultiIndex)的 Series,看起来像两层的表格。如果觉得这样不直观,可以调用 reset_index() 把它变成普通表格。
三、聚合函数与 agg 方法
3.1 常见聚合函数
Pandas 内置了很多聚合函数,常用的有:
sum():求和
mean():均值
count():非空值计数
min() / max():最小值 / 最大值
std() / var():标准差 / 方差
还是用刚才的员工数据,我们一次算出每个部门的总月薪、人数和最高月薪:
# 对月薪列同时做多个聚合
agg_result = df.groupby('部门')['月薪'].agg(['sum', 'count', 'max'])
print("各部门统计:")
print(agg_result)
输出:
sum count max
部门
市场 36000 3 13000
技术 53000 3 20000
agg 可以接收一个函数列表,Pandas 会自动把每个函数名作为列名。
3.2 使用 agg 进行多样化聚合
更强大的是,你可以为不同的列指定不同的聚合方式。比如你想同时看每个部门的“月薪均值”和“员工姓名列表”:
# 指定不同列的不同聚合
complex_agg = df.groupby('部门').agg({
'月薪': ['mean', 'max'],
'姓名': 'count' # 这里count就是人数
})
print("复杂聚合结果:")
print(complex_agg)
输出:
月薪 姓名
mean max count
部门
市场 12000 13000 3
技术 17666 20000 3
复杂的 agg 返回的列名是多层索引,有点儿不好看。你可以通过修改列名或者用 reset_index 来美化。不过在实际工作中,多数情况下我们不会同时用多个函数对多个列,而是按需选择。
四、转换与过滤:transform 与 filter
4.1 transform 的应用
聚合操作会返回一个缩减后的结果,比如分组后的平均值。但有时候你想保留原始数据的行数,同时给每一行添加上它所属组的某个统计值。比如你想知道每个员工的月薪相对于所在部门平均值的差值。这时候 transform 就派上用场了。
transform 和 agg 类似,但它返回的结果和原始 DataFrame 形状相同,不会缩减行数。
# 计算每个部门平均月薪
dept_mean = df.groupby('部门')['月薪'].transform('mean')
print("部门平均月薪(每行都有):")
print(dept_mean)
# 计算每个员工月薪与部门均值的差值
df['偏差'] = df['月薪'] - dept_mean
print("添加偏差列后的数据:")
print(df)
输出:
部门平均月薪(每行都有):
0 17666.666667
1 17666.666667
2 12000.000000
3 12000.000000
4 17666.666667
5 12000.000000
Name: 月薪, dtype: float64
添加偏差列后的数据:
部门 姓名 月薪 子公司 偏差
0 技术 张三 15000 北京 -2666.666667
1 技术 李四 18000 北京 333.333333
2 市场 王五 12000 上海 0.000000
3 市场 赵六 11000 上海 -1000.000000
4 技术 陈七 20000 上海 2333.333333
5 市场 周八 13000 北京 1000.000000
transform 内部也可以接受自定义函数,但要注意它必须返回与原始数组相同长度的结果。
4.2 filter 过滤分组
有时候我们只关心那些满足某些条件的组,比如只想保留员工人数大于等于2的部门。这时可以用 filter。
# 筛选出员工数 >=2 的部门
filtered = df.groupby('部门').filter(lambda x: len(x) >= 2)
print("过滤后(只保留员工数>=2的部门):")
print(filtered)
输出:
部门 姓名 月薪 子公司 偏差
0 技术 张三 15000 北京 -2666.666667
1 技术 李四 18000 北京 333.333333
2 市场 王五 12000 上海 0.000000
3 市场 赵六 11000 上海 -1000.000000
4 技术 陈七 20000 上海 2333.333333
5 市场 周八 13000 北京 1000.000000
因为我们这个例子中两个部门都有3个人,所以所有行都保留了。如果有一个部门只有一个人,那它就会被过滤掉。filter 的入参是一个函数,该函数接收一个分组后的 DataFrame 子集,返回 True 或 False。
五、apply 的灵活运用
5.1 apply 与自定义函数
当内置的聚合或者 transform 无法满足你的需求时,apply 就是万能钥匙。它可以对每个分组应用任意的函数,函数的返回值可以是标量、Series 或者 DataFrame。
比如,你想给每个部门的员工按照月薪从高到低排名(同部门内排名):
# 自定义排名函数
def rank_salary(group):
# group 是一个 DataFrame,包含该部门的所有行
group['部门内排名'] = group['月薪'].rank(ascending=False, method='dense')
return group
# 使用 apply,注意这里需要指定 group_keys=False 来避免自动添加分组键
ranked = df.groupby('部门', group_keys=False).apply(rank_salary)
print("每个部门内的月薪排名:")
print(ranked)
输出:
部门 姓名 月薪 子公司 偏差 部门内排名
0 技术 张三 15000 北京 -2666.666667 3.0
1 技术 李四 18000 北京 333.333333 2.0
2 市场 王五 12000 上海 0.000000 2.0
3 市场 赵六 11000 上海 -1000.000000 3.0
4 技术 陈七 20000 上海 2333.333333 1.0
5 市场 周八 13000 北京 1000.000000 1.0
apply 非常强大,但也可能带来性能问题,因为它是逐组调用函数。对于大数据集,尽量使用内置的向量化方法(如 transform、agg)代替。
六、分组后的重置索引与合并
6.1 reset_index
之前我们用 groupby 得到的结果,索引往往是分组键本身。如果希望把分组键变成普通的列,可以调用 reset_index()。
# 按子公司和部门分组,计算月薪总和
sum_result = df.groupby(['子公司', '部门'])['月薪'].sum()
print("分组后的结果(多级索引):")
print(sum_result)
print("\n重置索引后:")
print(sum_result.reset_index())
输出:
分组后的结果(多级索引):
子公司 部门
北京 市场 13000
技术 33000
上海 市场 23000
技术 20000
Name: 月薪, dtype: int64
重置索引后:
子公司 部门 月薪
0 北京 市场 13000
1 北京 技术 33000
2 上海 市场 23000
3 上海 技术 20000
重置索引后就能像普通 DataFrame 一样使用了,方便后续合并或者导出。
七、应用场景分析
场景1:电商销售分析
你想知道每个商品类目在每个季度的销售额趋势。先用 groupby('类目','季度') 汇总销售额,再用 pivot 把季度变成列,就能画出热力图。
场景2:用户行为分析
比如你有用户每周的登录天数,想找出那些平均登录天数大于5天的用户组,可以用 filter 按用户分组后过滤。
场景3:金融数据中的分组排名
基金公司需要每天计算每个投资组合中每只股票的占比排名,用 groupby('组合')['市值'].rank() 就可以轻松搞定。
八、技术优缺点
优点:
语法直观,链式调用让代码读起来像自然语言。
内置聚合函数丰富,能覆盖大部分常见需求。
transform 和 apply 提供了极高的灵活性,几乎任何分组逻辑都能实现。
缺点:
大数据集下性能可能不如 SQL 或专门的并行框架(如 Dask)。
多层索引有时让人头疼,需要配合 reset_index 或 droplevel 使用。
apply 如果写得不小心,容易引起性能灾难,因为它是逐组迭代。
九、注意事项
缺失值处理:分组时,如果分组键列中有 NaN,Pandas 会默认把这些行放在同一组中。你可以用 dropna=True 参数丢弃它们。
性能优化:能用 agg 或 transform 的地方,尽量别用 apply。如果必须用,考虑用 numba 加速。
索引混乱:多次链式操作后,索引可能会变成多层,记得在合适的时候 reset_index。
内存占用:分组结果如果是 DataFrame 的副本,会占用额外内存。可以用 inplace=False(默认)来避免意外修改原数据。
十、文章总结
Pandas 的分组与统计功能是数据分析的基本功。通过 groupby 可以快速把数据按维度切分,再用 agg、transform、filter 和 apply 来做不同类型的计算。掌握了这些高级方法,你就能从原始数据中提炼出有价值的信息,而不仅仅是会念“平均值”。希望这篇文章能让你在以后处理分组问题时更加得心应手。