pandas_3 分组

import numpy as np
import pandas as pd

一、分组模式及其对象

1. 分组的一般模式_df.groupby(分组依据)[数据来源].使用操作

分组操作在日常生活中使用极其广泛,例如:

  • 依据 性 别 \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()

现在返回到学生体测的数据集上,如果想要按照性别统计身高中位数,就可以如下写出:

df = pd.read_csv('../data/learn_pandas.csv')
df.groupby('Gender')['Height'].median()
# Gender
# Female    159.6
# Male      173.4
# Name: Height, dtype: float64

2. 分组依据的本质

上述例子是以单一维度进行分组的,比如根据性别;根据多个维度进行分组,需在groupby中传入相应列名构成的列表即可。
例如,现希望根据学校和性别进行分组,统计身高的均值就可以如下写出:

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

目前为止,groupby的分组依据都是直接可以从列中按照名字获取的,那如果希望通过一定的复杂逻辑来分组,例如根据学生体重是否超过总体均值来分组,同样还是计算身高的均值。

首先应该先写出分组条件:

condition = df.Weight > df.Weight.mean()

然后将其传入groupby中:

df.groupby(condition)['Height'].mean()
Weight
False    159.034646
True     172.705357
Name: Height, dtype: float64

从索引可以看出,其实最后产生的结果就是按照条件列表中元素的值(此处是TrueFalse来分组,下面用随机传入字母序列来验证这一想法:

item = np.random.choice(list('abc'), df.shape[0]) # df.shape[0] 返回行数
df.groupby(item)['Height'].mean()
a    163.094828
b    163.874603
c    162.666129
Name: Height, dtype: float64

此处的索引就是原先item中的元素,如果传入多个序列进入groupby,那么最后分组的依据就是这两个序列对应行的唯一组合

df.groupby([condition, item])['Height'].mean()
Weight   
False   a    159.334146
        b    159.257143
        c    158.543182
True    a    172.164706
        b    173.109524
        c    172.744444
Name: Height, dtype: float64

由此可以看出,之前传入列名只是一种简便的记号,事实上等价于传入的是一个或多个列,最后分组的依据来自于数据来源组合的unique值,通过drop_duplicates就能知道具体的组类别:

df[['School', 'Gender']].drop_duplicates()
SchoolGender
0Shanghai Jiao Tong UniversityFemale
1Peking UniversityMale
2Shanghai Jiao Tong UniversityMale
3Fudan UniversityFemale
4Fudan UniversityMale
5Tsinghua UniversityFemale
9Peking UniversityFemale
16Tsinghua UniversityMale
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'])

通过ngroups属性,可以得到分组个数:

gb.ngroups
# 16

通过groups属性,可以返回从 组 名 \color{#FF0000}{组名} 映射到 组 索 引 列 表 \color{#FF0000}{组索引列表} 的字典:

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')])

通过size属性,可以得到每个组的元素个数:
(注意区分:当size作为DataFrame的属性时,返回的是表长乘以表宽的大小)

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

先前的meanmedian都是groupby对象上的方法,这些函数和许多其他函数的操作具有高度相似性,将在后续介绍。

二、分组的三大操作

重新回到开头举的三个例子,会发现这三种类型分组返回的数据型态并不一样:

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

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

1.聚合

1. 内置聚合函数

直接定义在groupby对象的聚合函数,应当优先考虑**。根据返回标量值的原则,包括如下函数:max/min/mean/median(算术中位数)/count(数量)/all(是否所有元素都为真)/any(是否至少一个元素为真)/idxmax(最大值索引)/idxmin/mad(平均绝对偏差)/nunique(唯一值的数量)/skew(偏度)/quantile(分位数)/sum/std(无偏(分母为n-1)标准差/var(方差)/sem(均值的标准误差)/size/prod(积)。可以使用describe方法进行统计信息汇总

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

这些聚合函数当传入的数据来源包含多个列时,将按照列进行迭代计算

gb = df.groupby('Gender')[['Height', 'Weight']]
gb.max()
HeightWeight
Gender
Female170.263.0
Male193.989.0

2. agg方法

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

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

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

1.同时使用多个函数_列表

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

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

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

2.对特定的列使用特定的聚合函数_字典

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

gb.agg({'Height':['mean','max'], 'Weight':'count'})
HeightWeight
meanmaxcount
Gender
Female159.19697170.2135
Male173.62549193.954
3.使用自定义函数

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

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

由于传入的是序列,因此序列上的方法和属性都是可以在函数中使用的,只需保证返回值是标量即可。下面的例子是指,如果组的指标均值,超过该指标的总体均值,返回High,否则返回Low。

def my_func(s):
    res = 'High'
    if s.mean() <= df[s.name].mean():
        res = 'Low'
    return res
gb.agg(my_func)
HeightWeight
Gender
FemaleLowLow
MaleHighHigh
4.聚合结果重命名_元组

如果想要对聚合结果的列名进行重命名,只需要将上述函数的位置改写成元组,元组的第一个元素为新的名字,第二个位置为原来的函数,包括聚合字符串和自定义函数,现举若干例子说明:

gb.agg([('range', lambda x: x.max()-x.min()), ('my_sum', 'sum')])
HeightWeight
rangemy_sumrangemy_sum
Gender
Female24.821014.029.06469.0
Male38.28854.938.03929.0
gb.agg({'Height': [('my_func', my_func), 'sum'], 'Weight': lambda x:x.max()})
HeightWeight
my_funcsum<lambda>
Gender
FemaleLow21014.063.0
MaleHigh8854.989.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

2.变换

1.内置变换函数

累计函数

变换函数的返回值为同长度的序列,最常用的内置变换函数是累计函数:cumcount/cumsum/cumprod/cummax/cummin,它们的使用方式和聚合函数类似,只不过完成的是组内累计操作。

gb.cummax().head() # 当前元素和前一个元素之间的较大者
HeightWeight
0158.946.0
1166.570.0
2188.989.0
3NaN46.0
4188.989.0
rank函数

在groupby对象中,rank方法也是一个实用的变换函数(排序):
1.1按照年龄进行排序

df['rank'] = df['age'].rank()
df['rank_mean'] = df['age'].rank(method='average')
df['rank_min'] = df['age'].rank(method='min')
df['rank_max'] = df['age'].rank(method='max')
df['rank_first'] = df['age'].rank(method='first')
print(df)

在这里插入图片描述
1.2根据不同的性别对年龄进行排序

df['rank_g'] = df.groupby(['gender'])['age'].rank()
print(df)

在这里插入图片描述
rank()函数参数设置:

1.method : {‘average’, ‘min’, ‘max’, ‘first’, ‘dense’}主要用于 排序时存在相同值时 参数设置。
默认为average平均值:年龄为32的数值,排序应该为8,9取平均值则为8.5
min:排序中最小值,年龄排序中取值为8
max:排序中最大值,年龄排序中取值9
first:同样数值按照值出现的前后进行排序 5号性别为男的年龄排序为8,7号性别为女的排序为9
dense: like ‘min’, but rank always increases by 1 between groups 排序时当值相同时,相同的值为同一排名类似min值排序,后续值排名在此排名基础上加一

2.na_option : {‘keep’, ‘top’, ‘bottom’}当排序数据中存在空值时。
默认为keep: 默认空值不参与排序
top: 默认为升序时从空值为最小值排序
bottom: 默认升序时 空值为最大值排序

df['rank'] = df['age'].rank(method='first')
df['rank_k'] = df['age'].rank(method='first',na_option='keep')
df['rank_t'] = df['age'].rank(method='first',na_option='top')
df['rank_b'] = df['age'].rank(method='first',na_option='bottom')
print(df)

在这里插入图片描述
————————————————
rank()函数部分转载于CSDN博主「D_grey」的原创文章
原文链接:https://blog.csdn.net/baidu_38409988/article/details/102668006

填充类滑窗类变换函数

groupby对象上还定义了填充类和滑窗类的变换函数,这些函数的一般形式将会在后续文章中进行讨论,此处略过。

2.transform方法

当用自定义变换时需要使用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只能返回同长度的序列,但事实上还可以返回一个标量,这会使得结果被广播到其所在的整个组,这种 标 量 广 播 \color{#FF0000}{标量广播} 广的技巧在特征工程中是非常常见的。例如,构造两列新特征来分别表示样本所在性别组的身高均值和体重均值:

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

3. 过滤

索引和过滤区别:
过滤在分组中是对于组的过滤;而索引是对于行的过滤,即对于行的筛选。

组过滤作为行过滤的推广,指的是如果对一个组的全体所在行进行统计的结果返回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

三、跨列分组

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

pandas_udf是PySpark中的一个API,用于定义用户自定义函数(User Defined Functions,简称UDFs)。它使用Arrow传输数据并使用Pandas进行数据处理,可以进行向量化操作。有两种类型的pandas_udf,分别是Scalar(标量映射)和Grouped Map(分组映射)。 Scalar Pandas UDF用于向量化标量操作,常常与select和withColumn等函数一起使用。调用的Python函数需要使用pandas.Series作为输入并返回一个具有相同长度的pandas.Series。具体执行流程是,Spark将列分成批,并将每个批作为数据的子集进行函数的调用,进而执行Pandas UDF,最后将结果连接在一起。 Grouped Map Pandas UDF用于在分组的数据上进行向量化操作。它可以在GroupBy操作后的DataFrame上使用,相比于Scalar Pandas UDF,它可以处理更复杂的逻辑,例如聚合操作。使用Grouped Map Pandas UDF时,需要使用@pandas_udf装饰器或包装函数来定义函数。 通过使用pandas_udf,可以更高效地处理数据,提高数据处理的效率和性能。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [pyspark pandas_udf](https://blog.csdn.net/weixin_40161254/article/details/91548469)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [一文解读pandas_udf](https://blog.csdn.net/weixin_42223090/article/details/130126261)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值