Pandas分组聚合:数据分析核心技能,从原理到实战
2026/8/3 8:55:09 网站建设 项目流程

1. 项目概述:为什么“分组聚合”是数据分析的基石

如果你用过Excel的数据透视表,那你对“分组聚合”这个概念就不会陌生。简单来说,就是把一堆杂乱的数据,按照某个或某几个特征(比如“城市”、“产品类别”、“月份”)分成不同的组,然后对每个组内的数据进行统计计算(比如求和、平均、计数)。在Python的数据分析领域,Pandas库的groupby操作就是实现这一功能的瑞士军刀,其强大和灵活程度远超Excel。我见过太多新手数据分析师,拿到数据后第一反应就是写循环,一行行去判断、累加,代码冗长且效率低下。一旦掌握了Pandas的分组聚合,你会发现处理这类问题的代码可以简洁到令人发指,并且性能提升几个数量级。无论是分析销售数据看各区域业绩,还是处理用户行为数据计算人均访问时长,分组聚合都是你绕不开的核心技能。这个“第1关”,闯过去,你的数据分析之路才算真正入门。

2. 核心概念与groupby机制深度解析

2.1 “拆分-应用-合并”三部曲

Pandas的groupby操作遵循一个经典的三步流程:“拆分(Split)-应用(Apply)-合并(Combine)”。理解这个模型,是灵活运用groupby的关键。

  1. 拆分(Split):根据你提供的一个或多个“键”(Key),将原始的DataFrame拆分成若干个独立的子组(Group)。这个“键”可以是列名、数组、或一个函数。例如,一个包含“部门”和“销售额”的表格,按“部门”拆分,就会得到市场部、技术部、销售部等多个子数据集。
  2. 应用(Apply):对每个子组独立地执行一个操作。这个操作可以是:
    • 聚合(Aggregation):计算组的汇总统计量,如sum()mean()count()std()等。这是最常用的操作,输出结果的行数等于组的数量。
    • 转换(Transformation):对组内的每个元素进行计算,返回一个与原始组形状相同的对象。例如,计算每个组内的“标准化”值(组内值减去组均值)。结果行数与原始数据相同。
    • 过滤(Filtration):根据组的汇总统计量,丢弃某些组。例如,过滤出成员数量大于10的组。结果行数可能减少,但组内数据保持原样。
  3. 合并(Combine):将应用步骤产生的结果,按照分组键重新组合成一个新的数据结构(通常是DataFrame或Series),并呈现给用户。

这个模型的美妙之处在于,它将复杂的逻辑分解为清晰的步骤,让你可以像搭积木一样组合不同的操作。

2.2GroupBy对象:延迟计算的魔力

当你执行df.groupby(‘key’)时,返回的并不是一个直接的结果,而是一个DataFrameGroupBy对象。你可以把它理解为一个“视图”或“蓝图”,它记录了如何对数据进行分组,但并没有立即进行计算。这是一种“延迟计算”(Lazy Evaluation)策略。

注意:很多初学者会在这里困惑,打印groupby对象看不到具体数据。这是正常的,因为它只是一个分组方案。只有当你对它调用聚合方法(如.sum())或进行迭代时,真正的计算才会发生。

这种设计的优势在于:

  • 高效:避免了不必要的中间计算,只有在你需要结果时才执行。
  • 灵活:你可以在同一个GroupBy对象上链式调用多个操作,Pandas会优化执行过程。

2.3 单列分组与多列分组

分组可以基于一列,也可以基于多列,形成多级索引(Hierarchical Index),这对于分析多维数据至关重要。

  • 单列分组df.groupby(‘Department’)[‘Sales’].sum()。这是最常见的形式。
  • 多列分组df.groupby([‘Year’, ‘Quarter’])[‘Revenue’].mean()。这会先按‘Year’分,然后在每个‘Year’内再按‘Quarter’分,结果是一个具有(Year, Quarter)两级索引的Series。这对于时间序列和交叉分析非常有用。

3. 聚合操作实战:从基础到高阶

3.1 内置聚合方法速查与应用

Pandas提供了一系列开箱即用的聚合函数,以下是最常用的一些:

