6.数据分组与聚合

本文最后更新于 2025年7月27日 晚上

初始化与加载数据

首先,我们导入 pandas 库,并加载本次练习所需的数据集 某招聘网站数据.csv。题目要求对 createTime 列进行操作,所以在加载时使用 parse_dates 参数直接将其转换为日期时间类型,这是一个非常好的习惯。

1
2
3
4
5
import pandas as pd
import numpy as np # 导入numpy,后面聚合操作可能会用到

# 加载数据,并直接将 createTime 列解析为日期时间类型
df = pd.read_csv("某招聘网站数据.csv", parse_dates=['createTime'])

分组

groupby 是 Pandas 数据分析的基石,它遵循“分割-应用-合并”(Split-Apply-Combine)的原则来对数据进行处理和分析。

1. 分组统计|均值

  • 题目: 计算各区(district)的薪资(salary)均值。
  • 答案:
    1
    2
    mean_salary_by_district = df.groupby('district')['salary'].mean()
    print(mean_salary_by_district)
  • 解答过程:
    1. df.groupby('district'): 这是“分割”步骤。Pandas 会根据 district 列中的唯一值(如’上城区’, ‘下沙’等)将整个 DataFrame 分割成多个子集(组)。
    2. ['salary']: 在分割后的每个组上,我们指定要操作的列是 salary
    3. .mean(): 这是“应用-合并”步骤。对每个组的 salary 列计算其均值(mean),然后将所有组的结果合并成一个新的 Series,其中索引就是分组的依据——各个区名。

2. 分组统计|取消索引

  • 题目: 重新按照上一题要求进行分组,但不使用 district 做为索引。
  • 答案:
    1
    2
    mean_salary_no_index = df.groupby('district', as_index=False)['salary'].mean()
    print(mean_salary_no_index)
  • 解答过程:
    • groupby 函数有一个非常有用的参数 as_index
    • 默认情况下 as_index=True,会将分组的键(这里是 district)作为结果的索引。
    • 当我们设置为 as_index=False 时,分组的键会保留为一个普通的列,而结果会使用一个从 0 开始的默认整数索引。这在后续需要将结果作为普通表格处理时非常方便。

3. 分组统计|排序

  • 题目: 计算并提取平均薪资最高的区。
  • 答案:
    1
    2
    highest_salary_district = df.groupby('district')['salary'].mean().sort_values(by='salary', ascending=False).head(1)
    print(highest_salary_district)
  • 解答过程:
    • 这是一个链式操作,将多个步骤连接在一起:
    1. df.groupby('district')['salary'].mean(): 首先,像第一题一样计算出各区的平均薪资。
    2. .sort_values(ascending=False): 接着,对上一步得到的结果(一个以区名为索引的 Series)进行排序。ascending=False 表示降序排列,即薪资最高的区会排在最前面。
    3. .head(1): 最后,使用 .head(1) 提取排序后的第一行,即为平均薪资最高的那个区。

4. 分组统计|频率

  • 题目: 计算不同行政区(district),不同规模公司(companySize)出现的次数。
  • 答案:
    1
    2
    freq_by_dist_size = df.groupby(['district', 'companySize']).size()
    print(freq_by_dist_size)
  • 解答过程:
    1. df.groupby(['district', 'companySize']): 这里我们按多个列进行分组。只需将列名列表传给 groupby 即可。Pandas 会根据 districtcompanySize组合来创建分组。
    2. .size(): 这是一个专门用于 groupby 对象的聚合函数,它会直接返回每个分组包含的行数(即频率)。这比 .count() 更直接,因为它不关心任何特定列的内容。
    3. 结果的索引是一个**多级索引 (MultiIndex)**,由 districtcompanySize 两个层级构成。

5. 分组统计|修改索引名

  • 题目: 将上一题的索引名修改为 行政区公司规模
  • 答案:
    1
    2
    3
    4
    5
    6
    # 先得到上一题的结果
    freq_by_dist_size = df.groupby(['district', 'companySize']).size()

    # 修改索引名
    freq_by_dist_size.index.names = ['行政区', '公司规模']
    print(freq_by_dist_size)
  • 解答过程:
    • 对于一个带有索引的 Series 或 DataFrame,可以通过访问其 .index.names 属性来修改索引的名称。
    • 因为上一题的结果是多级索引,.index.names 是一个列表。我们直接将一个新的名称列表赋给它即可。

