如何高效使用 Pandas Groupby?看这篇教程就够了 – wiki基地


如何高效使用 Pandas Groupby?看这篇教程就够了

在数据分析和处理的领域,Pandas 库无疑是 Python 生态系统中的瑞士军刀。它提供了强大、灵活且高效的数据结构(如 DataFrame 和 Series),使得处理结构化数据变得轻而易举。而在 Pandas 的众多功能中,groupby() 操作无疑是核心中的核心。无论你是需要进行数据聚合、转换、过滤还是执行更复杂的组级运算,groupby() 都是你不可或缺的工具。

然而,groupby() 的强大功能也伴随着一定的学习曲线。如何高效、正确地使用它,充分发挥其潜力,是许多数据分析师和开发者关心的问题。本教程旨在深入、全面地探讨 Pandas Groupby 的方方面面,从基础概念到高级技巧,再到性能优化,力求让你在阅读完这篇教程后,能够自信地说:“关于 Pandas Groupby,看这篇就够了!”

本文将涵盖以下内容:

  1. Groupby 的核心思想:Split-Apply-Combine
  2. 创建 Groupby 对象:多种分组方式
  3. 聚合(Aggregation):洞察数据的核心
    • 常用聚合函数
    • 多重聚合与自定义聚合
    • 命名聚合结果
  4. 转换(Transformation):保持形状的组级运算
    • transform() 的工作原理
    • 常见用例:标准化、填充缺失值等
  5. 过滤(Filtration):基于组属性筛选数据
    • filter() 的工作原理
    • 常见用例:筛选特定规模或特征的组
  6. 应用(Apply):最灵活的组级操作
    • apply() 的强大之处与注意事项
    • 何时选择 apply()
  7. 高级 Groupby 技巧
    • 使用函数或字典进行分组
    • 利用 pd.Grouper 进行时间序列和分箱分组
    • 处理 MultiIndex 输出
    • observed 参数与分类数据
  8. 性能优化:让 Groupby 飞起来
    • 利用内置函数
    • 选择合适的 Apply 方法
    • 数据类型的重要性
    • 避免不必要的迭代
  9. 实战案例:综合运用 Groupby 解决问题
  10. 总结

准备好了吗?让我们一起深入 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。
  • 合并(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() 的关键特点:

  1. 它对每个组应用一个函数。
  2. 该函数必须返回一个与该组具有相同索引的 Series 或 DataFrame,或者一个可以广播到该组大小的标量。
  3. 结果会被合并回一个与原始对象具有相同索引的 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() 的工作原理:

  1. 对每个组应用一个函数(通常是 lambda 函数)。
  2. 这个函数必须返回一个布尔值(TrueFalse)。
  3. 如果函数对某个组返回 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() 操作的性能至关重要。以下是一些提高效率的建议:

  1. 优先使用内置聚合函数: sum(), mean(), count(), size(), std(), max(), min() 等内置函数通常是用 Cython 实现的,速度远快于在 Python 层面运行的自定义函数或 lambda 函数。

  2. 明智地选择 agg(), transform(), filter(), apply()

    • 如果只需要聚合结果,使用 agg() 或直接调用聚合方法(如 .sum())。
    • 如果需要保持原始形状的转换,使用 transform()
    • 如果需要根据组属性过滤整个组,使用 filter()
    • 只有在前三者无法满足需求时,才考虑使用 apply(),因为它通常最慢。
  3. size() vs count() size() 直接计算每个组的行数(包括 NaN),通常比 count()(需要检查每列的非 NaN 值)更快。如果只是想知道组的大小,用 size()

  4. 指定 as_index=Falsegroupby() 调用中设置 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)

  5. 数据类型很重要:

    • 确保用于分组的列具有合适的数据类型。数值类型通常比字符串类型分组更快。
    • 如果一个列只有少数几个唯一值,将其转换为 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)
  6. 避免在 apply() 中做可以通过 agg()transform() 完成的事: 例如,在 apply() 函数内部计算均值,不如直接使用 .mean().agg('mean')

  7. 向量化操作优于迭代: 尽量使用 Pandas 或 NumPy 的向量化操作,而不是在 apply() 或迭代 Groupby 对象时手动循环处理行。

  8. 对于超大数据集: 如果数据量超出单机内存,可以考虑:

    • 分块处理(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

目标:

  1. 计算每个月的总销售额。
  2. 找出每个区域最畅销的产品(按总销售额)。
  3. 计算每笔销售额占该产品当月总销售额的百分比。
  4. 筛选出月销售额超过 $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,这篇教程真的够了!


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部