方法描述典型应用场景
count()非NA值的数量统计每个组的有效数据条数
sum()求和计算各区域销售总额、总访问量
mean()算术平均数计算平均客单价、平均响应时间
median()中位数分析收入分布,避免极端值影响
std(),var()标准差、方差衡量组内数据的离散程度
min(),max()最小值、最大值找出每个品类的最低价和最高价
prod()乘积计算复合增长率时可能用到
first(),last()第一个、最后一个非NA值获取时间序列中每个周期的起始和结束值
size()组的大小(包含NA)count()区别在于size计入所有行

基础用法示例

import pandas as pd import numpy as np # 示例数据 data = { ‘City‘: [‘北京‘, ‘上海‘, ‘北京‘, ‘广州‘, ‘上海‘, ‘北京‘, ‘广州‘], ‘Product‘: [‘A‘, ‘B‘, ‘A‘, ‘A‘, ‘B‘, ‘B‘, ‘A‘], ‘Sales‘: [100, 200, 150, 90, 220, 130, 80], ‘Profit‘: [20, 50, 30, 15, 60, 35, 10] } df = pd.DataFrame(data) # 单列分组,单列聚合 city_sales = df.groupby(‘City‘)[‘Sales‘].sum() print(“各城市总销售额:“) print(city_sales) # 单列分组,多列聚合 city_stats = df.groupby(‘City‘)[[‘Sales‘, ‘Profit‘]].agg([‘sum‘, ‘mean‘, ‘count‘]) print(“\n各城市销售与利润统计:“) print(city_stats)

这个city_stats结果会是一个具有多级列索引的DataFrame,第一级是原始列名(‘Sales‘, ‘Profit‘),第二级是聚合函数名(‘sum‘, ‘mean‘, ‘count‘)。这种结构非常便于进行多维度的对比分析。

3.2 使用agg()aggregate()进行灵活聚合

agg()方法是分组聚合的“王牌”,它允许你进行高度定制化的聚合操作。

1. 对不同的列应用不同的聚合函数:这是agg()最强大的功能之一。你传入一个字典,键是列名,值可以是单个聚合函数、函数名的字符串,或函数列表。

# 对不同列使用不同聚合 custom_agg = df.groupby(‘City‘).agg({ ‘Sales‘: ‘sum‘, # 对Sales列求和 ‘Profit‘: [‘mean‘, ‘std‘], # 对Profit列求均值和标准差 ‘Product‘: ‘count‘ # 对Product列计数(非NA) }) print(custom_agg)

2. 对同一列应用多个聚合函数:

# 对Sales列同时进行多种聚合 sales_detail = df.groupby(‘City‘)[‘Sales‘].agg([‘sum‘, ‘mean‘, ‘max‘, lambda x: x.max() - x.min()]) # 重命名自定义的lambda函数列 sales_detail = sales_detail.rename(columns={‘<lambda_0>‘: ‘range‘}) print(sales_detail)

3. 使用自定义函数:你可以传入任何接收一个Series并返回一个标量值的函数。

# 定义一个计算变异系数的函数 def coefficient_of_variation(series): “““计算变异系数(标准差/均值),用于比较不同均值数据的离散程度。”“” return series.std() / series.mean() cv_result = df.groupby(‘City‘)[‘Sales‘].agg([‘mean‘, ‘std‘, coefficient_of_variation]) print(cv_result)

实操心得:在使用自定义函数时,尤其是通过lambda表达式,结果列的命名会变得不友好(如<lambda_0>)。一个好的习惯是,要么在agg中使用具名函数,要么在聚合完成后立即使用.rename()方法重命名列,这能让你的结果DataFrame更清晰易懂。

3.3 多级索引结果的处理技巧

经过复杂分组聚合后,你经常会得到一个具有多级索引(行或列)的DataFrame。处理它们需要一些技巧。

  • 重置索引:使用.reset_index()将多层行索引变回普通的列。这是最常用的操作,能让数据变“平”,方便后续处理或导出。
    multi_index_df = df.groupby([‘City‘, ‘Product‘])[‘Sales‘].sum() flat_df = multi_index_df.reset_index() print(flat_df)
  • 交换索引层级:对于行多级索引,可以使用.swaplevel()交换内外层索引的顺序。
  • 索引排序:使用.sort_index()对多级索引进行排序,让数据呈现更有序。
  • 访问特定层级:使用.xs()方法可以交叉选择特定层级索引的数据。