6. 分组统计|计数

  • 题目: 计算上一题,每个区出现的公司数量。
  • 答案:
    1
    2
    3
    4
    5
    # 先得到第4题的结果
    freq_by_dist_size = df.groupby(['district', 'companySize']).size()
    # 在多级索引上按第一层级('district')求和
    count_by_district = freq_by_dist_size.groupby(level = 'district').sum()
    print(count_by_district)
  • 解答过程:
    • 这个问题可以理解为:在第4题按“区-公司规模”分组计数的基础上,再按“区”进行一次聚合(求和)。
    • 对于一个多级索引的 Series,调用 .sum().mean() 等聚合函数时,可以通过 level 参数指定在哪一个索引层级上进行运算。这里 level='district' 告诉 Pandas 沿着 district 这一层索引来求和。

7. 分组查看|全部

  • 题目: 将数据按照 districtsalary 进行分组,并查看各分组内容。
  • 答案:
    1
    2
    grouped_obj = df.groupby(['district', 'salary'])
    print(grouped_obj.groups)
  • 解答过程:
    • 当我们执行 df.groupby(...) 时,会创建一个 DataFrameGroupBy 对象。这个对象本身并不直接显示数据。
    • 想要查看分组情况,可以访问其 .groups 属性。
    • .groups 会返回一个字典,其中:
      • 键 (key) 是一个元组,代表分组的依据,例如 ('上城区', 22500)
      • 值 (value) 是一个列表,包含了属于该分组的所有行的原始索引

8. 分组查看|指定

  • 题目: 将数据按照 districtsalary 进行分组,并查看西湖区薪资为 30000 的工作。
  • 答案:
    1
    2
    3
    grouped_obj = df.groupby(['district', 'salary'])
    xihu_30k_jobs = grouped_obj.get_group(('西湖区', 30000))
    print(xihu_30k_jobs)
  • 解答过程:
    • .get_group() 是从 GroupBy 对象中提取特定单个分组数据的方法。
    • 因为我们是按 ['district', 'salary'] 两个键分组的,所以需要向 .get_group() 传入一个元组 ('西湖区', 30000) 来指定想要查看的组。
    • 该方法会返回一个包含该组所有原始数据的完整 DataFrame。

9. 分组规则|通过匿名函数1

  • 题目: 根据 createTime 列,计算每天不同行政区新增的岗位数量。
  • 答案:
    1
    2
    daily_district_counts = df.groupby([df['createTime'].dt.day, 'district']).size().rename_axis(['发布日', '行政区'])
    print(daily_district_counts)
  • 解答过程:
    • groupby 的分组依据非常灵活,不一定非得是列名。它可以是一个与原 DataFrame 行数相同的数组或 Series
    1. df['createTime'].dt.day: .dt 是处理日期时间类型列的访问器 (accessor)。.dt.day 会提取出 createTime 列中每一天的“日”部分(如16, 15, 14等),形成一个新的 Series。
    2. df.groupby([..., 'district']): 我们将这个新生成的“日”Series和 district 列名一起放到一个列表中传给 groupby,实现按“日-区”的分组。
    3. .size(): 统计每个“日-区”组合的岗位数。
    4. .rename_axis(...): 为了让结果更清晰,使用 .rename_axis() 给两个索引层级命名。

10. 分组规则|通过匿名函数2

  • 题目: 计算各行政区的企业领域(industryField)包含电商的总数。
  • 答案:
    1
    2
    ecom_counts = df.groupby('district')['industryField'].apply(lambda x: x.str.contains('电商').sum())
    print(ecom_counts)
  • 解答过程:
    • .apply()groupby 中最灵活的方法,它可以接收一个自定义函数(这里用 lambda 匿名函数)。
    1. df.groupby('district')['industryField']: 首先,按区分组,并选取 industryField 列。
    2. .apply(lambda x: ...): apply 会将每个区的 industryField Series(记为 x)作为参数传递给 lambda 函数。
    3. x.str.contains('电商'): 对每个组的 industryField 列(x),使用 .str.contains('电商') 判断每一行是否包含“电商”二字,返回一个布尔值的 Series(True/False)。
    4. .sum(): 对上一步的布尔 Series 求和。在求和运算中,True 被视为 1,False 被视为 0。因此,.sum() 的结果就是该区包含“电商”的岗位总数。

11. 分组规则|通过内置函数

  • 题目: 通过 positionName 的长度进行分组,并计算不同长度岗位名称的薪资均值。
  • 答案:
    1
    2
    salary_by_len = df.groupby(df['positionName'].str.len())['salary'].mean()
    print(salary_by_len)
  • 解答过程:
    • 这与第9题类似,我们再次使用一个派生 Series 作为分组依据。
    1. df['positionName'].str.len(): .str.len() 会计算 positionName 列中每个字符串的长度,生成一个长度与原DataFrame相同的 Series。
    2. df.groupby(...): 使用这个长度 Series 作为 groupby 的键。
    3. ['salary'].mean(): 对每个“岗位名长度”组,计算其对应薪资的平均值。

