Pandas数据分析-Task4

本文详细介绍了Pandas中分组数据分析的任务,包括groupby()的基本操作、分组的聚合、变换和过滤功能。重点讲解了agg()、transform()、filter()方法的应用,并提供了丰富的练习示例,帮助读者深入理解Pandas分组处理的实践技巧。

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])
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值