分组操作
- 学习目标
- 应用groupby 进行分组,并对分组数据进行聚合,转换和过滤
- 应用自定义函数处理分组之后的数据
1 aggregate聚合
- 在SQL中我们经常使用 GROUP BY 将某个字段,按不同的取值进行分组, 在pandas中也有groupby函数
- 分组之后,每组都会有至少1条数据, 将这些数据进一步处理返回单个值的过程就是聚合,比如 分组之后计算算术平均值, 或者分组之后计算频数,都属于聚合
1.1 单变量分组聚合
需求:加载
data/gapminder
数据集,计算每一年的平均寿命
- 加载数据
import pandas as pd
df = pd.read_csv('data/gapminder.tsv', sep='\t')
df
- 通过
df.groupby('年份').平均寿命.mean()
来计算每一年的平均寿命
df.groupby('year').lifeExp.mean()
- 过程及输出截图如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sbQu4P9O-1631235139948)(./img/聚合-01.png)]
-
解析:
- groupby语句创建了若干组, 例如上面例子中, 对year字段分组, 会将数据中不同年份作为分组结果
# 对year字段(年份)进行去重,返回series对象 years = df.year.unique() years
- 挑选一个年份,提取df子集
# 针对1952年的数据取子集 y1952 = df.loc[df.year==1952,:] y1952
- 原df中某一年的子集,再选择平均寿命字段,最后计算该字段的平均值
y1952.lifeExp.mean()
- 整个运行过程如下图所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z5ShrsSH-1631235139950)(./img/聚合-02.png)]
- groupby 语句会针对每个不同年份重复上述过程,并把所有结果放入一个DataFrame中返回
- mean函数不是唯一的聚合函数, Pandas内置了许多方法, 都可以与groupby语句搭配使用
1.2 Pandas内置的聚合函数
- 可以与groupby一起使用的函数
Pandas方法 | Numpy函数 | 说明 |
---|---|---|
count | np.count_nonzero | 频率统计(不包含NaN值) |
size | 频率统计(包含NaN值) | |
mean | np.mean | 求平均值 |
median | 中位数 | |
std | np.std | 标准差 |
min | np.min | 最小值 |
quantile | np.percentile | 分位数 |
max | np.max | 求最大值 |
sum | np.sum | 求和 |
var | np.var | 方差 |
describe | 计数、平均值、标准差,最小值、分位数、最大值 | |
first | 返回第一行 | |
last | 返回最后一行 | |
nth | 返回第N行(Python从0开始计数) |
- 需求:使用
groupby
对洲进行分组,同时对分组结果利用describe
函数对平均寿命
进行统计,返回基本统计信息
df.groupby('continent').lifeExp.describe()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mxAD3AU0-1631235139951)(./img/聚合-03.png)]
1.3 其他聚合函数
1.3.1 使用numpy库的聚合函数
计算各大洲的平均寿命
- 可以使用Numpy库的mean函数
import numpy as np
df.groupby('continent').lifeExp.agg(np.mean)
# 输出结果如下
continent
Africa 48.865330
Americas 64.658737
Asia 60.064903
Europe 71.903686
Oceania 74.326208
Name: lifeExp, dtype: float64
- agg和aggregate效果一样
df.groupby('continent').lifeExp.aggregate(np.mean)
1.3.2 使用自定义的函数
计算各大洲的平均寿命
- 如果想在聚合的时候,使用非Pandas或其他库提供的计算, 可以自定义函数,然后再aggregate中调用它
def my_mean(values):
'''计算平均值
'''
n = len(values) # 获取数据条目数
sum = 0
for value in values:
sum += value
return(sum/n)
# 调用自定义函数
df.groupby('continent').lifeExp.agg(my_mean)
# 输出结果如下
continent
Africa 48.865330
Americas 64.658737
Asia 60.064903
Europe 71.903686
Oceania 74.326208
Name: lifeExp, dtype: float64
1.3.3 自定义聚合函数传入多个参数
需求:计算全球平均预期寿命的平均值,与分组之后的平均值做差
- 自定义函数可以有多个参数, 具体用法如下;第一个参数接受来自DataFrame分组这之后的值, 其余参数可自定义
df.groupby('指定分组的字段').lifeExp.agg(
自定义的函数名,
#自定义函数的默认参数是df.groupby分组后传入的seriers对象【不用写】
自定义函数所需要的额外参数1=参数1的值,
自定义函数所需要的额外参数2=参数2的值,
...
)
- 现在我们来计算本小节的需求,代码及计算结果如下
# 计算各大洲的平均寿命和全球平均寿命(整个数据集的平均寿命)的差值
def my_mean_diff(values, diff_value):
'''计算平均值和diff_value的差值并返回
'''
n = len(values)
sum = 0
for value in values:
sum+=value
mean = sum/n
return(mean-diff_value)
# 计算整个数据集的平均寿命
global_mean = df.lifeExp.mean()
print(global_mean)
# 调用自定义函数并传参
df.groupby('continent').lifeExp.agg(my_mean_diff,
diff_value=global_mean)
# 输出结果如下
59.47443936619713
continent
Africa -10.609109
Americas 5.184297
Asia 0.590464
Europe 12.429247
Oceania 14.851769
Name: lifeExp, dtype: float64
1.4 同时传入多个函数
需求:按年份计算全球寿命的最大值、最小值,以及平均值
- 分组之后可以同时使用多个聚合函数:可以把它们全部放入一个Python列表, 然后把整个列表传入agg或aggregate中
# 按年份计算全球寿命的最大值、最小值,以及平均值
df.groupby('year').lifeExp.agg([np.mean, np.max, np.min])
# 输出结果如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nWXaKvcx-1631235139953)(./img/聚合-04.png)]
1.5 向agg中传入字典
需求:按年计算全球平均寿命、全球总人口,以及人均GDP的平均值
agg
函数中可以传入字典,字典的key是df的列名,与key对应的value是pandas内置的聚合计算函数、其名称的字符串(可以查看本章1.2节内容可知Pandas常用内置聚合计算函数)
new_df = df.groupby('year').agg({'lifeExp':'mean',
'pop':'sum',
'gdpPercap':'mean'})
new_df
- 对返回的新df可以通过rename进行重命名
new_df.rename(columns={'lifeExp':'平均寿命','pop':'人口','gdpPercap':'人均Gdp'}).reset_index()
- 输出过程结果如下图所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-irqOiQdT-1631235139954)(./img/聚合-05.png)]
2 transform转换
- transform 转换,需要把DataFrame中的值传递给一个函数, 而后由该函数"转换"数据。
- aggregate(聚合) 返回单个聚合值,但transform 不会减少数据量
2.1 transform使用内置函数
- 加载数据
import pandas as pd
df = pd.read_excel('data/sales_transactions.xlsx')
df
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vErmSP6K-1631235139955)(./img/transfrom-01.png)]
数据解读:数据包含了不同的订单(order),以及订单里的不同商品的数量(quantity)、单价(unit price)和总价(ext price)
- 现在我们的任务是为数据表添加一列,该订单的价钱总和:即计算每一单的总价,并且在原df中添加该列
# 关键步骤:使用transform调用pandas内置聚合函数sum,根据相同的order,对ext price字段求和
df.groupby('order')['ext price'].transform('sum')
# 输出结果如下
0 576.12
1 576.12
2 576.12
3 8185.49
4 8185.49
5 8185.49
6 8185.49
7 8185.49
8 3724.49
9 3724.49
10 3724.49
11 3724.49
Name: ext price, dtype: float64
- 具体实现上述需求的完整代码
# 计算每一单的总价,并且在原df中添加该列
# 直接在原df中添加列
df['Order_Total'] = df.groupby('order')['ext price'].transform('sum')
df
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6QRvBe24-1631235139956)(./img/transfrom-02.png)]
- 整个过程中发生变化的过程如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CgzMqpdR-1631235139957)(./img/transfrom-03.png)]
2.2 transform使用自定义函数
transform除了可以使用pandas的内置函数以外,transform还可以使用自定义的函数
- 重新加载数据
# 重新加载数据
df = pd.read_excel('data/sales_transactions.xlsx')
df
- 需求:批量修改商品的单价,使其单价增加1元,并添加新列new unit price
# 自定义一个计算函数
def returnOrderTotal(x):
return x + 1
# transform使用自定义的函数,注意此时传入的函数名没有引号
df['new unit price'] = df.groupby('order')['unit price'].transform(returnOrderTotal)
df
- 输出结果如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gMmLzZeQ-1631235139958)(./img/transfrom-04.png)]
2.3 transform自定义函数使用多个参数
transform使用的自定义函数如果有多个参数时,如下
# transform自定义函数有多个参数
def returnOrderTotal(x, y):
return x + y
df['new unit price'] = df.groupby('order')['unit price'].transform(returnOrderTotal, y=3)
df
# 输出结果如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GchOUeOV-1631235139959)(./img/transfrom-05.png)]
2.4 transform分组填充缺失值
某些特定情况下,可以考虑将列进行分组,分组之后取平均再填充缺失值
- 加载数据
# 加载数据 设定随机种子 随机抽取10行数据
tips_10 = pd.read_csv('data/tips.csv').sample(10, random_state=42)
tips_10
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A54K7KPr-1631235139960)(./img/transfrom-06.png)]
- 构建缺失值
# 构建缺失值
print([i for i in tips_10['total_bill']])
import numpy as np
tips_10['total_bill'] = [
np.NaN, 8.77, 24.55, 25.89, 13.0,
np.NaN, 28.44, 12.48, np.NaN, np.NaN
]
tips_10
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bNNrTD0f-1631235139960)(./img/transfrom-07.png)]
- 查看缺失情况
# 通过groupby之后再通过count函数计算有效值的数量,即可看出缺失值的情况
count_sex = tips_10.groupby('sex').count()
count_sex
# 通过下图得知:total_bill列,最少有4个缺失值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4XGNzD2i-1631235139961)(./img/transfrom-08.png)]
- transform使用自定义函数填充缺失值
def fill_nan_mean(x):
# 求平均
avg = x.mean()
# 填充缺失值:非空值不变,空值被填入
return x.fillna(avg)
total_bill_group_mean = tips_10.groupby('sex').total_bill.transform(fill_nan_mean)
print(total_bill_group_mean)
tips_10['total_bill'] = total_bill_group_mean
tips_10
# 输出结果如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QJFWapvO-1631235139962)(./img/transfrom-09.png)]
- 对比total_bill 和 fill_total_bill 发现 Male 和 Female 的填充值不同
3 filter过滤
需求:某一家餐馆要开连锁分店,新分店要采购餐桌;买什么样的餐桌就是一个问题:需要确定餐桌的可坐人数;可以从老店的就餐情况数据中进行分析
- 加载
data/tips.csv
餐厅就餐数据
tips = pd.read_csv('data/tips.csv')
tips
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UgOBZagB-1631235139963)(./img/过滤-1.png)]
- 查看用餐人数统计
# 用餐人数统计:对size列的值、出现的次数进行统计
tips['size'].value_counts()
# 返回结果如下
2 156
3 38
4 37
5 5
6 4
1 4
Name: size, dtype: int64
- 结果显示,人数为1、5和6人的数据比较少,考虑将这部分数据过滤掉;这个时候就可以使用
df.groupby().filter()
;groupby方法后接filter方法,filter传入一个返回布尔值的函数,该函数的入参就是groupby分组之后的每一组数据,返回False的数据会被过滤掉
# 按人数分组之后过滤:只保留 相同人数出现的次数大于30的数据(lambda x: x['size'].count()>30)
tips_filtered = tips.groupby('size').filter(lambda x: x['size'].count()>30)
tips_filtered
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JP5q5F1y-1631235139963)(./img/过滤-2.png)]
- 重新统计得出结论:新餐厅应该买4人桌
tips_filtered['size'].value_counts()
# 返回结果如下
2 156
3 38
4 37
Name: size, dtype: int64
4 DataFrameGroupBy对象
4.1 分组对象
- 准备数据
tips_10 = pd.read_csv('data/tips.csv').sample(10, random_state=42)
tips_10
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vSORgvtB-1631235139964)(./img/分组-1.png)]
- 调用groupby 创建分组对象
# 调用groupby 创建分组对象
grouped = tips_10.groupby('sex')
grouped
# 返回结果如下
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x0000020591F63CF8>
- grouped是一个DataFrameGroupBy对象,如果想查看计算过的分组,可以借助groups属性实现,返回的结果是一个字典,字段的值是一个列表,列表中的每一个元素是原始数据DataFrame的行索引值
grouped.groups
# 返回结果如下
{'Female': [198, 124, 101], 'Male': [24, 6, 153, 211, 176, 192, 9]}
- 在DataFrameGroupBy对象基础上,直接就可以进行aggregate,transform计算了
# 计算按sex分组后,所有列的平均值,但只返回了数值列的结果,非数值列不会计算平均值
grouped.mean()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VA6BggBm-1631235139964)(./img/分组-2.png)]
- 通过get_group选择分组
female = grouped.get_group('Female')
female
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Y4Zf5tc-1631235139965)(./img/分组-3.png)]
4.2 遍历分组
DataFrameGroupBy对象只能遍历不能通过下标取值
- 通过DataFrameGroupBy对象,可以遍历所有分组,相比于在groupby之后使用aggregate、transform和filter,有时候使用for循环解决问题更简单
# 遍历返回的对象是一个元组
for sex_group in grouped:
print(sex_group)
显示结果:
('Female', total_bill tip sex smoker day time size 198 13.00 2.00 Female Yes Thur Lunch 2 124 12.48 2.52 Female No Thur Lunch 2 101 15.38 3.00 Female Yes Fri Dinner 2) ('Male', total_bill tip sex smoker day time size 24 19.82 3.18 Male No Sat Dinner 2 6 8.77 2.00 Male No Sun Dinner 2 153 24.55 2.00 Male No Sun Dinner 4 211 25.89 5.16 Male Yes Sat Dinner 4 176 17.89 2.00 Male Yes Sun Dinner 2 192 28.44 2.56 Male Yes Thur Lunch 2 9 14.78 3.23 Male No Sun Dinner 2)
- DataFrameGroupBy对象直接传入索引,会报错
grouped[0]
显示结果:
--------------------------------------------------------------------------- KeyError Traceback (most recent call last) <ipython-input-75-2ce84a56ac6b> in <module>() ----> 1 grouped[0] e:\python\data\lib\site-packages\pandas\core\groupby\generic.py in __getitem__(self, key) 1642 stacklevel=2, 1643 ) -> 1644 return super().__getitem__(key) 1645 1646 def _gotitem(self, key, ndim: int, subset=None): e:\python\data\lib\site-packages\pandas\core\base.py in __getitem__(self, key) 226 else: 227 if key not in self.obj: --> 228 raise KeyError(f"Column not found: {key}") 229 return self._gotitem(key, ndim=1) 230 KeyError: 'Column not found: 0'
for sex_group in grouped:
#遍历grouped对象,查看sex_group数据类型
print(type(sex_group))
# 查看元素个数
print(len(sex_group))
# 查看第一个元素
print(sex_group[0])
# 查看第一个元素数据类型
print(type(sex_group[0]))
# 查看第二个元素
print(sex_group[1])
# 查看第二个元素数据类型
print(type(sex_group[1]))
break
显示结果:
<class 'tuple'> 2 Female <class 'str'> total_bill tip sex smoker day time size 198 13.00 2.00 Female Yes Thur Lunch 2 124 12.48 2.52 Female No Thur Lunch 2 101 15.38 3.00 Female Yes Fri Dinner 2 <class 'pandas.core.frame.DataFrame'>
4.3 多个分组
前面使用的groupby语句只包含一个变量,可以在groupby中添加多个变量
- 比如上面用到的消费数据集,可以使用groupby按性别和用餐时间分别计算小费数据的平均值
group_avg = tips.groupby(['sex','time']).mean()
group_avg
# 通过下图可以得出看出:
# 1. 晚餐给的小费多
# 2. 男性给的小费多
# 3. 晚餐比午餐花费更多
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p57cMR53-1631235139965)(./img/分组-4.png)]
- 查看分组之后结果的列名
group_avg.columns
# 输出结果如下
Index(['total_bill', 'tip', 'size'], dtype='object')
- 查看分组之后结果的行索引值
group_avg.index
# 输出结果如下
MultiIndex([('Female', 'Dinner'),
('Female', 'Lunch'),
( 'Male', 'Dinner'),
( 'Male', 'Lunch')],
names=['sex', 'time'])
- 可以看到,多个分组之后返回的df的索引是MultiIndex多级索引,如果想得到一个普通的DataFrame,可以在结果上调用reset_index方法
group_avg.reset_index()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nIqHObCF-1631235139966)(./img/分组-5.png)]
- 也可以在分组的时候通过as_index = False参数(默认是True),效果与调用reset_index()一样
tips.groupby(['sex','time'], as_index=False).mean()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h5q7bc5z-1631235139966)(./img/分组-6.png)]
小结
- 分组是数据分析中常见的操作,有助于从不同角度观察数据
- 分组之后可以得到DataFrameGroupby对象,该对象可以进行聚合、转换、过滤操作
- 分组之后的数据处理可以使用已有的内置函数,也可以使用自定义函数
- 分组不但可以对单个字段进行分组,也可以对多个字段进行分组,多个字段分组之后可以得到MultiIndex数据,可以通过reset_index方法将数据变成普通的DataFrame