目录

一、基础数据统计与 GroupBy

1. 常用聚合函数汇总

2. GroupBy 机制

3. 代码实战

4. 查看与提取分组数据(.groups 与 get_group())

二、进阶分组计算

1. 进阶分组 API 

2. 多重与自定义聚合 (agg)

3. 分组转换与广播 (transform)

4. 分组过滤与筛选 (filter)

5. 综合案例实战

总结


在前几篇文章中,我们不仅掌握了数据的清洗、重塑与变形,还深入学习了向量化字符串和时间序列的处理技巧。至此,数据已经从杂乱无章的形态被清洗为了结构清晰、字段规范的整洁数据

数据清洗完毕后,我们便正式步入了数据分析最核心的阶段——数据统计与分组聚合

在实际业务场景中,我们很少只关注单一的数据点,而是需要回答诸如:各部门的平均薪资是多少?、不同地区的月度销售额趋势如何?、哪些客群的复购率最高?等问题。这背后的核心逻辑,就是 Pandas 中的 Split-Apply-Combine(拆分-应用-合并) 范式

本篇博客将分为两大核心大段进行深度剖析:

  1. 数据统计与 GroupBy(聚合函数、全局统计、分组拆分原理与多列分组)

  2. 分组计算(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)

输出结果:

关键细节

  1. 缺失值 NaN 默认忽略:

    • 默认情况下,如果分组列中存在 NaN,该行数据不会参与分组,也不会出现在最终结果中

    • 解决方案:如果希望将 NaN 也进行统计,可以传入参数 dropna=False:

      df.groupby('Department', dropna=False)['Salary'].mean()
      
  2. as_index=False 与 .reset_index():

    • df.groupby('Dept', as_index=False).mean() 在分组阶段就阻止将 'Dept' 设为行索引

    • df.groupby('Dept').mean().reset_index() 则是先生成索引再重置为列。两者效果基本相同,但前者更加简洁高效

  3. 内存与性能优化的 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,完成一个完整的企业部门薪酬分析表

需求:

  1. 部门报表:统计各部门的总人数、平均底薪、最高奖金、总薪酬支出(底薪 + 奖金)

  2. 员工个体相对定位:计算每位员工的总薪酬在其部门内的相对比值(个人总薪酬 / 部门平均总薪酬)

  3. 高薪酬预算部门提取:提取出部门总薪酬支出超过 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 解决实际数据分析问题的全过程,进一步提升综合实践能力

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