Pandas 04-分组
Pandas 04-分组
import numpy as np
import pandas as pd
一、分组模式及其对象
1. 分组的一般模式
分组操作在日常生活中使用极其广泛,例如:
- 依据 性 别 \color{#FF0000}{性别} 性别分组,统计全国人口 寿 命 \color{#00FF00}{寿命} 寿命的 平 均 值 \color{#0000FF}{平均值} 平均值
- 依据 季 节 \color{#FF0000}{季节} 季节分组,对每一个季节的 温 度 \color{#00FF00}{温度} 温度进行 组 内 标 准 化 \color{#0000FF}{组内标准化} 组内标准化
- 依据 班 级 \color{#FF0000}{班级} 班级筛选出组内 数 学 分 数 \color{#00FF00}{数学分数} 数学分数的 平 均 值 超 过 80 分 的 班 级 \color{#0000FF}{平均值超过80分的班级} 平均值超过80分的班级
从上述的几个例子中不难看出,想要实现分组操作,必须明确三个要素: 分 组 依 据 \color{#FF0000}{分组依据} 分组依据、 数 据 来 源 \color{#00FF00}{数据来源} 数据来源、 操 作 及 其 返 回 结 果 \color{#0000FF}{操作及其返回结果} 操作及其返回结果。同时从充分性的角度来说,如果明确了这三方面,就能确定一个分组操作,从而分组代码的一般模式即:
df.groupby(分组依据)[数据来源].使用操作
例如第一个例子中的代码就应该如下:
df.groupby('Gender')['Longevity'].mean()
现在返回到学生体测的数据集上,如果想要按照性别统计身高中位数,就可以如下写出:
df = pd.read_csv('../data/learn_pandas.csv')
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
目前为止,groupby
的分组依据都是直接可以从列中按照名字获取的,那如果想要通过一定的复杂逻辑来分组,例如根据学生体重是否超过总体均值来分组,同样还是计算身高的均值。
首先应该先写出分组条件:
condition = df.Weight > df.Weight.mean()
然后将其传入groupby
中:
df.groupby(condition)['Height'].mean()
Weight
False 159.034646
True 172.705357
Name: Height, dtype: float64
【练一练 - 01】
请根据上下四分位数分割,将体重分为high、normal、low三组,统计身高的均值。
My solution :
- 用
mask
对上下四分位点分割分别替换为high
,low
,normal
, 构造出分组condition
W = df.Weight
cond = W.mask(W>W.quantile(0.75),'high').mask(W<W.quantile(0.25),'low').mask((W<=W.quantile(0.75))&(W>=W.quantile(0.25)),'normal')
cond.head()
0 normal
1 high
2 high
3 low
4 high
Name: Weight, dtype: object
- 代入公式对
Height
求均值
df.groupby(cond).Height.mean()
Weight
high 174.935714
low 153.753659
normal 161.883516
Name: Height, dtype: float64
【END】
从索引可以看出,其实最后产生的结果就是按照条件列表中元素的值(此处是True
和False
)来分组,下面用随机传入字母序列来验证这一想法:
item = np.random.choice([*'abc'], df.shape[0])
df.groupby(item).Height.mean()
a 164.325455
b 162.975410
c 162.529851
Name: Height, dtype: float64
此处的索引就是原先item中的元素,如果传入多个序列进入groupby
,那么最后分组的依据就是这两个序列对应行的唯一组合:
df.groupby([condition, item])['Height'].mean()
Weight
False a 159.320513b 158.929268c 158.889362
True a 176.525000b 171.270000c 171.085000
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 0x000001F88619F208>
通过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')])
【练一练 - 02】
上一小节介绍了可以通过drop_duplicates
得到具体的组类别,现请用groups
属性完成类似的功能。
My solution :
df.groupby(['School','Gender']).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')])
【END】
当size
作为DataFrame
的属性时,返回的是表长乘以表宽的大小,但在groupby
对象上表示统计每个组的元素个数:
gb.size()
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
类型 - 第三个例子中,既不是标量也不是序列,返回的整个组所在行的本身,即返回了
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.idxmin()
Gender
Female 143
Male 199
Name: Height, dtype: int64
gb.quantile(0.05)
Gender
Female 151.365
Male 164.400
Name: Height, dtype: float64
【练一练 - 03】
请查阅文档,明确all/any/mad/skew/sem/prod
函数的含义。
My solution :
all
和any
一般用于bool
值列 ,all
表示分组后每一组中所有值都为True
则返回True
, 有一个False
就返回False
- 上述
gb
对象中的Height
都是非零数字 , 因此bool
值都为True
gb.all()
Gender
Female True
Male True
Name: Height, dtype: bool
any
与all
类似 , 表示bool
值列中只要有一个True
则返回true
, 只有全为False
才会返回False
gb.any()
Gender
Female True
Male True
Name: Height, dtype: bool
mad(mean absolute deviation)
平均绝对离差 , 用于统计学中对分组后的每组数据做离散程度分析的指标之一- 每一组的
mad
可表示为 :
M i = 1 n ∑ k = 1 n ∣ x k − x ˉ ∣ \displaystyle M_i=\frac{1}{n}\sum_{k=1}^n{|x_k-\bar{x}|} Mi=n1k=1∑n∣xk−xˉ∣
gb.mad()
Gender
Female 4.088108
Male 5.394617
Name: Height, dtype: float64
skew(skewness)
偏度 , 用来反映分组后每组数据分布的偏态程度 , 正值为右偏 , 绝对值越大 , 偏度越高- 每组的
skew
可表示为 :
S K i = n n − 1 n − 2 ∑ k = 1 n ( x k − x ˉ ) 3 ( ∑ k = 1 n ( x k − x ˉ ) 2 ) 3 2 SK_i=\frac{n\sqrt{n-1}}{n-2}\frac{\displaystyle\sum_{k=1}^n(x_k-\bar{x})^3}{(\displaystyle\sum_{k=1}^n(x_k-\bar{x})^2)^\frac{3}{2}} SKi=n−2nn−1 (k=1∑n(xk−xˉ)2)23k=1∑n(xk−xˉ)3 - 类似的 ,
kurt(kurtosis)
峰度 , 用来反映分组后每组数据分布的平尖程度 , 正值为尖峰分布 , 值越大越尖 , 负值为扁平程度 , 绝对值越大越平 - 每组的
kurt
可表示为 :
K i = n ( n + 1 ) ( n − 1 ) ( n − 2 ) ( n − 3 ) ∑ k = 1 n ( x k − x ˉ ) 4 ( ∑ k = 1 n ( x k − x ˉ ) 2 ) 2 − 3 ( n − 1 ) 2 ( n − 2 ) ( n − 3 ) K_i=\frac{n(n+1)(n-1)}{(n-2)(n-3)}\frac{\displaystyle\sum_{k=1}^n(x_k-\bar{x})^4}{(\displaystyle\sum_{k=1}^n(x_k-\bar{x})^2)^2}-\frac{3(n-1)^2}{(n-2)(n-3)} Ki=(n−2)(n−3)n(n+1)(n−1)(k=1∑n(xk−xˉ)2)2k=1∑n(xk−xˉ)4−(n−2)(n−3)3(n−1)2 - 不过 , 分组后的
gb
对象没有kurt
方法 , 如果需要计算峰度 , 需用apply
取出Series
方可调用 , 如下 :
gb.skew()
Gender
Female -0.219253
Male 0.437535
Name: Height, dtype: float64
try:gb.kurt()
except Exception as e:print(e)
'SeriesGroupBy' object has no attribute 'kurt'
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
可表示为 :
S E M i = s N SEM_i = \frac{s}{\sqrt{N}} SEMi=N s
gb.sem()
Gender
Female 0.439893
Male 0.986985
Name: Height, dtype: float64
prod(product)
连乘 , 每组prod
表示为 :
P R O D i = ∏ k = 1 n x k PROD_i=\displaystyle\prod_{k=1}^nx_k PRODi=k=1∏nxk- 本次分组后的
gb
对象用prod
将每组身高乘起来并无实际意义
gb.prod()
Gender
Female 4.232080e+290
Male 1.594210e+114
Name: Height, dtype: float64
【END】
这些聚合函数当传入的数据来源包含多个列时,将按照列进行迭代计算:
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 |
【练一练 - 04】
请使用【b】中的传入字典的方法完成【a】中等价的聚合任务。
My solution :
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 |
【END】
【c】使用自定义函数
在agg
中可以使用具体的自定义函数, 需 要 注 意 传 入 函 数 的 参 数 是 之 前 数 据 源 中 的 列 , 逐 列 进 行 计 算 \color{#FF0000}{需要注意传入函数的参数是之前数据源中的列,逐列进行计算} 需要注意传入函数的参数是之前数据源中的列,逐列进行计算。下面分组计算身高和体重的极差:
gb.agg(lambda x: x.max()-x.min())
Height | Weight | |
---|---|---|
Gender | ||
Female | 24.8 | 29.0 |
Male | 38.2 | 38.0 |
【练一练 - 05】
在groupby
对象中可以使用describe
方法进行统计信息汇总,请同时使用多个聚合函数,完成与该方法相同的功能。
My solution :
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%','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 |
【END】
由于传入的是序列,因此序列上的方法和属性都是可以在函数中使用的,只需保证返回值是标量即可。下面的例子是指,如果组的指标均值,超过该指标的总体均值,返回High,否则返回Low。
def my_func(s):return 'Low' if s.mean() <= df[s.name].mean() else 'High'
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({'Height': [('my_func', my_func), 'sum'], 'Weight': lambda x:x.max()})
Height | Weight | ||
---|---|---|---|
my_func | sum | <lambda> | |
Gender | |||
Female | Low | 21014.0 | 63.0 |
Male | High | 8854.9 | 89.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 |
【练一练 - 06】
在groupby
对象中,rank
方法也是一个实用的变换函数,请查阅它的功能并给出一个使用的例子。
My solution :
df6 = pd.DataFrame({'a':[0]*4 + [1]*2,'b':[2, 3, 3, 4, -1, -2],'c':[True, False, False, True, True, False],'d':[3, 2, np.nan, 1, -3, np.nan]})
df6
a | b | c | d | |
---|---|---|---|---|
0 | 0 | 2 | True | 3.0 |
1 | 0 | 3 | False | 2.0 |
2 | 0 | 3 | False | NaN |
3 | 0 | 4 | True | 1.0 |
4 | 1 | -1 | True | -3.0 |
5 | 1 | -2 | False | NaN |
- 对
a
列分组后排名 , 默认method
为average
- 相同排名的名次用均值代替
- 可以看到数值越小排名值越小 ,
bool
值视为0
和1
计算 ,NaN
值默认不参与排名
df6.groupby('a').rank()
b | c | d | |
---|---|---|---|
0 | 1.0 | 3.5 | 3.0 |
1 | 2.5 | 1.5 | 2.0 |
2 | 2.5 | 1.5 | NaN |
3 | 4.0 | 3.5 | 1.0 |
4 | 2.0 | 2.0 | 1.0 |
5 | 1.0 | 1.0 | NaN |
- 设置参数
method
为max
则相同排名用名次值max
代替 ,min
同理
df6.groupby('a').rank(method='max')
b | c | d | |
---|---|---|---|
0 | 1.0 | 4.0 | 3.0 |
1 | 3.0 | 2.0 | 2.0 |
2 | 3.0 | 2.0 | NaN |
3 | 4.0 | 4.0 | 1.0 |
4 | 2.0 | 2.0 | 1.0 |
5 | 1.0 | 1.0 | NaN |
- 设置
method
为dense
, 则相同值的名次并列排名 , 后面的值的名次紧随其后 , 只加1
df6.groupby('a').rank(method='dense')
b | c | d | |
---|---|---|---|
0 | 1.0 | 2.0 | 3.0 |
1 | 2.0 | 1.0 | 2.0 |
2 | 2.0 | 1.0 | NaN |
3 | 3.0 | 2.0 | 1.0 |
4 | 2.0 | 2.0 | 1.0 |
5 | 1.0 | 1.0 | NaN |
- 设置
method
为first
, 则从小到大排序 , 相同值按出现顺序先后名次依次递增 , 每次名次加1
df6.groupby('a').rank(method='first')
b | c | d | |
---|---|---|---|
0 | 1.0 | 3.0 | 3.0 |
1 | 2.0 | 1.0 | 2.0 |
2 | 3.0 | 2.0 | NaN |
3 | 4.0 | 4.0 | 1.0 |
4 | 2.0 | 2.0 | 1.0 |
5 | 1.0 | 1.0 | NaN |
ascending
控制排名升序或降序na_option
控制NaN
的处理方式 , 默认keep
不处理 ,top
表示优先排NaN
,bottom
表示最后排NaN
pct
表示将排名后的名次转化为前百分比形式
df6.groupby('a').rank(ascending=False, na_option='top', pct=True)
b | c | d | |
---|---|---|---|
0 | 1.000 | 0.375 | 0.50 |
1 | 0.625 | 0.875 | 0.75 |
2 | 0.625 | 0.875 | 0.25 |
3 | 0.250 | 0.375 | 1.00 |
4 | 0.500 | 0.500 | 1.00 |
5 | 1.000 | 1.000 | 0.50 |
【END】
当用自定义变换时需要使用transform
方法,被调用的自定义函数, 其 传 入 值 为 数 据 源 的 序 列 \color{#FF0000}{其传入值为数据源的序列} 其传入值为数据源的序列,与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 |
【练一练 - 07】
对于transform
方法无法像agg
一样,通过传入字典来对指定列使用特定的变换,如果需要在一次transform
的调用中实现这种功能,请给出解决方案。
My solution :
方法一 :
- 由于
transform
方法传入值为数据源的序列 , 因此若需要对指定列进行特定变换 , 就需要分支处理 - 那么根据这个逻辑 , 只要回归本源处理分支即可 , 一个最基本的分支就是
if-else
- 获取序列的
name
, 再对该分支的序列进行相应的处理
gb.transform(lambda x:x.cummin() if x.name=='Height' else x.rank()).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 |
方法二 :
- 和方法一一样 , 只要用处理分支的逻辑即可 , 这里将需要指定的变换列名作为字典的键 , 把对应的处理方法用
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 |
【END】
前面提到了 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 |
【练一练 - 08】
从概念上说,索引功能是组过滤功能的子集,请使用filter
函数完成loc[...]
的功能,这里假设"...
"是元素列表。
My solution :
- 假设要取的行索引为
[11,3,139,172,54]
- 将
df
的index
用isin
转化为bool
值 , 作为groupby
的condition
- 用
groupby
分组后调用filter
对分组依据进行筛选 , 分组依据来源于condition
condition
中只有True
和False
恰好作为filter
的筛选条件
df.groupby(df.index.isin([11,3,139,172,54])).filter(lambda x:x.name)
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 |
11 | Tsinghua University | Junior | Xiaoquan Lv | Female | 153.2 | 43.0 | N | 2 | 2019/9/16 | 0:04:49 |
54 | Peking University | Freshman | Xiaojuan Chu | Male | 162.4 | 58.0 | Y | 3 | 2019/11/29 | 0:03:42 |
139 | Tsinghua University | Sophomore | Qiang Zhou | Female | 150.5 | 36.0 | N | 1 | 2019/11/4 | 0:04:27 |
172 | Shanghai Jiao Tong University | Junior | Quan Zhao | Female | 160.6 | 53.0 | N | 2 | 2019/10/4 | 0:03:45 |
【END】
四、跨列分组
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 |
【练一练 - 09】
请尝试在apply
传入的自定义函数中,根据组的某些特征返回相同长度但索引不同的Series
,会报错吗?
My solution :
- 首先我们先定义一个
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:
- 会发现报错了 , 但是没有给出具体的错误类型
结论 : 会报一个未知类型的错误
【END】
【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')]))).head()
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 |
【练一练 - 10】
请尝试在apply
传入的自定义函数中,根据组的某些特征返回相同大小但列索引不同的DataFrame
,会报错吗?如果只是行索引不同,会报错吗?
My solution :
- 同样的 , 像上一题一样先构造一个行索引都一样的生成器
- 很显然 , 行索引和列索引都相同 , 分组后的
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 |
- 现在固定行索引 , 把列索引生成器设置为变化的 , 但变化的列中又夹杂着部分相同的列名
- 我们惊奇的发现 , 这和上一题传入
Series
不一样 , 并没有报错 - 而是相同列名被合并了 , 不同的列名被扩展开了 , 并且有值的填值 , 没值的填充为
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 |
结论 :
- 在
apply
传入的自定义函数中 , 无论每组返回的DataFrame
行索引列索引是否相同 , 都不会报错 - 每个分组并不需要共用行索引 , 所以行索引不报错也就很容易理解了
- 不同的列索引会被拆分为单列 , 并展开为所有单列集合的
unique
值形成最终列 , 然后有值的填值 , 没值的填NaN
【END】
最后需要强调的是,apply
函数的灵活性是以牺牲一定性能为代价换得的,除非需要使用跨列处理的分组处理,否则应当使用其他专门设计的groupby
对象方法,否则在性能上会存在较大的差距。同时,在使用聚合函数和变换函数时,也应当优先使用内置函数,它们经过了高度的性能优化,一般而言在速度上都会快于用自定义函数来实现。
【练一练 - 11】
在groupby
对象中还定义了cov
和corr
函数,从概念上说也属于跨列的分组处理。请利用之前定义的gb
对象,使用apply函数实现与gb.cov()
同样的功能并比较它们的性能。
My solution :
- 先来试试
gb
对象下的cov
gb.cov().head(3)
Height | Weight | |||
---|---|---|---|---|
Gender | Test_Number | |||
Female | 1 | Height | 20.963600 | 21.452034 |
Weight | 21.452034 | 26.438244 | ||
2 | Height | 31.615680 | 30.386170 |
- 用
apply
分别计算每个组的DataFrame
下的cov
gb.apply(lambda x:x.cov()).head(3)
Height | Weight | |||
---|---|---|---|---|
Gender | Test_Number | |||
Female | 1 | Height | 20.963600 | 21.452034 |
Weight | 21.452034 | 26.438244 | ||
2 | Height | 31.615680 | 30.386170 |
- 用
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(3)
Height | Weight | |||
---|---|---|---|---|
Gender | Test_Number | |||
Female | 1 | Height | 20.963600 | 21.452034 |
Weight | 21.452034 | 26.438244 | ||
2 | Height | 31.615680 | 30.386170 |
- 来分别测试这三种方法的性能 :
%timeit -n 100 gb.cov()
5.14 ms ± 178 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit -n 100 gb.apply(lambda x:x.cov())
5.54 ms ± 548 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%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))
10.5 ms ± 411 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
结论 :
- 可以看到
gb
对象下的cov
性能最好 - 而每个组的
DataFrame
单独调cov
则稍显逊色 , 毕竟拆分合并的过程也消耗点时间的 , 如果分组数很多 , 那么差异就更明显了 - 然而将每个组的各个列分别组合求协方差 , 再组成矩阵返回 , 性能就大大折扣了 , 这里的
cov
好歹还是Series
里的方法 , 如果自己手写一个cov
方法那将完全无法与gb
对象下的cov
媲美了 - 同理 , 上述分析过程中的
cov
换成corr
相关系数 , 也会得出同样的结论
【END】
五、练习
Ex1:汽车数据集
现有一份汽车数据集,其中Brand, Disp., HP
分别代表汽车品牌、发动机蓄量、发动机输出。
df = pd.read_csv('../data/car.csv')
df.head(3)
Brand | Price | Country | Reliability | Mileage | Type | Weight | Disp. | HP | |
---|---|---|---|---|---|---|---|---|---|
0 | Eagle Summit 4 | 8895 | USA | 4.0 | 33 | Small | 2560 | 97 | 113 |
1 | Ford Escort 4 | 7402 | USA | 2.0 | 33 | Small | 2345 | 114 | 90 |
2 | Ford Festiva 4 | 6319 | Korea | 4.0 | 37 | Small | 1845 | 81 | 63 |
- 先过滤出所属
Country
数超过2个的汽车,即若该汽车的Country
在总体数据集中出现次数不超过2则剔除,再按Country
分组计算价格均值、价格变异系数、该Country
的汽车数量,其中变异系数的计算方法是标准差除以均值,并在结果中把变异系数重命名为CoV
。 - 按照表中位置的前三分之一、中间三分之一和后三分之一分组,统计
Price
的均值。 - 对类型
Type
分组,对Price
和HP
分别计算最大值和最小值,结果会产生多级索引,请用下划线把多级列索引合并为单层索引。 - 对类型
Type
分组,对HP
进行组内的min-max
归一化。 - 对类型
Type
分组,计算Disp.
与HP
的相关系数。
My solution :
- 按
Country
分组后用filter
对每组的样本数过滤
df1 = df.groupby('Country').filter(lambda x:x.shape[0]>2)
df1.head(3)
Brand | Price | Country | Reliability | Mileage | Type | Weight | Disp. | HP | |
---|---|---|---|---|---|---|---|---|---|
0 | Eagle Summit 4 | 8895 | USA | 4.0 | 33 | Small | 2560 | 97 | 113 |
1 | Ford Escort 4 | 7402 | USA | 2.0 | 33 | Small | 2345 | 114 | 90 |
2 | Ford Festiva 4 | 6319 | Korea | 4.0 | 37 | Small | 1845 | 81 | 63 |
- 得到过滤结果再次按
Country
分组拿出Price
列用agg
分别将需要处理的函数名放入 , 其中变异系数用元祖的方式传入重命名
df1.groupby('Country').Price.agg(['mean',('CoV',lambda x:x.std()/x.mean()),'count'])
mean | CoV | count | |
---|---|---|---|
Country | |||
Japan | 13938.052632 | 0.387429 | 19 |
Japan/USA | 10067.571429 | 0.240040 | 7 |
Korea | 7857.333333 | 0.243435 | 3 |
USA | 12543.269231 | 0.203344 | 26 |
- 先构造一个长度与
df
样本数相同的condition
序列 , 设置初始值为middle
, 用mask
将前1/3
替换为front
, 后1/3
替换为back
condition = pd.Series(['middle']*df.shape[0]).mask(df.index<df.shape[0]/3,'front').mask(df.index>=2*df.shape[0]/3,'back')
condition.head(3)
0 front
1 front
2 front
dtype: object
- 对
df
按condition
分组后取出Price
列求均值
df.groupby(condition).Price.mean()
back 15420.65
front 9069.95
middle 13356.40
Name: Price, dtype: float64
- 对
df
按Type
分组后使用agg
以传入字典的方式对指定列进行聚合
df3 = df.groupby('Type').agg({'Price':['max'],'HP':['min']})
df3.head(3)
Price | HP | |
---|---|---|
max | min | |
Type | ||
Compact | 18900 | 95 |
Large | 17257 | 150 |
Medium | 24760 | 110 |
- 用上一章的知识对多级列索引映射
df3.columns = df3.columns.map(lambda x:'_'.join(x))
df3.head(3)
Price_max | Price_min | HP_max | HP_min | |
---|---|---|---|---|
Type | ||||
Compact | 18900 | 9483 | 142 | 95 |
Large | 17257 | 14525 | 170 | 150 |
Medium | 24760 | 9999 | 190 | 110 |
- 对
df
按Type
分组,由于归一化需返回同长度序列 , 故取出HP
列用transform
调用自定义归一化方法
df.groupby('Type').HP.transform(lambda x:(x-x.min())/(x.max()-x.min())).head(3)
0 1.00
1 0.54
2 0.00
Name: HP, dtype: float64
- 对
df
按Type
分组,取出Disp.
与HP
列调用corr
即可
df.groupby('Type')[['Disp.','HP']].corr().head(3)
Disp. | HP | ||
---|---|---|---|
Type | |||
Compact | Disp. | 1.000000 | 0.586087 |
HP | 0.586087 | 1.000000 | |
Large | Disp. | 1.000000 | -0.242765 |
Ex2:实现transform函数
groupby
对象的构造方法是my_groupby(df, group_cols)
- 支持单列分组与多列分组
- 支持带有标量广播的
my_groupby(df)[col].transform(my_func)
功能 pandas
的transform
不能跨列计算,请支持此功能,即仍返回Series
但col
参数为多列- 无需考虑性能与异常处理,只需实现上述功能,在给出测试样例的同时与
pandas
中的transform
对比结果是否一致
My solution :
- 我的思路是模仿
pandas
自带的groupby
, 用df.groupby
的方式创建一个分组对象 , 要用.
调用方法的话那么df
应该也要包装成类 ,GroupBy
又是一个类 , 所以一个初始的结构就出来了 :
class GroupBy():def __init__(self, df, cols):passclass MyDF():def __init__(self, df):self.__df = dfdef groupby(self, cols):return GroupBy(self.__df, cols)
此时着重设计
GroupBy
类 , 考虑到transform
是在分组的基础上把每个子组传给自定义函数 , 那么先做好分组就尤为重要了分组需要知道分了多少组 , 每组的组名和每组的内容是什么 , 可以想到一个组名对应一个分组的索引序列 , 正如
pandas
的gb
下的groups
属性 , 现在目标就是做一个groups
属性获取需要分组的列(先判断传入的
cols
是str
还是list
, 统一转换为list
) , 用enumerate
遍历分组的列中每行数据并取出对应的索引 , 把要分组的列中每个元素打包成键 , 由于键必须是不可变的 , 那么单列分组就用str
, 多列就打包成元组 , 接下来只要用字典特性get
获取 , 重复的键就在后面追加索引 , 否则新增一个键 , 这个过程和drop_duplicates
类似 , 最终得到不重复的组和对应的索引但是这样还不够 , 一般情况下分组后行都是被打乱的 , 考虑到每个组做完变换后 , 行索引还要恢复到原来的样子 , 就需要对行索引做一个有顺序的标记 , 刚好上一步中
enumerate
取出的第一个变量就是顺序索引 , 可以顺便同时构造出一个indices
方便后续做复原索引接下来就是再做一个
get_group
方法 , 获取指定的子组 , 同样是为后续操作做铺垫 , 取出先前做好的groups
属性中每个子组对应的行索引数组 , 用index
下的isin
方法筛选出子组 , 这里要考虑筛选后的子组可能会追加用[]
传入键值再筛选列的情况 , 此时类中添加一个__get_item__
方法就可以实现 , 需要判断传入的键是str
还是list
, 然后在__init__
中添加一个私有属性self.__op_col
, 专门记录传入的键值 , 默认为None
, 在使用的时候判断这个值是否为空 , 非空就用[]
取出子组的指定列到这里全部铺垫做好了 , 开始实现
transform
功能 , 遍历groups
中每个子组 , 获取子组后传给自定义function
, 每个子组都做完变换后再拼接起来 , 这里考虑到做完变换后得到的变换结果可能是 :标量
,Series
,DataFrame
, 前两种都用Series
处理 , 其中标量指定index
就可以自动广播 , 如果是DataFrame
不用管直接拼接起来最后就是更换顺序索引并排序恢复如初 , 被
transform
后的索引仍然和分组后的乱序一样 , 这时就需用到indices
的有序索引标记了 , 将transform
变换结果的索引换成有序的indices
里的索引值再排序 , 就像把打乱的魔方复原一样 , 让变换结果的行索引归位 , 然后就是起名字了 , 如果结果只有一列那就是Series
, 可以模仿pandas
的命名方式 , 用上面提到的[]
传入的列名命名 , 如果是多列操作 , 那就算了 , 可以用自定义方法的名字命名 , 也可以给transform
方法设置一个带默认参数为None
的rename
形参 , 就像agg
可以传入包含自定义命名的元组一样 , 如果变换结果是DataFrame
就判断[]
是否为空 , 非空就用[]
取的列命名 , 否则就用变换后的列命名 , 这样命名能满足一些基本需求 , 其他情况就还是用一个带默认参数的形参让调用者自定义命名了调试
my_groupby
功能与pandas
自带的groupby
作对比 , 调试过程会有很多次出错 , 然后想办法修改 , 边写边调边改 , 好的代码不是写出来的而是不断改出来的GroupBy
类 :
class GroupBy():def __init__(self, df, cols):self.__df = dfself.__cols = [cols] if type(cols)==str else colsself.groups = {}self.indices = {}for i,group in enumerate(self.__df[self.__cols].values):key = group[0] if len(self.__cols)==1 else (*group,)if self.groups.get(key):self.groups[key].append(self.__df.index[i])self.indices[key] = np.hstack((self.indices[key], np.array(i)))else:self.groups[key], self.indices[key] = [self.__df.index[i]], np.array(i)self.ngroups = len(self.groups)self.__op_col = Nonedef get_group(self, group):sub_group = self.__df[self.__df.index.isin(self.groups[group])]return sub_group[self.__op_col] if self.__op_col else sub_groupdef __getitem__(self, col):self.__op_col = col if type(col)==str else [*col]return selfdef __trans_group(self, group, func):grouped = func(self.get_group(group))return grouped if isinstance(grouped, pd.DataFrame) else pd.Series(grouped, index=self.groups[group])def transform(self, func):trans_group = pd.concat([self.__trans_group(group, func) for group in self.groups.keys()])indices = np.hstack((*self.indices.values(),))if isinstance(trans_group, pd.DataFrame):return pd.DataFrame(trans_group.values, index=indices, columns=self.__op_col if type(self.__op_col)==str else trans_group.columns).sort_index()return pd.Series(trans_group.values, index=indices, name=self.__op_col if type(self.__op_col)==str else func.__name__).sort_index()
MyDF
类 :
class MyDF():def __init__(self, df):self.__df = dfdef groupby(self, cols):return GroupBy(self.__df, cols)
- 将
df
传入MyDF
类中创建一个待分组实例
my_df = MyDF(df)
my_df
<__main__.MyDF at 0x13641062080>
- 调用类中的
groupby
方法 , 先传入一个单列 , 得到一个my_gb
对象
gb = df.groupby('Country')
my_gb = my_df.groupby('Country')
print(gb)
print(my_gb)
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x0000013640FB6F28>
<__main__.GroupBy object at 0x0000013640FB6E10>
- 先来看看自定义的
my_gb
对象中的属性
print(gb.ngroups)
print(my_gb.ngroups)
8
8
print(gb.groups)
print(my_gb.groups)
{'France': [33], 'Germany': [12, 22], 'Japan': [4, 8, 9, 11, 17, 18, 19, 21, 29, 30, 31, 37, 45, 46, 49, 56, 57, 58, 59], 'Japan/USA': [3, 6, 10, 27, 28, 34, 35], 'Korea': [2, 7, 44], 'Mexico': [5], 'Sweden': [36], 'USA': [0, 1, 13, 14, 15, 16, 20, 23, 24, 25, 26, 32, 38, 39, 40, 41, 42, 43, 47, 48, 50, 51, 52, 53, 54, 55]}
{'USA': [0, 1, 13, 14, 15, 16, 20, 23, 24, 25, 26, 32, 38, 39, 40, 41, 42, 43, 47, 48, 50, 51, 52, 53, 54, 55], 'Korea': [2, 7, 44], 'Japan/USA': [3, 6, 10, 27, 28, 34, 35], 'Japan': [4, 8, 9, 11, 17, 18, 19, 21, 29, 30, 31, 37, 45, 46, 49, 56, 57, 58, 59], 'Mexico': [5], 'Germany': [12, 22], 'France': [33], 'Sweden': [36]}
print(gb.indices)
print(my_gb.indices)
{'France': array([33], dtype=int64), 'Germany': array([12, 22], dtype=int64), 'Japan': array([ 4, 8, 9, 11, 17, 18, 19, 21, 29, 30, 31, 37, 45, 46, 49, 56, 57,58, 59], dtype=int64), 'Japan/USA': array([ 3, 6, 10, 27, 28, 34, 35], dtype=int64), 'Korea': array([ 2, 7, 44], dtype=int64), 'Mexico': array([5], dtype=int64), 'Sweden': array([36], dtype=int64), 'USA': array([ 0, 1, 13, 14, 15, 16, 20, 23, 24, 25, 26, 32, 38, 39, 40, 41, 42,43, 47, 48, 50, 51, 52, 53, 54, 55], dtype=int64)}
{'USA': array([ 0, 1, 13, 14, 15, 16, 20, 23, 24, 25, 26, 32, 38, 39, 40, 41, 42,43, 47, 48, 50, 51, 52, 53, 54, 55]), 'Korea': array([ 2, 7, 44]), 'Japan/USA': array([ 3, 6, 10, 27, 28, 34, 35]), 'Japan': array([ 4, 8, 9, 11, 17, 18, 19, 21, 29, 30, 31, 37, 45, 46, 49, 56, 57,58, 59]), 'Mexico': 5, 'Germany': array([12, 22]), 'France': 33, 'Sweden': 36}
- 上述两个属性排列顺序不一样 , 内容都一样
gb.get_group('Korea')
Brand | Price | Country | Reliability | Mileage | Type | Weight | Disp. | HP | |
---|---|---|---|---|---|---|---|---|---|
2 | Ford Festiva 4 | 6319 | Korea | 4.0 | 37 | Small | 1845 | 81 | 63 |
7 | Pontiac LeMans 4 | 7254 | Korea | 1.0 | 28 | Small | 2350 | 98 | 74 |
44 | Hyundai Sonata 4 | 9999 | Korea | NaN | 23 | Medium | 2885 | 143 | 110 |
my_gb.get_group('Korea')
Brand | Price | Country | Reliability | Mileage | Type | Weight | Disp. | HP | |
---|---|---|---|---|---|---|---|---|---|
2 | Ford Festiva 4 | 6319 | Korea | 4.0 | 37 | Small | 1845 | 81 | 63 |
7 | Pontiac LeMans 4 | 7254 | Korea | 1.0 | 28 | Small | 2350 | 98 | 74 |
44 | Hyundai Sonata 4 | 9999 | Korea | NaN | 23 | Medium | 2885 | 143 | 110 |
- 多列分组 :
df.groupby(['Type','Country'])['Price'].transform(lambda x:(x-x.mean())/x.std()).head()
0 0.707107
1 -0.707107
2 -0.707107
3 -0.896336
4 -0.322354
Name: Price, dtype: float64
my_df.groupby(['Type','Country'])['Price'].transform(lambda x:(x-x.mean())/x.std()).head()
0 0.707107
1 -0.707107
2 -0.707107
3 -0.896336
4 -0.322354
Name: Price, dtype: float64
- 标量广播 :
df.groupby(['Type','Country'])['Price'].transform(lambda x:x.std()).head()
0 1055.710424
1 1055.710424
2 661.144840
3 1069.911679
4 1672.077151
Name: Price, dtype: float64
my_df.groupby(['Type','Country'])['Price'].transform(lambda x:x.std()).head()
0 1055.710424
1 1055.710424
2 661.144840
3 1069.911679
4 1672.077151
Name: Price, dtype: float64
- 跨列计算 :
my_df.groupby(['Type','Country'])['HP','Disp.'].transform(lambda x:x.HP/x['Disp.']).head()
0 1.164948
1 0.789474
2 0.777778
3 1.010989
4 0.911504
Name: <lambda>, dtype: float64
- 不做任何操作 , 返回
DadaFrame
df.groupby(['Type','Country'])[['HP','Disp.']].transform(lambda x:x).head()
HP | Disp. | |
---|---|---|
0 | 113 | 97 |
1 | 90 | 114 |
2 | 63 | 81 |
3 | 92 | 91 |
4 | 103 | 113 |
my_df.groupby(['Type','Country'])[['HP','Disp.']].transform(lambda x:x).head()
HP | Disp. | |
---|---|---|
0 | 113 | 97 |
1 | 90 | 114 |
2 | 63 | 81 |
3 | 92 | 91 |
4 | 103 | 113 |
Pandas 04-分组相关推荐
- Python pandas dataframe 分组聚合时,分组组名并入列的方法
转载,侵删,感谢原作者 利用pandas做分组聚合时,分组组名默认是变化成index,如图 grouped = tdf.groupby('uid') mean=grouped.mean(); forw ...
- Python数据分析pandas之分组统计透视表
Python数据分析pandas之分组统计透视表 数据聚合统计 Padans里的聚合统计即是应用分组的方法对数据框进行聚合统计,常见的有min(最小).max(最大).avg(平均值).sum(求和) ...
- 用pandas批量分组处理excel数据
用pandas批量分组处理excel数据 需求比较简单,按照指定字段分组,计算指定字段的和或均值. 用SQL处理的话就一个groupby,可是没有数据库环境,只好用python(比起SQL真的是一点也 ...
- pandas dataframe 分组聚合时,分组组名并入列的方法
利用pandas做分组聚合时,分组组名默认是变化成index,如图 grouped = tdf.groupby('uid') mean=grouped.mean(); forwardmeandf=pd ...
- Pandas Task3 分组
Pandas Task3 分组 1.问题 [问题一] 什么是fillna的前向/后向填充,如何实现? [问题二] 下面的代码实现了什么功能?请仿照设计一个它的groupby版本. [问题三] 如何计算 ...
- 记录一下pandas的分组统计功能,agg
主要是记录一下pandas学习,最近要统计一个数据,我向以前stata里面有一个很好用的函数,就是tabstat,可以分组统计,并且输出很多指标. 最近处理数据我的数据是这样的. 我向按照 valid ...
- pandas数据处理分组聚合
import pandas as pd import numpy as np # 加载数据 users = pd.read_excel("./users.xlsx") print( ...
- pandas访问分组里面的数据_实战用pandas+PyQt5制作一款数据分组透视处理工具
早起导读:pandas是Python数据处理的利器,如果每天都要使用pandas执行同样的操作,如何制作一个有界面的软件更高效的完成?本文提供了一种基于PyQt5的实现思路. 关键词:pandas P ...
- Pandas GroupBy 分组(分割-应用-组合)
http://pandas.pydata.org/pandas-docs/stable/groupby.html#group-by-split-apply-combine import pandas ...
- pandas数据分组聚合——groupby()、aggregate()、apply()、transform()和filter()方法详解
数据分组 数据分组就是根据一个或多个键(可以是函数.数组或df列名)将数据分成若干组,然后对分组后的数据分别进行汇总计算,并将汇总计算后的结果进行合并,被用作汇总计算的函数称为聚合函数.数据分组的具体 ...
最新文章
- Mysql基础知识—索引
- class函数 python_python函数之classmethod()
- 使用 js替换网页中的关键词为链接
- 万能写入sql语句,并且防注入
- win11+AMD的cpu+3060GPU电脑安装 tensorflow-GPU+cuda11+cudnn
- CCNP实验【静态出接口配置】
- PHP实习之路—NO.2(看IPB博文的源码,理清别人开发的思路)
- centos 7 安装 google chrome 浏览器 (不是教程,只是为了以后自己可能用到)
- java中volatile的使用方式
- “一个千古绝伦的大智者”莱布尼茨
- Springboot2学习博客
- xargs 和 exec详解
- Linux 中断学习之前言篇---中断之原理篇
- 可牛影像动感渐隐闪图教程
- 讲座记录——科技论文写作及科研方法
- 关于amd cpu超频 个人心得
- hackintosh技巧
- UE4中VR项目的打包和发布
- 记录一个android性能优化宝藏级总结
- mysql+视频文件转成流_详解java调用ffmpeg转换视频格式为flv