拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Pandas agg函数深度解析:从基础聚合到高阶数据分析实战

1. 从“求和”到“洞察”为什么你需要深入了解agg如果你用过Pandas那你肯定用过.sum()、.mean()这些简单的聚合方法。它们很方便点一下就能出结果。但当你面对稍微复杂一点的数据分析任务时比如老板让你“按部门看看平均工资同时还要最高工资和最低工资的差距”或者“统计每个产品类别的销售额总和并且计算一下销售额的变异系数”你可能会开始写循环或者分好几步操作代码一下子就变得又长又乱。这时候.agg()函数就该登场了。很多人把它当作一个“高级版”的聚合工具知道它能一次干多件事但这远远低估了它的价值。在我看来.agg()是Pandas从“数据操作工具”迈向“灵活分析引擎”的一个关键接口。它不仅仅是为了省几行代码更重要的是它提供了一种声明式的、结构化的数据分析思维。你可以清晰地告诉Pandas“针对这组数据我分别要用这几种方法进行计算”而不是用一堆命令式的步骤去拼凑结果。最近在社区和搜索趋势里关于Pandas字符串分析、复杂数据转换如object类型处理、以及从MySQL读写数据的讨论非常多。这些场景恰恰是.agg()大显身手的地方。比如你从数据库读出的用户日志需要按用户ID分组同时计算访问次数、平均停留时长、以及最后访问时间一个.agg()调用就能优雅地完成。再比如处理文本数据时你想分组统计评论的情感倾向均值数值和最常见的词字符串.agg()也能很好地支持这种混合类型的聚合。所以这篇文章不会只罗列.agg()的几种调用方式。我想结合我这些年处理真实数据从Excel报表到数据库日志的经验带你深入理解.agg()的设计哲学、它的各种“骚操作”、以及那些官方文档里不会写的能让你真正提升效率的细节和避坑指南。我们会从最基础的用法开始逐步深入到自定义函数、条件聚合、处理NaN值等高级话题目标是让你下次面对复杂聚合需求时能条件反射般地写出清晰高效的.agg()代码。2. 核心机制拆解agg到底是如何工作的要玩转一个工具首先得理解它的运作机制。.agg()是DataFrameGroupBy或SeriesGroupBy对象的方法。它的核心任务很简单对分组后的每个子数据集应用一个或多个计算规则并将结果重新组合成一个新的数据结构。2.1 分组GroupBy是前提没有分组就谈不上聚合。df.groupby(‘key’)是第一步它根据指定的键一个或多个列将数据框拆分成多个组。.agg()是在这个分组对象上操作的。理解这一点至关重要因为.agg()的行为和结果与你的分组方式紧密相关。import pandas as pd import numpy as np # 示例数据 df pd.DataFrame({ ‘部门‘: [‘技术部‘, ‘技术部‘, ‘市场部‘, ‘市场部‘, ‘技术部‘], ‘员工‘: [‘张三‘, ‘李四‘, ‘王五‘, ‘赵六‘, ‘孙七‘], ‘工资‘: [15000, 18000, 12000, 10000, 16000], ‘奖金‘: [3000, 4000, 2000, 1500, 3500] }) print(“原始数据“) print(df) print(“\n按‘部门‘分组后的GroupBy对象“) grouped df.groupby(‘部门‘) # 我们可以窥探一下分组 for name, group in grouped: print(f“\n组名{name}“) print(group)运行上面的代码你会看到数据被清晰地分成了“技术部”和“市场部”两个组。.agg()后续的所有计算都是基于这样的一个个“小数据框”独立进行的。2.2 聚合函数的三种形态.agg()的强大之处在于它接受多种形式的“计算规则”我将其归纳为三类1. 字符串别名这是最简单直接的方式直接使用Pandas内置的聚合函数名。result df.groupby(‘部门‘)[‘工资‘].agg(‘mean‘) print(result)输出会是每个部门的平均工资。常用的字符串别名包括‘sum‘,‘mean‘,‘std‘,‘var‘,‘min‘,‘max‘,‘count‘,‘first‘,‘last‘等。对于分位数可以使用‘quantile‘但需要额外参数。注意字符串别名本质上是调用Series或DataFrame的对应方法。这意味着它很快因为是优化的Cython或C代码。在性能敏感的场景下优先使用字符串别名或NumPy函数。2. NumPy函数直接传入NumPy的函数对象如np.sum,np.mean,np.std。result df.groupby(‘部门‘)[‘工资‘].agg(np.mean)这和用字符串别名‘mean‘在大多数情况下结果一致。但使用NumPy函数有时能提供更精细的控制比如np.nanmean可以忽略NaN值计算平均值而Pandas的‘mean‘默认也是忽略NaN的行为一致。主要区别在于风格和某些边缘情况下的处理。3. 自定义函数这是.agg()灵活性最大的来源。你可以传入任何接受一个Series或DataFrame取决于上下文并返回一个标量值的函数。def 工资范围(series): “““计算工资的范围最大值-最小值“““ return series.max() - series.min() result df.groupby(‘部门‘)[‘工资‘].agg(工资范围) print(result)这个自定义函数工资范围会分别在“技术部”组和“市场部”组的工资数据上被调用并返回一个标量结果。2.3 结果的拼接逻辑这是理解.agg()输出结构的关键。.agg()会将每个分组、每列数据、每个聚合函数计算出的标量结果按照一种多维索引的方式巧妙地拼接起来。单列单函数对单个列应用单个聚合函数结果是一个Series索引是分组键。单列多函数对单个列应用多个聚合函数结果是一个DataFrame。列索引columns变成多级索引MultiIndex第一层是原来的列名第二层是聚合函数名行索引index是分组键。多列单函数对多个列应用同一个聚合函数结果是一个DataFrame列是原始列名行是分组键。多列多函数对不同的列应用不同的聚合函数结果是一个DataFrame拥有多级列索引。第一层是原始列名第二层是聚合函数名。# 多列多函数示例 agg_dict { ‘工资‘: [‘mean‘, ‘max‘, 工资范围], # 对‘工资‘列求平均、最大值和自定义范围 ‘奖金‘: ‘sum‘ # 对‘奖金‘列求和 } result df.groupby(‘部门‘).agg(agg_dict) print(“\n多列多函数聚合结果“) print(result) print(“\n结果的列索引“) print(result.columns)运行后你会看到一个结构清晰的DataFrame列是(‘工资‘, ‘mean‘),(‘工资‘, ‘max‘),(‘工资‘, ‘工资范围‘),(‘奖金‘, ‘sum‘)。这种结构对于后续的数据提取和可视化非常友好。3. 实战进阶解锁agg的高阶用法与性能技巧掌握了基础我们来看看那些能让你的代码更简洁、更强大的进阶用法。这些技巧往往来自于实际项目中的反复打磨。3.1 对不同的列应用不同的函数这是.agg()最经典的用法通过传递一个字典来实现。字典的键是列名值可以是单个函数字符串、可调用对象、函数列表或函数元组。# 更复杂的字典定义 aggregations { ‘工资‘: { ‘平均工资‘: ‘mean‘, # 给结果列起个别名 ‘工资中位数‘: ‘median‘, ‘人数‘: ‘count‘ }, ‘奖金‘: [‘sum‘, ‘mean‘, ‘std‘] # 对奖金列应用三个函数列名自动用函数名 } # 注意从Pandas 1.0版本开始更推荐在agg内部使用元组列表来命名但字典嵌套在某些场景下直观 result df.groupby(‘部门‘).agg(aggregations) print(result)实操心得当聚合逻辑非常复杂时先在一个单元格里把这个字典定义好检查无误后再放入.agg()。这比直接在.agg()里写一大段要清晰也便于调试。3.2 使用lambda表达式实现“一次性”逻辑对于简单的、无需复用的自定义逻辑lambda表达式是绝佳选择能让代码非常紧凑。# 计算每个部门“工资奖金”的平均值 df[‘总收入‘] df[‘工资‘] df[‘奖金‘] # 方法1创建新列 result1 df.groupby(‘部门‘)[‘总收入‘].mean() # 方法2在agg中使用lambda无需修改原数据框 result2 df.groupby(‘部门‘).agg(平均总收入(‘工资‘, lambda x: (x df.loc[x.index, ‘奖金‘]).mean())) print(result2)注意上面lambda的写法。x是每个分组的‘工资‘列一个Series我们通过x.index获取这个分组对应的行索引然后用它去df中定位并获取‘奖金‘列进行计算。这比先创建临时列更灵活尤其是在进行探索性分析时。3.3 处理讨厌的NaN值真实数据充满缺失值。.agg()的行为取决于你使用的函数Pandas内置的字符串别名如‘mean‘,‘sum‘和NumPy函数如np.sum默认会跳过NaN值进行计算。例如[1, 2, np.nan]用‘mean‘得到的是1.5。自定义函数你需要自己在函数里处理NaN。pd.isna()是你的好朋友。df_nan pd.DataFrame({ ‘组别‘: [‘A‘, ‘A‘, ‘B‘, ‘B‘, ‘A‘], ‘数值‘: [1, np.nan, 3, 4, 5] }) # 默认跳过NaN print(df_nan.groupby(‘组别‘)[‘数值‘].agg(‘mean‘)) # 输出A组 (15)/23.0, B组 (34)/23.5 # 自定义函数将NaN视为0 def mean_with_nan_as_zero(s): return s.fillna(0).mean() print(df_nan.groupby(‘组别‘)[‘数值‘].agg(mean_with_nan_as_zero)) # 输出A组 (105)/32.0, B组 (34)/23.5避坑指南如果你发现聚合结果和预期不符特别是计数count远小于分组大小第一个要怀疑的就是NaN值。count()是不忽略NaN的它统计非空值数量。而‘size‘会统计所有行数包括NaN。务必分清count和size的区别。3.4 性能优化为什么有时候agg会很慢当数据量很大百万行以上或分组很多时.agg()特别是使用自定义函数时可能会成为性能瓶颈。原因在于自定义函数通常是以Python循环的方式在每个分组上调用而不是向量化操作。优化策略优先使用内置函数字符串别名和NumPy函数是底层优化过的速度极快。能用‘mean‘就别用lambda x: x.mean()。向量化自定义函数如果逻辑允许尽量让自定义函数内部使用NumPy或Pandas的向量化操作而不是Python循环。# 慢在自定义函数里用循环 def slow_range(s): max_val -np.inf min_val np.inf for val in s: if val max_val: max_val val if val min_val: min_val val return max_val - min_val # 快利用Series的向量化方法 def fast_range(s): return s.max() - s.min()考虑使用transform如果你需要将聚合结果如组内均值广播回原数据的每一行用于数据清洗或特征工程.transform()比先.agg()再合并要高效得多它是为这种场景设计的。对于超大数据集可以考虑使用Dask并行计算或直接切换到PySpark等分布式计算框架。Pandas的.agg()本质是单机单核操作。4. 真实场景融合结合热词解决实际问题现在让我们把.agg()放到一些具体的、大家经常搜索的场景中看看它如何大显神通。这些例子都来源于我过去工作中遇到的实际需求。4.1 场景一字符串数据的聚合分析搜索词里有很多关于“pandas 字符串 分析”的。假设我们有一个产品评论数据集。reviews pd.DataFrame({ ‘产品‘: [‘手机A‘, ‘手机A‘, ‘手机B‘, ‘手机B‘, ‘手机A‘], ‘评论‘: [‘很好用速度快‘, ‘电池不耐用‘, ‘屏幕清晰‘, ‘价格有点高‘, ‘拍照效果好‘], ‘评分‘: [5, 2, 4, 3, 5] }) # 需求1统计每个产品的评论数量和平均评分数值聚合 print(“需求1 - 评论数与平均分“) print(reviews.groupby(‘产品‘).agg( 评论数量(‘评论‘, ‘count‘), 平均评分(‘评分‘, ‘mean‘) )) # 需求2找出每个产品最常出现的一个关键词字符串聚合 # 这里需要自定义一个函数进行简单的文本分析 import re from collections import Counter def 最常见形容词(text_series): “““从一个Series的文本中找出最常见的形容词简单示例“““ # 将所有评论拼接成一个字符串 all_text ‘ ‘.join(text_series.dropna().astype(str)) # 使用简单正则匹配中文形容词这里只是个粗略示例 words re.findall(r‘[\u4e00-\u9fa5]{2,4}‘, all_text) # 匹配2-4字中文 # 或者用jieba分词会更准确此处为演示简化 if not words: return None word_counts Counter(words) # 返回出现次数最多的词 return word_counts.most_common(1)[0][0] print(“\n需求2 - 最常见关键词“) print(reviews.groupby(‘产品‘)[‘评论‘].agg(最常见形容词).to_frame(name‘高频词‘))这个例子展示了如何将数值聚合评分和文本聚合评论关键词在同一个groupby操作中完成虽然最常见形容词函数需要单独对每个组的文本Series进行处理。4.2 场景二处理“object”类型与混合数据“pandas中的object是什么类型?”是一个常见问题。object类型通常存储字符串或Python对象如列表。对object列使用.agg()时需小心。mixed_df pd.DataFrame({ ‘类别‘: [‘X‘, ‘X‘, ‘Y‘, ‘Y‘], ‘数据‘: [‘100‘, ‘150‘, ‘200‘, ‘二百五‘], # 注意最后一个值是字符串 ‘数值‘: [10, 20, 30, 40] }) mixed_df[‘数据‘] mixed_df[‘数据‘].astype(‘object‘) # 明确指定为object print(“原始数据“) print(mixed_df) # 尝试直接对‘object‘类型的‘数据‘列求和会报错或得到字符串拼接 try: result mixed_df.groupby(‘类别‘)[‘数据‘].agg(‘sum‘) print(“\n对object列使用‘sum‘“, result) # 结果是字符串拼接‘100150‘, ‘200二百五‘ except Exception as e: print(f“错误{e}“) # 正确的做法先转换类型或使用能处理字符串的函数 # 方法1在agg内部转换如果确定可转换 def safe_sum_as_float(s): try: return pd.to_numeric(s, errors‘coerce‘).sum() except: return None print(“\n安全转换后求和“) print(mixed_df.groupby(‘类别‘).agg(数据总和(‘数据‘, safe_sum_as_float)))核心要点在对列进行聚合前务必了解其数据类型df.dtypes。对object列使用数值聚合函数如‘mean‘会引发错误。要么先使用pd.to_numeric进行转换配合errors‘coerce‘将错误值转为NaN要么就使用适用于字符串的聚合函数如‘count‘,‘first‘,‘last‘或者像上面例子中的自定义函数。4.3 场景三与数据IO操作结合如MySQL“利用pandas读取或写入mysql表数据”是另一个热点。.agg()在从数据库获取原始数据后的分析阶段至关重要。假设我们从一个订单表orders中读取了数据# 模拟从MySQL读取的数据 orders_df pd.DataFrame({ ‘order_id‘: [1, 2, 3, 4, 5], ‘user_id‘: [101, 102, 101, 103, 102], ‘product_id‘: [‘A‘, ‘B‘, ‘A‘, ‘C‘, ‘B‘], ‘amount‘: [299.0, 499.0, 299.0, 899.0, 499.0], ‘order_date‘: pd.to_datetime([‘2023-10-01‘, ‘2023-10-01‘, ‘2023-10-02‘, ‘2023-10-02‘, ‘2023-10-03‘]) }) # 业务分析需求 # 1. 每个用户的总消费金额和订单数 user_summary orders_df.groupby(‘user_id‘).agg( 总金额(‘amount‘, ‘sum‘), 订单数(‘order_id‘, ‘count‘), 首次购买日期(‘order_date‘, ‘min‘), 末次购买日期(‘order_date‘, ‘max‘) ).reset_index() # 将user_id从索引变回列方便后续操作 print(“用户消费摘要“) print(user_summary) # 2. 每日销售统计 daily_stats orders_df.groupby(orders_df[‘order_date‘].dt.date).agg( 日销售额(‘amount‘, ‘sum‘), 订单量(‘order_id‘, ‘nunique‘), # 使用nunique统计不重复订单数 客单价(‘amount‘, lambda x: x.sum() / x.count()) ).round(2) # 保留两位小数 print(“\n每日销售统计“) print(daily_stats)这里展示了如何将时间序列数据处理.dt.date与.agg()结合快速生成业务所需的日报、用户画像等统计指标。这些结果可以轻松地写回数据库的汇总表或导出为Excel报告。5. 避坑指南与调试技巧即使理解了原理在实际使用.agg()时还是会遇到一些意想不到的问题。下面是我踩过的一些坑和解决方法。5.1 列名重名与多层索引的访问当进行多列多函数聚合时会产生多层列索引MultiIndex。如何优雅地访问和修改这些列名result df.groupby(‘部门‘).agg({ ‘工资‘: [‘mean‘, ‘max‘], ‘奖金‘: ‘sum‘ }) print(“聚合结果多层列索引“) print(result) print(“\n列索引结构“) print(result.columns) # 访问特定列比较繁琐 print(“\n访问‘工资‘的‘mean‘列“) print(result[(‘工资‘, ‘mean‘)]) # 扁平化列索引让列名变简单 result_flattened result.copy() result_flattened.columns [‘_‘.join(col).strip() for col in result_flattened.columns.values] print(“\n扁平化后的列名“) print(result_flattened.columns) print(result_flattened)更常见的做法是在聚合时直接使用元组列表来命名这是Pandas现在推荐的方式代码更清晰result_named df.groupby(‘部门‘).agg( 平均工资(‘工资‘, ‘mean‘), 最高工资(‘工资‘, ‘max‘), 奖金总额(‘奖金‘, ‘sum‘) ) print(“\n使用命名聚合推荐“) print(result_named)5.2 聚合后索引的处理reset_index的妙用.groupby().agg()默认将分组键作为结果DataFrame的索引。这有时很方便但有时你需要它们作为普通列以便进行合并merge或输出。result_indexed df.groupby(‘部门‘)[‘工资‘].mean() print(“默认结果部门是索引“) print(result_indexed) print(type(result_indexed)) # Series result_reset df.groupby(‘部门‘, as_indexFalse)[‘工资‘].mean() # 或者 result_indexed.reset_index() print(“\n使用as_indexFalse或reset_index后部门是列“) print(result_reset) print(type(result_reset)) # DataFrame经验法则如果你后续需要将聚合结果与原表或其他表通过分组键进行连接merge一定要在聚合时使用as_indexFalse或事后reset_index()。5.3 当自定义函数返回非标量时.agg()期望每个函数返回一个标量值单个数字、字符串等。如果自定义函数错误地返回了一个Series或DataFrame你会得到一个令人困惑的多层嵌套结构或错误。# 错误示例函数返回了Series def 返回分位数(s): return s.quantile([0.25, 0.5, 0.75]) # 返回一个Series try: df.groupby(‘部门‘)[‘工资‘].agg(返回分位数) except Exception as e: print(f“可能出错{e}“) # 正确做法如果想计算多个分位数应该在agg函数列表中分别指定 result_quantiles df.groupby(‘部门‘)[‘工资‘].agg([(‘Q1‘, lambda x: x.quantile(0.25)), (‘中位数‘, ‘median‘), (‘Q3‘, lambda x: x.quantile(0.75))]) print(“\n正确计算多个分位数“) print(result_quantiles)5.4 调试自定义函数打印中间状态当自定义函数行为不符合预期时最简单的调试方法是在函数内部打印输入数据。def 调试函数(s): print(f“处理分组数据形状{s.shape}“) print(f“数据预览\n{s.head()}“) print(f“是否存在NaN{s.isna().any()}“) result s.mean() print(f“计算结果{result}\n{‘-‘*30}“) return result # 应用时会打印出每个分组的信息 df.groupby(‘部门‘)[‘工资‘].agg(调试函数)这能帮你快速定位问题是出在数据本身如NaN、类型错误还是逻辑计算上。6. 举一反三agg的“近亲”与替代方案.agg()并非孤立的Pandas提供了其他相关方法来完成特定任务了解它们能让你在合适的地方使用合适的工具。6.1.apply()vs.agg()这是最容易混淆的一对。核心区别在于.agg()旨在将输入每个组缩减为一个标量值。它是为了“聚合”而生的。.apply()更加通用。它可以将每个组转换为一个Series、DataFrame甚至是一个标量。它适用于更复杂的、无法用单一聚合函数描述的组级操作。# 使用apply实现agg的功能通常更慢 result_apply df.groupby(‘部门‘)[‘工资‘].apply(lambda x: x.mean()) print(“使用apply求均值“) print(result_apply) # apply能做但agg做不到的事对每个组进行排序并返回前N项 def top_n_salaries(group, n2): return group.nlargest(n, ‘工资‘)[[‘员工‘, ‘工资‘]] # 返回一个DataFrame print(“\n使用apply获取每个部门工资前2的员工“) print(df.groupby(‘部门‘).apply(top_n_salaries, n1))原则如果目标是将组数据汇总为标量永远优先使用.agg()因为它经过优化速度更快。只有在需要更复杂的、返回非标量结果的组操作时才使用.apply()。6.2.transform()不改变形状的“聚合”.transform()接受一个函数并将其结果广播回原始数据的形状。它常用于创建基于组的新特征。# 计算每个员工工资与其部门平均工资的差值 df[‘部门平均工资‘] df.groupby(‘部门‘)[‘工资‘].transform(‘mean‘) df[‘工资与部门均值差‘] df[‘工资‘] - df[‘部门平均工资‘] print(“使用transform创建新特征“) print(df[[‘部门‘, ‘员工‘, ‘工资‘, ‘部门平均工资‘, ‘工资与部门均值差‘]]).transform()要求函数返回的对象必须能与原始组索引对齐通常返回一个与组等长的Series。6.3.pipe()链式操作的利器.pipe()允许你将GroupBy对象传递给一个自定义函数进行更复杂的链式处理。它提升了代码的可读性和模块化。def 添加汇总行(grouped_df): “““在分组结果后添加一行‘总计’“““ summary grouped_df.agg({ ‘工资‘: ‘sum‘, ‘奖金‘: ‘sum‘, ‘员工‘: ‘count‘ }) summary.index [‘总计‘] # 注意这里简单拼接实际中索引处理可能更复杂 return pd.concat([grouped_df, summary]) # 先分组聚合再通过pipe添加总计 summary_df df.groupby(‘部门‘).agg({ ‘工资‘: ‘sum‘, ‘奖金‘: ‘sum‘, ‘员工‘: ‘count‘ }) final_result summary_df.pipe(添加汇总行) print(“使用pipe进行链式处理“) print(final_result)掌握.agg()及其相关方法意味着你掌握了Pandas进行数据汇总和分析的核心武器。从简单的统计到复杂的多指标计算它都能以清晰、高效的方式呈现。关键在于理解“分组-应用-合并”这一范式并根据具体场景选择最合适的“应用”方式。下次当你面对一堆需要分组统计的数据时别再写循环了试试.agg()你会发现你的代码和思路都会变得清爽许多。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门