基础篇11:数据分组与聚合 (groupby)

在数据分析过程中,数据预处理往往包含对数据的分组和聚合操作。Pandas 中的 groupby() 方法是实现这一功能的核心工具。本文将详细介绍 Pandas 中分组与聚合的基本原理、常见用法、函数应用以及如何利用自定义函数实现更复杂的聚合需求。通过本章内容,你不仅能掌握分组聚合的使用方法,还能理解其中的“拆分—应用—合并(Split-Apply-Combine)”思想,为后续更高级的数据处理打下坚实的基础。


1. 引言

在实际数据处理过程中,我们经常需要对数据进行分类统计。例如,统计不同部门的销售总额、计算各类别的平均成绩或分析不同时间段内的指标变化。利用 Pandas 的 groupby() 方法,我们可以轻松实现这些需求。
从数学角度看,假设数据集中每个样本 x i j x_{ij} xij 属于某个组 i i i,那么聚合操作可以表示为
g i = f ( { x i j } j = 1 n i ) g_i = f\Big(\{ x_{ij} \}_{j=1}^{n_i}\Big) gi=f({xij}j=1ni)
其中, f f f 表示聚合函数,如求和、均值、最大值等。本文将详细介绍如何利用 Pandas 对数据进行分组,并利用各种聚合函数得到我们所需的统计结果。


2. Pandas 中的 GroupBy 概念

2.1 GroupBy 的基本思想

Pandas 的 groupby() 方法可以看作是将一个 DataFrame 根据指定的键拆分成多个子 DataFrame,然后对每个子 DataFrame 分别应用某个函数,最后将结果合并起来。这个过程通常被称为“拆分—应用—合并(Split-Apply-Combine)”策略:

  • 拆分(Split):根据一个或多个键将数据拆分成若干组,每一组包含满足相同条件的数据子集。
  • 应用(Apply):对每个子集应用一个函数 f f f,可以是内置的聚合函数(如 summeancount 等),也可以是自定义的函数。
  • 合并(Combine):将所有组的结果合并成一个新的 DataFrame 或 Series。

例如,假设我们有一个销售数据表,其中包含“地区”和“销售额”等信息。如果我们希望统计每个地区的总销售额,可以使用 groupby 操作将数据按地区拆分,然后对每个组求和,最终合并出一个按地区统计的结果。

2.2 数学公式表示

对于一个分组操作,我们可以定义:

  • 设数据集为 D = { x 1 , x 2 , … , x N } D=\{x_1, x_2, \dots, x_N\} D={x1,x2,,xN},其中每个样本 x i x_i xi 都带有一个分组标签 g ( x i ) g(x_i) g(xi)
  • 将数据集拆分为若干组 D k = { x i ∣ g ( x i ) = k } D_k = \{x_i \mid g(x_i) = k\} Dk={xig(xi)=k},其中 k k k 为不同的组标识。
  • 对每一组 D k D_k Dk,应用聚合函数 f f f,计算结果为
    y k = f ( D k ) y_k = f(D_k) yk=f(Dk)
  • 最终将所有结果合并成集合 Y = { y k } Y = \{y_k\} Y={yk},这就是分组聚合操作的输出。

例如,若我们要求各组的平均值,则有
y k = 1 ∣ D k ∣ ∑ x ∈ D k x y_k = \frac{1}{|D_k|} \sum_{x \in D_k} x yk=Dk1xDkx


3. 基本用法与代码示例

接下来,我们通过几个简单的例子,介绍如何使用 Pandas 的 groupby() 方法进行数据分组和聚合操作。

3.1 创建示例数据

首先,我们构造一个简单的 DataFrame,包含姓名、部门、工资和工作年限等信息:

import pandas as pd

data = {
    "姓名": ["张三", "李四", "王五", "赵六", "孙七", "周八", "吴九", "郑十"],
    "部门": ["销售", "销售", "技术", "技术", "技术", "人事", "人事", "销售"],
    "工资": [8000, 8500, 12000, 11500, 11000, 7000, 7200, 9000],
    "工作年限": [3, 4, 5, 3, 4, 2, 3, 5]
}

df = pd.DataFrame(data)
print(df)

输出结果如下:

    姓名  部门    工资  工作年限
0   张三  销售   8000      3
1   李四  销售   8500      4
2   王五  技术  12000      5
3   赵六  技术  11500      3
4   孙七  技术  11000      4
5   周八  人事   7000      2
6   吴九  人事   7200      3
7   郑十  销售   9000      5

3.2 按部门分组并计算聚合指标

3.2.1 计算各部门的平均工资

我们可以通过以下代码实现:

# 按部门分组后计算平均工资
avg_salary = df.groupby("部门")["工资"].mean()
print("各部门平均工资:")
print(avg_salary)

输出结果类似于:

