一、分组模式及其对象
- 分组的一般模式
分组操作的三个要素:分组依据 、 数据来源 、 操作及其返回结果 。分组代码的一般模式即:
df.groupby(分组依据)[数据来源].使用操作
例如:现在返回到学生体测的数据集上,如果想要按照性别统计身高中位数,就可以如下写出:
df.groupby('Gender')['Height'].median()
# Gender
# Female 159.6
# Male 173.4
# Name: Height, dtype: float64
- 分组依据的本质
多个维度进行分组,只需在 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
通过一定的复杂逻辑来分组,例如根据学生体重是否超过总体均值来分组,同样还是计算身高的均值。
condition = df.Weight > df.Weight.mean()
df.groupby(condition)['Height'].mean()
# Weight
# False 159.034646
# True 172.705357
# Name: Height, dtype: float64
从索引可以看出,其实最后产生的结果就是按照条件列表中元素的值(此处是 True 和 False )来分组,下面用随机传入字母序列来验证这一想法:(这里就像随机把每一个样本分成a,b,c三个组)
item = np.random.choice(list('abc'), df.shape[0])#随机生成200个值(在['a', 'b', 'c']中随机取)
# numpy.random.choice(a, size=None, replace=True, p=None)
# #从a(只要是ndarray都可以,但必须是一维的)中随机抽取数字,并组成指定大小(size)的数组
# #replace:True表示可以取相同数字,False表示不可以取相同数字
# #数组p:与数组a相对应,表示取数组a中每个元素的概率,默认为选取每个元素的概率相同。
df.groupby(item)['Height'].mean()
# a 164.430189
# b 161.986441
# c 163.336620
# Name: Height, dtype: float64
如果传入多个序列进入 groupby ,那么最后分组的依据就是这两个序列对应行的唯一组合:个人理解,先根据学生体重是否超过总体均值来分组True or False,在True里面随机分成3个组a,b,c。同理,False也一样。
df.groupby([condition, item])['Height'].mean()
# Weight
# False a 159.269444
# b 158.326190
# c 159.469388
# True a 175.358824
# b 171.029412
# c 171.950000
# Name: Height, dtype: float64
由此可以看出,之前传入列名只是一种简便的记号,事实上等价于传入的是一个或多个列,最后分组的依据来自于**数据来源组合的unique值**,通过 drop_duplicates 就能知道具体的组类别:
#drop_duplicates()可以选择是否保留重复值,
#默认是保留重复值,想要不保留重复值的话直接设置参数keep为False即可。
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
- Groupby对象
做分组操作时,所调用的方法都来自于 pandas 中的 groupby 对象,这个对象上定义了许多方法,也具有一些方便的属性。
gb = df.groupby(['School', 'Grade'])
通过 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')])
res.values()# 字典的值
# dict_values([Int64Index([15, 28, 63, 70, 73, 105, 108, 157, 186], dtype='int64'), Int64Index([26, 41, 82, 84, 90, 107, 145, 152, 173, 187, 189, 195], dtype='int64'), Int64Index([39, 46, 49, 52, 66, 77, 112, 129, 131, 138, 144], dtype='int64'), Int64Index([3, 4, 37, 48, 68, 98, 135, 170], dtype='int64'), Int64Index([1, 32, 35, 36, 38, 45, 54, 57, 88, 96, 99, 140, 185], dtype='int64'), Int64Index([9, 20, 59, 72, 75, 102, 159, 183], dtype='int64'), Int64Index([30, 86, 116, 127, 130, 132, 147, 194], dtype='int64'), Int64Index([29, 61, 83, 101, 120], dtype='int64'), Int64Index([0, 6, 10, 60, 114, 117, 119, 121, 141, 148, 149, 153, 184], dtype='int64'), Int64Index([31, 42, 50, 56, 58, 64, 85, 93, 115, 122, 143, 155, 164, 172, 174,
# 188, 190],
# dtype='int64'), Int64Index([ 2, 12, 19, 21, 22, 23, 79, 87, 89, 103, 104, 109, 123,
# 134, 156, 161, 165, 166, 171, 192, 197, 198],
# dtype='int64'), Int64Index([13, 65, 71, 124, 167], dtype='int64'), Int64Index([5, 8, 33, 34, 43, 44, 47, 51, 62, 67, 81, 111, 125, 133, 136, 142,
# 146],
# dtype='int64'), Int64Index([ 7, 11, 16, 17, 27, 69, 94, 95, 113, 118, 128, 137, 150,
# 154, 158, 160, 162, 163, 169, 176, 177, 191],
# dtype='int64'), Int64Index([14, 18, 24, 25, 78, 92, 100, 126, 168, 175, 179, 180, 193, 196], dtype='int64'), Int64Index([40, 53, 55, 74, 76, 80, 91, 97, 106, 110, 139, 151, 178, 181, 182,
# 199],
# dtype='int64')])
当 size 作为 DataFrame 的属性时,返回的是表长乘以表宽的大小,但在 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] # 展示('Fudan University', 'Freshman')的前三行,前三列
# School Grade Name
# 15 Fudan University Freshman Changqiang Yang
# 28 Fudan University Freshman Gaoqiang Qin
# 63 Fudan University Freshman Gaofeng Zhao
- 分组的三大操作
三种类型分组返回的数据型态并不一样:
第一个例子中,每一个组返回一个标量值,可以是平均值、中位数、组容量 size 等
第二个例子中,做了原序列的标准化处理,也就是说每组返回的是一个 Series 类型
第三个例子中,既不是标量也不是序列,返回的整个组所在行的本身,即返回了 DataFrame 类型
二、聚合函数
- 内置聚合函数
直接定义在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.95)#分位数
# Gender
# Female 166.8
# Male 185.9
# Name: Height, dtype: float64
这些聚合函数当传入的数据来源包含多个列时,将按照列进行迭代计算:
gb = df.groupby('Gender')[['Height', 'Weight']]
print(gb.max())
# Height Weight
# Gender
# Female 170.2 63.0
# Male 193.9 89.0
- agg方法
虽然在 groupby 对象上定义了许多方便的函数,但仍然有以下不便之处:
1)无法同时使用多个函数
2)无法对特定的列使用特定的聚合函数
3)无法使用自定义的聚合函数
4)无法直接对结果的列名在聚合前进行自定义命名
下面说明如何通过 agg 函数解决这四类问题:
【a】使用多个函数
当使用多个聚合函数时,需要用列表的形式把内置聚合函数对应的字符串传入,先前提到的所有字符串都是合法的。
skew样本值的偏度
从结果看,此时的列索引为多级索引,第一层为数据源,第二层为使用的聚合方法,分别逐一对列使用聚合,因此结果为6列。
print(gb.agg(['sum', 'idxmax', 'skew']))#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
【b】对特定的列使用特定的聚合函数
对于方法和列的特殊对应,可以通过构造字典传入 agg 中实现,其中字典以列名为键,以聚合字符串或字符串列表为值。
print(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
【c】使用自定义函数
在 agg 中可以使用具体的自定义函数, 需要注意传入函数的参数是之前数据源中的列,逐列进行计算 。下面分组计算身高和体重的极差:
print(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():
print(s.name)
# Height
# Weight
res = 'Low'
return res
print(gb.agg(my_func))
# Height Weight
# Gender
# Female Low Low
# Male High High
【d】聚合结果重命名
如果想要对聚合结果的列名进行重命名,只需要将上述函数的位置改写成元组,元组的第一个元素为新的名字,第二个位置为原来的函数,包括聚合字符串和自定义函数,现举若干例子说明:
个人理解:第一个例子’range’为新的聚合结果的列名,lambda x: x.max()-x.min()为第一个聚合函数,'my_sum’为第二个聚合结果的列名,‘sum’为第二个聚合函数
第二个例子对’Height’这列数据第一个聚合操作my_func,聚合结果的列名为’my_func’,第二个聚合函数是sum(没有指定聚合结果的列名,所以也是sum),对’Weight’这列数据的操作是lambda x:x.max()。
print(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
print(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
另外需要注意,使用对一个或者多个列使用单个聚合的时候,重命名需要加方括号,否则就不知道是新的名字还是手误输错的内置函数字符串:
print(gb.agg([('my_sum', 'sum')]))
# Height Weight
# my_sum my_sum
# Gender
# Female 21014.0 6469.0
# Male 8854.9 3929.0
print(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
三、变换和过滤
- 变换函数与transform方法
变换函数的返回值为同长度的序列,最常用的内置变换函数是累计函数: cumcount/cumsum/cumprod/cummax/cummin ,它们的使用方式和聚合函数类似,只不过完成的是组内累计操作。
print(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 。
现对身高和体重进行分组标准化,即减去组均值后除以组的标准差:
print(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 只能返回同长度的序列,但事实上还可以返回一个标量,这会使得结果被广播到其所在的整个组,这种标量广播 的技巧在特征工程中是非常常见的。
例如,构造两列新特征来分别表示样本所在性别组的身高均值和体重均值:
print(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
-
组索引与过滤
索引和过滤有什么区别
过滤在分组中是对于组的过滤,而索引是对于行的过滤,在第二章中的返回值,无论是布尔列表还是元素列表或者位置列表,本质上都是对于行的筛选,即如果符合筛选条件的则选入结果表,否则不选入。
组过滤作为行过滤的推广,指的是如果对一个组的全体所在行进行统计的结果返回 True 则会被保留, False 则该组会被过滤,最后把所有未被过滤的组其对应的所在行拼接起来作为 DataFrame 返回。
在 groupby 对象中,定义了 filter 方法进行组的筛选,其中自定义函数的输入参数为数据源构成的 DataFrame 本身,在之前例子中定义的 groupby 对象中,传入的就是 df[[‘Height’, ‘Weight’]] ,因此所有表方法和属性都可以在自定义函数中相应地使用,同时只需保证自定义函数的返回为布尔值即可。
例如,在原表中通过过滤得到所有容量大于100的组:
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
四、跨列分组
-
apply的引入
-
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
【a】标量情况:结果得到的是 Series ,索引与 agg 的结果一致
gb = df.groupby(['Gender','Test_Number'])[['Height','Weight']]
gb.apply(lambda x: 0)
# Gender Test_Number
# Female 1 0
# 2 0
# 3 0
# Male 1 0
# 2 0
# 3 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 的索引
print(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
【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')])))
最后需要强调的是, apply 函数的灵活性是以牺牲一定性能为代价换得的,除非需要使用跨列处理的分组处理,否则应当使用其他专门设计的 groupby 对象方法,否则在性能上会存在较大的差距。同时,在使用聚合函数和变换函数时,也应当优先使用内置函数,它们经过了高度的性能优化,一般而言在速度上都会快于用自定义函数来实现。
五、练习
Ex1:汽车数据集
现有一份汽车数据集,其中 Brand, Disp., HP 分别代表汽车品牌、发动机蓄量、发动机输出。
# gb=df.groupby('Country')['Brand'].count()
# print(gb[gb.agg(lambda x: x>2)])
# # Country
# # Japan 19
# # Japan/USA 7
# # Korea 3
# # USA 26
# # Name: Brand, dtype: int64
# print(gb[gb.transform(lambda x: x>2)])
# # Country
# # Japan 19
# # Japan/USA 7
# # Korea 3
# # USA 26
# # Name: Brand, dtype: int64
df.groupby('Country').filter(
lambda x:x.shape[0]>2).groupby('Country')['Price'].agg(
[('CoV', lambda x: x.std()/x.mean()), 'mean', 'count'])
print(df.shape[0])
condition = ['Head']*20+['Mid']*20+['Tail']*20
df.groupby(condition)['Price'].mean()
res = df.groupby('Type').agg({'Price': ['max'], 'HP': ['min']})
res.columns = res.columns.map(lambda x:'_'.join(x))
res
def normalize(s):
s_min, s_max = s.min(), s.max()
res = (s - s_min)/(s_max - s_min)
return res
df.groupby('Type')['HP'].transform(normalize).head()
df.groupby('Type')[['HP', 'Disp.']].apply(
lambda x:np.corrcoef(x['HP'].values, x['Disp.'].values)[0,1])
Ex2:实现transform函数(参考)