文章目录
Pandas数据分析-Task4
记录DataWhale的Pandas数据分析的学习过程,使用的教材为 joyful-pandas。
Task4是pandas的分组相关的知识(相比前几天任务量小一些,菜鸡终于能喘口气了),内容基本可以分为三个部分,第一部分介绍groupby()函数的基本使用;第二部分介绍分组的三大操作:聚合agg()、变换transform()、以及过滤filter();第三部分介绍apply()函数在分组中的应用。本篇文章中所有的代码示例中用到的原始文件都可以在 此链接中下载。
分组函数
分组基本操作
pandas的分组操作使用的是groupby()函数,其一般的使用方法为:
df.groupby(分组依据)[数据来源].操作函数
比如,我们有学校学生情况的表格‘learn_pandas.csv’,要根据性别分类后统计身高的均值:
#语句1
df = pd.read_csv('data/learn_pandas.csv')
df.groupby('Gender')['Height'].mean()
语句1是最简单的根据一个列进行分组,如果要根据多个列进行分组,直接把多个列的名字组成字符串列表,当作groupby()函数的输入参数即可。
语句1其实是groupby的一个特殊情况,实际上,只有当分组条件是列表中的列时,才能使直接用列名字符做分组依据,直接用df中的列名做分组只是一个记号,等于传入的是一整列的值,也即,语句1实际上进行的操作为:
df = pd.read_csv('data/learn_pandas.csv')
df.groupby(df['Gender'])['Height'].mean()
知道了这个原理,我们就可以很方便扩展groupby()函数的输入,其输入参数不一定非要是df中的列,也可以是自己构造的一个数组,只要数组的长度和df的长度一致即可。所以,我们也可以构造逻辑表达式进行分组,例如,根据学生体重是否超过总体均值来分组:
condition=df.Weight > df.Weight.mean()#condition是一个长度与df相等的布尔Series.
df.groupby(condition)['Height'].mean()
Groupby对象
groupby()函数返回的是一个Groupby对象,教材介绍了几个常用的属性及方法:
- ngroups属性:查看分为了多少组。
- groups属性:返回从组名映射到组索引列表的字典。
- size():查看每个组元素的个数。
- get_group():获取一个组具体的内容,输入参数是这个组的index。
练一练
题目:可以通过drop_duplicates 得到具体的组类别,现请用groups 属性完成类似的功能。
思路:groups属性返回字典,字典的key即是不重复的组合。
df = pd.read_csv('data/learn_pandas.csv')
df[['Gender','School']].drop_duplicates(['Gender','School'])#使用drop_duplicates()方法
pd.DataFrame(df.groupby(['Gender','School']).groups.keys(),columns=['Gender','School'])#groupby结合groups属性。
聚合、变换、过滤
groupby对象无法直接显示,只能通过操作函数返回数据。其返回的数据可分为三类:标量,序列,DataFrame。分别对应了三类操作:聚合、变换、过滤。
聚合方法-agg()
聚合函数多为统计函数,从一组或几组序列中返回一个标量值。pandas有一些内置的聚合函数:max/min/mean/median/count,可以直接调用。
练一练
问题:请查阅文档,明确all/any/mad/skew/sem/prod 函数的含义。
- any(): Return True if any value in the group is truthful, else False。大概意思是如果组内有truthful的值就返回True。
- all():Return True if all values in the group are truthful, else False.组内所有元素都是truthful,返回True。
- mad():返回组内元素的绝对中位差。先计算出数据与它们的中位数之间的残差,MAD就是这些偏差的绝对值的中位数。MAD比方差鲁棒性更好。
- skew():组内数据的偏度。
- sem():组内数据的均值标准误差。
- prod() :组内所有元素的乘积。
简单的内置聚合函数无法完成更复杂的操作,agg()方法可进行更复杂的聚合操作。具体为:
1.对一个分组同时使用多个聚合函数:把多个内置聚合函数的名称字符组成列表。
gb = df.groupby('Gender')[['Height', 'Weight']]
gb.agg(['sum', 'idxmax', 'skew'])#把多个内置聚合函数的名称字符组成列表。
在练习的时候发现,如果agg()函数中只输入单个的内置聚合函数名称字符,最后的输出是不会显示列索引的。只有聚合函数名称字符加上[]括号以后才会显示列索引。
gb.agg('mean') #'mean'没加[],只显示一层的列索引。
> Height Weight
Gender
Female 159.19697 47.918519
Male 173.62549 72.759259
gb.agg(['mean'])#'mean'加了[],最后的结果是两层的类索引。
> Height Weight
mean mean
Gender
Female 159.19697 47.918519
Male 173.62549 72.759259
2.对特定的列使用特定的聚合函数:列名字符和函数名称字符组成的字典。
gb.agg({'Height':['mean','max'], 'Weight':'count'})#列名字符和函数名称字符组成的字典
练一练
问题:请使用2中的传入字典的方法完成1中等价的聚合任务。
gb.agg({'Height':['mean','max','min'],'Weight':['mean','max','min']})
3.自定义的聚合函数:传入函数的参数是之前数据源中的列,逐列进行计算
gb.agg(lambda x: x.mean()-x.min())#x代表的是分组后的一列。
练一练
问题:在groupby 对象中可以使用describe 方法进行统计信息汇总,请同时使用多个聚合函数,完成与该方法相同的功能。
思路:使用agg汇集多个基本的聚合函数即可,在做的时候发现分位数函数不能直接输入名字,因为要计算3个不同的分位数,最后使用lambda匿名函数完成分位数功能,并重命名。
gb.describe()
> Height Weight
count mean std min 25% 50% 75% max count mean std min 25% 50% 75% max
Gender
Female 132.0 159.19697 5.053982 145.4 155.675 159.6 162.825 170.2 135.0 47.918519 5.405983 34.0 44.0 48.0 52.00 63.0
Male 51.0 173.62549 7.048485 155.7 168.900 173.4 177.150 193.9 54.0 72.759259 7.772557 51.0 69.0 73.0 78.75 89.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
4.重命名-参数的位置改写成元组,元组的第一个元素为新的名字,第二个
位置为原来的函数名字。注意,重命名的元组参数需要加方括号。
gb.agg(['mean'])
> Height Weight
mean mean
Gender
Female 159.19697 47.918519
Male 173.62549 72.759259
gb.agg([('rename_mean','mean')])
> Height Weight
rename_mean rename_mean
Gender
Female 159.19697 47.918519
Male 173.62549 72.759259
变换方法-transfrom()
变换函数的返回是与输入长度相同的序列。pandas有一些默认的内置变换函数:cumcount,cumsum,cumprod,cummax,cummin。
练一练:
问题:在groupby 对象中,rank 方法也是一个实用的变换函数,请查阅它的功能并给出一个使用的例子。
解答:官方文档的解答是:Provide the rank of values within each group,大体意思是分别在分组后的每一个列中对值进行排序,输出的是样本的排序值。例如:
gd.rank()
> Height Weight
0 58.0 47.5
1 5.0 19.0
2 50.0 54.0
3 NaN 14.5
4 27.0 31.5
根据这个例子可以看出两点:1,对于值为NaN的,在排序时忽略,其排序后的值也为NaN。2.rank()默认的输入可以为小数,比如这个例子中Weithg这一列的值出现了47.5,14.5这种形式的数字,排序后的位置值应该是正整数才对,为什么会出现小数?这个问题的答案可以从官方文档中找到解答,rank()函数有一个参数为:method{‘average’, ‘min’, ‘max’, ‘first’, ‘dense’}, default ‘average’。指的是当要排序的序列中出现相同的值时返回什么结果,其默认值是method=‘average’,意思是,将相同排序的结果取平均。例如:
s=pd.Series([1,2,3,3,4],index=['a','b','c','d','e'])
s.rank()
>a 1.0
b 2.0
c 3.5
d 3.5
e 5.0
Series中有五个数,其中存在两个相同的3,3这个元素在Series中的排序应该是3,4。问题是设置s[‘c’]为3还是s[‘d’]为3,method=‘average’的意思是既然分不清谁应该排在第3个谁应该排在第4个,那就把s[‘c’]和s[‘d’]的结果输出为一样的,取平均值(3+4)/2=3.5作为s[‘c’]和s[‘d’]的结果。
除了基本的变换函数,有时候我们还需要一些自定义变化函数,这个时候用到了transfrom()方法,transfrom()方法调用我们自定义的函数完成变换操作。被调用的自定义函数,传入函数的参数是之前数据源中的列,逐列进行计算,返回结果是行列索引与数据源一致的DataFrame。
gb.transform(lambda x: (x-x.mean())/x.std())#x表示的是分组后的一列。
transfrom()方法还可以返回一个标量,这个标量值会广播到分组后的这一列中。在特征工程中很常用,例如:构造两列新特征来分别表示样本所在性别组的身高均值和体重均值。
gb.transform('mean')
> 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
练一练
问题:对于transform 方法无法像agg 一样,通过传入字典来对指定列使用特定的变换,如果需要在一次transform 的调用中实现这种功能,请给出解决方案。
思路:将列名和对应方法使用字典变量传递,然后自定义一个函数,识别除传入参数的列名,进而根据字典变量得到这一列对应的方法。
def my_func(x,dic):
func=dic[x.name]#x.name返回的是x所在这一列的列名。
if(func=='max'):
return x.max()
elif(func=='min'):
return x.min()
elif(func=='mean'):
return x.mean()
else:
return None
gb.transform(my_func,{'Height':'mean','Weight':'max'})
过滤方法-filter()
过滤方法直接对一组进行过滤,可以自定义函数完成条件筛选。被调用的自定义函数,传入函数的参数不再是agg()和transfrom()方法中的列,而是整个分组后的DataFrame。自定义函数的输出必须是单个布尔值。
练一练
问题:从概念上说,索引功能是组过滤功能的子集,请使用filter 函数完成loc[.] 的功能,这里假设”.“是元素列表。
思路:使用‘learn_pandas.csv’数据,其Index为默认的正整数。loc[.]可以直接对Index进行索引,使用分组的话,想法是先按index进行分组,这样每个样本为一组,然后使用filter函数再过滤,过滤条件为Index。
df.loc[[1,2,3]]
df.groupby([i for i in range(df.shape[0])]).filter(lambda x:x.index.isin([1,2,3])[0])#这个[0]必须要加,否则返回的不是标量布尔值。
apply()在分组中的应用
上面介绍的agg()和transfrom()方法自定义函数的输入都是一列,filter()方法自定义函数的输入虽然是DataFrame,但是只能返回一个标量布尔值。所以,当需要自定义函数跨列进行计算时,上面的方法都无法达到要求。apply()的引入就是为了解决这个问题,apply()自定义函数的传入参数是DataFrame,输出可以是标量,Series,DataFrame。
apply()方法也在DataFrame中出现过,我们上一节还学过map()方法,可以对比一下他们的使用。
- map()方法定义在Index上,其自定义函数传入的参数是索引的元组,常用于对多层索引压缩。
- DataFrame中的apply()方法,其自定义函数传入参数是DataFrame的一行或一列,默认axis=0时,为一列;axis=1时,为一行。一般对DataFrame做行迭代或列迭代。
练一练
问题:请尝试在apply 传入的自定义函数中,根据组的某些特征返回相同长度但索引不同的Series ,会报错吗?
思路:传入apply()自定义函数的参数是DataFrame,不同组样本的个数也不同,根据样本个数不同为不同组返回值一样但索引不一样的Series,利用第一节讲过的语法糖可以完成此操作。
gb = df.groupby(['Gender','Test_Number'])[['Height','Weight']]
gb.apply(lambda x:pd.Series([0,1],index=['a','b']) if (x.shape[0]>50) else pd.Series([0,1],index=['c','d']))
>TypeError: Series.name must be a hashable type
会报错。
练一练
问题:请尝试在apply 传入的自定义函数中,根据组的某些特征返回相同大小但列索引不同的DataFrame ,会报错吗?如果只是行索引不同,会报错吗?
思路:延续上一题的思路,还是以不同组个数不同作为返回不同值的标准。
#先测试行索引不一样时
gd.apply(lambda x:pd.DataFrame([[1,2],[3,4]],index=['a','b'],columns=['c','d']) if (x.shape[0]>50) else pd.DataFrame([[1,2],[3,4]],index=['aa','bb'],columns=['c','d']))
> c d
Gender Test_Number
Female 1 a 1 2
b 3 4
2 a 1 2
b 3 4
3 aa 1 2
bb 3 4
Male 1 aa 1 2
bb 3 4
2 aa 1 2
bb 3 4
3 aa 1 2
bb 3 4
#再测试列索引不一样时
gd.apply(lambda x:pd.DataFrame([[1,2],[3,4]],index=['a','b'],columns=['c','d']) if (x.shape[0]>50) else pd.DataFrame([[1,2],[3,4]],index=['a','b'],columns=['cc','dd']))
> c d cc dd
Gender Test_Number
Female 1 a 1.0 2.0 NaN NaN
b 3.0 4.0 NaN NaN
2 a 1.0 2.0 NaN NaN
b 3.0 4.0 NaN NaN
3 a NaN NaN 1.0 2.0
b NaN NaN 3.0 4.0
Male 1 a NaN NaN 1.0 2.0
b NaN NaN 3.0 4.0
2 a NaN NaN 1.0 2.0
b NaN NaN 3.0 4.0
3 a NaN NaN 1.0 2.0
b NaN NaN 3.0 4.0
#最后测试行列索引都不一样时
gd.apply(lambda x:pd.DataFrame([[1,2],[3,4]],index=['a','b'],columns=['c','d']) if (x.shape[0]>50) else pd.DataFrame([[1,2],[3,4]],index=['aa','bb'],columns=['cc','dd']))
> c d cc dd
Gender Test_Number
Female 1 a 1.0 2.0 NaN NaN
b 3.0 4.0 NaN NaN
2 a 1.0 2.0 NaN NaN
b 3.0 4.0 NaN NaN
3 aa NaN NaN 1.0 2.0
bb NaN NaN 3.0 4.0
Male 1 aa NaN NaN 1.0 2.0
bb NaN NaN 3.0 4.0
2 aa NaN NaN 1.0 2.0
bb NaN NaN 3.0 4.0
3 aa NaN NaN 1.0 2.0
bb NaN NaN 3.0 4.0
可以看出,当返回DataFrame时,即使不同组的行列索引不同,也不会报错。
练一练
问题:在groupby 对象中还定义了cov 和corr 函数,从概念上说也属于跨列的分组处理。请利用之前定义的gb对象,使用apply 函数实现与gb.cov() 同样的功能并比较它们的性能。
思路:实现cov()函数并不复杂,但要注意的是缺失值,如果某一组内数据存在NaN数据时,统计函数会返回NaN。在本题使用的数据中,存在NaN值,但是没有np.nancov()函数,需要自己手动剔除NaN值后计算协方差。
gb = df.groupby(['Gender','Test_Number'])[['Height','Weight']]
def my_cov(x,y):
x_var=np.nanvar(x) #计算自身的方差
y_var=np.nanvar(y) #计算自身的方差
x=x[x.notnull().values].reset_index(drop=True)#去除NaN值,并初始化索引。
y =y[y.notnull().values].reset_index(drop=True)
#去重后x与y的长度不同,计算协方差需要两个序列长度相同,所以对长度长的裁剪。
if(x.shape[0]<y.shape[0]):
y=y[0:x.shape[0]]
elif(x.shape[0]>y.shape[0]):
x = x[0:y.shape[0]]
x_y_var=(x-x.mean()).dot(y-y.mean())/(x.shape[0]-1)#计算协方差的无偏估计。
return [[x_var,x_y_var],[x_y_var,y_var]]
gb.apply(lambda x:pd.DataFrame(my_cov(x.Height,x.Weight),index=['Height','Weight'],columns=['Height','Weight']))
> Height Weight
Gender Test_Number
Female 1 Height 20.630844 11.063671
Weight 11.063671 26.025146
2 Height 30.970462 5.538308
Weight 5.538308 33.903476
3 Height 22.403275 10.048246
Weight 10.048246 22.005540
Male 1 Height 41.059040 26.732621
Weight 26.732621 65.336504
2 Height 53.872747 38.224183
Weight 38.224183 35.765432
3 Height 46.798056 65.860000
Weight 65.860000 77.061224
> 用时0.03291 s
#使用cov()函数
gb.cov()
> Height Weight
Gender Test_Number
Female 1 Height 20.963600 21.452034
Weight 21.452034 26.438244
2 Height 31.615680 30.386170
Weight 30.386170 34.568250
3 Height 23.582395 20.801307
Weight 20.801307 23.228070
Male 1 Height 42.638234 48.785833
Weight 48.785833 67.669951
2 Height 57.041732 38.224183
Weight 38.224183 37.869281
3 Height 56.157667 84.020000
Weight 84.020000 89.904762
> 用时0.006982 s
和函数的结果不一样,检查了一遍公式并没有发现错误,不知道怎么回事。
练习

