第四章 分组

本文详细介绍了Pandas中的分组操作,包括分组模式、Groupby对象、聚合函数、变换和过滤。讲解了如何根据不同维度进行分组,如依据性别、学校和体重分组,以及使用agg方法进行多函数聚合。还探讨了transform方法用于变换函数,filter方法进行组过滤。文章以实际例子和练习贯穿,帮助理解Pandas分组操作的各个方面。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第四章 分组

一、分组模式及其对象

1 、分组的一般模式

想要实现分组操作,必须明确三个元素:分组依据、数据来源、操作及返回结果,分组代码的一般模式即:

df.groupby(分组依据)[数据来源].使用操作

依据性别分组,统计全国人口寿命的平均值:

df.groupby('Gender')['Longevity'].mean( )

按照性别统计身高中位数:

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

2、分组依据的本质

在gruopby中传入多个列名构成的列表,即可实现根据多个维度进行分组。

根据学校和性别进行分组,统计身高的均值:

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

更复杂的分组条件:根据学生体重是否超过总体均值来分组,计算身高的均值

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

【练一练】

请根据上下四分位数分隔,将体重分为high、normal、low三组,统计身高的均值

# 根据上下四分位数,先将体重分为三组,新建一列命名为cond
condition1 = df.Weight > df.Weight.quantile(0.75)
condition2 = (df.Weight >= df.Weight.quantile(0.25)) & (df.Weight <= df.Weight.quantile(0.75))
condition3 = df.Weight < df.Weight.quantile(0.25)

df['cond'] = df['Weight'].mask(condition1,'high').mask(condition2,'normal').mask(condition3,'low')

# 分组计算身高均值
df.groupby('cond')['Height'].mean()
# cond
# high      174.935714
# low       153.753659
# normal    161.883516
# Name: Height, dtype: float64

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

item = np.random.choice(list('abc'), df.shape[0])
df.groupby(item)['Height'].mean()
# a    164.407143
# b    161.930508
# c    163.355882
# Name: Height, dtype: float64

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

df.groupby([condition, item])['Height'].mean()
# Weight   
# False   a    159.822222
#         b    158.338636
#         c    159.082979
# True    a    172.660000
#         b    172.466667
#         c    172.919048
# Name: Height, dtype: float64

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

df[['School', 'Gender']].drop_duplicates()
# School	Gender
# 0	Shanghai Jiao Tong University	Female
# 1	Peking University	Male
# 2	Shanghai Jiao Tong University	Male
# 3	Fudan University	Female
# 4	Fudan University	Male
# 5	Tsinghua University	Female
# 9	Peking University	Female
# 16	Tsinghua University	Male
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'])
gb
# <pandas.core.groupby.generic.DataFrameGroupBy object at 0x0000024E9F837288>
  • 通过ngroups属性,可以访问分为了多少组:
gb.ngroups
# 16
  • 通过groups属性,可以返回从组名映射到组索引列表的字典:
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')])

【练一练】

上一小节介绍了可以通过drop_duplicates得到具体的组类别,现请用groups属性完成类似的功能。

data = df.groupby(['School', 'Gender']).groups.keys()
pd.DataFrame(data,columns=['School','Gender'])
# 	School						Gender
# 0	Fudan University	Female
# 1	Fudan University	Male
#	2	Peking University	Female
# 3	Peking University	Male
# 4	Shanghai Jiao Tong University	Female
# 5	Shanghai Jiao Tong University	Male
# 6	Tsinghua University	Female
# 7	Tsinghua University	Male
  • 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'))
# School	Grade	Name	Gender	Height	Weight	Transfer	Test_Number	Test_Date	Time_Record	cond
# 15	Fudan University	Freshman	Changqiang Yang	Female	156.0	49.0	N	3	2020/1/1	0:05:25	normal
# 28	Fudan University	Freshman	Gaoqiang Qin	Female	170.2	63.0	N	2	2020/1/7	0:05:24	normal
# 63	Fudan University	Freshman	Gaofeng Zhao	Female	152.2	43.0	N	2	2019/10/31	0:04:00	low
# 70	Fudan University	Freshman	Yanquan Wang	Female	163.5	55.0	N	1	2019/11/19	0:04:07	normal
# 73	Fudan University	Freshman	Feng Wang	Male	176.3	74.0	N	1	2019/9/26	0:03:31	high
# 105	Fudan University	Freshman	Qiang Shi	Female	164.5	52.0	N	1	2019/12/11	0:04:23	normal
# 108	Fudan University	Freshman	Yanqiang Xu	Female	152.4	38.0	N	1	2019/12/8	0:05:03	low
# 157	Fudan University	Freshman	Xiaoli Lv	Female	152.5	45.0	N	2	2019/9/11	0:04:17	low
# 186	Fudan University	Freshman	Yanjuan Zhao	Female	NaN	53.0	N	2	2019/10/9	0:04:21	normal

4、分组的三大操作

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

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

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

二、聚合函数

