分组
分组模式及其对象
分组的基本用法
分组操作应用非常广泛:
- 依据性别分组,统计学生身高的中位数
- 依据季节分组,对每个季节的温度进行组内标准化
- 依据班级分组,筛选组内数学分数的平均值超过80分的班级
想要实现分组操作,必须明确三个要素:分组依据、数据来源、操作及其返回结果。
df.groupby(分组依据)[数据来源].使用操作
df = pd.read_csv('data/learn_pandas.csv')
df.groupby('Gender')['Height'].median()
# 根据学校和性别进行分组,统计身高的均值
df.groupby(['School', 'Gender'])['Height'].mean()
如果需要根据多个维度进行分组,只需要在groupby中的分组依据中传入相应列名构成的列表即可。
分组依据可以直接从列中按照名字获取。如果希望通过一定的复杂逻辑来分组,可以先写出分组条件,再将其传入groupby
中
# 根据学生体重是否超过总体均值来分组
# condition = df.weight > df.weight.mean()
df.groupby(condition)['Height'].mean()
# 根据上下四分位数分割,将体重分为high, normal, low三组,统计身高的均值
def my_group(x):
if x <= df.Weight.quantile(0.25):
return('low')
if x >= df.Weight.quantile(0.75):
return('high')
else:
return('normal')
condition = df.Weight.apply(my_groupby)
df.groupby(condition)['Height].mean()
groupby
方法最后产生的结果就是按照分组依据中元素的值(可以是布尔,可以是[‘low’,‘high’,‘normal’],'abc’等)来分组,例如上面和下面这个例子。(感觉也像根据自己给定的索引来分组)
my_series = np.random.choice(list('abc'), df.shape[0])
df.groupby(my_series)['Height'].mean()
如果传入多个序列进入groupby
, 最后分组的依据就是对这两个序列对应行的唯一组合。通过drop_duplicates()
方法能够知道来自于数据来源组合的unique值,也就是具体的组类别,后面的groupby
中的groups
属性也能完成类似的功能
# 传入多个序列进入groupby
df.groupby([condition, my_series])['Height'].mean()
# 来自不同学校的男生和女生的身高平均值
df.groupby([df['School'], df['Gender']])['Height'].mean()
Groupby对象
gb = df.groupby(['School', 'Grade'])
print(gb)
# <pandas.core.groupby.generic.DataFrameGroupBy object at 0x000001CBC8BE6220>
具体做分组操作时,所调用的方法都来自于 pandas 中的groupby
对象,这个对象上定义了许多方法,也具有一些方便的属性。
ngroups
属性:得到分组个数groups
属性:返回从组名映射到组索引列表的字典(字典的键是组合形成的元组列表,值是相应的元素在原表中的索引)
res = gb.groups
res.keys()
res[('Fudan University', 'Freshman')]
# -> Int64Index([15, 28, 63, 70, 73, 105, 108, 157, 186], dtype='int64')
# 表示原表中第15,28,...个的学生,School是复旦,Grade是Freshman
size
: 返回每个组的元素个数,作为DataFrame
的属性时返回的是表长乘以表宽的大小get_group
方法:直接获取所在组对应的行,此时必须知道组的具体名字,返回的是DataFrame
gb.get_group(('Fudan University', 'Freshman')).iloc[:3, :3]
# 展示前三行前三列
mean
,median
等(待补充
分组的三大操作
熟悉了一些分组的基本知识后,重新回到开头举的三个例子,可能会发现一些端倪,即这三种类型分组返回的数据型态并不一样:
第一个例子中,每一个组返回一个标量值,可以是平均值、中位数、组容量 size 等
第二个例子中,做了原序列的标准化处理,也就是说每组返回的是一个 Series 类型
第三个例子中,既不是标量也不是序列,返回的整个组所在行的本身,即返回了 DataFrame 类型
由此引申出了分组的三大操作
agg
: 操作transform
: 变换filter
: 过滤
聚合
内置聚合函数
使用聚合功能时应当优先考虑直接定义在groupby对象的聚合函数,因为它的速度基本都会经过内部的优化。
聚合函数返回标量值,包括:
max/min/mean/median/count/all/any /idxmax/idxmin/mad/nunique/skew /quantile/sum/std/var/sem/size/prod
这些函数顾名思义已经比较清楚了,不清楚的可以去查阅pandas官方文档。且这些聚合函数当传入的数据来源包含多个列时,将按照列进行迭代计算
agg方法
在groupby
对象上定义的内置聚合函数有以下的缺点:无法同时使用多个函数、无法对特定的列使用特定的聚合函数、无法使用自定义的聚合函数、无法直接对结果的列名在聚合前进行自定义命名
- 使用多个函数:
用列表的形式把内置聚合函数对应的字符串传入。先前提到的所有字符串都可以,例如gb.agg(['sum', 'idxmax', 'skew'])
此时的列索引为多级索引,第一层为数据源,第二层为使用的聚合方法,分别逐一对列使用聚合。 - 对特定的列使用特定的聚合函数:
通过构造字典传入agg
实现,其中字典以列名为键,以聚合字符串或字符串列表为值,例如gb.agg({'Height': ['mean','max'], 'Weight':'count'})
- 使用自定义函数:
自己定义函数,也可以是匿名函数,传入函数的参数是之前数据源中的列,agg
方法中的函数会逐列进行计算,例如分组计算身高和体重的极差gb.add(lambda x: x.max()-x.min())
由于传入的是序列,因此序列上的方法和属性都可以在函数中使用,只需要保证返回值是标量即可。
练一练:在groupby
对象中可以使用describe
方法进行统计信息汇总,请同时使用多个聚合函数,完成与该方法相同的功能。gb = df.groupby('Gender')[['Height', 'Weight']] gb.agg({'Height':['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'],'Weight':['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']})
# 分组的指标均值超过总体均值则返回High,否则返回Low def my_func(s): res = 'High' if s.mean() <= df[s.name].mean(): res = 'Low' return res gb.add(my_func)
- 聚合结果重命名:
将上述函数的位置改写成元组,元组的第一个元素为新的名字,第二个位置为原来的函数,包括聚合字符串和自定义函数,上面是一个很好的例子。
需要注意,使用对一个或者多个列使用单个聚合的时候,重命名需要加方括号,否则就不知道是新的名字还是手误输错的内置函数字符串。gb.agg([('range', lambda x: x.max()-x.min()), ('my_sum', 'sum')]) gb.agg({'Height': [('my_func', my_func), 'sum'], 'Weight': lambda x:x.max()}) gb.agg([('my_sum', 'sum')]) gb.agg({'Height': [('my_func', my_func), 'sum'], 'Weight': [('range', lambda x:x.max())]})
变换
变换函数的返回值为同长度的序列。
常用的内置变换函数有累计函数cumcount, cumsum, cumprod, cummax, cummin
,其使用方式和聚合函数类似,只不过完成的是组内累计操作。
groupby
对象上填充类和滑窗类的变换函数将在第七章和第十章中讨论。
cumcount
:
GroupBy.cumcount(ascending: bool = True)
# 等价于 self.apply(lambda x: pd.Series(np.arange(len(x)), x.index))
# 作用是给分组的组内各个项目进行编号
df = pd.DataFrame([['a'], ['a'], ['a'], ['b'], ['b'], ['a']],
columns=['A'])
df.groupby('A').cumcount()
df.groupby('A').cumcount(ascending=False)
# 参数为False没太搞明白
cumsum
:
# https://blog.csdn.net/qq_22238533/article/details/72900634?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-72900634-blog-79472875.t5_download_all&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-72900634-blog-79472875.t5_download_all&utm_relevant_index=1
cummax
: gb.cummax().head()
rank
:
# DataFrame等数据结构也有rank方法
#
自定义变换
transform
方法:被调用的自定义函数传入值为数据源的序列,与agg
的传入类型是一致的,其最后的返回结果是行列索引与数据源一致的DataFrame
# 对身高和体重进行分组标准化
# 即减去组均值后除以组的标准差
gb.transform(lambda x: (x-x.mean())/x.std()).head()
transform
只能返回同长度的序列,但事实上还可以返回一个标量,这会使得结果被广播到其所在的整个组,这种 标量广播 的技巧在特征工程中是非常常见的。
# 构造两列新特征来分别表示样本所在性别组的身高均值和体重均值
gb.transform('mean').head() # 传入返回标量的函数也是可以的
过滤
索引和过滤的区别:过滤在分组中是对于组的过滤,而索引是对于行的过滤(行过滤),在第二章中的返回值,无论是布尔列表还是元素列表或者位置列表,本质上都是对于行的筛选,即如果符合筛选条件的则选入结果表,否则不选入。
组过滤作为索引(行过滤)的推广,指的是如果对一个组的全体所在行进行统计的结果返回True
则会被保留,False
则该组会被过滤,最后把所有未被过滤的组其对应的所在行拼接起来作为DataFrame
返回。
在groupby
对象中,定义了filter
方法进行组的筛选,其中自定义函数的输入参数为数据源构成的DataFrame
本身,在之前例子中定义的groupby
对象中,传入的就是df[['Height', 'Weight']]
,因此所有表方法和属性都可以在自定义函数中相应地使用,同时只需保证自定义函数的返回为布尔值即可。
# 在原表中通过过滤得到所有容量大于100的组
gb.filter(lambda x: x.shape[0] > 100).head()
跨列分组
之前几节介绍了三大分组操作,但事实上还有一种常见的分组场景,无法用前面介绍的任何一种方法处理:
定义身体质量指数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的均值
#
def BMI(x):
Height = x['Height']/100
Weight = x['Weight']
BMI_value = Weight/Height**2
return BMI_value.mean()
gb.apply(BMI)
首先,这不是过滤操作,因此filter
不符合要求;其次,返回的均值是标量而不是序列,因此transform
不符合要求;最后,似乎使用agg
函数能够处理,但是之前强调过聚合函数是逐列处理的,而不能够多列数据同时处理 。由此,引出了 apply
函数来解决这一问题。
除了返回标量之外, apply 方法还可以返回一维 Series 和二维 DataFrame
待补充
练习
待补充