第三章 分组

# 导入需要的模块
import numpy as np
import pandas as pd

一、分组模式及其对象

1. 分组的一般模式

分组操作常见于生活中,例如:
1.按照 性 别 \color{#FF0000}{性别} 性别分组,统计全国人口平均寿命
2.按照 城 市 \color{#FF0000}{城市} 城市分组分组,统计每个城市的销售额
3.按照 渠 道 \color{#FF0000}{渠道} 渠道分组分组,统计每个渠道的平均流量情况

实现分组操作,必须明确三个要素:
①分组依据;②数据来源;③操作及其返回结果

分组代码一般语法:
df.groupby(分组依据)[数据来源].使用操作
eg:
df.groupby(‘性别’)[‘寿命’].mean()

# 以下为学生体测的数据集
df = pd.read_csv('data/learn_pandas.csv')
df.head()
School Grade Name Gender Height Weight Transfer Test_Number Test_Date Time_Record
0 Shanghai Jiao Tong University Freshman Gaopeng Yang Female 158.9 46.0 N 1 2019/10/5 0:04:34
1 Peking University Freshman Changqiang You Male 166.5 70.0 N 1 2019/9/4 0:04:20
2 Shanghai Jiao Tong University Senior Mei Sun Male 188.9 89.0 N 2 2019/9/12 0:05:22
3 Fudan University Sophomore Xiaojuan Sun Female NaN 41.0 N 2 2020/1/3 0:04:08
4 Fudan University Sophomore Gaojuan You Male 174.0 74.0 N 2 2019/11/6 0:05:22
# 求按照性别统计身高中位数
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.776923Male      174.212500
Peking University              Female    158.666667Male      172.030000
Shanghai Jiao Tong University  Female    159.122500Male      176.760000
Tsinghua University            Female    159.753333Male      171.638889
Name: Height, dtype: float64

以上都是单一维度或者多维度分组,直接按照列名字段即可,如果按照逻辑分组,就相对复杂点。

# 例如,根据学生体重是否超过总体均值来分组,计算身高的均值
# 第一步,先写出分组条件
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三组,统计身高的均值。

# 逻辑替换包括了where和mask
# where函数在传入条件为False的对应行进行替换
# mask在传入条件为True的对应行进行替换,当不指定替换值时,替换为缺失值
# 为了简写方便,先把体重定义为m
m = df.Weight
cond = m.mask(m>m.quantile(0.75),'high').mask(m<m.quantile(0.25),'low').mask((m>=m.quantile(0.25))&(m<=m.quantile(0.75)),'normal')
df.groupby(cond)['Height'].mean()
Weight
high      174.935714
low       153.753659
normal    161.883516
Name: Height, dtype: float64

从索引可以看出,其实最后产生的结果就是按照条件列表中元素的值(此处是TrueFalse)来分组

# 随机传入字母序列来验证:
item = np.random.choice(list('abc'), df.shape[0])
df.groupby(item)['Height'].mean()
a    163.762121
b    163.515493
c    161.978261
Name: Height, dtype: float64

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

df.groupby([condition, item])['Height'].mean()
Weight
False   a    158.926190b    158.975510c    159.241667
True    a    172.225000b    173.627273c    171.830000
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.776923Male      174.212500
Peking University              Female    158.666667Male      172.030000
Shanghai Jiao Tong University  Female    159.122500Male      176.760000
Tsinghua University            Female    159.753333Male      171.638889
Name: Height, dtype: float64

3. Groupby对象

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

gb = df.groupby(['School','Grade'])
gb
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x000002A4DDF839B0>

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

【练一练】

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

ex1 = df.groupby(['School','Gender'])
ex1.groups.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()
# 类似于excel表的透视表,按照需要的字段分组、计数
School                         Grade
Fudan University               Freshman      9Junior       12Senior       11Sophomore     8
Peking University              Freshman     13Junior        8Senior        8Sophomore     5
Shanghai Jiao Tong University  Freshman     13Junior       17Senior       22Sophomore     5
Tsinghua University            Freshman     17Junior       22Senior       14Sophomore    16
dtype: int64

通过get_group方法可以直接获取所在组对应的行,此时必须知道组的具体名字:

gb.get_group(('Fudan University', 'Freshman'))
School Grade Name Gender Height Weight Transfer Test_Number Test_Date Time_Record
15 Fudan University Freshman Changqiang Yang Female 156.0 49.0 N 3 2020/1/1 0:05:25
28 Fudan University Freshman Gaoqiang Qin Female 170.2 63.0 N 2 2020/1/7 0:05:24
63 Fudan University Freshman Gaofeng Zhao Female 152.2 43.0 N 2 2019/10/31 0:04:00
70 Fudan University Freshman Yanquan Wang Female 163.5 55.0 N 1 2019/11/19 0:04:07
73 Fudan University Freshman Feng Wang Male 176.3 74.0 N 1 2019/9/26 0:03:31
105 Fudan University Freshman Qiang Shi Female 164.5 52.0 N 1 2019/12/11 0:04:23
108 Fudan University Freshman Yanqiang Xu Female 152.4 38.0 N 1 2019/12/8 0:05:03
157 Fudan University Freshman Xiaoli Lv Female 152.5 45.0 N 2 2019/9/11 0:04:17
186 Fudan University Freshman Yanjuan Zhao Female NaN 53.0 N 2 2019/10/9 0:04:21

这里列出了2个属性和2个方法,而先前的mean、median都是groupby对象上的方法

4. 分组的三大操作

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

  • 第一个例子:依据性别分组,统计全国人口寿命的平均值:
    每一个组返回一个标量值,可以是平均值、中位数、组容量size

  • 第二个例子:依据季节分组,对每一个季节的温度进行组内标准化
    做了原序列的标准化处理,也就是说每组返回的是一个Series类型

  • 第三个例子:依据班级筛选出组内数学分数的平均值超过80分的班级
    既不是标量也不是序列,返回的整个组所在行的本身,即返回了DataFrame类型

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

二、聚合函数

1. 内置聚合函数

在介绍agg之前,首先要了解一些直接定义在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.idxmax()
# 最大值对应的索引
Gender
Female     28
Male      193
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函数的含义。

all和any一般用于bool值列,根据条件判断是真是假,跟and和or判断真假类似
gb对象中的Height都是非零数字 , 因此bool值都为True

gb.all()
# all表示分组后每一组中所有值都为True则返回True , 有一个False就返回False
Gender
Female    True
Male      True
Name: Height, dtype: bool
gb.any()
Gender
Female    True
Male      True
Name: Height, dtype: bool

mad(mean absolute deviation)平均绝对离差 , 用于统计学中对分组后的每组数据做离散程度分析。每组平均绝对离差的公式为:

gb.mad()
Gender
Female    4.088108
Male      5.394617
Name: Height, dtype: float64

skew(skewness)偏度 , 用来反映分组后每组数据分布的偏态程度 , 正值为右偏 , 绝对值越大 , 偏度越高。每组偏度的公式为:

gb.skew()
Gender
Female   -0.219253
Male      0.437535
Name: Height, dtype: float64

kurt(kurtosis)峰度,用来反映分组后每组数据分布的平尖程度,正值为尖峰分布,值越大越尖,负值为扁平程度,绝对值越大越平。每组峰度的公式为:

# gb.kurt()
# 会报错,'SeriesGroupBy' object has no attribute 'kurt'

注意:分组后的gb对象没有kurt方法,想要计算峰度,用apply取出Series方可调用,如下:

gb.apply(lambda x:x.kurt())
Gender
Female   -0.324085
Male      0.920630
Name: Height, dtype: float64

sem(standard error of mean)均值标准误差,描述的是多个均值样本的标准差,体现均值抽样分布的离散程度,反映样本均值之间的差异。
设样本无偏估计标准差为s,样本大小为N,则分组后每组的sem可表示为:

gb.sem()
Gender
Female    0.439893
Male      0.986985
Name: Height, dtype: float64

prod(product)连乘 , 每组prod表示为 :

本次分组后的gb对象用prod将每组身高乘起来并无实际意义

gb.prod()
Gender
Female    4.232080e+290
Male      1.594210e+114
Name: Height, dtype: float64

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

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

2. agg方法

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

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

下面说明如何通过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

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

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

【c】使用自定义函数

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

# 下面分组计算身高和体重各自的极差:
gb.agg(lambda x:x.mean()-x.min())
Height Weight
Gender
Female 13.79697 13.918519
Male 17.92549 21.759259

【练一练】

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

# 先用describe显示统计汇总信息
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
# 聚合函数显示,注意25% ,50%,75%的表达方式
gb.agg(['count','mean','std','min',('25%',lambda x:x.quantile(0.25)),('50%','quantile'),('75%',lambda x:x.quantile(0.75)),'max'])
Height Weight
count mean std min 25% 50% 75% max count mean std min 25% 50% 75% max
Gender
Female 132 159.19697 5.053982 145.4 155.675 159.6 162.825 170.2 135 47.918519 5.405983 34.0 44.0 48.0 52.00 63.0
Male 51 173.62549 7.048485 155.7 168.900 173.4 177.150 193.9 54 72.759259 7.772557 51.0 69.0 73.0 78.75 89.0

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

def my_func(s):res = 'High'if s.mean()<=df[s.name].mean():res = 'Low'return res
gb.agg(my_func)
Height Weight
Gender
Female Low Low
Male High High

【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([('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方法也是一个实用的变换函数,请查阅它的功能并给出一个使用的例子。

df_demo = pd.DataFrame({'city':['cq','cq','bj','sh','bj'],'income':[2000,500,90000,90000,124000],'count_sum':[3,8,np.nan,20,12]})
df_demo
city income count_sum
0 cq 2000 3.0
1 cq 500 8.0
2 bj 90000 NaN
3 sh 90000 20.0
4 bj 124000 12.0
# 对city列分组后排名,默认method为average
# 相同排名的名次用均值代替
# 数值越小排名值越小 , NaN值默认不参与排名
df_demo.groupby('city').rank()
income count_sum
0 2.0 1.0
1 1.0 2.0
2 1.0 NaN
3 1.0 1.0
4 2.0 1.0
df_demo.groupby('city').rank(method='max')
# 参数method为max则相同排名用名次值max代替 , min同理
income count_sum
0 2.0 1.0
1 1.0 2.0
2 1.0 NaN
3 1.0 1.0
4 2.0 1.0

method为dense,则相同值的名次并列排名 ,后面的值的名次+1
method为first,则从小到大排序,相同值按出现顺序先后名次依次递增,每次名次加1
ascending控制排名升序或降序
na_option控制NaN的处理方式,默认keep不处理, top表示优先排NaN, bottom表示最后排NaN
pct表示将排名后的名次转化为前百分比形式

df_demo.groupby('city').rank(ascending=False, na_option='top', pct=True)
income count_sum
0 0.5 1.0
1 1.0 0.5
2 1.0 0.5
3 1.0 1.0
4 0.5 1.0

当用自定义变换时需要使用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方法传入值为数据源的序列, 因此若需要对指定列进行特定变换 , 就需要分支处理
获取序列的name,再对该分支的序列进行相应的处理

gb.transform(lambda x:x.cummax() if x.name=='Height' else x.rank()).head()
Height Weight
0 158.9 47.5
1 166.5 19.0
2 188.9 54.0
3 NaN 14.5
4 188.9 31.5

方法二 :
将指定的变换列名作为字典的键,把对应的处理方法用eval()包装起来作为字典的值就可以实现分支逻辑了
当然也可以自定义其他的分支逻辑 , 如switch-case等

gb.transform(lambda x:{'Height':eval('x.cummin()'),'Weight':eval('x.rank()')}[x.name]).head()
Height Weight
0 158.9 47.5
1 166.5 19.0
2 166.5 54.0
3 NaN 14.5
4 166.5 31.5

前面提到了 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[...]的功能,这里假设"..."是元素列表。

df.groupby(df.index.isin([10,3,100,132,24])).filter(lambda x:x.name)
# 假设取[10,3,100,132,24]行索引
# 将df的index用isin转化为bool值 , 作为groupby的condition
# 用groupby分组后调用filter对分组依据进行筛选 , 分组依据来源于condition
# condition中只有True和False恰好作为filter的筛选条件
School Grade Name Gender Height Weight Transfer Test_Number Test_Date Time_Record
3 Fudan University Sophomore Xiaojuan Sun Female NaN 41.0 N 2 2020/1/3 0:04:08
10 Shanghai Jiao Tong University Freshman Xiaopeng Zhou Male 174.1 74.0 N 1 2019/9/29 0:05:16
24 Tsinghua University Senior Chunmei You Male 167.4 69.0 N 1 2019/11/17 0:04:32
100 Tsinghua University Senior Xiaofeng Shi Female 164.4 55.0 N 1 2019/11/19 0:03:33
132 Peking University Senior Chunpeng Qian Female 161.6 NaN N 1 2019/11/10 0:04:10

四、跨列分组

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']/100Weight = x['Weight']BMI_value = Weight/Height**2return 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              02              03              0
Male    1              02              03              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

【练一练】

请尝试在apply传入的自定义函数中,根据组的某些特征返回相同长度但索引不同的Series,会报错吗?

# 先定义一个index生成器 , 并设置每次生成的index都是相同的值
# 给每组返回的Series的index都设置为a
generator = (i for i in 'a'*6) #生成器推导式 , 和列表推导时很神似 , []换成()即可
gb.apply(lambda x:pd.Series(0, index = [*next(generator)]))
a
Gender Test_Number
Female 1 0
2 0
3 0
Male 1 0
2 0
3 0
# 我们发现每次传入Series的index都相同是可以正常运行的
# 现在将生成器中的index序列换成不同值再次使用apply试试
generator = (i for i in 'abcdef')
try :gb.apply(lambda x:pd.Series(0, index = [*next(generator)]))
except Exception as e:print(f'Error:{e}')
Error:

结论 : 会报一个未知类型的错误

【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传入的自定义函数中,根据组的某些特征返回相同大小但列索引不同的DataFrame,会报错吗?如果只是行索引不同,会报错吗?

# 先构造一个行索引都一样的生成器
# 行索引和列索引都相同 , 分组后的gb对象使用apply是没问题的
generator = (i for i in ['ab']*6)
gb.apply(lambda x: pd.DataFrame(np.ones((2,2)), index = [*next(generator)],columns=[*'xy'])).head()
x y
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
# 将行索引设置为变化的 , 再次运行 , 也是没问题的
generator = (i for i in ['ab','cd','ef','gh','ij','kl'])
gb.apply(lambda x: pd.DataFrame(np.ones((2,2)), index = [*next(generator)],columns=[*'xy'])).head()
x y
Gender Test_Number
Female 1 a 1.0 1.0
b 1.0 1.0
2 c 1.0 1.0
d 1.0 1.0
3 e 1.0 1.0
# 我们固定行索引,设置相同的列索引生成器,此时行索引列索引都相同,很显然也是没问题的
generator = (i for i in ['xy']*6)
gb.apply(lambda x: pd.DataFrame(np.ones((2,2)), index = [*'ab'],columns=[*next(generator)])).head()
x y
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
# 固定行索引 , 把列索引生成器设置为变化的 , 但变化的列中又夹杂着部分相同的列名
# 同列名被合并了 , 不同的列名被扩展开了 , 并且有值的填值 , 没值的填充为NaN
generator = (i for i in ['tu','uv','vw','wx','xy','yz'])
gb.apply(lambda x: pd.DataFrame(np.ones((2,2)), index = [*'ab'],columns=[*next(generator)]))
t u v w x y z
Gender Test_Number
Female 1 a 1.0 1.0 NaN NaN NaN NaN NaN
b 1.0 1.0 NaN NaN NaN NaN NaN
2 a NaN 1.0 1.0 NaN NaN NaN NaN
b NaN 1.0 1.0 NaN NaN NaN NaN
3 a NaN NaN 1.0 1.0 NaN NaN NaN
b NaN NaN 1.0 1.0 NaN NaN NaN
Male 1 a NaN NaN NaN 1.0 1.0 NaN NaN
b NaN NaN NaN 1.0 1.0 NaN NaN
2 a NaN NaN NaN NaN 1.0 1.0 NaN
b NaN NaN NaN NaN 1.0 1.0 NaN
3 a NaN NaN NaN NaN NaN 1.0 1.0
b NaN NaN NaN NaN NaN 1.0 1.0

结论 :

1.在apply传入的自定义函数中 , 无论每组返回的DataFrame行索引列索引是否相同 , 都不会报错
2.行索引并没有被当成真正的索引 , 而是归为第一列数据 , 列中数据本身就可以互异的 , 不报错也就很容易理解了
3.不同的列索引会被拆分为单列 , 并展开为所有单列集合的unique值形成最终列 , 然后有值的填值 , 没值的填NaN

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

【练一练】

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

# 先来试试gb对象下的cov
gb.cov().head()
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
# 用apply分别计算每个组的DataFrame下的cov
gb.apply(lambda x:x.cov()).head()
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
# 用apply将每个组的每个列拆开分别计算各列之间的协方差矩阵
gb.apply(lambda x:pd.DataFrame([[x[i].cov(x[j]) for j in x.columns] for i in x.columns],index=x.columns,columns=x.columns)).head()
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

来分别测试这三种方法的性能 :

%timeit -n 100 gb.cov()
100 loops, best of 3: 8.98 ms per loop
%timeit -n 100 gb.apply(lambda x:x.cov())
100 loops, best of 3: 9.49 ms per loop
%timeit -n 100 gb.apply(lambda x:pd.DataFrame([[x[i].cov(x[j]) for j in x.columns] for i in x.columns],index=x.columns,columns=x.columns))
100 loops, best of 3: 26.3 ms per loop

结论 :

可以看到gb对象下的cov性能最好

第三章 分组-学习笔记相关推荐

  1. 王道《计算机网络》第三章数据链路层 学习笔记

    数据链路层 链路层的功能 链路层的两种信道 局域网.广域网 链路层的设备 数据链路层的功能概述 数据链路层的基本概念 结点:主机.路由器 链路:网络中两个结点之间的物理通道,根据传输介质的不同分为有线 ...

  2. head first python(第三章)–学习笔记

    1.介绍基础文件,输入,输出 open() 打开文件,一次传入一行数据,可以结合for循环和readline()来使用 close() 用来关闭open打开的文件 the_file = open('s ...

  3. Effective Java(第三版) 学习笔记 - 第四章 类和接口 Rule20~Rule25

    Effective Java(第三版) 学习笔记 - 第四章 类和接口 Rule20~Rule25 目录 Rule20 接口优于抽象类 Rule21 为后代设计接口 Rule22 接口只用于定义类型 ...

  4. 传感器自学笔记第十一章——三色RGB学习笔记+高感度声音检测模块+KY-010光遮断传感器+TCRT5000循迹传感器+倾斜模块

    作者:GWD 时间:2019.06.28 三色RGB学习笔记(开关量类传感器) 一.学习要点:无 二.手册分析(开关量传感器) 1.产品用途:RGB LED 模块由一个贴片全彩 LED 制成,通过 R ...

  5. 第三章 进程管理笔记

    第三章 进程管理笔记 20135109 高艺桐 3.1进程 1.程序本身并不是进程,进程是处于执行期的程序以及相关资源的总称. 2.执行线程,简称线程,是进程中活动的对象.每个线程都拥有一个独立的计数 ...

  6. 控制系统仿真与CAD-薛定宇-第四章matlab学习笔记

    控制系统仿真与CAD-薛定宇-第四章matlab学习笔记 04-02传递函数模型 tfdata() 传递函数属性法 04-07典型系统连接计算 pretty 用法 04-08方框图简化 04-09代数 ...

  7. homeassistant mysql_学习笔记 篇三:HomeAssistant学习笔记docker安装的ha更换数据库

    学习笔记 篇三:HomeAssistant学习笔记docker安装的ha更换数据库 2018-11-15 12:06:58 4点赞 18收藏 3评论 是返乡过年?还是就地过年?最新一届#双面过节指南# ...

  8. 第三章 matlab学习入门之编程基础

    系列文章目录 第三章 matlab学习入门之编程基础 在这一章,你会学到的知识: 变量与语句: 程序控制: M文件: 脚本: 函数: 变量检测: 程序调试: 文章目录 系列文章目录 前言 一.变量与语 ...

  9. 《机器学习系列教程》第三章 深度学习基础

    @[第三章 深度学习基础] 第三章 深度学习基础 3.1 基本概念 3.1.1 神经网络组成? 为了描述神经网络,我们先从最简单的神经网络说起. 感知机 简单的感知机如下图所示: [外链图片转存失败( ...

最新文章

  1. 四川第七届 I Travel(bfs)
  2. Go 开源说第五期:MOSN Go语言网络代理软件
  3. 使用PhoneGap开启移动开发之旅
  4. jmeter名词解释之聚合报告
  5. 循环首次适应算法_面向6G的极化编码链路自适应技术
  6. 编译我的第一个c语言,linux菜鸟学习写第一个C语言代码--“hello Linux!”
  7. android布局中画圆角矩形,Android 自定义View之圆角矩形轨迹图
  8. Delphi XE5教程4:程序和单元概述
  9. VS工程切换cuda版本
  10. oracle怎样避免脑裂的,redis集群怎么防止脑裂
  11. 关于IT结合测试,事前DB与事后DB的问题(之一:如何能更好的看出更新效果)。
  12. 生成式对抗网络GAN生成手写数字
  13. js中的instanceof运算符
  14. python实现strand_sort排序算法
  15. access ea 可以联网吗_如何看待EA在STEAM上推出EA Play(原EA Access会员)?
  16. CentOS 7安装WRF,SMOKE,CMAQ
  17. 关于AOSP与AOKP
  18. 芯片——摩尔定律的传奇(下)
  19. 如何在H5页面中实现长按二维码关注微信公众号?
  20. A systems-biology model of the tumor necrosis factor (TNF) interactions with TNF receptor 1 and 2

热门文章

  1. 英飞凌 AURIX 系列单片机的HSM详解(2)——与HSM相关的UCB和寄存器
  2. Spring框架中的核心技术之AOP
  3. 2048游戏代码,可以玩下游戏
  4. 在阿里云平台注册一个域名
  5. linux xen卸载,超级简单安装xen和虚拟机以及解决其中出现的问题
  6. 软件测试-金融银行项目怎么测?系统业务测试总结分析...
  7. 推荐几个Robot Learning的开源项目[1]
  8. 在Kali中利用Aircrack破解无线密码
  9. 开心一刻丨程序员,才是真正的段子手
  10. 游戏成瘾的成因有哪些?如何防沉迷呢?