在介绍agg之前,首先要了解一些直接定义在groupby对象的聚合函数,在能直接使用groupby函数,因为它的速度基本都会经过内部的优化,使用功能时应当优化考虑。根据返回标量值的原则,包括如下函数:

max/min/mean/median/count/all/any/idxmax/idxmin/mad/nunique/skew/quantile/sum/std/var/sem/size/prod

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

【练一练】

请查阅文档,明确all/any/mad/skew/sem/prod函数的含义。

functiondesciption
all分组后各组中的所有值都是真实的,则返回True,否则返回False。
any分组后各组中数值存在True则返回True,全为False则返回False
mad返回所请求轴的值的平均绝对偏差(mean absolute deviation)
skew偏度系数
sem标准误差(standard error of mean)
prod连乘的乘积
nunique统计每个group中不同类别的数目
count计算每个组的元素个数,不包含NaN值
size计算每个组的元素个数,包含NaN值
gb.all()
# Gender
# Female    True
# Male      True
# Name: Height, dtype: bool

gb.any()
# Gender
# Female    True
# Male      True
# Name: Height, dtype: bool
gb.nunique()
# Gender
# Female    99
# Male      47
# Name: Height, dtype: int64
# 注意size和count的区别
gb.count()
# Gender
# Female    132
# Male       51
# Name: Height, dtype: int64

gb.size()
# Gender
# Female    141
# Male       59
# Name: Height, dtype: int64

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

gb = df.groupby('Gender')[['Height', 'Weight']]
gb.max()
# Height	Weight
# Gender		
# Female	170.2	63.0
# Male	193.9	89.0

2、agg方法

agg可以进一步使用自定义函数、同时应用多个函数等

a 使用多个函数

用列表的形式传入多个聚合函数对应的字符串

gb.agg(['sum','idxmax','skew'])
				Height	Weight
				sum	idxmax	skew	sum	idxmax	skew
Gender						
Female	21014.0	28	-0.219253	6469.0	28	-0.268482
Male	8854.9	193	0.437535	3929.0	2	-0.332393

此时的列索引为多级索引,第一层为数据源,第二层为使用的聚合方法

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

通过构造字典传入agg中实现,字典以列名为键,以聚合字符串或字符串列表为值

gb.agg({'Height':['mean','max'],'Weight':'count'})
# 				Height		Weight
# 				mean	max	count
# Gender			
# Female	159.19697	170.2	135
# Male	173.62549	193.9	54

【练一练】

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

gb.agg({'Height':['sum','idxmax','skew'],'Weight':['sum','idxmax','skew']})
c 使用自定义函数

agg中可以使用具体的自定义函数,需要注意传入函数的参数是之前数据源中的列,逐列进行计算

下面分组计算身高和体重的极差:

Height	Weight
Gender		
Female	13.79697	13.918519
Male	17.92549	21.759259

【练一练】

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

gb.describe()

gb.agg(['count','mean','std','min',('25%',lambda x:x.quantile(0.25)),('50%',lambda x:x.quantile(0.50)),('75%',lambda x:x.quantile(0.75))])

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

def my_func(s):
    res = 'High'
    if s.mean() <= df[s.name].mean():
        res = 'Low'
    return res
gb.agg(my_func)
d 聚合结果重命名

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

gb.agg([('range', lambda x: x.max()-x.min()), ('my_sum', 'sum')])
# 			Height				Weight
# 			range	my_sum	range	my_sum
# Gender				
#	Female	24.8	21014.0	29.0	6469.0
#	Male	38.2	8854.9	38.0	3929.0

gb.agg({'Height': [('my_func', my_func), 'sum'], 'Weight': lambda x:x.max()})
#							  Height	Weight
				my_func	sum			<lambda>
Gender			
Female 	Low			21014.0	63.0
Male	 High			8854.9	89.0

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

gb.agg([('my_sum', 'sum')])
# 				Height	Weight
# 				my_sum	my_sum
# Gender		
# Female	21014.0	6469.0
# Male		8854.9	3929.0

gb.agg({'Height': [('my_func', my_func), 'sum'], 'Weight': [('range', lambda x:x.max())]})
#   								Height	Weight
# 				my_func		sum			range
# Gender			
# Female	Low				21014.0	63.0
# Male		High			8854.9	89.0

三、变换和过滤

1、变换函数与transform方法

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

gb.cummax().head()
#     Height	Weight
#   0	158.9	  46.0
#   1	166.5	  70.0
#   2	188.9	  89.0
#   3	NaN   	46.0
#   4	188.9	  89.0

【练一练】

groupby对象中,rank方法也是一个实用的变换函数,请查阅它的功能并给出一个使用的例子。

rank(method='average',ascending=True,na_option='keep',pct=False,axis=0)

对每组进行排序,得到的是每个值对应的排序值。

参数:

  • method:默认为average,即当出现n个相同值时,取排名的平均值为排名

    • min:相同值的排名按最小的名次值排序,有n个相同值,之后值的排名需+n

    • max:相同值的排名按最大的名次值排序

    • first:从小到大排序,相同值按出现顺序先后排序依次递增

    • dense:相同值的排名相同,其它只在该名次的基础依次上+1

  • ascending:默认为True,从低到高排列,False则从高到低排列。

  • na_option:默认为keep,空值不参与排序

    • top:默认为升序时,空值为最小值进行排序
    • bottom:默认为升序时,空值为最大值进行排序
  • pct: 默认为False,True则将排名转化为百分比形式