12. 分组规则|通过字典

  • 题目: 将 scorematchScore 的和记为总分,与 salary 列同时进行分组,并查看结果。
  • 答案:
    1
    2
    3
    4
    5
    # 定义分组映射规则的字典
    mapping = {'score': '总分', 'matchScore': '总分', 'salary': '薪资'}
    # 按列名进行分组
    grouped_by_dict = df.T.groupby(mapping).sum().T
    print(grouped_by_dict.sum().head()) # 以sum聚合为例查看结果
  • 解答过程:
    • groupby 不仅可以按行分组,也可以按列分组。通过设置 axis=1 来实现。
    1. mapping = {...}: 我们创建一个字典。字典的 key 是原始的列名,value 是我们想把它们分到的组名。这里 scorematchScore 都被分到了“总分”组,salary 被分到了“薪资”组。
    2. df.groupby(mapping, axis=1): 将这个映射字典和 axis=1 传给 groupby。Pandas 就会按列进行分组。
    3. .sum(): 在每个列分组上进行聚合操作,例如求和。结果中,“总分”这一列的值就是原 scorematchScore 的和。

13. 分组规则|通过多列

  • 题目: 计算不同 工作年限(workYear)和 学历(education)之间的薪资均值。
  • 答案:
    1
    2
    mean_salary_by_exp_edu = df.groupby(['workYear', 'education'])['salary'].mean()
    print(mean_salary_by_exp_edu)
  • 解答过程:
    • 这与第4题是完全相同的逻辑,只是换了分组的列。
    • 通过向 groupby 传递一个包含多个列名的列表 ['workYear', 'education'],即可实现按“工作年限-学历”这个组合进行分组,并计算每个组合的平均薪资。

14. 分组转换|transform

  • 题目: 在原数据框 df 新增一列,数值为该区的平均薪资水平。
  • 答案:
    1
    2
    3
    df_transformed = df.copy() # 创建副本进行操作
    df_transformed['区平均工资'] = df.groupby('district')['salary'].transform('mean')
    print(df_transformed[['district', 'salary', '区平均工资']].head())
    • 解答过程:
    • transformgroupby 的一个非常强大的方法,它与 agg/apply 的核心区别在于:transform 返回一个与原始数据形状相同的 Series 或 DataFrame
    1. df.groupby('district')['salary'].transform('mean'): 这个操作会先按区计算好每个区的平均薪资。
    2. 然后,它会将计算出的平均值“广播”回原始的行。例如,所有 district 为“西湖区”的行,它们在新列中的值都会是同一个数字——“西湖区”的平均薪资。
    3. 这使得我们可以直接将结果作为新列 ['区平均工资'] 添加回 DataFrame,用于后续的比较或计算。

15. 分组过滤|filter

  • 题目: 提取平均工资小于 30000 的行政区的全部数据。
  • 答案:
    1
    2
    filtered_districts = df.groupby('district').filter(lambda x: x['salary'].mean() < 30000)
    print(filtered_districts.head())
  • 解答过程:
    • filter 是另一个 groupby 的核心方法,用于按组的性质来筛选数据。
    1. .filter(lambda x: ...): filter 会将每个分组(一个子 DataFrame,记为 x)传递给 lambda 函数。
    2. x['salary'].mean() < 30000: 这个函数必须返回一个布尔值(TrueFalse)。这里我们计算每个组 x 的平均薪资,并判断是否小于 30000。
    3. 如果函数返回 True,那么这个组的所有原始行都会被保留下来。
    4. 如果函数返回 False,这个组的所有原始行都会被丢弃。
    5. 最终,filter 会返回一个由所有被保留的组拼接而成的 DataFrame。

16. 分组可视化

  • 题目: 对杭州市各区公司数量进行分组,并使用柱状图进行可视化。
  • 答案:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import matplotlib.pyplot as plt
    # 解决中文显示问题
    plt.rcParams['font.sans-serif'] = ['SimHei']
    plt.rcParams['axes.unicode_minus'] = False

    district_counts = df['district'].value_counts()
    district_counts.plot(kind='bar', figsize=(12, 6), title='杭州市各区招聘岗位数量')
    plt.ylabel('岗位数量')
    plt.xlabel('行政区')
    plt.xticks(rotation=45)
    plt.show()
  • 解答过程:
    1. df['district'].value_counts(): 这是统计各区出现次数(即岗位数量)最快的方法。它会返回一个按数量降序排列的 Series。
    2. .plot(kind='bar', ...): Pandas 的 Series 和 DataFrame 对象内置了 .plot() 方法,可以方便地快速绘图(底层是 Matplotlib)。
    • kind='bar' 指定了图表类型为柱状图。
    • figsize 设置图表大小。
    • title 设置图表标题。
    1. plt.ylabel/xlabel/xticks: 使用 matplotlib.pyplot 的函数来进一步美化图表,如添加坐标轴标签、旋转x轴刻度标签以防重叠。
    2. plt.show(): 显示图表。