(1)思路:根据Country进行分组,使用filter过滤样本个数小于等于2的分组。然后再使用agg()函数。
df = pd.read_csv('data/car.csv')
df_new=df.groupby('Country').filter(lambda x:x.shape[0]>2) #选出样本个数大于2的分组。
df_new.groupby('Country').Price.agg(['mean',('CoV',lambda x:x.std()/x.mean()),'count'])
(2)思路:先根据位置的前中后生成一个列表,然后根据列表分组。
l=[i//((df.shape[0]+1)//3) for i in range(df.shape[0])]#列表l的前三分之一为0,中间三分之一为1,最后三分之一为2.
t=df.groupby(l).Price.mean()#根据列表分组。
(3)思路:agg()方法传入字典变量,对不同列进行不同操作,然后用map()函数对多级索引合并。
t=df.groupby('Type')[['Price','HP']].agg({'Price':['max'],'HP':['min']}) #不加['min']就显示不出来min索引。
t.columns=t.columns.map(lambda x:x[0]+'_'+x[1])
(4)思路:返回的是序列,所以使用transform()方法。
df.groupby('Type')['HP'].transform(lambda x:(x-x.min())/(x.max()-x.min()))
(5)思路:返回的是一个跨行计算的值,用apply()函数。
df.groupby('Type')[['Disp.','HP']].apply(lambda x:np.corrcoef(x['Disp.'].values,x.HP.values)[0,1])
本文详细介绍了Pandas中分组数据分析的任务,包括groupby()的基本操作、分组的聚合、变换和过滤功能。重点讲解了agg()、transform()、filter()方法的应用,并提供了丰富的练习示例,帮助读者深入理解Pandas分组处理的实践技巧。
927

被折叠的 条评论
为什么被折叠?