各部门平均工资:
部门
人事    7100.0
销售    8500.0
技术   11500.0
Name: 工资, dtype: float64

这里,df.groupby("部门") 将 DataFrame 按“部门”列进行分组,后续通过 ["工资"].mean() 对每个组中“工资”这一列求平均值。

3.2.2 同时计算多个聚合指标

可以使用 agg() 方法同时对多个列应用多个聚合函数,例如计算每个部门的平均工资和总工资:

aggregated = df.groupby("部门").agg({
    "工资": ["mean", "sum"],
    "工作年限": "mean"
})
print("各部门聚合统计结果:")
print(aggregated)

输出结果可能为:

         工资           工作年限
        mean    sum      mean
部门                           
人事    7100.0   14200    2.5
销售    8500.0   25500    4.0
技术   11500.0   34500    4.0

在上述代码中,我们为“工资”列同时应用了 meansum 两个函数,为“工作年限”应用了 mean 函数。


4. 多重分组

在实际情况中,我们常常需要根据多个列进行分组。例如,我们有一个销售数据表,既要按地区分组,又要按产品类别分组。

4.1 示例数据

假设我们有如下数据:

data_sales = {
    "地区": ["华东", "华东", "华北", "华北", "华南", "华南", "华东", "华南"],
    "产品": ["手机", "笔记本", "手机", "笔记本", "手机", "笔记本", "手机", "笔记本"],
    "销售额": [12000, 25000, 15000, 30000, 10000, 20000, 13000, 22000]
}
df_sales = pd.DataFrame(data_sales)
print(df_sales)

输出结果:

    地区    产品    销售额
0  华东   手机   12000
1  华东  笔记本   25000
2  华北   手机   15000
3  华北  笔记本   30000
4  华南   手机   10000
5  华南  笔记本   20000
6  华东   手机   13000
7  华南  笔记本   22000

4.2 按“地区”和“产品”分组

我们可以同时按照“地区”和“产品”两列进行分组,并计算每组的销售总额:

grouped_sales = df_sales.groupby(["地区", "产品"])["销售额"].sum()
print("各地区、各产品销售总额:")
print(grouped_sales)

输出结果:

各地区、各产品销售总额:
地区  产品  
华东  笔记本    25000
     手机     25000
华北  笔记本    30000
     手机     15000
华南  笔记本    42000
     手机     10000
Name: 销售额, dtype: int64

这样,我们可以直观地看到每个地区中各产品的销售情况。


5. 自定义聚合函数

除了使用内置的聚合函数外,我们还可以传入自定义函数来对每个组的数据进行处理。假设我们想计算每个部门工资的极差(最大值减去最小值),可以自定义一个 lambda 表达式或函数来实现。

5.1 使用 lambda 表达式

salary_range = df.groupby("部门")["工资"].agg(lambda x: x.max() - x.min())
print("各部门工资极差:")
print(salary_range)

输出结果类似于:

各部门工资极差:
部门
人事      200
销售     1000
技术     500
Name: 工资, dtype: int64

5.2 定义自定义函数

我们也可以定义一个函数,并传入 agg() 方法。例如:

def calc_range(series):
    return series.max() - series.min()

salary_range_func = df.groupby("部门")["工资"].agg(calc_range)
print("各部门工资极差(自定义函数):")
print(salary_range_func)

输出与上例相同。


6. 分组后数据的变换与过滤

除了聚合操作外,Pandas 的 groupby 还提供了 transform 和 filter 方法,用于对分组后的数据进行变换或筛选。

6.1 transform 方法

transform 方法返回一个与原数据形状相同的对象,常用于数据标准化或归一化操作。举例来说,我们可以对每个部门的工资做标准化处理,使得每个组内数据转换为 (x - 均值) / 标准差 的形式。

# 对工资进行标准化处理
standardized_salary = df.groupby("部门")["工资"].transform(lambda x: (x - x.mean()) / x.std())
df["标准化工资"] = standardized_salary
print("添加标准化工资后的 DataFrame:")
print(df)

此时,每一行的“标准化工资”均反映了该员工工资相对于所在部门工资分布的标准差单位。

6.2 filter 方法

filter 方法可以根据各组的特征筛选出符合条件的组。例如,我们希望筛选出平均工资高于 9000 的部门:

high_salary_dept = df.groupby("部门").filter(lambda group: group["工资"].mean() > 9000)
print("平均工资高于9000的部门数据:")
print(high_salary_dept)

只有满足条件的部门(组)才会出现在结果中。


7. 利用 pipe() 实现链式调用

在实际项目中,我们往往希望将多个操作串联在一起,从而使代码更加清晰。Pandas 提供了 pipe() 方法,允许我们将分组操作与后续的函数调用结合起来。

例如,我们希望在对每个部门分组后,计算该部门所有员工工资的总额占公司总工资的百分比,可以使用如下方式:

def calc_pct(group):
    total = group["工资"].sum()
    company_total = df["工资"].sum()
    return total / company_total * 100

dept_pct = df.groupby("部门").pipe(lambda g: g.apply(calc_pct))
print("各部门工资总额占比:")
print(dept_pct["工资"])

这种链式调用使得代码逻辑更加清晰,便于阅读和维护。


8. Split-Apply-Combine 的流程图示意

为了更直观地理解 groupby 的拆分—应用—合并过程,我们可以使用 Mermaid 语法绘制一个简单的示意图。

flowchart LR
    A[原始 DataFrame]
    B[拆分:根据指定键将数据分为若干组]
    C[应用:对每个组应用聚合或变换函数]
    D[合并:将各组结果合并成最终输出]
    A --> B
    B --> C
    C --> D

上述流程图展示了 groupby 操作的基本步骤:首先从原始 DataFrame 拆分出多个组,然后对每个组执行某种函数操作,最后合并成一个新的 DataFrame 或 Series。


9. 实战案例:学生成绩数据分析

下面通过一个实际案例来演示如何利用 groupby 对数据进行分组与聚合操作。假设我们有一份学生成绩数据,包含学生姓名、科目和分数,我们希望统计每个科目的最高分、最低分和平均分。

9.1 构造数据

import pandas as pd

data_scores = {
    "学生": ["张三", "李四", "王五", "赵六", "孙七", "周八", "吴九", "郑十"],
    "科目": ["数学", "数学", "英语", "英语", "数学", "英语", "数学", "英语"],
    "分数": [88, 92, 75, 80, 85, 78, 95, 82]
}

df_scores = pd.DataFrame(data_scores)
print("学生成绩数据:")
print(df_scores)

输出结果:

    学生  科目  分数
0   张三  数学   88
1   李四  数学   92
2   王五  英语   75
3   赵六  英语   80
4   孙七  数学   85
5   周八  英语   78
6   吴九  数学   95
7   郑十  英语   82

9.2 按科目分组并聚合

我们希望统计每个科目的最高分、最低分和平均分,可以这样做:

scores_agg = df_scores.groupby("科目")["分数"].agg(
    最高分 = "max",
    最低分 = "min",
    平均分 = "mean"
)
print("各科目成绩统计:")
print(scores_agg)

输出结果可能为:

      最高分  最低分       平均分
科目                        
英语    82   75  78.75
数学    95   85  90.00

可以看到,每个科目的分数分布情况一目了然。


10. 进阶操作:多重分组与复杂聚合

在一些复杂场景下,我们可能需要对数据进行多重分组,并对不同列使用不同的聚合函数。假设我们有一个销售数据集,其中不仅包含产品销售额,还包含销售数量。我们希望统计每个区域、每个产品的总销售额、平均销售额以及销售数量总和。

10.1 构造销售数据

data_complex = {
    "区域": ["华东", "华东", "华北", "华北", "华南", "华南", "华东", "华南"],
    "产品": ["A", "B", "A", "B", "A", "B", "A", "B"],
    "销售额": [12000, 25000, 15000, 30000, 10000, 20000, 13000, 22000],
    "销售量": [100, 150, 80, 120, 90, 130, 110, 140]
}
df_complex = pd.DataFrame(data_complex)
print("销售数据:")
print(df_complex)

输出结果:

    区域 产品   销售额  销售量
0  华东  A  12000  100
1  华东  B  25000  150
2  华北  A  15000   80
3  华北  B  30000  120
4  华南  A  10000   90
5  华南  B  20000  130
6  华东  A  13000  110
7  华南  B  22000  140

10.2 多重分组聚合

我们可以同时按照“区域”和“产品”两列进行分组,并对“销售额”与“销售量”分别使用不同的聚合函数:

agg_result = df_complex.groupby(["区域", "产品"]).agg({
    "销售额": ["sum", "mean"],
    "销售量": "sum"
})
print("多重分组聚合结果:")
print(agg_result)

输出结果可能为:

         销售额           销售量
          sum    mean    sum
区域   产品                    
华东   A   25000  12500.0   210
      B   25000  25000.0   150
华北   A   15000  15000.0    80
      B   30000  30000.0   120
华南   A   10000  10000.0    90
      B   42000  21000.0   270

在这里,我们通过传入字典,将不同的列映射到不同的聚合函数。注意,聚合后的 DataFrame 会产生多级列索引,如果需要可以进一步重命名或“扁平化”列名。

例如,可以使用以下方式扁平化多级列索引:

# 扁平化列索引
agg_result.columns = ['_'.join(col).strip() for col in agg_result.columns.values]
agg_result = agg_result.reset_index()
print("扁平化后的聚合结果:")
print(agg_result)