聚合

聚合(Aggregation)是指对分组后的数据进行汇总计算,如求和、求均值等。.agg() 是实现聚合操作最核心、最灵活的函数。

17. 聚合统计

  • 题目: 分组计算不同行政区,薪水的最小值、最大值和平均值。
  • 答案:
    1
    2
    salary_stats = df.groupby('district')['salary'].agg(['min', 'max', 'mean'])
    print(salary_stats)
  • 解答过程:
    • .agg(): 当你想对一个分组应用多个聚合函数时,.agg() 是最佳选择。
    • 将一个包含函数名称字符串的列表 ['min', 'max', 'mean'] 传递给 .agg(),它就会为每个区计算出这三个统计量,并生成一个以这些统计量名称为列名的 DataFrame。

18. 聚合统计|修改列名

  • 题目: 将上一题的列名(包括索引名)修改为中文。
  • 答案:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 先得到上一题的结果
    salary_stats = df.groupby('district')['salary'].agg(['min', 'max', 'mean'])
    # 使用 rename 方法修改
    salary_stats_renamed = salary_stats.rename(
    columns={'min': '最低工资', 'max': '最高工资', 'mean': '平均工资'},
    index={'district': '行政区'} # rename也可以修改索引名
    )
    salary_stats_renamed.index.name = '行政区' # 或者用这种方式修改索引名
    print(salary_stats_renamed)
  • 解答过程:
    • rename() 方法既可以修改列名(通过 columns 参数),也可以修改索引标签(通过 index 参数)。
    • 分别传入一个“原名:新名”的字典即可。
    • 对于索引名,也可以像第5题一样,通过 .index.name 属性直接赋值。

19. 聚合统计|组合

  • 题目: 对不同岗位(positionName)进行分组,并统计其薪水(salary)中位数和得分(score)均值。
  • 答案:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
      position_stats = df.groupby('positionName').agg(
    薪水中位数=('salary', 'median'),
    得分均值=('score', 'mean')
    )
    print(position_stats.head())
    ```- **解答过程**:
    - 这是 `.agg()` 最强大、最推荐的用法:**命名聚合 (Named Aggregation)**。
    - 语法是 `agg(新列名=('操作的原始列', '聚合函数'))`。
    - 这种方式可以一次性对**不同的列**应用**不同的聚合函数**,并且直接生成表意清晰的**新列名**,非常高效和优雅。

    ### **20. 聚合统计|多层**
    - **题目**: 对不同行政区进行分组,并统计薪水的均值、中位数、方差,以及得分的均值。
    - **答案**:
    ```python
    district_multi_stats = df.groupby('district').agg({
    'salary': ['mean', 'median', 'std'],
    'score': 'mean'
    })
    print(district_multi_stats)
  • 解答过程:
    • 当需要对多个列应用不同的聚合函数组合时,可以向 .agg() 传递一个字典。
    • 字典的键 (key) 是要操作的列名,如 'salary'
    • 字典的值 (value) 是要应用的聚合函数。如果对一列应用多个函数,值就是一个函数列表,如 ['mean', 'median', 'std'];如果只应用一个,值就是函数名字符串,如 'mean'
    • 结果会生成一个**多级列索引 (MultiIndex column)**。

21. 聚合统计|自定义函数

  • 题目: 在 18 题基础上,在聚合计算时新增一列计算最大值与平均值的差值。
  • 答案:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 定义一个自定义函数
    def max_mean_diff(x):
    return x.max() - x.mean()

    salary_custom_agg = df.groupby('district')['salary'].agg(
    最低工资='min',
    最高工资='max',
    平均工资='mean',
    最大值与均值差值=max_mean_diff
    )
    print(salary_custom_agg)
  • 解答过程:
    • .agg() 不仅能接受内置的函数名字符串,还能接受用户自己定义的函数
    1. 我们先定义一个函数 max_mean_diff,它接收一个 Series (x),并返回其最大值与平均值的差。
    2. .agg() 的命名聚合中,我们直接将这个函数对象 max_mean_diff 作为聚合函数传入。Pandas 会自动将每个区的 salary Series 传给这个函数并计算结果。
    3. 使用 lambda 匿名函数也可以实现同样的效果:最大值与均值差值=lambda x: x.max() - x.mean()。这使得 .agg() 的功能几乎是无限的。

6.数据分组与聚合
https://blog.wyyy.dpdns.org/2025/6-数据分组与聚合/
作者
lwy
发布于
2025年7月27日
许可协议