这一个知识点感觉是目前接触的Pandas中最难的了,故写篇博客记录一下,这一节有点函数式编程的味道~
先说一下goupby,顾名思义,就是分组的意思,给你一个DataFrame,以某一列为标准,分成若干个“子DataFrame”,这些个“子DataFram”由两部分组成,一个是索引index,即类别,一个是“子DataFrame”的内容,数据类型也是DataFrame,不过行数少点罢了,说白了,就是把那一列相同类别的所有行单独提出来,凑成一个DataFrame,该列有N种类别就有N个“子DataFrame”
下面说的所有例子,都遵循这个图
顺序为 1. 分组 -> 2. 对每个子数据帧进行某种操作,并返回操作后的子数据帧 -> 3. 将返回后的子数据帧进行合并
先看一个例子
df = pd.DataFrame({'Country':['China','China', 'India', 'India', 'America', 'Japan', 'China', 'India'],
'Income':[10000, 10000, 5000, 5002, 40000, 50000, 8000, 5000],
'Age':[5000, 4321, 1234, 4010, 250, 250, 4500, 4321]})
print(df)
# Country Income Age
# 0 China 10000 5000
# 1 China 10000 4321
# 2 India 5000 1234
# 3 India 5002 4010
# 4 America 40000 250
# 5 Japan 50000 250
# 6 China 8000 4500
# 7 India 5000 4321
上面是一个DataFrame,以后用数据帧来称呼DataFrame
现在根据国家来分类,可想而知,结果应该有四个子数据帧
groups属性返回一个字典,key为刚才说的索引index,value为子数据帧
print(df.groupby('Country'))
#
print(df.groupby('Country').groups)
# {'America': Int64Index([4], dtype='int64'),
# 'China': Int64Index([0, 1, 6], dtype='int64'),
# 'India': Int64Index([2, 3, 7], dtype='int64'),
# 'Japan': Int64Index([5], dtype='int64')}
groupby返回的对象是可迭代的,因此可以用迭代的方法去遍历输出它,迭代的方式和上面说的不谋而合,输出的就是索引index和data子数据帧
# 遍历该对象输出
for index, data in df.groupby('Country'):
print(index)
print(data)
# America
# Country Income Age
# 4 America 40000 250
# China
# Country Income Age
# 0 China 10000 5000
# 1 China 10000 4321
# 6 China 8000 4500
# India
# Country Income Age
# 2 India 5000 1234
# 3 India 5002 4010
# 7 India 5000 4321
# Japan
# Country Income Age
# 5 Japan 50000 250
当然,我们也可以按多个列为标准进行分组,如下
# 根据国家和收入分组
for (index1, index2), data in df.groupby(['Country', 'Income']):
print(index1, index2)
print(data)
# America 40000
# Country Income Age
# 4 America 40000 250
# China 8000
# Country Income Age
# 6 China 8000 4500
# China 10000
# Country Income Age
# 0 China 10000 5000
# 1 China 10000 4321
# India 5000
# Country Income Age
# 2 India 5000 1234
# 7 India 5000 4321
# India 5002
# Country Income Age
# 3 India 5002 4010
# Japan 50000
# Country Income Age
# 5 Japan 50000 250
下面来讲聚合操作
上面说到,分组之后返回多个子数据帧,如果我想知道每个子数据帧的某些列的某些信息,如方差,极差,最值等,就可以用这个聚合操作
aggregate(函数 / lambda表达式 / 函数列表 / 字典) -> return 一个数(即标量,注意这里是标量,而不是向量),然后各个子数据帧都变成一行,再合并
(1)当传入函数的时候,这个函数的形参为每个子数据帧的所有列,可以这么理解,相当于这个函数对每个子数据帧的每一列都进行了相同操作,这个操作指的是函数体本身的内容,因此这个函数传进来的是一个列向量,这个列向量为“每个子数据帧的每一列”。返回的结果是一个数,因此,在用了这个函数之后,每个子数据帧的每一列,都会变成一个数。
def add(x):
return np.max(x)
print(df.groupby('Country').agg(add))
or
print(df.groupby('Country').agg(np.max))
(2)lambda表达式即匿名函数,道理和(1)相同,不再赘述。
print(df.groupby('Country').agg(lambda : np.max(x)))
(3)如果传入的是函数列表,假设每个列表里面装了M个函数,代表要对每个子数据帧的每一列,进行M个操作,每个操作也是返回一个数。因此,以这种方式用了聚合函数之后,每个子数据帧的每一列,都会变成M个数。
print(df.groupby('Country').aggregate([np.max, np.min, np.mean]))
(4)传入的字典,key为列名,value为函数 / 函数列表,这样做的好处是不用对每一列都进行操作,有时候我们只关心某几列,因此这样就能对某几列进行某些操作。
print(df.groupby('Country').aggregate({'Age' : np.max, np.sum}))
print(df.groupby('Country').aggregate({'Age' : [np.max, np.sum]}))
(5)当然,因为函数传入的是列向量,我们当然可以认为的选择我们要的列向量,再进行聚合,如下,下述方法可以代替传入字典的方法,更加直观,高效。
print(df.groupby('Country')['Age'].aggregate(np.max))
(6)如果只对某一个分组感兴趣,可以用get_group单独获取你想要的那个子数据帧,再对其进行操作。
print(df.groupby('Country').get_group('China').aggregate({'Income' : [np.mean, np.sum]}))
# Income
# mean 9333.333333
# sum 28000.000000
(7)此外,这个aggregate的输出,列为所有列,只不过会把分类的那一列提到第一列的位置上,剩下的顺着排。
这个函数和aggregate函数有点类似,但是又不完全一样,我们来看看
transform(函数 / lambda表达式) -> 返回一个列向量,该列向量和原子数据帧等长,然后各个子数据帧还是和原来一样的行数,再合并,合并之后的新数据帧和原始数据帧等行数
(1)这个函数只能传入函数和匿名函数,这个函数的形参和aggregate一样,也是每个子数据帧的每个列,但是输出的却是向量。因此这个用了transform函数之后,每个子数据帧返回每个子数据帧原有的行数,而上面的聚合就不一样了,上面的聚合每个子数据帧最后都只能返回一行,因为聚合函数传入的每一列都只能返回一个数。比如下面这个代码,transform就不报错,aggregate就报错。因为transorm的函数,传入的是列向量,返回的也是等长的列向量,所以不报错,而aggregate本应该返回一个数,他也返回一个列向量,注定是出错的。
print(df.groupby('order').transform(lambda x : x - np.mean(x)))
# print(df.groupby('order').aggregate(lambda x : x - np.mean(x)) # 报错
(2)如果要返回标量呢?如果每个子数据帧的每一列都返回一个标量,那么这个标量会广播,广播成一个列向量,填充成和原子数据帧一样行数的列向量。
(3)和聚合一样,这里函数传入的依然是列向量,因此可以指定对哪几列进行操作
print(df.groupby('order')['ext price'].transform(lambda x : x - np.mean(x)))
(4)transform的输出和aggregate不太一样,因为等行数,所以没
这个用法非常灵活
apply(函数 / lambda表达式) -> 返回标量或者列向量,然后各个子数据帧合并成新的数据帧
(1)apply只能传入函数或者lambda表达式,这个函数和上面的aggregate以及transform有区别,上面两者的函数传入的是每个子数据帧的每一列,而这里的apply的函数,传入的是“每个子数据帧的所有列 / 行”,一般默认为列。对比一下,上面两个函数传入的是“每个子数据帧的每个列”,这个是“每个子数据帧的所有列 / 行(可以理解为整个子数据帧)”,是不一样的仔细体味一下。所以,apply函数可以对多列进行操作,这是上面这两个函数做不到的。
如果是自定义函数 / 自定义lambda,函数传入的是整个子数据帧,那么可以用['列名']的形式选中数据帧的某几行进行操作
def add(x): # 这里的x指的是整个数据帧
return x['quantity'] + x['unit price']
print(df.groupby('order').apply(add)) # apply的第一个形参是self,传入的是分好后的每组
or
print(df.groupby('order').apply(lambda x : x['quantity'] + x['unit price']))
(2)如果传入的是numpy自带的函数,默认axis = 0,即对每一列进行操作,这一点不要搞混,因为numpy中,如果不写axis的话默认是对所有数进操作。
print(df.groupby('order').apply(np.min))
(3)apply返回的结果根据传入函数的返回值决定。
如果传入函数的返回值是标量,则和aggregate很类似,只不过aggregate最后把分类的那一列提到第一列,而apply是在原来列顺序的基础上,在最前面的一列补充了分类的那一列,即分类的那一列展示了两次。
如果传入函数的返回值是向量,会返回若干个子数据帧,类似这样
print(df.groupby('order').apply(lambda x : x['quantity'] + x['unit price']))
# order
# 10001 0 40.69
# 1 32.12
# 2 38.99
# 10005 3 103.82
# 4 34.62
# 5 101.55
# 6 122.91
# 7 61.42
# 10006 8 127.66
# 9 45.55
# 10 75.30
# 11 71.18
# dtype: float64
如果不想要每个子数据帧的index,想把这些子数据帧合并的话,可以将groupby参数中的 group_keys = False
print(df.groupby('order', group_keys = False).apply(lambda x : x['quantity'] + x['unit price']))
# 0 40.69
# 1 32.12
# 2 38.99
# 3 103.82
# 4 34.62
# 5 101.55
# 6 122.91
# 7 61.42
# 8 127.66
# 9 45.55
# 10 75.30
# 11 71.18
# dtype: float64
(4)尽管apply传入的函数的形参不是列向量而是整个子数据帧,但是apply仍能用df.group('XXX')['列名'].apply的形式,可以这么理解,apply传入的是数据帧的所有列,如果这个子数据帧只有一列,那他也是子数据帧啊,所以就算指定列名,传入的列向量也可以看作是子数据帧。
这个有点像SQL里的where,这个filter接在groupby的后面,得到满足条件的那些子数据帧,并且将这些子数据帧合并再返回
filter(函数 / lambda表达式) -> return 得到满足条件的那些子数据帧,并且将这些子数据帧合并再返回
(1)filter里面传入的函数或者lambda表达式,即满足条件,这个函数返回一个布尔表达式,注意这里是“一个”布尔表达式,即要么是True,要么是False,而不是“布尔表达式组成的列表”
这个函数或者lambda表达式的输入形参,和apply一样,都是整个子数据帧(也可以叫做子数据帧的所有列),返回True or False
print(df.groupby('order').filter(lambda x : np.max(x['quantity']) <= 32))
(2)整个filter返回的结果,是那些满足条件的子数据帧,这些子数据帧要么全返回,要么一个都不返回,所以,groupby后面用filter,是挑选“我想要的那些子数据帧”,最小单位为子数据帧,而不是子数据帧里面的某些行,这点要尤其注意!
(3)filter是唯一一个不能用df.group('XXX')['列名'].filter来指定列的函数,因为filter挑选出来的是子数据帧,跟列无关。