第三章 分组-学习笔记
第三章 分组
# 导入需要的模块
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
从索引可以看出,其实最后产生的结果就是按照条件列表中元素的值(此处是True
和False
)来分组
# 随机传入字母序列来验证:
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性能最好
第三章 分组-学习笔记相关推荐
- 王道《计算机网络》第三章数据链路层 学习笔记
数据链路层 链路层的功能 链路层的两种信道 局域网.广域网 链路层的设备 数据链路层的功能概述 数据链路层的基本概念 结点:主机.路由器 链路:网络中两个结点之间的物理通道,根据传输介质的不同分为有线 ...
- head first python(第三章)–学习笔记
1.介绍基础文件,输入,输出 open() 打开文件,一次传入一行数据,可以结合for循环和readline()来使用 close() 用来关闭open打开的文件 the_file = open('s ...
- Effective Java(第三版) 学习笔记 - 第四章 类和接口 Rule20~Rule25
Effective Java(第三版) 学习笔记 - 第四章 类和接口 Rule20~Rule25 目录 Rule20 接口优于抽象类 Rule21 为后代设计接口 Rule22 接口只用于定义类型 ...
- 传感器自学笔记第十一章——三色RGB学习笔记+高感度声音检测模块+KY-010光遮断传感器+TCRT5000循迹传感器+倾斜模块
作者:GWD 时间:2019.06.28 三色RGB学习笔记(开关量类传感器) 一.学习要点:无 二.手册分析(开关量传感器) 1.产品用途:RGB LED 模块由一个贴片全彩 LED 制成,通过 R ...
- 第三章 进程管理笔记
第三章 进程管理笔记 20135109 高艺桐 3.1进程 1.程序本身并不是进程,进程是处于执行期的程序以及相关资源的总称. 2.执行线程,简称线程,是进程中活动的对象.每个线程都拥有一个独立的计数 ...
- 控制系统仿真与CAD-薛定宇-第四章matlab学习笔记
控制系统仿真与CAD-薛定宇-第四章matlab学习笔记 04-02传递函数模型 tfdata() 传递函数属性法 04-07典型系统连接计算 pretty 用法 04-08方框图简化 04-09代数 ...
- homeassistant mysql_学习笔记 篇三:HomeAssistant学习笔记docker安装的ha更换数据库
学习笔记 篇三:HomeAssistant学习笔记docker安装的ha更换数据库 2018-11-15 12:06:58 4点赞 18收藏 3评论 是返乡过年?还是就地过年?最新一届#双面过节指南# ...
- 第三章 matlab学习入门之编程基础
系列文章目录 第三章 matlab学习入门之编程基础 在这一章,你会学到的知识: 变量与语句: 程序控制: M文件: 脚本: 函数: 变量检测: 程序调试: 文章目录 系列文章目录 前言 一.变量与语 ...
- 《机器学习系列教程》第三章 深度学习基础
@[第三章 深度学习基础] 第三章 深度学习基础 3.1 基本概念 3.1.1 神经网络组成? 为了描述神经网络,我们先从最简单的神经网络说起. 感知机 简单的感知机如下图所示: [外链图片转存失败( ...
最新文章
- 四川第七届 I Travel(bfs)
- Go 开源说第五期:MOSN Go语言网络代理软件
- 使用PhoneGap开启移动开发之旅
- jmeter名词解释之聚合报告
- 循环首次适应算法_面向6G的极化编码链路自适应技术
- 编译我的第一个c语言,linux菜鸟学习写第一个C语言代码--“hello Linux!”
- android布局中画圆角矩形,Android 自定义View之圆角矩形轨迹图
- Delphi XE5教程4:程序和单元概述
- VS工程切换cuda版本
- oracle怎样避免脑裂的,redis集群怎么防止脑裂
- 关于IT结合测试,事前DB与事后DB的问题(之一:如何能更好的看出更新效果)。
- 生成式对抗网络GAN生成手写数字
- js中的instanceof运算符
- python实现strand_sort排序算法
- access ea 可以联网吗_如何看待EA在STEAM上推出EA Play(原EA Access会员)?
- CentOS 7安装WRF,SMOKE,CMAQ
- 关于AOSP与AOKP
- 芯片——摩尔定律的传奇(下)
- 如何在H5页面中实现长按二维码关注微信公众号?
- A systems-biology model of the tumor necrosis factor (TNF) interactions with TNF receptor 1 and 2