gb.rank(method='first',ascending=False,pct=True,na_option='top').head()
# 	Height		Weight
# 0	0.588652	0.652482
# 1	0.932203	0.661017
# 2	0.169492	0.101695
# 3	0.007092	0.893617
# 4	0.559322	0.457627

当用自定义变换时需要使用transform方法,被调用的自定义函数,其传入值为数据源的序列,与agg的传入类型是一致的,其最后的返回结果是行列索引与数据源一致的Dataframe

对身高和体重进行分组标准化,即减去组均值除以组的标准差:

gb.transform(lambda x:(x-x.mean())/x.std()).head()
# 	Height		Weight
# 0	-0.058760	-0.354888
# 1	-1.010925	-0.355000
# 2	2.167063	2.089498
# 3	NaN				-1.279789
# 4	0.053133	0.159631

【练一练】

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

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

gb.transform('mean').head()
#  	Height		Weight
# 0	159.19697	47.918519
# 1	173.62549	72.759259
# 2	173.62549	72.759259
# 3	159.19697	47.918519
# 4	173.62549	72.759259

2、组索引与过滤

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

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

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

gb.filter(lambda x: x.shape[0] > 100 ).head()
# 	Height	Weight
# 0	158.9		46.0
# 3	NaN			41.0
# 5	158.0		51.0
# 6	162.5		52.0
# 7	161.9		50.0

【练一练】

从概念上说,索引功能是组过过滤功能的子集,请使用filter函数完成loc[...]的功能,这里假设"…"是元素列表。

四、跨列分组

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的均值。

首先,这显然不是过滤操作,因此不能使用fliter,返回的均值是标量而不是序列,因此transform不符合要求;最后,似乎使用agg函数能够处理,但是之前强调过聚合函数是逐列处理的,而不能够多列数据同时处理。由此,引出了用apply函数来解决这一问题。

2、apply的使用

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】标量情况:结果得到的是seires,索引与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']))
# 										a	b
# Gender	Test_Number		
# Female						1	0	0
# 									2	0	0
# 									3	0	0
# Male							1	0	0
# 									2	0	0
# 									3	0	0

【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')])))
# 												w    y
#                         x    z
# Gender Test_Number            
# Female 1           a  1.0  1.0
#                    b  1.0  1.0
#        2           a  1.0  1.0
#                    b  1.0  1.0
#        3           a  1.0  1.0
#                    b  1.0  1.0
# Male   1           a  1.0  1.0
#                    b  1.0  1.0
#        2           a  1.0  1.0
#                    b  1.0  1.0
#        3           a  1.0  1.0
#                    b  1.0  1.0

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

五、练习

EX1:汽车数据集

现有一份汽车数据集,其中Brand, Disp., HP分别代表汽车品牌、发动机蓄量、发动机输出。

car = pd.read_csv('joyful-pandas/data/car.csv')
car.head()
# 						 Brand  Price    Country  Reliability  Mileage   Type  Weight  Disp.   HP
# 0   Eagle Summit 4   8895        USA          4.0       33  Small    2560   97  113
# 1  Ford Escort   4   7402        USA          2.0       33  Small    2345   114   90
# 2   Ford Festiva 4   6319      Korea          4.0       37  Small    1845   81   63
# 3    Honda Civic 4   6635  Japan/USA          5.0       32  Small    2260   91   92 
# 4  Mazda Protege 4   6599      Japan          5.0       32  Small    2440   113  103
  1. 先过滤出所属Country数超过2个的汽车,即若该汽车的Country在总体数据集中出现次数不超过2则剔除,再按Country分组计算价格均值、价格变异系数、该Country的汽车数量,其中变异系数的计算方法是标准差除以均值,并在结果中把变异系数重命名为CoV
car2=car.groupby('Country').filter(lambda x:x.shape[0]>2)
car2.head()
# 						 Brand  Price    Country  Reliability  Mileage   Type  Weight  Disp.   HP
# 0   Eagle Summit 4   8895        USA          4.0       33  Small    2560   97  113
# 1  Ford Escort   4   7402        USA          2.0       33  Small    2345   114   90
# 2   Ford Festiva 4   6319      Korea          4.0       37  Small    1845   81   63
# 3    Honda Civic 4   6635  Japan/USA          5.0       32  Small    2260   91   92 
# 4  Mazda Protege 4   6599      Japan          5.0       32  Small    2440   113  103   


car2.groupby('Country').Price.agg(['mean',('CoV',lambda x:x.std()/x.mean()),'count'])
# 										mean       CoV  count
# Country                                 
# Japan      13938.052632  0.387429     19
# Japan/USA  10067.571429  0.240040      7
# Korea       7857.333333  0.243435      3
# USA        12543.269231  0.203344     26
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值