输出结果:

    区域 产品  销售额_sum  销售额_mean  销售量_sum
0  华东  A    25000    12500.0      210
1  华东  B    25000    25000.0      150
2  华北  A    15000    15000.0       80
3  华北  B    30000    30000.0      120
4  华南  A    10000    10000.0       90
5  华南  B    42000    21000.0      270

11. 性能优化与注意事项

在使用 groupby 操作时,需要注意以下几点问题:

  1. 分组键的数据类型
    使用分类数据(category)作为分组键可以显著降低内存占用和加快分组速度。
    例如:

    df["部门"] = df["部门"].astype("category")
    
  2. 避免使用过多的 .apply()
    虽然 .apply() 提供了灵活性,但它往往会导致性能下降。应尽量使用内置的聚合函数或 vectorized 操作。
    如:

    # 尽量用 agg() 或 transform() 替代 apply()
    df.groupby("部门")["工资"].agg("mean")
    
  3. 合理使用 as_index 参数
    默认情况下,groupby 会将分组键设置为结果的索引。如果后续需要对结果进行进一步处理,可以设置 as_index=False
    例如:

    df.groupby("部门", as_index=False)["工资"].mean()
    
  4. 链式调用与 pipe()
    利用 pipe() 方法可以将多个操作串联在一起,使代码更简洁且易于调试。
    如前文所示,结合 groupby 与 pipe() 进行数据转换。

  5. 检查缺失值
    在分组前,检查分组键是否存在缺失值(NaN),因为默认情况下这些值会被忽略。必要时可先进行填充或剔除操作。

  6. 小批量处理
    对于超大数据集,可考虑使用分块读取(chunksize 参数)或结合 Dask、PySpark 等工具实现并行计算。


12. 实战案例总结

通过以上各部分内容,我们详细学习了 Pandas 中的 groupby 操作,从最基础的单列分组、聚合函数的使用,到多列分组、多个聚合函数的应用,再到自定义聚合函数、transform 和 filter 方法的使用,以及链式调用的高级技巧。下面我们对整个流程做一个总结:

  1. 拆分(Split):
    利用 df.groupby(分组键) 将 DataFrame 拆分为若干子集。
    数学上可表示为: D k = { x i ∣ g ( x i ) = k } D_k = \{ x_i \mid g(x_i) = k \} Dk={xig(xi)=k}

  2. 应用(Apply):
    对每个子集应用聚合函数 f f f,如求和、均值、计数等。
    表示为: y k = f ( D k ) y_k = f(D_k) yk=f(Dk)

  3. 合并(Combine):
    将所有组的结果合并成最终的输出对象。
    最终输出为: Y = { y k } Y = \{ y_k \} Y={yk}

通过这些操作,我们可以快速从原始数据中提取出有用的信息,从而支持进一步的数据分析、可视化和建模工作。


13. 总结与展望

Pandas 的 groupby 操作为数据分析提供了强大的分组和聚合功能。无论是对销售数据、学生成绩还是其他业务数据,我们都可以利用 groupby 快速实现分组统计、数据标准化以及自定义计算。掌握 split-apply-combine 思想,有助于我们更高效地处理海量数据和复杂数据结构。

在今后的学习中,可以进一步探索以下内容:

  • 如何利用多重索引管理复杂分组后的结果,并进行扁平化处理;
  • 与时间序列数据结合,使用 resample() 方法进行时间分组;
  • 利用 transform() 方法进行数据归一化、标准化等操作;
  • 探索 groupby 的性能优化技巧,如使用 categorical 类型、避免过多使用 apply() 等;
  • 将分组结果与数据可视化结合,利用 Matplotlib 或 Seaborn 展示分组统计结果。

通过不断实践和探索,你将能更熟练地利用 Pandas 的 groupby 操作,从而在数据预处理和特征工程阶段大大提高工作效率。


14. 结语

本文详细介绍了 Pandas 中数据分组与聚合的基本概念、常用方法和实战案例。希望你能够通过这些示例掌握 groupby 的使用技巧,并将其应用于实际数据分析中。如果在使用过程中遇到问题,不妨参考 Pandas 官方文档或查阅更多相关资料。
同时,建议大家多实践、多总结经验,只有不断动手操作,才能真正体会到 groupby 操作在大数据分析中的威力。

无论你是数据分析初学者,还是希望优化数据处理流程的高级用户,掌握 groupby 操作都是提升数据处理效率的重要技能。希望本章节内容能为你的数据科学之路增添一份信心和动力!


以上就是关于“基础篇11:数据分组与聚合 (groupby)”的完整博客内容。通过本文的学习,你应能理解并熟练使用 Pandas 的 groupby 方法,实现各类分组统计和自定义聚合操作,为进一步的数据分析和机器学习模型构建打下坚实的基础。

更多推荐