Python数据分析(十):Pandas 数据统计与 GroupBy
目录
4. 查看与提取分组数据(.groups 与 get_group())
在前几篇文章中,我们不仅掌握了数据的清洗、重塑与变形,还深入学习了向量化字符串和时间序列的处理技巧。至此,数据已经从杂乱无章的形态被清洗为了结构清晰、字段规范的整洁数据
数据清洗完毕后,我们便正式步入了数据分析最核心的阶段——数据统计与分组聚合
在实际业务场景中,我们很少只关注单一的数据点,而是需要回答诸如:各部门的平均薪资是多少?、不同地区的月度销售额趋势如何?、哪些客群的复购率最高?等问题。这背后的核心逻辑,就是 Pandas 中的 Split-Apply-Combine(拆分-应用-合并) 范式
本篇博客将分为两大核心大段进行深度剖析:
-
数据统计与 GroupBy(聚合函数、全局统计、分组拆分原理与多列分组)
-
分组计算(agg 灵活聚合、transform 广播转换、filter 分组过滤与部门薪资综合案例)
一、基础数据统计与 GroupBy
在大规模数据分析中,将成千上万行的数据压缩为有代表性的统计指标(如均值、总量、中位数等)是最基本的需求。Pandas 提供了丰富的聚合函数以及强大灵活的 GroupBy 分组引擎
1. 常用聚合函数汇总
所谓聚合,是指将多行数值压缩计算为单个数值的过程。Pandas 为 Series 和 DataFrame 提供了大量内置的统计方法:
| 聚合方法 | 语法 | 意义 |
|---|---|---|
| mean() | df['Salary'].mean() | 计算算术平均值 |
| sum() | df['Sales'].sum() | 计算数值总和 |
| count() | df['Order_ID'].count() | 统计非空(non-NaN)数据条数 |
| nunique() | df['User_ID'].nunique() | 去重计数,统计唯一元素数量,常用于 UV、独立用户统计 |
| median() | df['Salary'].median() | 计算中位数,抗异常极值干扰,适合偏态分布数据 |
| std() / var() | df['Score'].std() | 计算标准差 / 方差,衡量一组数据的离散波动程度 |
| min() / max() | df['Age'].min() | 获取序列最小值 / 最大值 |
| describe() | df.describe() | 批量输出描述统计:计数、均值、标准差、最值、四分位数 |
2. GroupBy 机制
当你需要按类别(如部门、城市、性别)分别统计上述指标时,groupby() 就派上用场了。
groupby 并非只是简单的分组,其底层遵循了经典的 Split-Apply-Combine(拆分-应用-合并) 三步曲:

-
拆分(Split):根据指定的键将 DataFrame 划分为若干个独立的组(Group。
-
应用(Apply):对每个独立的组应用指定的函数(如求和、求均值、自定义函数)
-
合并(Combine):将每个组计算得到的结果拼接到一个新的 Pandas 对象中
3. 代码实战
让我们通过一份模拟的企业员工薪酬数据集,展示从全局统计到 groupby 分组计算的全过程
(1) 基础数据集
import pandas as pd
import numpy as np
# 员工薪酬模拟数据
df_emp = pd.DataFrame({
'Emp_ID': [101, 102, 103, 104, 105, 106, 107, 108],
'Name': ['Alice', 'Bob', 'Charlie', 'David', 'Eva', 'Frank', 'Grace', 'Henry'],
'Department': ['HR', 'IT', 'IT', 'Sales', 'HR', 'IT', 'Sales', 'Sales'],
'City': ['Beijing', 'Beijing', 'Shanghai', 'Shanghai', 'Beijing', 'Shanghai', 'Beijing', 'Shanghai'],
'Salary': [8000, 15000, 18000, 12000, 9000, 20000, 11000, 13000],
'Bonus': [1500, 3000, 4000, 2000, 1800, 5000, 1800, 2500]
})
print(df_emp[['Salary', 'Bonus']].describe())
输出结果:

(2) 单列分组聚合
指定单列(如 'Department')作为分组键,并选择需要计算的目标列(如 'Salary'):
# 按部门分组,计算薪资平均值
dept_salary = df_emp.groupby('Department')['Salary'].mean()
print("--- 各部门平均薪资 ---")
print(dept_salary)
输出结果:

(3) 多列分组聚合
当需要跨多个维度(如 "部门 + 城市")进行交叉统计时,只需将多个列名放入列表中传入 groupby(['col1', 'col2']):
# 按部门和城市组合分组,计算平均薪资
multi_group = df_emp.groupby(['Department', 'City'])['Salary'].mean()
print("--- 部门 + 城市交叉统计 ---")
print(multi_group)
输出结果:
多列分组后返回的对象默认包含多重索引。如果需要将其转换回平铺的普通表格结构,可以使用 as_index=False 或 .reset_index()
dept_salary = df_emp.groupby(['Department', 'City'], as_index=False)['Salary'].mean()
print("--- 部门 + 城市交叉统计 ---")
print(dept_salary)
输出结果:

4. 查看与提取分组数据(.groups 与 get_group())
df.groupby() 执行后,返回的是一个 DataFrameGroupBy 延迟计算对象,它在没有接聚合函数时不会直接输出具体数值,而是保存了分组的映射关系。我们可以对该对象进行内部探查或独立提取某个分组:
# 创建 GroupBy 对象
grouped = df_emp.groupby('Department')
# 1. 查看分组的索引映射关系 (.groups)
print("--- 1. 各组包含的行索引映射 ---")
print(grouped.groups)
# 2. 提取指定分组的完整原始数据 (get_group)
it_team = grouped.get_group('IT')
print("\n--- 2. 仅提取 IT 部门的原始数据 ---")
print(it_team)
输出结果:

关键细节
-
缺失值 NaN 默认忽略:
-
默认情况下,如果分组列中存在 NaN,该行数据不会参与分组,也不会出现在最终结果中
-
解决方案:如果希望将 NaN 也进行统计,可以传入参数 dropna=False:
df.groupby('Department', dropna=False)['Salary'].mean()
-
-
as_index=False 与 .reset_index():
-
df.groupby('Dept', as_index=False).mean() 在分组阶段就阻止将 'Dept' 设为行索引
-
df.groupby('Dept').mean().reset_index() 则是先生成索引再重置为列。两者效果基本相同,但前者更加简洁高效
-
-
内存与性能优化的 observed=True:
-
如果分组列是分类类型,默认会为所有未在数据中出现的分类类别也生成空组。建议加上 observed=True 以仅对实际观察到的分类进行分组,大幅提升性能并减少冗余行
-
二、进阶分组计算
有时我们会遇到更复杂的需求:例如 "需要同时看各部门的平均薪资、最高薪资和标准差" 、" 计算每位员工的薪资占其所在部门总薪资的比例"、或者 "想要剔除平均薪资低于 10000 的整个部门"
为了满足这些高级分析需求,Pandas 提供了 agg()、transform() 与 filter()。接下来,我们将通过具体的语法讲解与案例来串联这些技巧
1. 进阶分组 API
下表汇总了三个进阶 API 的核心区别与适用场景
| 方法 | 返回结果形状 | 核心作用 |
|---|---|---|
| agg ()(或 aggregate) | 降维(行数等于分组数) | 多重聚合:对一列计算多种指标,或对不同列计算不同指标 |
| transform() | 保持原样 | 组内广播计算:计算组内占比、组内 Z-Score 标准化、用组内均值填充缺失值 |
| filter() | 返回满足条件的原始行子集 | 组级条件筛选:基于小组整体特征(如 “组均值> 10000”)筛选数据 |
2. 多重与自定义聚合 (agg)
groupby().mean() 一次只能计算一种指标,而 agg() 允许我们进行多维度、个性化的复杂聚合
(1) 单列多种聚合 / 多列不同聚合
我们可以通过传入函数列表或列名映射字典,实现灵活的聚合计算:
import pandas as pd
# 员工数据集
df_emp = pd.DataFrame({
'Emp_ID': [101, 102, 103, 104, 105, 106, 107, 108],
'Name': ['Alice', 'Bob', 'Charlie', 'David', 'Eva', 'Frank', 'Grace', 'Henry'],
'Department': ['HR', 'IT', 'IT', 'Sales', 'HR', 'IT', 'Sales', 'Sales'],
'Salary': [8000, 15000, 18000, 12000, 9000, 20000, 11000, 13000],
'Bonus': [1500, 3000, 4000, 2000, 1800, 5000, 1800, 2500]
})
# 1. 对同一列 ('Salary') 计算多种统计量
salary_stats = df_emp.groupby('Department')['Salary'].agg(['mean', 'max', 'std'])
print("--- 1. 对 Salary 列应用多种聚合 ---")
print(salary_stats)
# 2. 对不同列计算不同的统计量 (使用字典)
custom_agg = df_emp.groupby('Department').agg({
'Salary': ['mean', 'max'],
'Bonus': 'sum'
})
print("\n--- 2. 不同列指定不同的聚合函数 ---")
print(custom_agg)
输出结果:

(2) 命名聚合与 Lambda 表达式
为了避免 agg() 生成复杂且难以处理的多重列索引,Pandas 推荐使用命名聚合语法;同时还可以结合 lambda 编写自定义的组内计算逻辑:
# 命名聚合语法:新列名 = ('目标原列', '聚合函数/Lambda')
clean_agg = df_emp.groupby('Department').agg(
Emp_Count=('Emp_ID', 'count'),
Avg_Salary=('Salary', 'mean'),
Total_Bonus=('Bonus', 'sum'),
Salary_Range=('Salary', lambda x: x.max() - x.min()) # 自定义计算差
)
print("--- 3. 命名聚合与自定义极差函数 ---")
print(clean_agg)
输出结果:

3. 分组转换与广播 (transform)
agg() 的本质是 "压缩/降维",而 transform() 的本质是 "计算组内统计量后,广播还原到每一行"。它的输入和输出具有完全相同的行数
场景:计算员工薪资在所在部门的占比
若需要计算每位员工的薪资占其所在部门平均薪资的百分比,用 transform() 可以一行代码搞定:
# 1. 计算每位员工所在部门的平均薪资,并广播回原始每一行
df_emp['Dept_Avg_Salary'] = df_emp.groupby('Department')['Salary'].transform('mean')
# 2. 计算个人薪资相对部门均值的百分比
df_emp['Salary_Pct'] = (df_emp['Salary'] / df_emp['Dept_Avg_Salary'] * 100).round(2)
print("--- transform 组内广播与占比计算 ---")
print(df_emp[['Name', 'Department', 'Salary', 'Dept_Avg_Salary', 'Salary_Pct']])
输出结果:

4. 分组过滤与筛选 (filter)
Pandas 的普通过滤(如 df[df['Salary'] > 10000])是针对个体的。如果需要针对整个小组(例如: "只保留平均薪资超过 10000 元的部门的所有员工数据"),则必须使用 filter()
filter() 接收一个函数,该函数的入参是每个小组的 DataFrame,返回一个新的 DataFrame
# 筛选条件:组内的平均薪资 > 10000
df_high_salary_dept = (df_emp.groupby('Department').
filter(lambda g: g['Salary'].mean() > 10000))
print("--- filter 部门级条件筛选结果 ---")
print(df_high_salary_dept[['Name', 'Department', 'Salary']])
输出结果:

5. 综合案例实战
我们将结合 groupby、agg、transform 与 filter,完成一个完整的企业部门薪酬分析表
需求:
-
部门报表:统计各部门的总人数、平均底薪、最高奖金、总薪酬支出(底薪 + 奖金)
-
员工个体相对定位:计算每位员工的总薪酬在其部门内的相对比值(个人总薪酬 / 部门平均总薪酬)
-
高薪酬预算部门提取:提取出部门总薪酬支出超过 40,000 元的部门名册
# 计算个人总薪酬
df_case['Total_Comp'] = df_case['Salary'] + df_case['Bonus']
# 需求 1:汇总部门宏观指标
dept_summary = df_case.groupby('Department').agg(
Emp_Count=('Emp_ID', 'count'),
Avg_Base_Salary=('Salary', 'mean'),
Max_Bonus=('Bonus', 'max'),
Total_Cost=('Total_Comp', 'sum')
).round(2)
print("=== 1. 部门宏观薪酬报表 ===")
print(dept_summary)
# 需求 2:计算员工个人总薪酬在部门内的比值
df_case['Dept_Avg_Comp'] = df_case.groupby('Department')['Total_Comp'].transform('mean')
df_case['Comp_Ratio'] = (df_case['Total_Comp'] / df_case['Dept_Avg_Comp']).round(2)
print("\n=== 2. 员工个人薪酬定位分析 ===")
print(df_case[['Name', 'Department', 'Total_Comp', 'Dept_Avg_Comp', 'Comp_Ratio']])
# 需求 3:筛选出总薪酬支出 > 40,000 的员工
high_budget_dept_emps = df_case.groupby('Department').filter(lambda g: g['Total_Comp'].sum() > 40000)
print("\n=== 3. 总支出 > 40,000 元的部门员工 ===")
print(high_budget_dept_emps[['Name', 'Department', 'Total_Comp']])
输出结果:

关键细节
1. transform() 接受的函数类型:
-
传入 transform() 的聚合函数必须能够返回一个标量(如 mean, sum),或者返回一个与原小组等长的 Series。否则会抛出 ValueError
2. filter() 必须返回严格的单值 Boolean:
-
在 filter(lambda x: ...) 中,写出的表达式必须是对整个小组状态的唯一评估(例如 x['Salary'].mean() > 10000),千万不要误写成针对每行的向量表达式(例如 x['Salary'] > 10000),后者会引发 ValueError 报错
3. 链式调用中的索引恢复:
-
agg() 在默认情况下会将分组列设为行索引。如果你后续需要绘图或导出 CSV,建议结合 .reset_index() 或在 groupby 中显式使用 as_index=False
总结
本章围绕 Pandas 的数据统计与分组聚合展开,学习了常用聚合函数、GroupBy 对象以及分组聚合、分组转换和分组过滤等核心内容,掌握了如何按照不同维度对数据进行统计分析。同时,通过多个案例进一步理解了分组操作在实际数据分析中的应用,能够更加高效地从大量数据中提取有价值的信息
至此,Pandas 系列的核心知识已经基本学习完成。下一篇将通过多个综合案例,将数据读取、清洗、转换、统计分析等内容串联起来,完整演示使用 Pandas 解决实际数据分析问题的全过程,进一步提升综合实践能力

更多推荐


所有评论(0)