1. 分组的基本概况
分组,即对原始数据的行按照一定的条件重新组合,将具有相同属性的行聚合到一起在计算其他数值。在Pandas里面提供了一个groupby函数,非常灵活而且高效。分组操作可以来干嘛?参考下面三个应用:
- 依据 性别 分组,统计全国人口 寿命 的 平均值
- 依据 季节 分组,对每一个季节的温度进行组内标准化
- 依据 班级 分组,筛选出组内 数学分数的平均值超过80分的班级
以上三个问题,都可以用分组来解决。通过观察,可以发现上面的三个问题都涉及到3个方面的内容:
- 分组依据(性别/季节/班级)
- 需要聚合/计算等操作的字段,或者是数据来源(人口寿命/温度/数学分数)
- 需要返回的结果
这3项是完成一个分组操作的3个必须要素。
下面做一个简单演示:
im
port pandas as pd
import numpy as np
df = pd.read_csv('learn_pandas.csv')
df.head()
这是一份包含学生学校性别等信息的表格, 下面需要依据学校和性别分组,统计身高均值,代码如下:
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方法的标准语法:
其中:
m: 分组依据,但需要按照多个条件分组时,需要把条件放到一个列表中
n: 数据来源,即需要计算的字段,同样的,需要多个字段也是放入一个列表
k: 聚合函数,常用的有min/max/mean/count等,也可以传入自定义参数
上面的例子中,分组的依据(School,Gender)为原始表格中已有的字段,那么能不能不使用原有的字段,而是按照一定的条件来分组呢?答案是可以的。例如我们需要按照体重是否大于均值分成两组,分别统计两组的身高均值:
df.groupby(df['Weight'] > df['Weight'].mean())['Height'].mean()
>>>
Weight
False 159.034646
True 172.705357
Name: Height, dtype: float64
上面的代码中df['Weight'] > df['Weight'].mean()即是分组依据,通过结果的索引(False/True)可以看出,其实最后产生的结果就是按照条件列表中元素的值(此处是 True 和 False )来分组。下面用随机传入字母序列来验证这一想法:
item = np.random.choice(list('abc'), df.shape[0])
df.groupby(item)['Height'].mean()
>>>
a 162.567347
b 164.367606
c 162.428571
Name: Height, dtype: float64
上面的代码先创建了一个和原DataFrame等长的序列,并将这个序列作为分组依据。
从上面的例子中我们可以总结出分组依据的本质:
分组的依据来自于数据来源组合的unique值。 例如在上面的学生信息表格中按照学校School和性别Gender来分组,如果学校的个数为m, 性别个数为2,并且在原始数据,每个学校都存在2中性别的行,则最终分组的个数为2m
2. Groupby对象
最终具体做分组操作时,所调用的方法都来自于 pandas 中的 groupby 对象,这个对象上定义了许多方法,也具有一些方便的属性。
# groupby返回一个groupby对象
df1 = df.groupby(['School', 'Grade'])
type(df1)
>>>
pandas.core.groupby.generic.DataFrameGroupBy
可以看到,groupby后返回一个groupby对象,且是一个生成器。既然是生成器我们就可以用for循环遍历里面的元素:
for i in df1:
print(i)
结果太长,下面是部分截图。通过截图可以看到每个元素是一个tuple, tuple的第一个元素是分组的依据,第二个是具体的值,是一个DataFrame
for i in df1:
print(type(i), i[0])
>>>
('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')
其他常用属性:
print(df1.ngroups) # ngroups:分组个数
print("-" * 10)
print(df1.groups[('Fudan University', 'Freshman')]) # 返回改组的索引
print("-" * 10)
print(df1.size()) # 每个组别的个数
>>>
16
----------
Int64Index([15, 28, 63, 70, 73, 105, 108, 157, 186], dtype='int64')
----------
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
3. 分组后3大基本操作
熟悉了一些分组的基本知识后,重新回到开头举的三个例子,可能会发现一些端倪,即这三种类型分组返回的数据型态并不一样:
第一个例子中,每一个组返回一个标量值,可以是平均值、中位数、组容量 size 等
第二个例子中,做了原序列的标准化处理,也就是说每组返回的是一个 Series 类型
第三个例子中,既不是标量也不是序列,而是通过筛选返回的整个组所在行的本身,即返回了 DataFrame 类型
由此,引申出分组的三大操作:
- 聚合- agg、
- 变换 - transform
- 过滤 - filter
下面分别介绍
3.1 聚合 aggregation (agg)
- 内置聚合函数
在介绍agg之前,首先要了解一些直接定义在groupby对象的聚合函数,因为它的速度基本都会经过内部的优化,使用功能时应当优先考虑。
包括如下函数:
- max/min/mean/median/count/
- all/any/idxmax/idxmin/
- mad/nunique/skew/quantile/
- sum/std/var/sem/size/prod
其中有些不常用的函数如下: - any(): 如果组内有truthful的值就返回True。
- all(): 组内所有元素都是truthful,返回True。
- mad():返回组内元素的绝对中位差。先计算出数据与它们的中位数之间的残差,MAD就是这些偏差的绝对值的中位数。MAD比方差鲁棒性更好。
- skew():组内数据的偏度。
- sem():组内数据的均值标准误差。
- prod() :组内所有元素的乘积。
df.groupby('Gender')['Height'].idxmin()
>>>
Gender
Female 143
Male 199
Name: Height, dtype: int64
df.groupby('Gender')[['Height', 'Weight']].max()
>>>
Height Weight
Gender
Female 170.2 63.0
Male 193.9 89.0
2. agg方法
虽然在 groupby
对象上定义了许多方便的函数,但仍然有以下不便之处:
- 无法同时使用多个函数
- 无法对特定的列使用特定的聚合函数
- 无法使用自定义的聚合函数
- 无法直接对结果的列名在聚合前进行自定义命名
下面说明如何通过 agg
函数解决这四类问题:
【a】使用多个函数
当使用多个聚合函数时,需要用列表的形式把内置聚合函数对应的字符串传入,先前提到的所有字符串都是合法的。
df.groupby('School')['Height', 'Weight'].agg(['max', 'min', 'count', 'idxmax', 'idxmin'])
结果如下:
从结果看,此时的列索引为多级索引,第一层为数据源,第二层为使用的聚合方法,分别逐一对列使用聚合,因此结果为10列。
【b】对特定的列使用特定的聚合函数
对于方法和列的特殊对应,可以通过构造字典传入 agg 中实现,其中字典以列名为键,以聚合字符串或字符串列表为值。
df.groupby('Gender')['Height', 'Weight'].agg({'Height': ['max', 'idxmax'], 'Weight': ['mean', 'count']})
【c】使用自定义函数
在 agg 中可以使用具体的自定义函数, 需要注意传入函数的参数是之前数据源中的列,逐列进行计算 。下面分组计算身高和体重的极差:
df.groupby('School')['Weight', 'Height'].agg([lambda x: x.max() - x.min()])
【d】聚合结果重命名
如果想要对聚合结果的列名进行重命名,只需要将上述函数的位置改写成元组,元组的第一个元素为新的名字,第二个位置为原来的函数,包括聚合字符串和自定义函数,现举例子说明:
df.groupby('School')['Weight', 'Height'].agg([('range', lambda x: x.max() - x.min())])
3.2 变换函数与transfrom方法
变换函数的返回值为同长度的序列,最常用的内置变换函数是累计函数: cumcount/cumsum/cumprod/cummax/cummin ,它们的使用方式和聚合函数类似,只不过完成的是组内累计操作。
各个函数的意义如下:
cumsum(): 依次给出前1、2、… 、n个数的和
cumprod(): : 依次给出前1、2、… 、n个数的积
cummax(): 依次给出前1、2、… 、n个数的最大值
cummin(): 依次给出前1、2、… 、n个数的最小值
df.groupby('Gender')['Height', 'Weight'].cumsum().head(7)
>>>
Height Weight
0 158.9 46.0
1 166.5 70.0
2 355.4 159.0
3 NaN 87.0
4 529.4 233.0
5 316.9 138.0
6 479.4 190.0
当用自定义变换时需要使用 transform 方法,被调用的自定义函数, 其传入值为数据源的序列 ,与 agg 的传入类型是一致的,其最后的返回结果是行列索引与数据源一致的 DataFrame 。
例如现对身高和体重进行分组标准化,即减去组均值后除以组的标准差:
df.groupby('Gender')['Height', 'Weight'].transform(lambda x: (x-x.mean())/x.std()).head(7)
>>>
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
5 -0.236837 0.570013
6 0.653550 0.754993
3.3 过滤
首先明确一下索引和过滤的区别
过滤在分组中是对于组的过滤,而索引是对于行的过滤,在第二章中的返回值,无论是布尔列表还是元素列表或者位置列表,本质上都是对于行的筛选,即如果符合筛选条件的则选入结果表,否则不选入。
组过滤作为行过滤的推广,指的是如果对一个组的全体所在行进行统计的结果返回 True 则会被保留, False 则该组会被过滤,最后把所有未被过滤的组其对应的所在行拼接起来作为 DataFrame 返回。
在 groupby 对象中,定义了 filter 方法进行组的筛选,其中自定义函数的输入参数为数据源构成的 DataFrame 本身,在之前例子中定义的 groupby 对象中,传入的就是 df[['Height', 'Weight']] ,因此所有表方法和属性都可以在自定义函数中相应地使用,同时只需保证自定义函数的返回为布尔值即可。
4. 练习:
现有一份汽车数据集,其中 Brand, Disp., HP 分别代表汽车品牌、发动机排量、发动机功率:
car = pd.read_csv('car.csv')
print(car.shape)
car.head()
>>>
(60, 9)
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
3 Honda Civic 4 6635 Japan/USA 5.0 32 Small 2260 91 92
4 Mazda Protege 4 6599 Japan 5.0 32 Small 2440 113 103
问题1: 先过滤出所属 Country 数超过2个的汽车,即若该汽车的 Country 在总体数据集中出现次数不超过2则剔除, 再按 Country 分组计算价格均值、价格变异系数、该 Country 的汽车数量, 其中变异系数的计算方法是标准差除以均值,并在结果中把变异系数重命名为 CoV
1.1 先按照country分组, 筛选出数量大于2的汽车品牌
df1 = car.groupby('Country').filter(lambda x: x.shape[0] > 2)
df1.head()
验证一下是否Country统计数据都大于2:
df1['Country'].value_counts()
>>>
USA 26
Japan 19
Japan/USA 7
Korea 3
Name: Country, dtype: int64
1.2 再按 Country 分组计算价格均值、价格变异系数、该 Country 的汽车数量,
其中汽车的数量可以统计任意字段的count即可,因而,可以值聚合Price列,
并分别计算均值,变异系数,数量
res = df1.groupby('Country')['Price'].agg(['mean', ('Cov', lambda x: x.std()/x.mean()), 'count'])
res
问题2: 按照表中位置的前三分之一、中间三分之一和后三分之一分组,统计 Price 的均值。
分析: 构建一个新的序列用于分组,取值为前20-Front,中间20-middle,后20-back.
# 先创建一个与原DataFrame等长的序列,前20为front, 中间为middle,最后为back
car['position'] = ['front'] * 20 + ['middle'] * 20 + ['back']*20
car.groupby('position')['Price'].mean()
>>>
position
back 15420.65
front 9069.95
middle 13356.40
Name: Price, dtype: float64
问题3: 对类型 Type 分组,对 Price 和 HP 分别计算最大值和最小值,结果会产生多级索引,请用下划线把多级列索引合并为单层索引。
type_group = car.groupby('Type')['Price', 'HP'].agg(['max', 'min'])
type_group
# 使用多级索引映射
type_group.columns = type_group.columns.map(lambda x: '_'.join(x))
type_group
问题4. 对类型 Type 分组,对 HP 进行组内的 min-max 归一化。
df2 = car.groupby('Type')['HP'].transform(lambda x:(x-x.min())/(x.max()-x.min()))
df2.head(5)
>>>
0 1.00
1 0.54
2 0.00
3 0.58
4 0.80
Name: HP, dtype: float64
问题5: 对类型 Type 分组,计算 Disp. 与 HP 的相关系数。
car.groupby('Type')[['Disp.','HP']].apply(lambda x:np.corrcoef(x['Disp.'].values,x.HP.values)[0,1])
>>>
Type
Compact 0.586087
Large -0.242765
Medium 0.370491
Small 0.603916
Sporty 0.871426
Van 0.819881
dtype: float64