Pandas进阶肆 分组

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

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()
HeightWeightTest_Number
count183.000000189.000000200.000000
mean163.21803355.0158731.645000
std8.60887912.8242940.722207
min145.40000034.0000001.000000
25%157.15000046.0000001.000000
50%161.90000051.0000001.500000
75%167.50000065.0000002.000000
max193.90000089.0000003.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'))
SchoolGradeNameGenderHeightWeightTransferTest_NumberTest_DateTime_Record
15Fudan UniversityFreshmanChangqiang YangFemale156.049.0N32020/1/10:05:25
28Fudan UniversityFreshmanGaoqiang QinFemale170.263.0N22020/1/70:05:24
63Fudan UniversityFreshmanGaofeng ZhaoFemale152.243.0N22019/10/310:04:00
70Fudan UniversityFreshmanYanquan WangFemale163.555.0N12019/11/190:04:07
73Fudan UniversityFreshmanFeng WangMale176.374.0N12019/9/260:03:31
105Fudan UniversityFreshmanQiang ShiFemale164.552.0N12019/12/110:04:23
108Fudan UniversityFreshmanYanqiang XuFemale152.438.0N12019/12/80:05:03
157Fudan UniversityFreshmanXiaoli LvFemale152.545.0N22019/9/110:04:17
186Fudan UniversityFreshmanYanjuan ZhaoFemaleNaN53.0N22019/10/90:04:21

17

这里列出了2个属性和2个方法,而先前的meanmedian都是groupby对象上的方法,这些函数和许多其他函数的操作具有高度相似性,将在之后的小节进行专门介绍。

4. 分组的三大操作

熟悉了一些分组的基本知识后,重新回到开头举的三个例子,可能会发现一些端倪,即这三种类型的分组返回数据的结果型态并不一样:

  • 第一个例子中,每一个组返回一个标量值,可以是平均值、中位数、组容量size
  • 第二个例子中,做了原序列的标准化处理,也就是说每组返回的是一个Series类型
  • 第三个例子中,既不是标量也不是序列,返回的整个组所在行的本身,即返回了DataFrame类型

由此,引申出分组的三大操作:聚合、变换和过滤,分别对应了三个例子的操作,下面就要分别介绍相应的aggtransformfilter函数及其操作。

二、聚合函数

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()
HeightWeight
Gender
Female170.263.0
Male193.989.0

2. agg方法

虽然在groupby对象上定义了许多方便的函数,但仍然有以下不便之处:

  • 无法同时使用多个函数
  • 无法对特定的列使用特定的聚合函数
  • 无法使用自定义的聚合函数
  • 无法直接对结果的列名在聚合前进行自定义命名

下面说明如何通过agg函数解决这四类问题:

【a】使用多个函数

当使用多个聚合函数时,需要用列表的形式把内置聚合函数的对应的字符串传入,先前提到的所有字符串都是合法的。

gb.agg(['sum', 'idxmax', 'skew'])
HeightWeight
sumidxmaxskewsumidxmaxskew
Gender
Female21014.028-0.2192536469.028-0.268482
Male8854.91930.4375353929.02-0.332393

从结果看,此时的列索引为多级索引,第一层为数据源,第二层为使用的聚合方法,分别逐一对列使用聚合,因此结果为6列。

【b】对特定的列使用特定的聚合函数

对于方法和列的特殊对应,可以通过构造字典传入agg中实现,其中字典以列名为键,以聚合字符串或字符串列表为值。

gb.agg({'Height':['mean','max'], 'Weight':'count'})
HeightWeight
meanmaxcount
Gender
Female159.19697170.2135
Male173.62549193.954
gb.agg({'Height':['sum', 'idxmax', 'skew'], 'Weight':['sum', 'idxmax', 'skew']})
HeightWeight
sumidxmaxskewsumidxmaxskew
Gender
Female21014.028-0.2192536469.028-0.268482
Male8854.91930.4375353929.02-0.332393
【练一练】

请使用【b】中的传入字典的方法完成【a】中等价的聚合任务。

【我的思路】如上

【c】使用自定义函数

