第四章 分组
一、分组模式及其对象
1 、分组的一般模式
想要实现分组操作,必须明确三个元素:分组依据、数据来源、操作及返回结果,分组代码的一般模式即:
df.groupby(分组依据)[数据来源].使用操作
依据性别分组,统计全国人口寿命的平均值:
df.groupby('Gender')['Longevity'].mean( )
按照性别统计身高中位数:
df = pd.read_csv('joyful-pandas/data/learn_pandas.csv')
df.groupby('Gender')['Height'].median()
# Gender
# Female 159.6
# Male 173.4
# Name: Height, dtype: float64
2、分组依据的本质
在gruopby中传入多个列名构成的列表,即可实现根据多个维度进行分组。
根据学校和性别进行分组,统计身高的均值:
df.groupby(['School', 'Gender'])['Height'].mean()
# School Gender
# Fudan University Female 158.776923
# Male 174.212500
# Peking University Female 158.666667
# Male 172.030000
# Shanghai Jiao Tong University Female 159.122500
# Male 176.760000
# Tsinghua University Female 159.753333
# Male 171.638889
# Name: Height, dtype: float64
更复杂的分组条件:根据学生体重是否超过总体均值来分组,计算身高的均值
condition = df.Weight > df.Weight.mean()
df.groupby(condition)['Height'].mean()
# Weight
# False 159.034646
# True 172.705357
# Name: Height, dtype: float64
【练一练】
请根据上下四分位数分隔,将体重分为high、normal、low三组,统计身高的均值
# 根据上下四分位数,先将体重分为三组,新建一列命名为cond
condition1 = df.Weight > df.Weight.quantile(0.75)
condition2 = (df.Weight >= df.Weight.quantile(0.25)) & (df.Weight <= df.Weight.quantile(0.75))
condition3 = df.Weight < df.Weight.quantile(0.25)
df['cond'] = df['Weight'].mask(condition1,'high').mask(condition2,'normal').mask(condition3,'low')
# 分组计算身高均值
df.groupby('cond')['Height'].mean()
# cond
# high 174.935714
# low 153.753659
# normal 161.883516
# Name: Height, dtype: float64
从索引可以看出,其实最后产生的结果就是按照条件列表中元素的值(此处是True和False)来分组,下面用随机传入字母序列来验证这一想法
item = np.random.choice(list('abc'), df.shape[0])
df.groupby(item)['Height'].mean()
# a 164.407143
# b 161.930508
# c 163.355882
# Name: Height, dtype: float64
此处的索引就是原先item中的元素,如果传入多个序列进入groupby,那么最后分组的依据就是这两个序列对应行的唯一组合:
df.groupby([condition, item])['Height'].mean()
# Weight
# False a 159.822222
# b 158.338636
# c 159.082979
# True a 172.660000
# b 172.466667
# c 172.919048
# Name: Height, dtype: float64
由此可以看出,之前传入列名只是一种简便的记号,事实上等价于传入的是一个或多个列,最后分组的依据来自于数据来源组合的unique值,通过drop_duplicates
就能知道具体的组类别:
df[['School', 'Gender']].drop_duplicates()
# School Gender
# 0 Shanghai Jiao Tong University Female
# 1 Peking University Male
# 2 Shanghai Jiao Tong University Male
# 3 Fudan University Female
# 4 Fudan University Male
# 5 Tsinghua University Female
# 9 Peking University Female
# 16 Tsinghua University Male
df.groupby([df['School'], df['Gender']])['Height'].mean()
# School Gender
# Fudan University Female 158.776923
# Male 174.212500
# Peking University Female 158.666667
# Male 172.030000
# Shanghai Jiao Tong University Female 159.122500
# Male 176.760000
# Tsinghua University Female 159.753333
# Male 171.638889
# Name: Height, dtype: float64
3、Groupby对象
最终具体做分组操作时,所调用的方法都来自于pandas中的groupby对象,这个对象上定义了许多方法,也有一些方便的属性。
gb = df.groupby(['School','Grade'])
gb
# <pandas.core.groupby.generic.DataFrameGroupBy object at 0x0000024E9F837288>
- 通过
ngroups
属性,可以访问分为了多少组:
gb.ngroups
# 16
- 通过
groups
属性,可以返回从组名映射到组索引列表的字典:
res = gb.groups
res.keys() # 字典的值由于是索引,元素个数过多,此处只展示字典的键
# dict_keys([('Fudan University', 'Freshman'), ('Fudan University', 'Junior'), ('Fudan University', 'Senior'), ('Fudan University', 'Sophomore'), ('Peking University', 'Freshman'), ('Peking University', 'Junior'), ('Peking University', 'Senior'), ('Peking University', 'Sophomore'), ('Shanghai Jiao Tong University', 'Freshman'), ('Shanghai Jiao Tong University', 'Junior'), ('Shanghai Jiao Tong University', 'Senior'), ('Shanghai Jiao Tong University', 'Sophomore'), ('Tsinghua University', 'Freshman'), ('Tsinghua University', 'Junior'), ('Tsinghua University', 'Senior'), ('Tsinghua University', 'Sophomore')])
【练一练】
上一小节介绍了可以通过drop_duplicates
得到具体的组类别,现请用groups
属性完成类似的功能。
data = df.groupby(['School', 'Gender']).groups.keys()
pd.DataFrame(data,columns=['School','Gender'])
# School Gender
# 0 Fudan University Female
# 1 Fudan University Male
# 2 Peking University Female
# 3 Peking University Male
# 4 Shanghai Jiao Tong University Female
# 5 Shanghai Jiao Tong University Male
# 6 Tsinghua University Female
# 7 Tsinghua University Male
- 当
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 cond
# 15 Fudan University Freshman Changqiang Yang Female 156.0 49.0 N 3 2020/1/1 0:05:25 normal
# 28 Fudan University Freshman Gaoqiang Qin Female 170.2 63.0 N 2 2020/1/7 0:05:24 normal
# 63 Fudan University Freshman Gaofeng Zhao Female 152.2 43.0 N 2 2019/10/31 0:04:00 low
# 70 Fudan University Freshman Yanquan Wang Female 163.5 55.0 N 1 2019/11/19 0:04:07 normal
# 73 Fudan University Freshman Feng Wang Male 176.3 74.0 N 1 2019/9/26 0:03:31 high
# 105 Fudan University Freshman Qiang Shi Female 164.5 52.0 N 1 2019/12/11 0:04:23 normal
# 108 Fudan University Freshman Yanqiang Xu Female 152.4 38.0 N 1 2019/12/8 0:05:03 low
# 157 Fudan University Freshman Xiaoli Lv Female 152.5 45.0 N 2 2019/9/11 0:04:17 low
# 186 Fudan University Freshman Yanjuan Zhao Female NaN 53.0 N 2 2019/10/9 0:04:21 normal
4、分组的三大操作
熟悉了一些分组的基本知识后,重新回到开头举的三个例子,可能会发现一些端倪,即这三种类型的分组返回数据的结果型态并不一样:
- 第一个例子中,每一个组返回一个标量值,可以是平均值、中位数、组容量
size
等 - 第二个例子中,做了原序列的标准化处理,也就是说每组返回的是一个
Series
类型 - 第三个例子中,既不是标量也不是序列,返回的整个组所在行的本身,即返回了
DataFrame
类型
由此,引申出分组的三大操作:聚合、变换和过滤,分别对应了三个例子的操作,下面就要分别介绍相应的agg
、transform
和filter
函数及其操作。
二、聚合函数
在介绍agg之前,首先要了解一些直接定义在groupby对象的聚合函数,在能直接使用groupby函数,因为它的速度基本都会经过内部的优化,使用功能时应当优化考虑。根据返回标量值的原则,包括如下函数:
max/min/mean/median/count/all/any/idxmax/idxmin/mad/nunique/skew/quantile/sum/std/var/sem/size/prod
gb = df.groupby('Gender')['Height']
gb.idxmin()
# Gender
# Female 143
# Male 199
# Name: Height, dtype: int64
gb.quantile(0.95)
# Gender
# Female 166.8
# Male 185.9
# Name: Height, dtype: float64
【练一练】
请查阅文档,明确all/any/mad/skew/sem/prod
函数的含义。
function | desciption |
---|---|
all | 分组后各组中的所有值都是真实的,则返回True,否则返回False。 |
any | 分组后各组中数值存在True则返回True,全为False则返回False |
mad | 返回所请求轴的值的平均绝对偏差(mean absolute deviation) |
skew | 偏度系数 |
sem | 标准误差(standard error of mean) |
prod | 连乘的乘积 |
nunique | 统计每个group中不同类别的数目 |
count | 计算每个组的元素个数,不包含NaN值 |
size | 计算每个组的元素个数,包含NaN值 |
gb.all()
# Gender
# Female True
# Male True
# Name: Height, dtype: bool
gb.any()
# Gender
# Female True
# Male True
# Name: Height, dtype: bool
gb.nunique()
# Gender
# Female 99
# Male 47
# Name: Height, dtype: int64
# 注意size和count的区别
gb.count()
# Gender
# Female 132
# Male 51
# Name: Height, dtype: int64
gb.size()
# Gender
# Female 141
# Male 59
# Name: Height, dtype: int64
这些聚合函数当传入的数据来源包含多个列时,将按照列进行迭代计算:
gb = df.groupby('Gender')[['Height', 'Weight']]
gb.max()
# Height Weight
# Gender
# Female 170.2 63.0
# Male 193.9 89.0
2、agg方法
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
此时的列索引为多级索引,第一层为数据源,第二层为使用的聚合方法
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
【练一练】
请使用b中的传入字典的方法完成a中等价的聚合任务。
gb.agg({'Height':['sum','idxmax','skew'],'Weight':['sum','idxmax','skew']})
c 使用自定义函数
在agg
中可以使用具体的自定义函数,需要注意传入函数的参数是之前数据源中的列,逐列进行计算
下面分组计算身高和体重的极差:
Height Weight
Gender
Female 13.79697 13.918519
Male 17.92549 21.759259
【练一练】
在groupby
对象中可以使用describe
方法进行统计信息汇总,请同时使用多个聚合函数,完成与该方法相同的功能。
gb.describe()
gb.agg(['count','mean','std','min',('25%',lambda x:x.quantile(0.25)),('50%',lambda x:x.quantile(0.50)),('75%',lambda x:x.quantile(0.75))])
由于传入的是序列,因此序列上的方法和属性都是可以在函数中使用的,只需保证返回值是标量即可。下面的例子是指,如果组的指标均值,超过该指标的总体均值,返回High,否则返回Low
def my_func(s):
res = 'High'
if s.mean() <= df[s.name].mean():
res = 'Low'
return res
gb.agg(my_func)
d 聚合结果重命名
如果想要对结果进行重命名,只需要将上述函数的位置改写为元组,元组的第一个元素为新的名字。第二位置为原来的函数,包括聚合字符串和自定义函数,现举若干例子说明:
gb.agg([('range', lambda x: x.max()-x.min()), ('my_sum', 'sum')])
# Height Weight
# range my_sum range my_sum
# Gender
# Female 24.8 21014.0 29.0 6469.0
# Male 38.2 8854.9 38.0 3929.0
gb.agg({'Height': [('my_func', my_func), 'sum'], 'Weight': lambda x:x.max()})
# Height Weight
my_func sum <lambda>
Gender
Female Low 21014.0 63.0
Male High 8854.9 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(method='average',ascending=True,na_option='keep',pct=False,axis=0)
对每组进行排序,得到的是每个值对应的排序值。
参数:
-
method
:默认为average,即当出现n个相同值时,取排名的平均值为排名-
min
:相同值的排名按最小的名次值排序,有n个相同值,之后值的排名需+n -
max
:相同值的排名按最大的名次值排序 -
first
:从小到大排序,相同值按出现顺序先后排序依次递增 -
dense
:相同值的排名相同,其它只在该名次的基础依次上+1
-
-
ascending
:默认为True,从低到高排列,False则从高到低排列。 -
na_option
:默认为keep,空值不参与排序top
:默认为升序时,空值为最小值进行排序bottom
:默认为升序时,空值为最大值进行排序
-
pct
: 默认为False,True则将排名转化为百分比形式
gb.rank(method='first',ascending=False,pct=True,na_option='top').head()
# Height Weight
# 0 0.588652 0.652482
# 1 0.932203 0.661017
# 2 0.169492 0.101695
# 3 0.007092 0.893617
# 4 0.559322 0.457627
当用自定义变换时需要使用transform方法,被调用的自定义函数,其传入值为数据源的序列,与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
的调用中实现这种功能,请给出解决方案。
前面提到了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[...]
的功能,这里假设"…"是元素列表。
四、跨列分组
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的均值。
首先,这显然不是过滤操作,因此不能使用fliter,返回的均值是标量而不是序列,因此transform
不符合要求;最后,似乎使用agg
函数能够处理,但是之前强调过聚合函数是逐列处理的,而不能够多列数据同时处理。由此,引出了用apply
函数来解决这一问题。
2、apply的使用
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】标量情况:结果得到的是seires
,索引与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
【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
函数的灵活性是以牺牲一定性能为代价换得到,除非需要使用跨列处理的分组处理,否则应当使用其他专门设计的groupby
对象方法,否则在性能上会存在较大的差距。同时,在使用聚合函数和变换函数时,也应当优先使用内置函数,它们经过了高度的性能优化,一般而言在速度上都会快于自定义函数来实现。
五、练习
EX1:汽车数据集
现有一份汽车数据集,其中Brand, Disp., HP
分别代表汽车品牌、发动机蓄量、发动机输出。
car = pd.read_csv('joyful-pandas/data/car.csv')
car.head()
# Brand Price Country Reliability Mileage Type Weight Disp. HP
# 0 Eagle Summit 4 8895 USA 4.0 33 Small 2560 97 113
# 1 Ford Escort 4 7402 USA 2.0 33 Small 2345 114 90
# 2 Ford Festiva 4 6319 Korea 4.0 37 Small 1845 81 63
# 3 Honda Civic 4 6635 Japan/USA 5.0 32 Small 2260 91 92
# 4 Mazda Protege 4 6599 Japan 5.0 32 Small 2440 113 103
- 先过滤出所属
Country
数超过2个的汽车,即若该汽车的Country
在总体数据集中出现次数不超过2则剔除,再按Country
分组计算价格均值、价格变异系数、该Country
的汽车数量,其中变异系数的计算方法是标准差除以均值,并在结果中把变异系数重命名为CoV
。
car2=car.groupby('Country').filter(lambda x:x.shape[0]>2)
car2.head()
# Brand Price Country Reliability Mileage Type Weight Disp. HP
# 0 Eagle Summit 4 8895 USA 4.0 33 Small 2560 97 113
# 1 Ford Escort 4 7402 USA 2.0 33 Small 2345 114 90
# 2 Ford Festiva 4 6319 Korea 4.0 37 Small 1845 81 63
# 3 Honda Civic 4 6635 Japan/USA 5.0 32 Small 2260 91 92
# 4 Mazda Protege 4 6599 Japan 5.0 32 Small 2440 113 103
car2.groupby('Country').Price.agg(['mean',('CoV',lambda x:x.std()/x.mean()),'count'])
# mean CoV count
# Country
# Japan 13938.052632 0.387429 19
# Japan/USA 10067.571429 0.240040 7
# Korea 7857.333333 0.243435 3
# USA 12543.269231 0.203344 26