Pandas:细说groupby和aggregate、transform、apply以及filter

这一个知识点感觉是目前接触的Pandas中最难的了,故写篇博客记录一下,这一节有点函数式编程的味道~

(一)groupby

先说一下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'))
# <pandas.core.groupby.generic.DataFrameGroupBy object at 0x000001F536989888>

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 聚合

下面来讲聚合操作

上面说到,分组之后返回多个子数据帧,如果我想知道每个子数据帧的某些列的某些信息,如方差,极差,最值等,就可以用这个聚合操作

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的输出,列为所有列,只不过会把分类的那一列提到第一列的位置上,剩下的顺着排。

(三)transform

这个函数和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

这个用法非常灵活

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传入的是数据帧的所有列,如果这个子数据帧只有一列,那他也是子数据帧啊,所以就算指定列名,传入的列向量也可以看作是子数据帧。

(五)filter

这个有点像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挑选出来的是子数据帧,跟列无关。

  • 57
    点赞
  • 233
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值