系统重温Pandas笔记:(四)分组

写在前面

本文内容源自Datawhale 组队学习教程,并结合了部分自己的笔记和感悟。对Datawhale感兴趣且想进一步了解:https://github.com/datawhalechina/joyful-pandas

一、分组模式及其对象

1. 分组的一般模式

想要实现分组操作,必须明确三个要素: 分 组 依 据 \color{#FF0000}{分组依据} 数 据 来 源 \color{#00FF00}{数据来源} 操 作 及 其 返 回 结 果 \color{#0000FF}{操作及其返回结果}

  • 分组代码的一般模式:
df.groupby(分组依据)[数据来源].使用操作

e.g.

df.groupby('Gender')['Longevity'].mean() #依据性别分组,统计全国人口寿命的平均值

df.groupby('Gender')['Height'].median() #按照性别统计身高中位数

2. 分组依据的本质

  • groupby中传入相应列名构成的列表,实现根据多个维度进行分组:
df.groupby(['School', 'Gender'])['Height'].mean()
  • 通过一定的复杂逻辑来分组:

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

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

然后将其传入groupby中:

df.groupby(condition)['Height'].mean()

Weight
False    159.034646
True     172.705357
Name: Height, dtype: float64
【练一练】

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

解:

condition_high = df.Weight >= df.Weight.quantile(0.75)
condition_normal = (df.Weight < df.Weight.quantile(0.75))&(df.Weight >= df.Weight.quantile(0.25))
condition_low = df.Weight < df.Weight.quantile(0.25)

df.groupby(condition_high)['Height'].mean()
Weight
False    159.643165
True     174.511364
Name: Height, dtype: float64

df.groupby(condition_normal)['Height'].mean()
Weight
False    164.560638
True     161.800000
Name: Height, dtype: float64

df.groupby(condition_low)['Height'].mean()
Weight
False    165.950704
True     153.753659
Name: Height, dtype: float64

综上所述,体重为high、normal、low三组的身高的均值分别是:174.511364,161.800000和153.753659。

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

item = np.random.choice(list('abc'), df.shape[0])
df.groupby(item)['Height'].mean()

a    165.347059
b    161.250877
c    162.655172
Name: Height, dtype: float64

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

df.groupby([condition, item])['Height'].mean()

Weight   
False   a    160.082927
        b    158.629545
        c    158.435714
True    a    173.340741
        b    170.123077
        c    173.731250
Name: Height, dtype: float64

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

3. Groupby对象

最终具体做分组操作时,所调用的方法都来自于pandas中的groupby对象,这个对象上定义了许多方法,也具有一些方便的属性。

gb = df.groupby(['School', 'Grade'])

通过ngroups属性,可以访问分为了多少组:

gb.ngroups

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

res = gb.groups
【练一练】

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

gb_lx = df.groupby(['School', 'Gender'])
res_lx = gb_lx.groups
res_lx.keys()

dict_keys([('Fudan University', 'Female'), ('Fudan University', 'Male'), ('Peking University', 'Female'), ('Peking University', 'Male'), ('Shanghai Jiao Tong University', 'Female'), ('Shanghai Jiao Tong University', 'Male'), ('Tsinghua University', 'Female'), ('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'))

在这里插入图片描述

4. 分组的三大操作

分组的三大操作:聚合、变换和过滤,对应aggtransformfilter函数及其操作。

二、聚合函数

1. 内置聚合函数

在介绍agg之前,首先要了解一些直接定义在groupby对象的聚合函数,因为它的速度基本都会经过内部的优化,使用功能时应当优先考虑。根据返回标量值的原则,包括如下函数:max/min/mean/median/count/all/any/idxmax/idxmin/mad/nunique/skew/quantile/sum/std/var/sem/size/prod
e.g.

gb = df.groupby('Gender')['Height']
gb.idxmin()
gb.quantile(0.95)
【练一练】

请查阅文档,明确all/any/mad/skew/sem/prod函数的含义。
解:
all:是否全为True
any:是否有一个为True
mad:根据平均值计算平均绝对离差
skew:样本值的偏度(三阶矩)
sem:返回所请求轴上的平均值的无偏标准误差
prod:返回不同维度上的乘积

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

gb = df.groupby('Gender')[['Height', 'Weight']]
gb.max()

在这里插入图片描述

2. agg方法

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

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

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

  • 【a】使用多个函数

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

gb.agg(['sum', 'idxmax', 'skew'])

在这里插入图片描述
从结果看,此时的列索引为多级索引,第一层为数据源,第二层为使用的聚合方法,分别逐一对列使用聚合,因此结果为6列。

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

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

gb.agg({'Height':['mean','max'], 'Weight':'count'})

在这里插入图片描述

【练一练】

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

gb.agg({'Height':['sum', 'idxmax', 'skew'], 'Weight':['sum', 'idxmax', 'skew']})

在这里插入图片描述

  • 【c】使用自定义函数

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

gb.agg(lambda x: x.mean()-x.min())
【练一练】

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

gb.describe()

在这里插入图片描述

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

在这里插入图片描述

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

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

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

gb.agg([('range', lambda x: x.max()-x.min()), ('my_sum', 'sum')])

gb.agg({'Height': [('my_func', my_func), 'sum'], 'Weight': lambda x:x.max()})

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

gb.agg([('my_sum', 'sum')])

gb.agg({'Height': [('my_func', my_func), 'sum'], 'Weight': [('range', lambda x:x.max())]})

三、变换和过滤

1. 变换函数与transform方法

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

gb.cummax()
【练一练】

groupby对象中,rank方法也是一个实用的变换函数,请查阅它的功能并给出一个使用的例子。
解:
rank功能:对DataFrame某列的数据进行聚类,然后对其它列的属于同类数据进行数值大小排序。
rank使用例子:

gb2 = df[['Gender','Height', 'Weight']]
gb2['rank_g'] = gb2.groupby('Gender')['Height'].rank(method='first',na_option='keep')

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

gb.transform(lambda x: (x-x.mean())/x.std())
【练一练】

对于transform方法无法像agg一样,通过传入字典来对指定列使用特定的变换,如果需要在一次transform的调用中实现这种功能,请给出解决方案。
解:
TAT这道练一练的题目没太读懂,脑袋空空TAT 希望未来能通过视频教程学会补上

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

gb.transform('mean')# 传入返回标量的函数也是可以的

2. 组索引与过滤

索引和过滤区别:
过滤在分组中是对于组的过滤,而索引是对于行的过滤

  • 在第二章中的返回值,无论是布尔列表还是元素列表或者位置列表,本质上都是对于行的筛选,即如果筛选条件的则选入结果的表,否则不选入。
  • 组过滤作为行过滤的推广,指的是如果对一个组的全体所在行进行统计的结果返回True则会被保留,False则该组会被过滤,最后把所有未被过滤的组其对应的所在行拼接起来作为DataFrame返回。

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

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

gb.filter(lambda x: x.shape[0] > 100)
【练一练】

从概念上说,索引功能是组过滤功能的子集,请使用filter函数完成loc[...]的功能,这里假设"..."是元素列表。
解:
以第三章索引中的loc例子为例:
用loc可以实现:

df_demo = df.set_index('Name')
df_demo.loc[['Qiang Sun','Quan Zhao'], ['School','Gender']]

在这里插入图片描述
下面使用filter函数实现:

gb1 = df_demo.groupby('Name')[['School','Gender']]
gb1.filter((lambda x: x.name in['Qiang Sun','Quan Zhao']))

在这里插入图片描述

四、跨列分组

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完全一致,只不过后者只允许返回布尔值

e.g.

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

在这里插入图片描述

【练一练】

请尝试在apply传入的自定义函数中,根据组的某些特征返回相同长度但索引不同的Series,会报错吗?
解:
TAT这道练一练的题目没太读懂,想了两天没什么思路,希望未来能通过视频教程学会补上!

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

在这里插入图片描述

【练一练】

请尝试在apply传入的自定义函数中,根据组的某些特征返回相同大小但列索引不同的DataFrame,会报错吗?如果只是行索引不同,会报错吗?
解:
TAT这道练一练的题目没太读懂,想了两天没什么思路,希望未来能通过视频教程学会补上!

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

【练一练】

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

gb.cov()

在这里插入图片描述
使用apply函数:

lyl = gb.apply(lambda x:x.cov())

在这里插入图片描述
性能对比:

%timeit -n 30 gb.cov()

4.68 ms ± 257 µs per loop (mean ± std. dev. of 7 runs, 30 loops each)
%timeit -n 30 gb.apply(lambda x:x.cov())

4.26 ms ± 206 µs per loop (mean ± std. dev. of 7 runs, 30 loops each)

竟然是使用apply函数的性能更好????这和前文中的结论似乎有些矛盾?不知道是我的操作出了问题?还是其它原因?

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

五、练习

Ex1:汽车数据集

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

df = pd.read_csv('data/car.csv')
df.head(3)

在这里插入图片描述

1.先过滤出所属Country数超过2个的汽车,即若该汽车的Country在总体数据集中出现次数不超过2则剔除,再按Country分组计算价格均值、价格变异系数、该Country的汽车数量,其中变异系数的计算方法是标准差除以均值,并在结果中把变异系数重命名为CoV
解: 先过滤出所属Country数超过2个的汽车:

python df1 = df.groupby('Country').filter(lambda x:x.shape[0]>2)

上面这一步看似简单,但是足足纠结了两天,一开始一直没读懂题目,以为所属国家是’Japan/USA’的才算国家,导致和参考答案的做法一直有出入。直到25号白天看了群里的解释才明白题意。
然后继续计算结果:

df1.groupby('Country')['Price'].agg(['mean', ('CoV', lambda x: x.std()/x.mean()), 'count'])

在这里插入图片描述

2.按照表中位置的前三分之一、中间三分之一和后三分之一分组,统计Price的均值。
解:
先计算总个数:

gs = df.shape[0]

60

然后分组统计均值:

condition = ['Head']*20+['Mid']*20+['Tail']*20
df.groupby(condition)['Price'].mean()

Head     9069.95
Mid     13356.40
Tail    15420.65
Name: Price, dtype: float64
  1. 对类型Type分组,对PriceHP分别计算最大值和最小值,结果会产生多级索引,请用下划线把多级列索引合并为单层索引。
    解:
    分别计算最大值和最小值:
res = df.groupby('Type').agg({'Price': ['max'], 'HP': ['min']})
res

在这里插入图片描述
用下划线把多级列索引合并为单层索引:

res.columns = res.columns.map(lambda x:'_'.join(x))
res

在这里插入图片描述

  1. 对类型Type分组,对HP进行组内的min-max归一化。
    解:
def normalize(s):
    s_min, s_max = s.min(), s.max()
    res = (s - s_min)/(s_max - s_min)
    return res
df.groupby('Type')['HP'].transform(normalize).head(10)

0    1.00
1    0.54
2    0.00
3    0.58
4    0.80
5    0.38
6    0.54
7    0.22
8    0.54
9    0.20
Name: HP, dtype: float64
  1. 对类型Type分组,计算Disp.HP的相关系数。
    解:
df.groupby('Type')[['HP', 'Disp.']].apply(lambda x:np.corrcoef(x['HP'].values, x['Disp.'].values)[0,1])

Type
Compact    0.586087
Large     -0.242765
Medium     0.370491
Small      0.603916
Sporty     0.871426
Van        0.819881
dtype: float64

Ex2:实现transform函数

  • groupby对象的构造方法是my_groupby(df, group_cols)
  • 支持单列分组与多列分组
  • 支持带有标量广播的my_groupby(df)[col].transform(my_func)功能
  • pandastransform不能跨列计算,请支持此功能,即仍返回Seriescol参数为多列
  • 无需考虑性能与异常处理,只需实现上述功能,在给出测试样例的同时与pandas中的transform对比结果是否一致
    解:
    我这个菜鸡真的不会做这道题,太难了TAT 于是,这道题我是阅读理解的参考答案。为了笔记的完整性(方便自己和大家系统性重温pandas),以下搬运的是参考答案:(希望未来重温时我有新的理解)
class my_groupby:
    def __init__(self, my_df, group_cols):
        self.my_df = my_df.copy()
        self.groups = my_df[group_cols].drop_duplicates()
        if isinstance(self.groups, pd.Series):
            self.groups = self.groups.to_frame()
        self.group_cols = self.groups.columns.tolist()
        self.groups = {i: self.groups[i].values.tolist() for i in self.groups.columns}
        self.transform_col = None
    def __getitem__(self, col):
        self.pr_col = [col] if isinstance(col, str) else list(col)
        return self
    def transform(self, my_func):
        self.num = len(self.groups[self.group_cols[0]])
        L_order, L_value = np.array([]), np.array([])
        for i in range(self.num):
            group_df = self.my_df.reset_index().copy()
            for col in self.group_cols:
                group_df = group_df[group_df[col]==self.groups[col][i]]
            group_df = group_df[self.pr_col]
            if group_df.shape[1] == 1:
                group_df = group_df.iloc[:, 0]
            group_res = my_func(group_df)
            if not isinstance(group_res, pd.Series):
                group_res = pd.Series(group_res,index=group_df.index,name=group_df.name)
            L_order = np.r_[L_order, group_res.index]
            L_value = np.r_[L_value, group_res.values]
        self.res = pd.Series(pd.Series(L_value, index=L_order).sort_index().values,index=self.my_df.reset_index().index, name=my_func.__name__)
        return self.res
​my_groupby(df, 'Type')

<__main__.my_groupby at 0x26bec232940>

单列分组:

def f(s):
    res = (s-s.min())/(s.max()-s.min())
    return res
my_groupby(df, 'Type')['Price'].transform(f).head()

0    0.733592
1    0.372003
2    0.109712
3    0.186244
4    0.177525
Name: f, dtype: float64
df.groupby('Type')['Price'].transform(f).head()

0    0.733592
1    0.372003
2    0.109712
3    0.186244
4    0.177525
Name: Price, dtype: float64

多列分组:

my_groupby(df, ['Type','Country'])['Price'].transform(f).head()

0    1.000000
1    0.000000
2    0.000000
3    0.000000
4    0.196357
Name: f, dtype: float64
df.groupby(['Type','Country'])['Price'].transform(f).head()
0    1.000000
1    0.000000
2    0.000000
3    0.000000
4    0.196357
Name: Price, dtype: float64

标量广播:

my_groupby(df, 'Type')['Price'].transform(lambda x:x.mean()).head()

0    7682.384615
1    7682.384615
2    7682.384615
3    7682.384615
4    7682.384615
Name: <lambda>, dtype: float64
df.groupby('Type')['Price'].transform(lambda x:x.mean()).head()

0    7682.384615
1    7682.384615
2    7682.384615
3    7682.384615
4    7682.384615
Name: Price, dtype: float64

跨列计算:

my_groupby(df, 'Type')['Disp.', 'HP'].transform(lambda x: x['Disp.']/x.HP).head()

0    0.858407
1    1.266667
2    1.285714
3    0.989130
4    1.097087
Name: <lambda>, dtype: float64
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值