Pandas进阶肆 分组
pandas进阶系列根据datawhale远昊大佬的joyful pandas教程写一些自己的心得和补充,本文部分引用了原教程,并参考了《利用Python进行数据分析》、pandas官网
目前的进度是学习了整个课程并做了几个练一练,习题还没有做,练一练的思考过程在下文中有展示
一、分组模式及其对象
1. 分组的一般模式
分组操作在日常生活中使用极其广泛,例如:
- 依据 性 别 \color{#FF0000}{性别} 性别分组,统计全国人口 寿 命 \color{#00FF00}{寿命} 寿命的 平 均 值 \color{#0000FF}{平均值} 平均值
- 依据 季 节 \color{#FF0000}{季节} 季节分组,对每一个季节的 温 度 \color{#00FF00}{温度} 温度进行 组 内 标 准 化 \color{#0000FF}{组内标准化} 组内标准化
- 依据 班 级 \color{#FF0000}{班级} 班级筛选出组内 数 学 分 数 \color{#00FF00}{数学分数} 数学分数的 平 均 值 超 过 80 分 的 班 级 \color{#0000FF}{平均值超过80分的班级} 平均值超过80分的班级
从上述的几个例子中不难看出,想要实现分组操作,必须明确三个要素: 分 组 依 据 \color{#FF0000}{分组依据} 分组依据、 数 据 来 源 \color{#00FF00}{数据来源} 数据来源、 操 作 及 其 返 回 结 果 \color{#0000FF}{操作及其返回结果} 操作及其返回结果。同时从充分性的角度来说,如果明确了这三方面,就能确定一个分组操作,从而分组代码的一般模式即:
df.groupby(分组依据)[数据来源].使用操作
例如第一个例子中的代码就应该如下:
df.groupby('Gender')['Longevity'].mean()
import numpy as np
import pandas as pd
df = pd.read_csv('../data/learn_pandas.csv')
2. 分组依据的本质
分组依据的本质是所依据的项的unique值
【练一练】
请根据上下四分位数分割,将体重分为high、normal、low三组,统计身高的均值。
【我的思路】
#先看下数据的分布
df.describe()
| Height | Weight | Test_Number | |
|---|---|---|---|
| count | 183.000000 | 189.000000 | 200.000000 |
| mean | 163.218033 | 55.015873 | 1.645000 |
| std | 8.608879 | 12.824294 | 0.722207 |
| min | 145.400000 | 34.000000 | 1.000000 |
| 25% | 157.150000 | 46.000000 | 1.000000 |
| 50% | 161.900000 | 51.000000 | 1.500000 |
| 75% | 167.500000 | 65.000000 | 2.000000 |
| max | 193.900000 | 89.000000 | 3.000000 |
一开始的写法是查看describe的数据分布,然后手工取其中的分位数,实际工程肯定不能这么做,然后搜了下series.quantile是求分位数的实例方法,之前几章应该有学过,用的少还是不熟练得搜一下才知道,感谢这道题帮我强化记忆!
这里查了一下,groupby的分组依据可以是mapping, function, label, or list of labels
不管依据是什么,最终都是根据某列的唯一值的种类为分组依据,所以我根据Weight新建了一个分桶的特征Weight_quantile,并依据这个特征分组
def f(i):
a, b = df.Weight.quantile([0.25, 0.75])
if i <= a:
return 'low'
elif i <= b:
return 'normal'
else:
return 'high'
df['Weight_quantile'] = df.Weight.apply(f)
df.groupby('Weight_quantile')['Height'].mean()
Weight_quantile
high 173.207843
low 154.119149
normal 162.255294
Name: Height, dtype: float64
3. Groupby对象
能够注意到,最终具体做分组操作时,所调用的方法都来自于pandas中的groupby对象,这个对象上定义了许多方法,也具有一些方便的属性。
gb = df.groupby(['School', 'Grade'])
gb
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x0000024E9F837288>
【练一练】
上一小节介绍了可以通过drop_duplicates得到具体的组类别,现请用groups属性完成类似的功能。
【我的思路】
以School列为例,groupby对象有属性group, 该属性的值是School列的值到最终分的组的映射,由第一部分所讲,最终分的组实际上是分组依据的唯一值,因此这个映射关系的key值就是组的唯一值
下面对比一下效果
df['School'].drop_duplicates()
0 Shanghai Jiao Tong University
1 Peking University
3 Fudan University
5 Tsinghua University
Name: School, dtype: object
gb = df.groupby('School')
pd.Series(gb.groups.keys())
0 Fudan University
1 Peking University
2 Shanghai Jiao Tong University
3 Tsinghua University
dtype: object
当size作为DataFrame的属性时,返回的是表长乘以表宽的大小,但在groupby对象上表示统计每个组的元素个数:
gb.size()
School Grade
Fudan University Freshman 9
Junior 12
Senior 11
Sophomore 8
Peking University Freshman 13
Junior 8
Senior 8
Sophomore 5
Shanghai Jiao Tong University Freshman 13
Junior 17
Senior 22
Sophomore 5
Tsinghua University Freshman 17
Junior 22
Senior 14
Sophomore 16
dtype: int64
通过get_group方法可以直接获取所在组对应的行,此时必须知道组的具体名字:
gb.get_group(('Fudan University', 'Freshman'))
| School | Grade | Name | Gender | Height | Weight | Transfer | Test_Number | Test_Date | Time_Record | |
|---|---|---|---|---|---|---|---|---|---|---|
| 15 | Fudan University | Freshman | Changqiang Yang | Female | 156.0 | 49.0 | N | 3 | 2020/1/1 | 0:05:25 |
| 28 | Fudan University | Freshman | Gaoqiang Qin | Female | 170.2 | 63.0 | N | 2 | 2020/1/7 | 0:05:24 |
| 63 | Fudan University | Freshman | Gaofeng Zhao | Female | 152.2 | 43.0 | N | 2 | 2019/10/31 | 0:04:00 |
| 70 | Fudan University | Freshman | Yanquan Wang | Female | 163.5 | 55.0 | N | 1 | 2019/11/19 | 0:04:07 |
| 73 | Fudan University | Freshman | Feng Wang | Male | 176.3 | 74.0 | N | 1 | 2019/9/26 | 0:03:31 |
| 105 | Fudan University | Freshman | Qiang Shi | Female | 164.5 | 52.0 | N | 1 | 2019/12/11 | 0:04:23 |
| 108 | Fudan University | Freshman | Yanqiang Xu | Female | 152.4 | 38.0 | N | 1 | 2019/12/8 | 0:05:03 |
| 157 | Fudan University | Freshman | Xiaoli Lv | Female | 152.5 | 45.0 | N | 2 | 2019/9/11 | 0:04:17 |
| 186 | Fudan University | Freshman | Yanjuan Zhao | Female | NaN | 53.0 | N | 2 | 2019/10/9 | 0:04:21 |
17
这里列出了2个属性和2个方法,而先前的mean、median都是groupby对象上的方法,这些函数和许多其他函数的操作具有高度相似性,将在之后的小节进行专门介绍。
4. 分组的三大操作
熟悉了一些分组的基本知识后,重新回到开头举的三个例子,可能会发现一些端倪,即这三种类型的分组返回数据的结果型态并不一样:
- 第一个例子中,每一个组返回一个标量值,可以是平均值、中位数、组容量
size等 - 第二个例子中,做了原序列的标准化处理,也就是说每组返回的是一个
Series类型 - 第三个例子中,既不是标量也不是序列,返回的整个组所在行的本身,即返回了
DataFrame类型
由此,引申出分组的三大操作:聚合、变换和过滤,分别对应了三个例子的操作,下面就要分别介绍相应的agg、transform和filter函数及其操作。
二、聚合函数
1. 内置聚合函数
在介绍agg之前,首先要了解一些直接定义在groupby对象的聚合函数,因为它的速度基本都会经过内部的优化,使用功能时应当优先考虑。根据返回标量值的原则,包括如下函数:max/min/mean/median/count/all/any/idxmax/idxmin/mad/nunique/skew/quantile/sum/std/var/sem/size/prod。
【练一练】
请查阅文档,明确all/any/mad/skew/sem/prod函数的含义。
【我的思路】
- skew 每个组的偏度
- all 一个组内所有值为真则返回真,否则假
- any 一个组内任何值为真则返回真,否则假
- sem 标准差
- mad 平均绝对值偏差
- prod 乘积
gb = df.groupby('Gender')['Height']
gb.skew()
#Female左偏,Male右偏
Gender
Female -0.219253
Male 0.437535
Name: Height, dtype: float64
这里需要注意的是,groupby函数默认有个参数dropna值为True,所以如果使用默认参数,那么缺失值已经跳过了,会对all和any的结果产生影响。
如下实验:
第一组,使用默认参数求all和any
gb = df.groupby('Gender')['Height']
gb.all()
Gender
Female True
Male True
Name: Height, dtype: bool
gb.any()
Gender
Female True
Male True
Name: Height, dtype: bool
第二组,dropna=False再做测试
gb = df.groupby('Gender', dropna=False)['Height']
gb.all(skipna=False)
Gender
Female True
Male True
Name: Height, dtype: bool
gb.any()
Gender
Female True
Male True
Name: Height, dtype: bool
上述结果和我想的不太一样,本想证明all在分组不跳过空值的情况下因为有空值的存在会返回False,看来all并不会将空值识别为False
gb.sem()
Gender
Female 0.439893
Male 0.986985
Name: Height, dtype: float64
gb.prod()
Gender
Female 4.232080e+290
Male 1.594210e+114
Name: Height, dtype: float64
这些聚合函数当传入的数据来源包含多个列时,将按照列进行迭代计算:
gb = df.groupby('Gender')[['Height', 'Weight']]
gb.max()
| Height | Weight | |
|---|---|---|
| Gender | ||
| Female | 170.2 | 63.0 |
| Male | 193.9 | 89.0 |
2. agg方法
虽然在groupby对象上定义了许多方便的函数,但仍然有以下不便之处:
- 无法同时使用多个函数
- 无法对特定的列使用特定的聚合函数
- 无法使用自定义的聚合函数
- 无法直接对结果的列名在聚合前进行自定义命名
下面说明如何通过agg函数解决这四类问题:
【a】使用多个函数
当使用多个聚合函数时,需要用列表的形式把内置聚合函数的对应的字符串传入,先前提到的所有字符串都是合法的。
gb.agg(['sum', 'idxmax', 'skew'])
| Height | Weight | |||||
|---|---|---|---|---|---|---|
| sum | idxmax | skew | sum | idxmax | skew | |
| Gender | ||||||
| Female | 21014.0 | 28 | -0.219253 | 6469.0 | 28 | -0.268482 |
| Male | 8854.9 | 193 | 0.437535 | 3929.0 | 2 | -0.332393 |
从结果看,此时的列索引为多级索引,第一层为数据源,第二层为使用的聚合方法,分别逐一对列使用聚合,因此结果为6列。
【b】对特定的列使用特定的聚合函数
对于方法和列的特殊对应,可以通过构造字典传入agg中实现,其中字典以列名为键,以聚合字符串或字符串列表为值。
gb.agg({'Height':['mean','max'], 'Weight':'count'})
| Height | Weight | ||
|---|---|---|---|
| mean | max | count | |
| Gender | |||
| Female | 159.19697 | 170.2 | 135 |
| Male | 173.62549 | 193.9 | 54 |
gb.agg({'Height':['sum', 'idxmax', 'skew'], 'Weight':['sum', 'idxmax', 'skew']})
| Height | Weight | |||||
|---|---|---|---|---|---|---|
| sum | idxmax | skew | sum | idxmax | skew | |
| Gender | ||||||
| Female | 21014.0 | 28 | -0.219253 | 6469.0 | 28 | -0.268482 |
| Male | 8854.9 | 193 | 0.437535 | 3929.0 | 2 | -0.332393 |
【练一练】
请使用【b】中的传入字典的方法完成【a】中等价的聚合任务。
【我的思路】如上
【c】使用自定义函数
在agg中可以使用具体的自定义函数,
需
要
注
意
传
入
函
数
的
参
数
是
之
前
数
据
源
中
的
列
,
逐
列
进
行
计
算
\color{#FF0000}{需要注意传入函数的参数是之前数据源中的列,逐列进行计算}
需要注意传入函数的参数是之前数据源中的列,逐列进行计算。下面分组计算身高和体重的极差:
gb.agg(lambda x: x.mean()-x.min())
| Height | Weight | |
|---|---|---|
| Gender | ||
| Female | 13.79697 | 13.918519 |
| Male | 17.92549 | 21.759259 |
【练一练】
在groupby对象中可以使用describe方法进行统计信息汇总,请同时使用多个聚合函数,完成与该方法相同的功能。
【我的思路】
gb.describe()
| Height | Weight | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | mean | std | min | 25% | 50% | 75% | max | count | mean | std | min | 25% | 50% | 75% | max | |
| Gender | ||||||||||||||||
| Female | 132.0 | 159.19697 | 5.053982 | 145.4 | 155.675 | 159.6 | 162.825 | 170.2 | 135.0 | 47.918519 | 5.405983 | 34.0 | 44.0 | 48.0 | 52.00 | 63.0 |
| Male | 51.0 | 173.62549 | 7.048485 | 155.7 | 168.900 | 173.4 | 177.150 | 193.9 | 54.0 | 72.759259 | 7.772557 | 51.0 | 69.0 | 73.0 | 78.75 | 89.0 |
gb.agg(['count', 'mean', 'std', 'min', ('25%', lambda x: x.quantile(0.25)),
('50%', lambda x: x.quantile(0.5)), ('75%', lambda x: x.quantile(0.75)), 'max'])
| Height | Weight | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | mean | std | min | 25% | 50% | 75% | max | count | mean | std | min | 25% | 50% | 75% | max | |
| Gender | ||||||||||||||||
| Female | 132 | 159.19697 | 5.053982 | 145.4 | 155.675 | 159.6 | 162.825 | 170.2 | 135 | 47.918519 | 5.405983 | 34.0 | 44.0 | 48.0 | 52.00 | 63.0 |
| Male | 51 | 173.62549 | 7.048485 | 155.7 | 168.900 | 173.4 | 177.150 | 193.9 | 54 | 72.759259 | 7.772557 | 51.0 | 69.0 | 73.0 | 78.75 | 89.0 |
另外需要注意,使用对一个或者多个列使用单个聚合的时候,重命名需要加方括号,否则就不知道是新的名字还是手误输错的内置函数字符串:
gb.agg([('my_sum', 'sum')])
| Height | Weight | |
|---|---|---|
| my_sum | my_sum | |
| Gender | ||
| Female | 21014.0 | 6469.0 |
| Male | 8854.9 | 3929.0 |
gb.agg({'Height': [('my_func', my_func), 'sum'], 'Weight': [('range', lambda x:x.max())]})
| Height | Weight | ||
|---|---|---|---|
| my_func | sum | range | |
| Gender | |||
| Female | Low | 21014.0 | 63.0 |
| Male | High | 8854.9 | 89.0 |
三、变换和过滤
1. 变换函数与transform方法
变换函数的返回值为同长度的序列,最常用的内置变换函数是累计函数:cumcount/cumsum/cumprod/cummax/cummin,它们的使用方式和聚合函数类似,只不过完成的是组内累计操作。此外在groupby对象上还定义了填充类和滑窗类的变换函数,这些函数的一般形式将会分别在第七章和第十章中讨论,此处略过。
gb.cummax().head()
| Height | Weight | |
|---|---|---|
| 0 | 158.9 | 46.0 |
| 1 | 166.5 | 70.0 |
| 2 | 188.9 | 89.0 |
| 3 | NaN | 46.0 |
| 4 | 188.9 | 89.0 |
【练一练】
在groupby对象中,rank方法也是一个实用的变换函数,请查阅它的功能并给出一个使用的例子。
【我的思路】
rank是计算每个组按一定聚合函数聚合后的排序值。
使用案例:
在推荐系统中,设想有user和item的表,所有数据按item进行分组,之后求聚合函数average,即求每个item的popularity,如果将average改成rank,参数method设置为average,则值存在序数意义,表示某item比另一个item流行度更高。
这里我是这么理解的,如果助教大大发现我理解的不对,欢迎在评论区指正!
【END】
当用自定义变换时需要使用transform方法,被调用的自定义函数,
其
传
入
值
为
数
据
源
的
序
列
\color{#FF0000}{其传入值为数据源的序列}
其传入值为数据源的序列,与agg的传入类型是一致的,其最后的返回结果是行列索引与数据源一致的DataFrame。
现对身高和体重进行分组标准化,即减去组均值后除以组的标准差:
gb.transform(lambda x: (x-x.mean())/x.std()).head()
| Height | Weight | |
|---|---|---|
| 0 | -0.058760 | -0.354888 |
| 1 | -1.010925 | -0.355000 |
| 2 | 2.167063 | 2.089498 |
| 3 | NaN | -1.279789 |
| 4 | 0.053133 | 0.159631 |
【练一练】
对于transform方法无法像agg一样,通过传入字典来对指定列使用特定的变换,如果需要在一次transform的调用中实现这种功能,请给出解决方案。
【END】
前面提到了 transform 只能返回同长度的序列,但事实上还可以返回一个标量,这会使得结果被广播到其所在的整个组,这种 :red:标量广播 的技巧在特征工程中是非常常见的。例如,构造两列新特征来分别表示样本所在性别组的身高均值和体重均值:
gb.transform('mean').head() # 传入返回标量的函数也是可以的
| Height | Weight | |
|---|---|---|
| 0 | 159.19697 | 47.918519 |
| 1 | 173.62549 | 72.759259 |
| 2 | 173.62549 | 72.759259 |
| 3 | 159.19697 | 47.918519 |
| 4 | 173.62549 | 72.759259 |
2. 组索引与过滤
在上一章中介绍了索引的用法,那么索引和过滤有什么区别呢?
过滤在分组中是对于组的过滤,而索引是对于行的过滤,在第二章中的返回值,无论是布尔列表还是元素列表或者位置列表,本质上都是对于行的筛选,即如果筛选条件的则选入结果的表,否则不选入。
组过滤作为行过滤的推广,指的是如果对一个组的全体所在行进行统计的结果返回True则会被保留,False则该组会被过滤,最后把所有未被过滤的组其对应的所在行拼接起来作为DataFrame返回。
在groupby对象中,定义了filter方法进行组的筛选,其中自定义函数的输入参数为数据源构成的DataFrame本身,在之前例子中定义的groupby对象中,传入的就是df[['Height', 'Weight']],因此所有表方法和属性都可以在自定义函数中相应地使用,同时只需保证自定义函数的返回为布尔值即可。
例如,在原表中通过过滤得到所有容量大于100的组:
gb.filter(lambda x: x.shape[0] > 100).head()
| Height | Weight | |
|---|---|---|
| 0 | 158.9 | 46.0 |
| 3 | NaN | 41.0 |
| 5 | 158.0 | 51.0 |
| 6 | 162.5 | 52.0 |
| 7 | 161.9 | 50.0 |
【练一练】
从概念上说,索引功能是组过滤功能的子集,请使用filter函数完成loc[...]的功能,这里假设"..."是元素列表。
【END】
四、跨列分组
1. apply的引入
之前几节介绍了三大分组操作,但事实上还有一种常见的分组场景,无法用前面介绍的任何一种方法处理,例如现在如下定义身体质量指数BMI:
B
M
I
=
W
e
i
g
h
t
H
e
i
g
h
t
2
{\rm BMI} = {\rm\frac{Weight}{Height^2}}
BMI=Height2Weight
其中体重和身高的单位分别为千克和米,需要分组计算组BMI的均值。
首先,这显然不是过滤操作,因此filter不符合要求;其次,返回的均值是标量而不是序列,因此transform不符合要求;最后,似乎使用agg函数能够处理,但是之前强调过聚合函数是逐列处理的,而不能够
多
列
数
据
同
时
处
理
\color{#FF0000}{多列数据同时处理}
多列数据同时处理。由此,引出了apply函数来解决这一问题。
2. apply的使用
在设计上,apply的自定义函数传入参数与filter完全一致,只不过后者只允许返回布尔值。现如下解决上述计算问题:
def BMI(x):
Height = x['Height']/100
Weight = x['Weight']
BMI_value = Weight/Height**2
return BMI_value.mean()
gb.apply(BMI)
Gender
Female 18.860930
Male 24.318654
dtype: float64
除了返回标量之外,apply方法还可以返回一维Series和二维DataFrame,但它们产生的数据框维数和多级索引的层数应当如何变化?下面举三组例子就非常容易明白结果是如何生成的:
【a】标量情况:结果得到的是 Series ,索引与 agg 的结果一致
gb = df.groupby(['Gender','Test_Number'])[['Height','Weight']]
gb.apply(lambda x: 0)
Gender Test_Number
Female 1 0
2 0
3 0
Male 1 0
2 0
3 0
dtype: int64
gb.apply(lambda x: [0, 0]) # 虽然是列表,但是作为返回值仍然看作标量
Gender Test_Number
Female 1 [0, 0]
2 [0, 0]
3 [0, 0]
Male 1 [0, 0]
2 [0, 0]
3 [0, 0]
dtype: object
【b】Series情况:得到的是DataFrame,行索引与标量情况一致,列索引为Series的索引
gb.apply(lambda x: pd.Series([0,0],index=['a','b']))
| a | b | ||
|---|---|---|---|
| Gender | Test_Number | ||
| Female | 1 | 0 | 0 |
| 2 | 0 | 0 | |
| 3 | 0 | 0 | |
| Male | 1 | 0 | 0 |
| 2 | 0 | 0 | |
| 3 | 0 | 0 |
【练一练】
请尝试在apply传入的自定义函数中,根据组的某些特征返回相同长度但索引不同的Series,会报错吗?
【END】
【c】DataFrame情况:得到的是DataFrame,行索引最内层在每个组原先agg的结果索引上,再加一层返回的DataFrame行索引,同时分组结果DataFrame的列索引和返回的DataFrame列索引一致。
gb.apply(lambda x: pd.DataFrame(np.ones((2,2)), index = ['a','b'], columns=pd.Index([('w','x'),('y','z')])))
| w | y | |||
|---|---|---|---|---|
| x | z | |||
| Gender | Test_Number | |||
| Female | 1 | a | 1.0 | 1.0 |
| b | 1.0 | 1.0 | ||
| 2 | a | 1.0 | 1.0 | |
| b | 1.0 | 1.0 | ||
| 3 | a | 1.0 | 1.0 | |
| b | 1.0 | 1.0 | ||
| Male | 1 | a | 1.0 | 1.0 |
| b | 1.0 | 1.0 | ||
| 2 | a | 1.0 | 1.0 | |
| b | 1.0 | 1.0 | ||
| 3 | a | 1.0 | 1.0 | |
| b | 1.0 | 1.0 |
【练一练】
请尝试在apply传入的自定义函数中,根据组的某些特征返回相同大小但列索引不同的DataFrame,会报错吗?如果只是行索引不同,会报错吗?
【END】
最后需要强调的是,apply函数的灵活性是以牺牲一定性能为代价换得的,除非需要使用跨列处理的分组处理,否则应当使用其他专门设计的groupby对象方法,否则在性能上会存在较大的差距。同时,在使用聚合函数和变换函数时,也应当优先使用内置函数,它们经过了高度的性能优化,一般而言在速度上都会快于用自定义函数来实现。
【练一练】
在groupby对象中还定义了cov和corr函数,从概念上说也属于跨列的分组处理。请利用之前定义的gb对象,使用apply函数实现与gb.cov()同样的功能并比较它们的性能。

本文详细讲解了Pandas中数据分组的概念和方法,包括分组模式、分组依据的本质、Groupby对象及其操作。文章通过实例演示了如何使用内置聚合函数、agg方法、变换和过滤,以及跨列分组的apply方法。强调了agg、transform和apply的区别,同时提醒在实际使用中应注意性能优化,优先选择内置函数。

2688

被折叠的 条评论
为什么被折叠?



