一、分组模式及其对象
1. 分组的一般模式
实现分组操作,必须明确三个要素:
- 分组依据
- 数据来源
- 操作及其返回结果
同时从充分性的角度来说,明确了这三方面,就能确定一个分组操作,从而分组代码的一般模式即:
# 引用方法
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.776923
Male 174.212500
Peking University Female 158.666667
Male 172.030000
Shanghai Jiao Tong University Female 159.122500
Male 176.760000
Tsinghua University Female 159.753333
Male 171.638889
Name: Height, dtype: float64
groupby
的分组依据都是直接可以从列中按照名字获取的。
如果希望通过一定的复杂逻辑来分组,首先先写出分组条件,然后将其传入 groupby
中:
# 根据学生体重是否超过总体均值来分组,计算身高的均值
# 首先先写出分组条件
condition = df.Weight > df.Weight.mean()
# 然后将其传入 groupby 中
df.groupby(condition)['Height'].mean()
## 输出
Weight
False 159.034646
True 172.705357
Name: Height, dtype: float64
上面从索引可以看出,最后产生的结果就是按照条件列表中元素的值`( True
和 False
)`来分组。
# 在列表中 用随机传入字母序列
item = np.random.choice(list('abc'), df.shape[0])
df.groupby(item)['Height'].mean()
## 输出
a 163.924242
b 162.928814
c 162.708621
Name: Height, dtype: float64
这里的索引就是原先item中的元素。
如果传入多个序列进入 groupby
,那么最后分组的依据就是这两个序列对应行的唯一组合
df.groupby([condition, item])['Height'].mean()
## 输出
Weight
False a 160.193617
b 158.921951
c 157.756410
True a 173.152632
b 172.055556
c 172.873684
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.776923
Male 174.212500
Peking University Female 158.666667
Male 172.030000
Shanghai Jiao Tong University Female 159.122500
Male 176.760000
Tsinghua University Female 159.753333
Male 171.638889
Name: Height, dtype: float64
3. Groupby对象
在具体做分组操作时,所调用的方法都来自于 pandas
中的 groupby
对象,这个对象上定义了许多方法,也具有一些方便的属性,下面列出2个属性和2个方法。
gb = df.groupby(['School', 'Grade'])
gb
## 输出
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x0000024372714408>
-
ngroups
属性,可以得到分组个数
gb.ngroups
## 输出
16
-
groups
属性,可以返回从 组名 映射到 组索引列表 的字典
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')])
-
size
作为DataFrame
的属性时,返回的是表长乘以表宽的大小 -
size
方法在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')).iloc[:3, :3] # 展示一部分
## 输出
School Grade Name
15 Fudan University Freshman Changqiang Yang
28 Fudan University Freshman Gaoqiang Qin
63 Fudan University Freshman Gaofeng Zhao
mean
、 median
都是 groupby
对象上的方法,这些函数和许多其他函数的操作具有高度相似性。
4. 分组的三大操作
对例子进行分析:
-
依据 性别 分组,统计全国人口 寿命 的 平均值
-
依据 季节 分组,对每一个季节的 温度 进行 组内标准化
-
依据 班级 分组,筛选出组内 数学分数 的 平均值超过80分的班级
这三种类型分组返回的数据型态并不一样:
-
对于第一个例子,每一个组返回一个标量值,可以是平均值、中位数、组容量
size
等 -
对于第二个例子,做了原序列的标准化处理,也就是说每组返回的是一个
Series
类型 -
对于第三个例子,既不是标量也不是序列,返回的整个组所在行的本身,即返回了
DataFrame
类型
由此,就分别对应了分组的三大操作:
- 聚合(
agg
) - 变换(
transform
) - 过滤(
filter
)
二、聚合函数
1. 内置聚合函数
-
agg
:聚合
在此之前,先要了解一些直接定义在groupby对象的聚合函数,因为它的速度基本都会经过内部的优化,使用功能时应当优先考虑。根据返回标量值的原则,包括如下函数:
max
min
mean
median
count
all:
表示分组后每一组中所有bool值都为True则返回True , 有一个False就返回False。any:
表示bool值列中只要有一个True则返回True , 只有全为False才会返回False,如果非0,则全为True。idxmax
idxmin
mad:
mad(mean absolute deviation)平均绝对离差 , 用于统计学中对分组后的每组数据做离散程度分析的指标之一,公式为:nunique
skew:
skew(skewness)偏度 , 用来反映分组后每组数据分布的偏态程度 , 正值为右偏 , 绝对值越大 , 偏度越高,公式为:quantile
sum
std
var
sem:
sem(standard error of mean)均值标准误差 , 描述的是多个均值样本的标准差,体现均值抽样分布的离散程度,反映样本均值之间的差异。设样本无偏估计标准差为s , 样本大小为N , 则分组后每组的sem可表示为:size
prod:
prod(product)连乘 , 每组prod表示为:
聚合函数当传入的数据来源包含多个列时,将按照列进行迭代计算:
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
函数解决这四类问题:
- 使用多个函数
- 对特定的列使用特定的聚合函数
- 使用自定义函数
- 聚合结果重命名
1. 使用多个函数
当使用多个聚合函数时,需要用列表的形式把内置聚合函数对应的字符串传入,先前提到的所有字符串都是合法的。
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列。
2. 对特定的列使用特定的聚合函数
对于方法和列的特殊对应,可以通过构造字典传入 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
3. 使用自定义函数
在 agg
中可以使用具体的自定义函数。
注意:传入函数的参数是之前数据源中的列,逐列进行计算 。
# 分组计算身高和体重的 极差
gb.agg(lambda x: x.mean()-x.min())
## 输出
Height Weight
Gender
Female 13.79697 13.918519
Male 17.92549 21.759259
传入的是序列,因此序列上的方法和属性都是可以在函数中使用的,但是需保证返回值是标量。
# 表示如果组的指标均值,超过该指标的总体均值,返回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
4. 聚合结果重命名
对聚合结果的列名进行重命名,只需要将上述函数的位置改写成元组。
- 元组的第一个元素为新的名字
- 元组的第二个位置为原来的函数,包括聚合字符串和自定义函数
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
用自定义变换需要使用 transform
方法,被调用的自定义函数, 其传入值为数据源的序列 ,与 agg
的传入类型是一致的,其最后的返回结果是行列索引与数据源一致的 DataFrame
。
# 对身高和体重进行分组标准化 即 减去 组均值后 除以 组的标准差
gb.transform(lambda x: (x-x.mean())/x.std()).head()
Out[35]:
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
只能返回同长度的序列,但其实还可以返回一个标量,这会使得结果被广播到其所在的整个组,这种 标量广播 的技巧在特征工程中是非常常见的。
# 构造两列新特征来分别表示样本所在性别组的 身高均值 和 体重均值
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
返回。
# 在原表中通过过滤得到所有容量大于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
四、跨列分组
1. apply的引入
除了三大分组操作,还有一种常见的分组场景,这种情况用之前的方法是无法处理的。
例如:使用 apply 函数解决 身体质量的指数BMI,需要分组计算组BMI的均值:
首先,这不是过滤操作, filter
不符合要求;其次,返回的均值是标量而不是序列,因此 transform
不符合要求;最后,似乎使用 agg
函数能够处理,但是之前强调过聚合函数是逐列处理的,而不能够 多列数据同时处理 。
2. apply的使用
注意:
apply
的自定义函数传入参数与 filter
完全一致,只不过后者只允许返回布尔值。
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
除了返回标量之外, apply
方法还可以返回一维 Series
和二维 DataFrame
,但它们产生的数据框维数和多级索引的层数有三种变化:
- 标量情况:结果得到的是
Series
,索引与agg
的结果一致 -
Series
情况:得到的是DataFrame
,行索引与标量情况一致,列索引为Series
的索引 -
DataFrame
情况:得到的是DataFrame
,行索引最内层在每个组原先agg
的结果索引上,再加一层返回的DataFrame
行索引,同时分组结果DataFrame
的列索引和返回的DataFrame
列索引一致。
补充:
apply
函数的灵活性是以牺牲一定性能为代价换得的,除非需要使用跨列处理的分组处理,否则应当使用其他专门设计的 groupby
对象方法,否则在性能上会存在较大的差距。同时,在使用聚合函数和变换函数时,也应当优先使用内置函数,它们经过了高度的性能优化,一般而言在速度上都会快于用自定义函数来实现。