如何高效使用 Pandas Groupby?看这篇教程就够了
在数据分析和处理的领域,Pandas 库无疑是 Python 生态系统中的瑞士军刀。它提供了强大、灵活且高效的数据结构(如 DataFrame 和 Series),使得处理结构化数据变得轻而易举。而在 Pandas 的众多功能中,groupby()
操作无疑是核心中的核心。无论你是需要进行数据聚合、转换、过滤还是执行更复杂的组级运算,groupby()
都是你不可或缺的工具。
然而,groupby()
的强大功能也伴随着一定的学习曲线。如何高效、正确地使用它,充分发挥其潜力,是许多数据分析师和开发者关心的问题。本教程旨在深入、全面地探讨 Pandas Groupby 的方方面面,从基础概念到高级技巧,再到性能优化,力求让你在阅读完这篇教程后,能够自信地说:“关于 Pandas Groupby,看这篇就够了!”
本文将涵盖以下内容:
- Groupby 的核心思想:Split-Apply-Combine
- 创建 Groupby 对象:多种分组方式
- 聚合(Aggregation):洞察数据的核心
- 常用聚合函数
- 多重聚合与自定义聚合
- 命名聚合结果
- 转换(Transformation):保持形状的组级运算
transform()
的工作原理- 常见用例:标准化、填充缺失值等
- 过滤(Filtration):基于组属性筛选数据
filter()
的工作原理- 常见用例:筛选特定规模或特征的组
- 应用(Apply):最灵活的组级操作
apply()
的强大之处与注意事项- 何时选择
apply()
- 高级 Groupby 技巧
- 使用函数或字典进行分组
- 利用
pd.Grouper
进行时间序列和分箱分组 - 处理 MultiIndex 输出
observed
参数与分类数据
- 性能优化:让 Groupby 飞起来
- 利用内置函数
- 选择合适的 Apply 方法
- 数据类型的重要性
- 避免不必要的迭代
- 实战案例:综合运用 Groupby 解决问题
- 总结
准备好了吗?让我们一起深入 Pandas Groupby 的世界!
1. Groupby 的核心思想:Split-Apply-Combine
理解 Groupby 操作最重要的一点是掌握其背后的“Split-Apply-Combine”(拆分-应用-合并)策略。这个策略由 Hadley Wickham(著名 R 包 ggplot2 和 dplyr 的作者)提出,并被 Pandas 完美实现。
- 拆分(Split): 根据一个或多个键(key),将 DataFrame 或 Series 中的数据拆分成若干个组(group)。这些键可以是列名、索引级别、数组、Series 或函数等。Pandas 会根据键的值将具有相同值的数据行划分到同一个组中。
- 应用(Apply): 对每个独立的组应用一个函数。这个函数可以是:
- 聚合函数(Aggregation): 计算每个组的汇总统计量(如
sum()
,mean()
,count()
),返回一个标量值。 - 转换函数(Transformation): 对每个组进行特定的计算,返回与该组具有相同形状(相同索引)的对象(如组内标准化、填充缺失值)。
- 过滤函数(Filtration): 根据组的某些属性或计算结果,决定是否保留整个组。
- 通用函数(General Apply): 执行任意复杂的操作,可以返回标量、Series 或 DataFrame。
- 聚合函数(Aggregation): 计算每个组的汇总统计量(如
- 合并(Combine): 将应用函数后得到的结果重新组合成一个新的 Pandas 对象(通常是 Series 或 DataFrame)。结果的结构取决于应用阶段的操作类型。
理解这个流程是高效使用 groupby()
的基础。接下来,我们将详细探讨每个步骤。
2. 创建 Groupby 对象:多种分组方式
groupby()
方法可以在 DataFrame 或 Series 上调用。它本身并不直接进行计算,而是返回一个 GroupBy
对象。这个对象包含了分组信息,等待后续的应用(Apply)操作。
“`python
import pandas as pd
import numpy as np
创建一个示例 DataFrame
data = {‘Company’: [‘Google’, ‘Google’, ‘Microsoft’, ‘Microsoft’, ‘Meta’, ‘Meta’],
‘Department’: [‘HR’, ‘Sales’, ‘HR’, ‘Sales’, ‘HR’, ‘Sales’],
‘Salary’: [90000, 120000, 100000, 110000, 95000, 130000],
‘Years’: [5, 8, 3, 6, 4, 9]}
df = pd.DataFrame(data)
print(“原始 DataFrame:”)
print(df)
“`
常用的分组方式:
-
按单列分组: 这是最常见的用法。
python
gb_company = df.groupby('Company')
# gb_company 是一个 DataFrameGroupBy 对象
print("\n按 'Company' 分组后的 GroupBy 对象:", type(gb_company)) -
按多列分组: 传入一个包含列名的列表。分组将基于这些列值的唯一组合。
python
gb_company_dept = df.groupby(['Company', 'Department'])
# gb_company_dept 是一个 DataFrameGroupBy 对象
print("\n按 'Company' 和 'Department' 分组后的 GroupBy 对象:", type(gb_company_dept)) -
按 Series 分组: 可以提供一个与 DataFrame 行数相同的 Series 作为分组键。这在你需要根据外部条件或计算结果分组时非常有用。
python
mapping = pd.Series(['Tech Giant', 'Tech Giant', 'Tech Giant', 'Tech Giant', 'Social Media', 'Social Media'], index=df.index)
gb_mapping = df.groupby(mapping)
print("\n按外部 Series 分组后的 GroupBy 对象:", type(gb_mapping)) -
按索引级别分组: 如果 DataFrame 有 MultiIndex,可以使用
level
参数按索引级别分组。
python
df_indexed = df.set_index(['Company', 'Department'])
print("\n带 MultiIndex 的 DataFrame:")
print(df_indexed)
gb_level0 = df_indexed.groupby(level=0) # 按第一个索引级别 ('Company') 分组
gb_level_dept = df_indexed.groupby(level='Department') # 按名为 'Department' 的索引级别分组
print("\n按索引级别 'Company' 分组后的 GroupBy 对象:", type(gb_level0)) -
按函数分组: 可以传递一个函数,该函数将应用于索引的每个元素,返回值作为分组键。
“`python
# 假设索引是日期,按月份分组
# df_time = df.set_index(pd.to_datetime([‘2023-01-05’, ‘2023-01-15’, ‘2023-02-08’, …]))
# gb_month = df_time.groupby(lambda x: x.month)对普通索引应用函数,例如按索引奇偶性分组
gb_index_func = df.groupby(lambda x: ‘Even’ if x % 2 == 0 else ‘Odd’)
print(“\n按索引奇偶性函数分组后的 GroupBy 对象:”, type(gb_index_func))
“`
检查 Groupby 对象:
创建 GroupBy
对象后,可以查看其内容:
groups
: 返回一个字典,键是分组键,值是对应的行索引。
python
print("\n'Company' 分组的 groups 属性:")
print(gb_company.groups)ngroups
: 返回分组的数量。
python
print("\n'Company' 分组的数量:", gb_company.ngroups)get_group(name)
: 获取指定名称的组,返回一个 DataFrame 或 Series。
python
google_group = gb_company.get_group('Google')
print("\n获取 'Google' 组:")
print(google_group)- 迭代
GroupBy
对象:可以像迭代元组列表一样迭代GroupBy
对象,每个元组包含 (分组键, 对应的数据子集)。
python
print("\n迭代 'Company' 分组:")
for name, group_df in gb_company:
print(f"--- Group: {name} ---")
print(group_df)
print("-" * 20)
3. 聚合(Aggregation):洞察数据的核心
聚合是 Groupby 最常见的应用场景,它将每个组的数据压缩成一个或多个汇总统计值。
常用内置聚合函数:
GroupBy
对象提供了许多内置的聚合方法,它们通常经过优化,性能很好。
count()
: 计算每组非 NA 值的数量。size()
: 计算每组的总行数(包括 NA 值)。sum()
: 计算每组数值的总和。mean()
: 计算每组数值的平均值。median()
: 计算每组数值的中位数。min()
,max()
: 计算每组数值的最小值、最大值。std()
,var()
: 计算每组数值的标准差、方差。first()
,last()
: 获取每组的第一个、最后一个非 NA 值。nunique()
: 计算每组唯一值的数量。
“`python
计算每个公司的平均工资和总年限
avg_salary = gb_company[‘Salary’].mean()
total_years = gb_company[‘Years’].sum()
print(“\n各公司平均工资:”)
print(avg_salary)
print(“\n各公司总工作年限:”)
print(total_years)
对整个 DataFrame 应用聚合 (只对数值列有效)
company_stats = gb_company.mean()
print(“\n各公司各项指标平均值:”)
print(company_stats)
计算每个公司部门组合的人数 (使用 size)
group_size = gb_company_dept.size()
print(“\n各公司各部门的人数 (size):”)
print(group_size) # 输出是 Series, Index 是 MultiIndex
计算每个公司部门组合非空 Salary 的数量 (使用 count)
group_count = gb_company_dept[‘Salary’].count()
print(“\n各公司各部门非空 Salary 的数量 (count):”)
print(group_count) # 输出是 Series, Index 是 MultiIndex
“`
多重聚合与 .agg()
(.aggregate()
):
当你需要对同一列或不同列应用多个聚合函数时,或者想使用自定义聚合函数时,agg()
方法是你的利器。
-
对所有数值列应用多个聚合函数:
python
multi_agg_all = gb_company.agg(['mean', 'sum', 'count'])
print("\n对所有数值列应用 mean, sum, count:")
print(multi_agg_all) # 列会变成 MultiIndex: (列名, 聚合函数名) -
对特定列应用特定聚合函数(使用字典):
这是最常用、最灵活的方式。字典的键是目标列名,值是聚合函数或函数列表。
python
agg_dict = {
'Salary': ['mean', 'max'], # 对 Salary 列计算均值和最大值
'Years': 'sum' # 对 Years 列计算总和
}
specific_agg = gb_company.agg(agg_dict)
print("\n对特定列应用特定聚合:")
print(specific_agg) -
使用自定义聚合函数:
agg()
可以接受自定义函数(包括lambda
函数)。
“`python
def salary_range(x):
return x.max() – x.min()custom_agg = gb_company.agg(
avg_salary = pd.NamedAgg(column=’Salary’, aggfunc=’mean’), # 使用 NamedAgg 重命名
salary_range = pd.NamedAgg(column=’Salary’, aggfunc=salary_range),
total_years = pd.NamedAgg(column=’Years’, aggfunc=’sum’),
unique_dept_count = pd.NamedAgg(column=’Department’, aggfunc=’nunique’)
)
print(“\n使用自定义函数和 NamedAgg 进行聚合:”)
print(custom_agg)
“`
命名聚合结果 (pd.NamedAgg
):
从 Pandas 0.25.0 开始,推荐使用 pd.NamedAgg
(或直接在 agg()
中使用关键字参数)来指定聚合结果的列名,使输出更清晰。
python
named_agg_result = gb_company.agg(
AverageSalary=('Salary', 'mean'), # 元组形式: (列名, 函数) -> 结果列名 = AverageSalary
MaxYears=('Years', 'max'),
SalarySpread=('Salary', lambda x: x.max() - x.min()) # 也可以用 lambda
)
print("\n使用元组命名聚合结果:")
print(named_agg_result)
使用关键字参数的方式(更简洁):
python
named_agg_kwargs = gb_company.agg(
AverageSalary = pd.NamedAgg(column='Salary', aggfunc='mean'),
MaxYears = pd.NamedAgg(column='Years', aggfunc='max'),
SalarySpread = pd.NamedAgg(column='Salary', aggfunc=lambda x: x.max() - x.min())
)
print("\n使用 NamedAgg 关键字参数命名聚合结果:")
print(named_agg_kwargs)
聚合后重置索引 (.reset_index()
):
groupby().agg()
的结果通常会将分组键作为索引。如果你希望将分组键变回普通列,可以使用 .reset_index()
。
python
agg_result_reset = custom_agg.reset_index()
print("\n聚合结果重置索引:")
print(agg_result_reset)
4. 转换(Transformation):保持形状的组级运算
有时,你需要的不是对每个组进行汇总,而是基于组的信息对组内的每个元素进行计算,并返回一个与原始 DataFrame 具有相同索引(相同形状)的对象。这就是转换(Transformation)操作,通过 .transform()
方法实现。
transform()
的关键特点:
- 它对每个组应用一个函数。
- 该函数必须返回一个与该组具有相同索引的 Series 或 DataFrame,或者一个可以广播到该组大小的标量。
- 结果会被合并回一个与原始对象具有相同索引的 Series 或 DataFrame。
常见用例:
-
组内数据标准化(Z-score):
python
# 计算每个公司内部员工工资的 Z-score
zscore = lambda x: (x - x.mean()) / x.std()
df['Salary_ZScore_Company'] = gb_company['Salary'].transform(zscore)
print("\n添加了按公司标准化的工资 Z-score:")
print(df) -
用组内均值/中位数填充缺失值:
“`python
# 假设 ‘Years’ 列有缺失值
df_with_na = df.copy()
df_with_na.loc[1, ‘Years’] = np.nan
df_with_na.loc[4, ‘Years’] = np.nan
print(“\n包含缺失值的 DataFrame:”)
print(df_with_na)用每个公司内部的平均年限填充缺失值
df_filled = df_with_na.copy()
注意: transform 会为每个元素返回组均值,fillna 只填充 NA
更 Pythonic 的方式是用 fillna 结合 transform
df_filled[‘Years_Filled’] = df_filled.groupby(‘Company’)[‘Years’] \
.transform(lambda x: x.fillna(x.mean()))或者直接作用于原列
df_filled[‘Years’] = df_filled.groupby(‘Company’)[‘Years’].transform(lambda x: x.fillna(x.mean()))
另一种常见写法 (可能更清晰)
group_means = df_filled.groupby(‘Company’)[‘Years’].transform(‘mean’)
df_filled[‘Years_Filled_Alt’] = df_filled[‘Years’].fillna(group_means)print(“\n用公司平均年限填充缺失值后:”)
print(df_filled[[‘Company’, ‘Years’, ‘Years_Filled’, ‘Years_Filled_Alt’]])
“` -
组内排名或百分位数:
python
# 计算员工工资在各自公司内的排名
df['Salary_Rank_Company'] = gb_company['Salary'].rank(method='dense', ascending=False)
print("\n添加了按公司排名的工资:")
print(df)
注意:.rank()
本身不是transform
函数,但其效果类似(返回与输入相同索引的 Series)。一些直接在 GroupBy 对象上调用的方法(如rank
,fillna
等)也具有转换的特性。
transform()
非常强大,因为它允许你在保持原始数据结构的同时,利用分组信息来丰富数据。
5. 过滤(Filtration):基于组属性筛选数据
有时你需要根据组的整体特性来筛选数据,保留或剔除整个组。例如,只保留那些包含超过 N 个成员的组,或者只保留那些平均工资高于某个阈值的组。这时就需要使用 .filter()
方法。
filter()
的工作原理:
- 对每个组应用一个函数(通常是
lambda
函数)。 - 这个函数必须返回一个布尔值(
True
或False
)。 - 如果函数对某个组返回
True
,则该组的所有行都被保留在结果中;如果返回False
,则该组的所有行都被剔除。
“`python
筛选出成员数大于 1 的公司 (在这个例子中所有公司都大于1)
为了演示,我们改一下数据,让 Meta 只有一个 Sales
df_filter_demo = df.copy()
df_filter_demo = df_filter_demo.drop(4) # 删除 Meta 的 HR
print(“\n用于 Filter 演示的 DataFrame (Meta 只有一个 Sales):”)
print(df_filter_demo)
gb_company_filter = df_filter_demo.groupby(‘Company’)
筛选出至少有 2 名员工的公司
filtered_df_size = gb_company_filter.filter(lambda x: len(x) >= 2)
等价于: filtered_df_size = gb_company_filter.filter(lambda x: x.shape[0] >= 2)
print(“\n筛选出员工数 >= 2 的公司:”)
print(filtered_df_size) # Meta 公司的数据被过滤掉了
筛选出平均工资超过 105000 的公司
filtered_df_salary = gb_company_filter.filter(lambda x: x[‘Salary’].mean() > 105000)
print(“\n筛选出平均工资 > 105000 的公司:”)
print(filtered_df_salary) # Google 和 Meta 被过滤掉了 (Meta 只有一个 Sales 130000, Microsoft 是 (100k+110k)/2 = 105k)
筛选出既有 HR 部门又有 Sales 部门的公司 (使用原始 df)
gb_company_orig = df.groupby(‘Company’)
filtered_df_depts = gb_company_orig.filter(lambda x: set(x[‘Department’]) == {‘HR’, ‘Sales’})
print(“\n筛选出同时拥有 HR 和 Sales 部门的公司:”)
print(filtered_df_depts)
“`
filter()
对于根据组的聚合属性来选择数据子集非常有用。
6. 应用(Apply):最灵活的组级操作
.apply()
方法是 Groupby 中最通用、最灵活的方法。它可以接受一个函数,该函数会作用于每个组的 DataFrame (或 Series),并且可以返回标量、Series 或 DataFrame。Pandas 会尝试将这些返回结果智能地组合起来。
与 agg()
和 transform()
不同:
agg()
要求函数返回标量(聚合值)。transform()
要求函数返回与输入组具有相同索引的 Series/DataFrame。apply()
对返回值的类型和形状没有严格限制。
何时使用 apply()
:
当 agg()
, transform()
, filter()
无法满足你的需求时,apply()
通常是最后的选择。例如:
- 需要返回 DataFrame 的函数,且结果的索引或列与原始组不同。
- 执行的操作既不是纯粹的聚合也不是纯粹的转换(例如,获取每个组的 top N 行)。
- 需要在一个函数内执行非常复杂的、多步骤的组级逻辑。
示例:获取每个公司工资最高的员工信息
“`python
def top_n_by_salary(df_group, n=1):
“””返回每个组按 Salary 降序排列的前 n 行”””
return df_group.sort_values(by=’Salary’, ascending=False).head(n)
top_earners = gb_company.apply(top_n_by_salary, n=1)
print(“\n获取每个公司工资最高的员工 (使用 apply):”)
print(top_earners)
注意:结果的索引可能是 MultiIndex (分组键 + 原始索引),取决于 apply 函数的行为
如果需要去掉分组键索引,可以 .reset_index(level=0, drop=True)
示例:为每个公司计算一个复杂的指标
def complex_metric(group):
metric = (group[‘Salary’].mean() / group[‘Years’].mean()) * group[‘Department’].nunique()
# 返回一个标量
return metric
company_complex_metric = gb_company.apply(complex_metric)
print(“\n为每个公司计算复杂指标 (使用 apply 返回标量):”)
print(company_complex_metric) # 结果是 Series, 索引是分组键
示例: apply 返回一个 Series
def normalize_salary_in_group(group):
# 返回一个与 group[‘Salary’] 索引相同的 Series
return (group[‘Salary’] – group[‘Salary’].min()) / (group[‘Salary’].max() – group[‘Salary’].min())
normalized_salary_series = gb_company.apply(normalize_salary_in_group)
print(“\n使用 apply 返回 Series (组内归一化工资):”)
print(normalized_salary_series)
注意:这种情况下,transform 通常更高效且更直接
df[‘Salary_Normalized_Transform’] = gb_company[‘Salary’].transform(lambda x: (x-x.min())/(x.max()-x.min()))
print(df)
“`
apply()
的注意事项:
- 性能:
apply()
通常比agg()
和transform()
慢,因为它需要在 Python 层面迭代每个组,并且 Pandas 需要猜测如何组合结果。如果可能,优先使用专门的agg()
,transform()
,filter()
或其他内置的 Groupby 方法。 - 结果组合: Pandas 组合
apply()
结果的逻辑有时可能不直观,特别是当函数返回不同类型或形状的对象时。需要仔细检查输出。
7. 高级 Groupby 技巧
掌握了基础的 Split-Apply-Combine 操作后,我们来看一些更高级的技巧。
-
使用函数或字典进行分组:
除了列名,还可以直接用函数或字典来定义分组。函数会应用于索引,字典则提供了一个从索引到分组键的映射。
“`python
# 假设 df 有更有意义的索引
df_idx = df.set_index(pd.Index([‘row1’, ‘row2’, ‘row3’, ‘row4’, ‘row5’, ‘row6’]))按索引长度分组
gb_index_len = df_idx.groupby(len) # 函数 len 会作用于每个索引 ‘row1’, ‘row2’…
print(“\n按索引长度分组:”)
print(gb_index_len.sum())使用字典映射分组
mapping_dict = {‘row1’: ‘GroupA’, ‘row2’: ‘GroupA’,
‘row3’: ‘GroupB’, ‘row4’: ‘GroupB’,
‘row5’: ‘GroupC’, ‘row6’: ‘GroupC’}
gb_dict = df_idx.groupby(mapping_dict)
print(“\n按字典映射分组:”)
print(gb_dict.mean())
“` -
利用
pd.Grouper
进行时间序列和分箱分组:
pd.Grouper
是一个强大的对象,特别适用于按时间频率或自定义区间(分箱)进行分组。
“`python
# 时间序列分组
time_idx = pd.to_datetime([‘2023-01-15 10:00’, ‘2023-01-15 12:30’,
‘2023-01-16 09:00’, ‘2023-01-16 15:00’,
‘2023-01-17 11:00’, ‘2023-01-17 18:00’])
df_time = df.set_index(time_idx)
print(“\n带时间索引的 DataFrame:”)
print(df_time)按天分组聚合
gb_daily = df_time.groupby(pd.Grouper(freq=’D’)) # ‘D’ 表示按天
print(“\n按天聚合 (平均工资):”)
print(gb_daily[‘Salary’].mean())按公司和每 6 小时分组
gb_company_6h = df_time.groupby([‘Company’, pd.Grouper(freq=’6H’)])
print(“\n按公司和每 6 小时聚合 (计数):”)
print(gb_company_6h.size())分箱分组 (假设我们想按工资范围分组)
salary_bins = [80000, 100000, 120000, 140000]
labels = [’80k-100k’, ‘100k-120k’, ‘120k-140k’]使用 pd.cut 创建分箱 Series
df[‘Salary_Bin’] = pd.cut(df[‘Salary’], bins=salary_bins, labels=labels, right=False)
gb_salary_bin = df.groupby(‘Salary_Bin’)或者直接在 groupby 中使用 pd.cut (虽然不太常见,通常先创建分箱列)
gb_salary_bin_direct = df.groupby(pd.cut(df[‘Salary’], bins=salary_bins, labels=labels, right=False))
print(“\n按工资范围分组 (平均年限):”)
print(gb_salary_bin[‘Years’].mean())
*注意:从 Pandas 1.1.0 开始,`pd.Grouper` 中的 `key` 参数用于在 `groupby` 中指定要应用 Grouper 的列名,使得可以在 `groupby()` 中直接对某列进行时间或分箱分组,而无需先将其设为索引。*
python使用 pd.Grouper(key=’col’, …) 对列进行分组
df_with_date_col = df.copy()
df_with_date_col[‘Date’] = pd.to_datetime([‘2023-01-15’, ‘2023-01-20’, ‘2023-02-10’, ‘2023-02-15’, ‘2023-03-05’, ‘2023-03-12′])
gb_month_col = df_with_date_col.groupby(pd.Grouper(key=’Date’, freq=’M’))[‘Salary’].sum()
print(“\n使用 Grouper 对日期列按月分组求和:”)
print(gb_month_col)
“` -
处理 MultiIndex 输出:
当按多列分组或使用agg()
应用多函数时,结果通常带有 MultiIndex(多级索引)。理解如何操作 MultiIndex 很重要。reset_index()
: 将部分或全部索引级别转换为列。stack()
,unstack()
: 在索引级别和列之间移动。- 直接使用元组访问 MultiIndex 列:
multi_agg_all[('Salary', 'mean')]
- 使用
.xs()
进行交叉切片。
-
observed
参数与分类数据:
当分组键是Categorical
类型时,groupby()
默认只对实际出现的类别进行分组计算 (observed=False
,Pandas 1.5 之前默认为True
)。如果你想包含所有定义的类别(即使某些类别在数据中没有出现),可以设置observed=True
(Pandas 1.5 及之后默认为False
)。
“`python
df[‘Company_Cat’] = pd.Categorical(df[‘Company’], categories=[‘Google’, ‘Microsoft’, ‘Meta’, ‘Apple’], ordered=True)
print(“\n带 Categorical 列的 DataFrame:”)
print(df)
print(“\nCategorical 列的类别:”, df[‘Company_Cat’].cat.categories)默认 observed=False (Pandas 1.5+)
print(“\n按 Categorical 列分组 (observed=False, 默认):”)
print(df.groupby(‘Company_Cat’)[‘Salary’].mean()) # 只显示 Google, Microsoft, Meta设置 observed=True (如果 Pandas < 1.5,这是默认行为)
需要显式设置 observed=True
try:
print(“\n按 Categorical 列分组 (observed=True):”)
print(df.groupby(‘Company_Cat’, observed=True)[‘Salary’].mean()) # 会包含 Apple,值为 NaN 或根据聚合函数行为决定
except TypeError: # 老版本 Pandas 可能不支持 observed 参数
print(“当前 Pandas 版本可能不支持 observed=True 参数。旧版本默认行为类似 observed=True。”)
# 如果在 Pandas 1.5 之前的版本,以下代码会默认包含 Apple
print(df.groupby(‘Company_Cat’)[‘Salary’].mean())Pandas 1.5+ 中的行为可以通过设置 observed 参数控制
print(“\n按 Categorical 列分组 (observed=True in Pandas 1.5+):”)
print(df.groupby(‘Company_Cat’, observed=True)[‘Salary’].mean()) # 应该包含 Apple (值为 NaN)
注意:
observed
的默认值在 Pandas 1.5 中发生了改变,请留意你的 Pandas 版本。“`
8. 性能优化:让 Groupby 飞起来
对于大数据集,groupby()
操作的性能至关重要。以下是一些提高效率的建议:
-
优先使用内置聚合函数:
sum()
,mean()
,count()
,size()
,std()
,max()
,min()
等内置函数通常是用 Cython 实现的,速度远快于在 Python 层面运行的自定义函数或lambda
函数。 -
明智地选择
agg()
,transform()
,filter()
,apply()
:- 如果只需要聚合结果,使用
agg()
或直接调用聚合方法(如.sum()
)。 - 如果需要保持原始形状的转换,使用
transform()
。 - 如果需要根据组属性过滤整个组,使用
filter()
。 - 只有在前三者无法满足需求时,才考虑使用
apply()
,因为它通常最慢。
- 如果只需要聚合结果,使用
-
size()
vscount()
:size()
直接计算每个组的行数(包括 NaN),通常比count()
(需要检查每列的非 NaN 值)更快。如果只是想知道组的大小,用size()
。 -
指定
as_index=False
: 在groupby()
调用中设置as_index=False
可以阻止将分组键设置为结果的索引,直接返回一个带有普通列的 DataFrame。这可以避免后续调用.reset_index()
,有时能略微提高效率并简化代码。
python
agg_no_index = df.groupby('Company', as_index=False)['Salary'].mean()
print("\n聚合时不使用索引 (as_index=False):")
print(agg_no_index) -
数据类型很重要:
- 确保用于分组的列具有合适的数据类型。数值类型通常比字符串类型分组更快。
- 如果一个列只有少数几个唯一值,将其转换为
Categorical
类型可以显著提高groupby()
的性能,并减少内存占用。
python
df['Company_Cat'] = df['Company'].astype('category')
# 使用 Company_Cat 进行分组通常比使用 Company (object 类型) 更快
gb_cat = df.groupby('Company_Cat')['Salary'].mean()
print("\n使用 Category 类型分组:")
print(gb_cat)
-
避免在
apply()
中做可以通过agg()
或transform()
完成的事: 例如,在apply()
函数内部计算均值,不如直接使用.mean()
或.agg('mean')
。 -
向量化操作优于迭代: 尽量使用 Pandas 或 NumPy 的向量化操作,而不是在
apply()
或迭代 Groupby 对象时手动循环处理行。 -
对于超大数据集: 如果数据量超出单机内存,可以考虑:
- 分块处理(Chunking):逐块读取数据,进行 Groupby,然后合并结果。
- 使用 Dask:一个并行计算库,可以处理大于内存的数据集,其 API 与 Pandas 非常相似。
- 数据库解决方案:利用 SQL 数据库的
GROUP BY
功能。
9. 实战案例:综合运用 Groupby 解决问题
假设我们有一个更复杂的销售数据集 sales.csv
:
Date,Region,Product,Quantity,UnitPrice
2023-01-05,East,Apple,100,1.2
2023-01-05,West,Banana,80,0.5
2023-01-12,East,Banana,50,0.6
2023-01-19,South,Apple,120,1.1
2023-01-26,West,Orange,90,0.8
2023-02-02,East,Apple,110,1.25
2023-02-09,West,Banana,95,0.55
2023-02-16,South,Orange,70,0.9
2023-02-23,East,Orange,60,0.85
目标:
- 计算每个月的总销售额。
- 找出每个区域最畅销的产品(按总销售额)。
- 计算每笔销售额占该产品当月总销售额的百分比。
- 筛选出月销售额超过 $150 的区域。
“`python
假设 sales_df 是从 csv 加载的 DataFrame
sales_df = pd.read_csv(‘sales.csv’, parse_dates=[‘Date’])
为了演示,我们手动创建它
sales_data = {‘Date’: pd.to_datetime([‘2023-01-05’, ‘2023-01-05’, ‘2023-01-12’, ‘2023-01-19’, ‘2023-01-26’, ‘2023-02-02’, ‘2023-02-09’, ‘2023-02-16’, ‘2023-02-23’]),
‘Region’: [‘East’, ‘West’, ‘East’, ‘South’, ‘West’, ‘East’, ‘West’, ‘South’, ‘East’],
‘Product’: [‘Apple’, ‘Banana’, ‘Banana’, ‘Apple’, ‘Orange’, ‘Apple’, ‘Banana’, ‘Orange’, ‘Orange’],
‘Quantity’: [100, 80, 50, 120, 90, 110, 95, 70, 60],
‘UnitPrice’: [1.2, 0.5, 0.6, 1.1, 0.8, 1.25, 0.55, 0.9, 0.85]}
sales_df = pd.DataFrame(sales_data)
sales_df[‘Revenue’] = sales_df[‘Quantity’] * sales_df[‘UnitPrice’]
print(“销售数据 DataFrame:”)
print(sales_df)
1. 计算每个月的总销售额
使用 pd.Grouper 按月分组
monthly_revenue = sales_df.groupby(pd.Grouper(key=’Date’, freq=’M’))[‘Revenue’].sum()
print(“\n1. 每月总销售额:”)
print(monthly_revenue)
2. 找出每个区域最畅销的产品(按总销售额)
先按区域和产品分组,计算总销售额
region_product_revenue = sales_df.groupby([‘Region’, ‘Product’])[‘Revenue’].sum().reset_index()
print(“\n中间步骤:各区域各产品总销售额:”)
print(region_product_revenue)
再按区域分组,找出每个区域内销售额最高的行
方法一:使用 apply
def top_product_in_group(group):
return group.loc[group[‘Revenue’].idxmax()]
top_products = region_product_revenue.groupby(‘Region’).apply(top_product_in_group)
方法二:使用 sort_values + drop_duplicates (通常更高效)
top_products = region_product_revenue.sort_values(‘Revenue’, ascending=False) \
.drop_duplicates(subset=[‘Region’], keep=’first’) \
.sort_values(‘Region’) # 可选,按区域排序
print(“\n2. 各区域最畅销产品:”)
print(top_products[[‘Region’, ‘Product’, ‘Revenue’]])
3. 计算每笔销售额占该产品当月总销售额的百分比
先计算产品每月的总销售额
sales_df[‘Month’] = sales_df[‘Date’].dt.to_period(‘M’)
product_monthly_revenue = sales_df.groupby([‘Month’, ‘Product’])[‘Revenue’].sum()
使用 transform 将月度总销售额广播回原始 DataFrame
sales_df[‘Product_Monthly_Total_Revenue’] = sales_df.groupby([‘Month’, ‘Product’])[‘Revenue’] \
.transform(‘sum’)
计算百分比
sales_df[‘Revenue_Percentage_of_MonthProduct’] = (sales_df[‘Revenue’] / sales_df[‘Product_Monthly_Total_Revenue’]) * 100
print(“\n3. 添加了占产品月度总销售额百分比:”)
print(sales_df[[‘Date’, ‘Region’, ‘Product’, ‘Revenue’, ‘Product_Monthly_Total_Revenue’, ‘Revenue_Percentage_of_MonthProduct’]].round(2))
4. 筛选出月销售额超过 $150 的区域
先计算每个区域每月的销售额
region_monthly_revenue = sales_df.groupby([‘Month’, ‘Region’])[‘Revenue’].sum().reset_index()
print(“\n中间步骤:各区域每月销售额:”)
print(region_monthly_revenue)
筛选出 Revenue > 150 的月份和区域组合
qualifying_groups = region_monthly_revenue[region_monthly_revenue[‘Revenue’] > 150][[‘Month’, ‘Region’]]
print(“\n月销售额 > 150 的区域和月份:”)
print(qualifying_groups)
使用这些组合去过滤原始 DataFrame (需要合并或 MultiIndex 过滤)
方法一:使用 merge
filtered_sales_df = pd.merge(sales_df, qualifying_groups, on=[‘Month’, ‘Region’], how=’inner’)
方法二:使用 filter (更 Groupby 风格)
需要先按月和区域分组
gb_month_region = sales_df.groupby([‘Month’, ‘Region’])
filtered_sales_df_filter = gb_month_region.filter(lambda x: x[‘Revenue’].sum() > 150)
print(“\n4. 仅保留月销售额 > 150 的区域的记录 (使用 filter):”)
print(filtered_sales_df_filter)
“`
这个案例展示了如何结合使用 groupby()
, agg()
, transform()
, filter()
, pd.Grouper
, 以及一些标准的 DataFrame 操作来回答复杂的数据分析问题。
10. 总结
Pandas 的 groupby()
是一个极其强大的数据分析工具,它完美实现了 Split-Apply-Combine 策略,使我们能够高效地对数据进行分组、聚合、转换和过滤。
关键要点回顾:
- 核心流程: 拆分(Split)、应用(Apply)、合并(Combine)。
- 主要操作:
agg()
/aggregate()
: 用于计算汇总统计量。灵活使用字典和pd.NamedAgg
来定制聚合。transform()
: 用于执行保持原始形状的组级计算,如标准化或填充缺失值。filter()
: 用于根据组的整体属性筛选数据,保留或剔除整个组。apply()
: 最通用的方法,适用于agg
/transform
/filter
无法处理的复杂组级操作,但要注意性能。
- 分组方式多样: 可以按列名、Series、索引级别、函数、字典、
pd.Grouper
等进行分组。 - 性能优化: 优先使用内置函数,明智选择 Apply 方法,利用
Categorical
类型,避免不必要的迭代。
掌握 groupby()
的用法是精通 Pandas 的关键一步。它不仅能让你写出更简洁、更 Pythonic 的代码,更能显著提升你处理和分析数据的效率。虽然本文涵盖了 groupby()
的绝大部分重要方面,但最好的学习方式仍然是实践。尝试将这些技巧应用到你自己的数据分析项目中,不断探索和实验。
希望这篇详尽的教程能为你扫清使用 Pandas Groupby 的障碍,让你在数据分析的道路上更加得心应手。现在,你可以自信地说,关于 Pandas Groupby,这篇教程真的够了!