4. 分组后的转换与过滤

4.1 转换操作:transform

transformagg的关键区别在于,它返回一个与原始分组对象形状相同的对象。它通常用于组内的标准化、填充或基于组的计算。

典型场景:计算组内排名或Z-Score

# 计算每个城市内部,销售额的Z-Score(标准分数) df[‘Sales_Z‘] = df.groupby(‘City‘)[‘Sales‘].transform( lambda x: (x - x.mean()) / x.std() ) print(df[[‘City‘, ‘Sales‘, ‘Sales_Z‘]])

这里,transform接收一个函数,该函数应用于每个城市分组下的‘Sales‘ Series,计算其Z-Score,并将结果按照原始索引对齐后赋值回原DataFrame的新列‘Sales_Z‘。原DataFrame的行数没有改变。

另一个场景:用组均值填充缺失值

# 假设Profit有缺失值 df.loc[2, ‘Profit‘] = np.nan df[‘Profit_Filled‘] = df.groupby(‘City‘)[‘Profit‘].transform( lambda x: x.fillna(x.mean()) )

4.2 过滤操作:filter

filter用于根据组的属性(如大小、汇总统计量)来筛选组。它接收一个返回布尔值的函数,该函数以整个组(一个小的DataFrame)作为参数。

典型场景:筛选出记录条数大于阈值的组

# 只保留至少有2条记录的城市数据 filtered_df = df.groupby(‘City‘).filter(lambda group: len(group) >= 2) print(“过滤后的数据:“) print(filtered_df)

注意,filter返回的是过滤掉整个组后的原始数据行,而不是聚合后的摘要。在上例中,如果一个城市只有一条记录,那么这条记录会被完全移除。

复杂场景:筛选出销售额波动大的组

# 筛选出销售额标准差大于50的城市组 filtered_by_std = df.groupby(‘City‘).filter(lambda g: g[‘Sales‘].std() > 50)

5. 高级分组技巧与性能优化

5.1 按字典或Series进行分组

有时,分组键并不直接存在于列中,或者你想根据一个映射关系来分组。这时可以传入一个与DataFrame行数相同的Series或字典。

# 假设我们有一个将产品映射到大类的字典 product_category = {‘A‘: ‘Electronics‘, ‘B‘: ‘Furniture‘} # 创建一个映射Series df[‘Category‘] = df[‘Product‘].map(product_category) # 或者,直接在groupby中使用映射 category_sales = df.groupby(df[‘Product‘].map(product_category))[‘Sales‘].sum() print(category_sales)

5.2 按函数分组

分组键可以是一个函数,该函数会作用在索引上,根据函数返回值进行分组。这对于时间序列数据按特定周期分组非常有用。

# 创建一个带时间索引的示例数据 date_rng = pd.date_range(start=‘2023-01-01‘, end=‘2023-01-10‘, freq=‘D‘) ts_df = pd.DataFrame({‘value‘: np.random.randn(len(date_rng))}, index=date_rng) # 按星期几分组(0=Monday, 6=Sunday) weekly_group = ts_df.groupby(lambda x: x.weekday()).mean() print(weekly_group)

5.3 迭代GroupBy对象

虽然大多数时候我们直接使用聚合方法,但迭代GroupBy对象可以让你更细致地控制每个组。

for name, group in df.groupby(‘City‘): print(f“\n城市: {name}“) print(group) # 在这里可以对每个group进行任何复杂的操作

name是分组键的值,group是对应的子DataFrame。

5.4 性能优化要点

  1. 避免在循环中分组:如果你需要对同一数据框按不同键多次分组,最好先复制数据或重新组织逻辑,而不是在循环内反复调用groupby
  2. 使用内置函数sum(),mean()等内置的Cython优化函数远比使用agg(lambda x: x.sum())快。
  3. 谨慎使用applygroupby().apply(func)非常灵活,但性能开销较大,因为func是按组在Python层面调用的。如果func本身可以用向量化操作或内置聚合实现,应优先选择后者。
  4. 关注数据类型:确保用于分组的列是类别型(category)或简单的对象类型(如字符串),数值型数据作为分组键通常效率较低且不符合逻辑。