agg中可以使用具体的自定义函数, 需 要 注 意 传 入 函 数 的 参 数 是 之 前 数 据 源 中 的 列 , 逐 列 进 行 计 算 \color{#FF0000}{需要注意传入函数的参数是之前数据源中的列,逐列进行计算} 。下面分组计算身高和体重的极差:

gb.agg(lambda x: x.mean()-x.min())
HeightWeight
Gender
Female13.7969713.918519
Male17.9254921.759259
【练一练】

groupby对象中可以使用describe方法进行统计信息汇总,请同时使用多个聚合函数,完成与该方法相同的功能。

【我的思路】
gb.describe()
HeightWeight
countmeanstdmin25%50%75%maxcountmeanstdmin25%50%75%max
Gender
Female132.0159.196975.053982145.4155.675159.6162.825170.2135.047.9185195.40598334.044.048.052.0063.0
Male51.0173.625497.048485155.7168.900173.4177.150193.954.072.7592597.77255751.069.073.078.7589.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'])
HeightWeight
countmeanstdmin25%50%75%maxcountmeanstdmin25%50%75%max
Gender
Female132159.196975.053982145.4155.675159.6162.825170.213547.9185195.40598334.044.048.052.0063.0
Male51173.625497.048485155.7168.900173.4177.150193.95472.7592597.77255751.069.073.078.7589.0

另外需要注意,使用对一个或者多个列使用单个聚合的时候,重命名需要加方括号,否则就不知道是新的名字还是手误输错的内置函数字符串:

gb.agg([('my_sum', 'sum')])
HeightWeight
my_summy_sum
Gender
Female21014.06469.0
Male8854.93929.0
gb.agg({'Height': [('my_func', my_func), 'sum'], 'Weight': [('range', lambda x:x.max())]})
HeightWeight
my_funcsumrange
Gender
FemaleLow21014.063.0
MaleHigh8854.989.0

三、变换和过滤

1. 变换函数与transform方法

变换函数的返回值为同长度的序列,最常用的内置变换函数是累计函数:cumcount/cumsum/cumprod/cummax/cummin,它们的使用方式和聚合函数类似,只不过完成的是组内累计操作。此外在groupby对象上还定义了填充类和滑窗类的变换函数,这些函数的一般形式将会分别在第七章和第十章中讨论,此处略过。

gb.cummax().head()
HeightWeight
0158.946.0
1166.570.0
2188.989.0
3NaN46.0
4188.989.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()
HeightWeight
0-0.058760-0.354888
1-1.010925-0.355000
22.1670632.089498
3NaN-1.279789
40.0531330.159631
【练一练】

对于transform方法无法像agg一样,通过传入字典来对指定列使用特定的变换,如果需要在一次transform的调用中实现这种功能,请给出解决方案。

【END】

前面提到了 transform 只能返回同长度的序列,但事实上还可以返回一个标量,这会使得结果被广播到其所在的整个组,这种 :red:标量广播 的技巧在特征工程中是非常常见的。例如,构造两列新特征来分别表示样本所在性别组的身高均值和体重均值:

gb.transform('mean').head() # 传入返回标量的函数也是可以的
HeightWeight
0159.1969747.918519
1173.6254972.759259
2173.6254972.759259
3159.1969747.918519
4173.6254972.759259

2. 组索引与过滤

在上一章中介绍了索引的用法,那么索引和过滤有什么区别呢?

过滤在分组中是对于组的过滤,而索引是对于行的过滤,在第二章中的返回值,无论是布尔列表还是元素列表或者位置列表,本质上都是对于行的筛选,即如果筛选条件的则选入结果的表,否则不选入。

组过滤作为行过滤的推广,指的是如果对一个组的全体所在行进行统计的结果返回True则会被保留,False则该组会被过滤,最后把所有未被过滤的组其对应的所在行拼接起来作为DataFrame返回。

groupby对象中,定义了filter方法进行组的筛选,其中自定义函数的输入参数为数据源构成的DataFrame本身,在之前例子中定义的groupby对象中,传入的就是df[['Height', 'Weight']],因此所有表方法和属性都可以在自定义函数中相应地使用,同时只需保证自定义函数的返回为布尔值即可。

例如,在原表中通过过滤得到所有容量大于100的组:

gb.filter(lambda x: x.shape[0] > 100).head()
HeightWeight
0158.946.0
3NaN41.0
5158.051.0
6162.552.0
7161.950.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']))
ab
GenderTest_Number
Female100
200
300
Male100
200
300
【练一练】

请尝试在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')])))
wy
xz
GenderTest_Number
Female1a1.01.0
b1.01.0
2a1.01.0
b1.01.0
3a1.01.0
b1.01.0
Male1a1.01.0
b1.01.0
2a1.01.0
b1.01.0
3a1.01.0
b1.01.0
【练一练】

请尝试在apply传入的自定义函数中,根据组的某些特征返回相同大小但列索引不同的DataFrame,会报错吗?如果只是行索引不同,会报错吗?

【END】

最后需要强调的是,apply函数的灵活性是以牺牲一定性能为代价换得的,除非需要使用跨列处理的分组处理,否则应当使用其他专门设计的groupby对象方法,否则在性能上会存在较大的差距。同时,在使用聚合函数和变换函数时,也应当优先使用内置函数,它们经过了高度的性能优化,一般而言在速度上都会快于用自定义函数来实现。

【练一练】

groupby对象中还定义了covcorr函数,从概念上说也属于跨列的分组处理。请利用之前定义的gb对象,使用apply函数实现与gb.cov()同样的功能并比较它们的性能。

【END】
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值