6.数据分组与聚合
本文最后更新于 2025年7月27日 晚上
初始化与加载数据
首先,我们导入 pandas
库,并加载本次练习所需的数据集 某招聘网站数据.csv
。题目要求对 createTime
列进行操作,所以在加载时使用 parse_dates
参数直接将其转换为日期时间类型,这是一个非常好的习惯。
1 |
|
分组
groupby
是 Pandas 数据分析的基石,它遵循“分割-应用-合并”(Split-Apply-Combine)的原则来对数据进行处理和分析。
1. 分组统计|均值
- 题目: 计算各区(
district
)的薪资(salary
)均值。 - 答案:
1
2mean_salary_by_district = df.groupby('district')['salary'].mean()
print(mean_salary_by_district) - 解答过程:
df.groupby('district')
: 这是“分割”步骤。Pandas 会根据district
列中的唯一值(如’上城区’, ‘下沙’等)将整个 DataFrame 分割成多个子集(组)。['salary']
: 在分割后的每个组上,我们指定要操作的列是salary
。.mean()
: 这是“应用-合并”步骤。对每个组的salary
列计算其均值(mean),然后将所有组的结果合并成一个新的 Series,其中索引就是分组的依据——各个区名。
2. 分组统计|取消索引
- 题目: 重新按照上一题要求进行分组,但不使用
district
做为索引。 - 答案:
1
2mean_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
2highest_salary_district = df.groupby('district')['salary'].mean().sort_values(by='salary', ascending=False).head(1)
print(highest_salary_district) - 解答过程:
- 这是一个链式操作,将多个步骤连接在一起:
df.groupby('district')['salary'].mean()
: 首先,像第一题一样计算出各区的平均薪资。.sort_values(ascending=False)
: 接着,对上一步得到的结果(一个以区名为索引的 Series)进行排序。ascending=False
表示降序排列,即薪资最高的区会排在最前面。.head(1)
: 最后,使用.head(1)
提取排序后的第一行,即为平均薪资最高的那个区。
4. 分组统计|频率
- 题目: 计算不同行政区(
district
),不同规模公司(companySize
)出现的次数。 - 答案:
1
2freq_by_dist_size = df.groupby(['district', 'companySize']).size()
print(freq_by_dist_size) - 解答过程:
df.groupby(['district', 'companySize'])
: 这里我们按多个列进行分组。只需将列名列表传给groupby
即可。Pandas 会根据district
和companySize
的组合来创建分组。.size()
: 这是一个专门用于groupby
对象的聚合函数,它会直接返回每个分组包含的行数(即频率)。这比.count()
更直接,因为它不关心任何特定列的内容。- 结果的索引是一个**多级索引 (MultiIndex)**,由
district
和companySize
两个层级构成。
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
是一个列表。我们直接将一个新的名称列表赋给它即可。
- 对于一个带有索引的 Series 或 DataFrame,可以通过访问其
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. 分组查看|全部
- 题目: 将数据按照
district
、salary
进行分组,并查看各分组内容。 - 答案:
1
2grouped_obj = df.groupby(['district', 'salary'])
print(grouped_obj.groups) - 解答过程:
- 当我们执行
df.groupby(...)
时,会创建一个DataFrameGroupBy
对象。这个对象本身并不直接显示数据。 - 想要查看分组情况,可以访问其
.groups
属性。 .groups
会返回一个字典,其中:- 键 (key) 是一个元组,代表分组的依据,例如
('上城区', 22500)
。 - 值 (value) 是一个列表,包含了属于该分组的所有行的原始索引。
- 键 (key) 是一个元组,代表分组的依据,例如
- 当我们执行
8. 分组查看|指定
- 题目: 将数据按照
district
、salary
进行分组,并查看西湖区薪资为 30000 的工作。 - 答案:
1
2
3grouped_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
2daily_district_counts = df.groupby([df['createTime'].dt.day, 'district']).size().rename_axis(['发布日', '行政区'])
print(daily_district_counts) - 解答过程:
groupby
的分组依据非常灵活,不一定非得是列名。它可以是一个与原 DataFrame 行数相同的数组或 Series。
df['createTime'].dt.day
:.dt
是处理日期时间类型列的访问器 (accessor)。.dt.day
会提取出createTime
列中每一天的“日”部分(如16, 15, 14等),形成一个新的 Series。df.groupby([..., 'district'])
: 我们将这个新生成的“日”Series和district
列名一起放到一个列表中传给groupby
,实现按“日-区”的分组。.size()
: 统计每个“日-区”组合的岗位数。.rename_axis(...)
: 为了让结果更清晰,使用.rename_axis()
给两个索引层级命名。
10. 分组规则|通过匿名函数2
- 题目: 计算各行政区的企业领域(industryField)包含电商的总数。
- 答案:
1
2ecom_counts = df.groupby('district')['industryField'].apply(lambda x: x.str.contains('电商').sum())
print(ecom_counts) - 解答过程:
.apply()
是groupby
中最灵活的方法,它可以接收一个自定义函数(这里用lambda
匿名函数)。
df.groupby('district')['industryField']
: 首先,按区分组,并选取industryField
列。.apply(lambda x: ...)
:apply
会将每个区的industryField
Series(记为x
)作为参数传递给lambda
函数。x.str.contains('电商')
: 对每个组的industryField
列(x
),使用.str.contains('电商')
判断每一行是否包含“电商”二字,返回一个布尔值的 Series(True
/False
)。.sum()
: 对上一步的布尔 Series 求和。在求和运算中,True
被视为 1,False
被视为 0。因此,.sum()
的结果就是该区包含“电商”的岗位总数。
11. 分组规则|通过内置函数
- 题目: 通过
positionName
的长度进行分组,并计算不同长度岗位名称的薪资均值。 - 答案:
1
2salary_by_len = df.groupby(df['positionName'].str.len())['salary'].mean()
print(salary_by_len) - 解答过程:
- 这与第9题类似,我们再次使用一个派生 Series 作为分组依据。
df['positionName'].str.len()
:.str.len()
会计算positionName
列中每个字符串的长度,生成一个长度与原DataFrame相同的 Series。df.groupby(...)
: 使用这个长度 Series 作为groupby
的键。['salary'].mean()
: 对每个“岗位名长度”组,计算其对应薪资的平均值。
12. 分组规则|通过字典
- 题目: 将
score
和matchScore
的和记为总分,与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
来实现。
mapping = {...}
: 我们创建一个字典。字典的key
是原始的列名,value
是我们想把它们分到的组名。这里score
和matchScore
都被分到了“总分”组,salary
被分到了“薪资”组。df.groupby(mapping, axis=1)
: 将这个映射字典和axis=1
传给groupby
。Pandas 就会按列进行分组。.sum()
: 在每个列分组上进行聚合操作,例如求和。结果中,“总分”这一列的值就是原score
和matchScore
的和。
13. 分组规则|通过多列
- 题目: 计算不同 工作年限(
workYear
)和 学历(education
)之间的薪资均值。 - 答案:
1
2mean_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
3df_transformed = df.copy() # 创建副本进行操作
df_transformed['区平均工资'] = df.groupby('district')['salary'].transform('mean')
print(df_transformed[['district', 'salary', '区平均工资']].head())- 解答过程:
transform
是groupby
的一个非常强大的方法,它与agg
/apply
的核心区别在于:transform
返回一个与原始数据形状相同的 Series 或 DataFrame。
df.groupby('district')['salary'].transform('mean')
: 这个操作会先按区计算好每个区的平均薪资。- 然后,它会将计算出的平均值“广播”回原始的行。例如,所有
district
为“西湖区”的行,它们在新列中的值都会是同一个数字——“西湖区”的平均薪资。 - 这使得我们可以直接将结果作为新列
['区平均工资']
添加回 DataFrame,用于后续的比较或计算。
15. 分组过滤|filter
- 题目: 提取平均工资小于 30000 的行政区的全部数据。
- 答案:
1
2filtered_districts = df.groupby('district').filter(lambda x: x['salary'].mean() < 30000)
print(filtered_districts.head()) - 解答过程:
filter
是另一个groupby
的核心方法,用于按组的性质来筛选数据。
.filter(lambda x: ...)
:filter
会将每个分组(一个子 DataFrame,记为x
)传递给lambda
函数。x['salary'].mean() < 30000
: 这个函数必须返回一个布尔值(True
或False
)。这里我们计算每个组x
的平均薪资,并判断是否小于 30000。- 如果函数返回
True
,那么这个组的所有原始行都会被保留下来。 - 如果函数返回
False
,这个组的所有原始行都会被丢弃。 - 最终,
filter
会返回一个由所有被保留的组拼接而成的 DataFrame。
16. 分组可视化
- 题目: 对杭州市各区公司数量进行分组,并使用柱状图进行可视化。
- 答案:
1
2
3
4
5
6
7
8
9
10
11import 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() - 解答过程:
df['district'].value_counts()
: 这是统计各区出现次数(即岗位数量)最快的方法。它会返回一个按数量降序排列的 Series。.plot(kind='bar', ...)
: Pandas 的 Series 和 DataFrame 对象内置了.plot()
方法,可以方便地快速绘图(底层是 Matplotlib)。
kind='bar'
指定了图表类型为柱状图。figsize
设置图表大小。title
设置图表标题。
plt.ylabel/xlabel/xticks
: 使用matplotlib.pyplot
的函数来进一步美化图表,如添加坐标轴标签、旋转x轴刻度标签以防重叠。plt.show()
: 显示图表。
聚合
聚合(Aggregation)是指对分组后的数据进行汇总计算,如求和、求均值等。.agg()
是实现聚合操作最核心、最灵活的函数。
17. 聚合统计
- 题目: 分组计算不同行政区,薪水的最小值、最大值和平均值。
- 答案:
1
2salary_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
19position_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()
不仅能接受内置的函数名字符串,还能接受用户自己定义的函数。
- 我们先定义一个函数
max_mean_diff
,它接收一个 Series (x
),并返回其最大值与平均值的差。 - 在
.agg()
的命名聚合中,我们直接将这个函数对象max_mean_diff
作为聚合函数传入。Pandas 会自动将每个区的salary
Series 传给这个函数并计算结果。 - 使用
lambda
匿名函数也可以实现同样的效果:最大值与均值差值=lambda x: x.max() - x.mean()
。这使得.agg()
的功能几乎是无限的。
6.数据分组与聚合
https://blog.wyyy.dpdns.org/2025/6-数据分组与聚合/