6. 常见问题与排查技巧实录

在实际工作中,你肯定会遇到各种奇怪的问题。下面是我踩过的一些坑和解决方案。

6.1 分组键包含缺失值NaN

NaN在分组时会被自动排除。这意味着,如果某行的分组键是NaN,它不会出现在任何组中,相应的数据在聚合结果中也会消失。

df_nan = pd.DataFrame({‘key‘: [‘A‘, ‘B‘, np.nan, ‘A‘], ‘value‘: [1, 2, 3, 4]}) print(df_nan.groupby(‘key‘).sum()) # 输出结果只有A和B组,值为NaN的那一行(value=3)没有被计入。

解决方案:在分组前,需要决定如何处理缺失的分组键。通常有两种选择:

  • 填充缺失值df[‘key‘].fillna(‘Unknown‘, inplace=True)
  • 删除该行df.dropna(subset=[‘key‘], inplace=True)

6.2 聚合结果列名混乱

当使用agg()进行复杂聚合,特别是混合了内置函数字符串、自定义函数和lambda时,生成的列名可能难以理解。

result = df.groupby(‘City‘).agg({ ‘Sales‘: [‘sum‘, lambda x: x.quantile(0.9)] }) print(result.columns) # 可能是MultiIndex, 包含一个难懂的‘<lambda_0>‘

解决方案

  1. agg中使用元组来指定列名:
    result = df.groupby(‘City‘).agg( total_sales=(‘Sales‘, ‘sum‘), sales_p90=(‘Sales‘, lambda x: x.quantile(0.9)) )
    这是Pandas较新版本提供的更清晰的语法。
  2. 聚合后重命名:
    result.columns = [‘total_sales‘, ‘sales_p90‘] # 或者对于多级列索引,使用result.rename(columns={...})

6.3 分组后数据顺序问题

groupby默认会对分组键进行排序。有时你可能希望保持数据在原DataFrame中出现的顺序。

# 默认排序 sorted_groups = df.groupby(‘City‘, sort=True).sum() # 保持原始顺序(按分组键在数据中首次出现的顺序) unsorted_groups = df.groupby(‘City‘, sort=False).sum()

sort=False参数在分组键是类别型(category)且你有自定义顺序时特别有用。

6.4 内存占用过大

对非常大的数据集进行多列分组和复杂聚合可能导致内存激增。排查与优化

  1. 检查分组键基数:如果分组键的唯一值非常多(例如,对用户ID分组),那么分组数量会巨大,导致内存爆炸。考虑是否可以先进行数据采样或分层汇总。
  2. 使用as_index=False:在groupby中设置as_index=False会让分组键作为普通列返回,而不是作为索引。在某些情况下,这可以改变内部表示,有时能节省内存,但更主要的是为了结果格式的方便。
  3. 分块处理:对于超大数据,可以考虑使用pandaschunksize参数读取文件,或使用Dask这类并行计算库。

6.5 与时间序列重采样混淆

对于时间序列数据,按时间频率分组(如“按M月度”)通常使用resample方法而非groupby

# 正确的时间序列分组(重采样) ts_df.resample(‘M‘).mean() # 按月重采样求平均 # 对比:按月份名称分组(不连续) ts_df.groupby(ts_df.index.month).mean()

resample处理的是连续时间窗口,而groupby基于离散的标签。在处理规则时间序列时,resample是更合适、功能更强大的工具。

掌握Pandas分组聚合,就像掌握了数据分析的“语法”。它让你能从海量数据中快速提炼出有意义的模式和信息。我个人的体会是,初期多花时间理解“拆分-应用-合并”这个核心模型,并熟练使用aggtransform,后期在遇到复杂业务逻辑时,才能灵活地将这些基础操作组合起来,优雅地解决问题。当你不再需要为每一个分组统计去写循环时,你就真正跨过了数据分析的第一道门槛。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询