目录
5. groupby()的小伙伴们:agg(), filter(), transform()和apply()方法
1. 学习内容
1. 了解什么是SAC过程
2. 学习如何利用groupby()实现对表格的分组
3. 学习如何在分组的基础上进行进一步的数据处理
2. 什么是SAC过程
SAC指的是分组操作中的split-apply-combine过程。其中,split指基于某一些规则,将数据拆成若干组;apply是指对每一组独立地使用函数;而combine指将每一组的结果组合成某一类新的数据结构。
那么在pandas中,这一套操作是如何完成的呢?首先,split即分组操作由groupby()来实现;然后,apply操作由agg()、filter()、transform()和apply()方法实现;最后,combine操作由concat()函数实现。在这里,我们只讨论前两个操作。
3. 准备工作
import numpy as np
import pandas as pd
df = pd.read_csv(r'./data/table.csv',index_col = 'ID')
print(df.head())
School Class Gender Address Height Weight Math Physics
ID
1101 S_1 C_1 M street_1 173 63 34.0 A+
1102 S_1 C_1 F street_2 192 73 32.5 B+
1103 S_1 C_1 M street_2 186 82 87.2 B+
1104 S_1 C_1 F street_2 167 81 80.4 B-
1105 S_1 C_1 F street_4 159 64 84.8 B+
4. groupby()方法
4.1 一般用法
4.1.1 按某一列分组
表格经过groupby()分组处理后会生成一个groupby对象。该对象本身不会返回任何东西,只有当相应的方法被调用才会起作用。
grouped_single = df.groupby('School')
print(grouped_single.get_group('S_1').head())
School Class Gender Address Height Weight Math Physics
ID
1101 S_1 C_1 M street_1 173 63 34.0 A+
1102 S_1 C_1 F street_2 192 73 32.5 B+
1103 S_1 C_1 M street_2 186 82 87.2 B+
1104 S_1 C_1 F street_2 167 81 80.4 B-
1105 S_1 C_1 F street_4 159 64 84.8 B+
4.1.2 按多列分组
grouped_mul = df.groupby(['School', 'Class'])
print(grouped_mul.get_group(('S_2', 'C_4')))
School Class Gender Address Height Weight Math Physics
ID
2401 S_2 C_4 F street_2 192 62 45.3 A
2402 S_2 C_4 M street_7 166 82 48.7 B
2403 S_2 C_4 F street_6 158 60 59.7 B+
2404 S_2 C_4 F street_2 160 84 67.7 B
2405 S_2 C_4 F street_6 193 54 47.6 B
4.1.3 查看每一组的容量和分成的组数
print(grouped_single.size())
print(grouped_single.ngroups)
School
S_1 15
S_2 20
dtype: int64
2
print(grouped_mul.size())
print(grouped_mul.ngroups)
School Class
S_1 C_1 5
C_2 5
C_3 5
S_2 C_1 5
C_2 5
C_3 5
C_4 5
dtype: int64
7
4.1.4 遍历每一组
for name, group in grouped_single:
print(name)
print(group)
S_1
School Class Gender Address Height Weight Math Physics
ID
1101 S_1 C_1 M street_1 173 63 34.0 A+
1102 S_1 C_1 F street_2 192 73 32.5 B+
1103 S_1 C_1 M street_2 186 82 87.2 B+
1104 S_1 C_1 F street_2 167 81 80.4 B-
1105 S_1 C_1 F street_4 159 64 84.8 B+
1201 S_1 C_2 M street_5 188 68 97.0 A-
1202 S_1 C_2 F street_4 176 94 63.5 B-
1203 S_1 C_2 M street_6 160 53 58.8 A+
1204 S_1 C_2 F street_5 162 63 33.8 B
1205 S_1 C_2 F street_6 167 63 68.4 B-
1301 S_1 C_3 M street_4 161 68 31.5 B+
1302 S_1 C_3 F street_1 175 57 87.7 A-
1303 S_1 C_3 M street_7 188 82 49.7 B
1304 S_1 C_3 M street_2 195 70 85.2 A
1305 S_1 C_3 F street_5 187 69 61.7 B-
S_2
School Class Gender Address Height Weight Math Physics
ID
2101 S_2 C_1 M street_7 174 84 83.3 C
2102 S_2 C_1 F street_6 161 61 50.6 B+
2103 S_2 C_1 M street_4 157 61 52.5 B-
2104 S_2 C_1 F street_5 159 97 72.2 B+
2105 S_2 C_1 M street_4 170 81 34.2 A
2201 S_2 C_2 M street_5 193 100 39.1 B
2202 S_2 C_2 F street_7 194 77 68.5 B+
2203 S_2 C_2 M street_4 155 91 73.8 A+
2204 S_2 C_2 M street_1 175 74 47.2 B-
2205 S_2 C_2 F street_7 183 76 85.4 B
2301 S_2 C_3 F street_4 157 78 72.3 B+
2302 S_2 C_3 M street_5 171 88 32.7 A
2303 S_2 C_3 F street_7 190 99 65.9 C
2304 S_2 C_3 F street_6 164 81 95.5 A-
2305 S_2 C_3 M street_4 187 73 48.9 B
2401 S_2 C_4 F street_2 192 62 45.3 A
2402 S_2 C_4 M street_7 166 82 48.7 B
2403 S_2 C_4 F street_6 158 60 59.7 B+
2404 S_2 C_4 F street_2 160 84 67.7 B
2405 S_2 C_4 F street_6 193 54 47.6 B
for name, group in grouped_mul:
print(name)
print(group)
('S_1', 'C_1')
School Class Gender Address Height Weight Math Physics
ID
1101 S_1 C_1 M street_1 173 63 34.0 A+
1102 S_1 C_1 F street_2 192 73 32.5 B+
1103 S_1 C_1 M street_2 186 82 87.2 B+
1104 S_1 C_1 F street_2 167 81 80.4 B-
1105 S_1 C_1 F street_4 159 64 84.8 B+
('S_1', 'C_2')
School Class Gender Address Height Weight Math Physics
ID
1201 S_1 C_2 M street_5 188 68 97.0 A-
1202 S_1 C_2 F street_4 176 94 63.5 B-
1203 S_1 C_2 M street_6 160 53 58.8 A+
1204 S_1 C_2 F street_5 162 63 33.8 B
1205 S_1 C_2 F street_6 167 63 68.4 B-
('S_1', 'C_3')
School Class Gender Address Height Weight Math Physics
ID
1301 S_1 C_3 M street_4 161 68 31.5 B+
1302 S_1 C_3 F street_1 175 57 87.7 A-
1303 S_1 C_3 M street_7 188 82 49.7 B
1304 S_1 C_3 M street_2 195 70 85.2 A
1305 S_1 C_3 F street_5 187 69 61.7 B-
('S_2', 'C_1')
School Class Gender Address Height Weight Math Physics
ID
2101 S_2 C_1 M street_7 174 84 83.3 C
2102 S_2 C_1 F street_6 161 61 50.6 B+
2103 S_2 C_1 M street_4 157 61 52.5 B-
2104 S_2 C_1 F street_5 159 97 72.2 B+
2105 S_2 C_1 M street_4 170 81 34.2 A
('S_2', 'C_2')
School Class Gender Address Height Weight Math Physics
ID
2201 S_2 C_2 M street_5 193 100 39.1 B
2202 S_2 C_2 F street_7 194 77 68.5 B+
2203 S_2 C_2 M street_4 155 91 73.8 A+
2204 S_2 C_2 M street_1 175 74 47.2 B-
2205 S_2 C_2 F street_7 183 76 85.4 B
('S_2', 'C_3')
School Class Gender Address Height Weight Math Physics
ID
2301 S_2 C_3 F street_4 157 78 72.3 B+
2302 S_2 C_3 M street_5 171 88 32.7 A
2303 S_2 C_3 F street_7 190 99 65.9 C
2304 S_2 C_3 F street_6 164 81 95.5 A-
2305 S_2 C_3 M street_4 187 73 48.9 B
('S_2', 'C_4')
School Class Gender Address Height Weight Math Physics
ID
2401 S_2 C_4 F street_2 192 62 45.3 A
2402 S_2 C_4 M street_7 166 82 48.7 B
2403 S_2 C_4 F street_6 158 60 59.7 B+
2404 S_2 C_4 F street_2 160 84 67.7 B
2405 S_2 C_4 F street_6 193 54 47.6 B
4.1.5 level参数和axis参数
grouped = df.set_index(['Gender','School']).groupby(level = 0, axis = 0)
for name, group in grouped:
print(name)
print(group)
F
Class Address Height Weight Math Physics
Gender School
F S_1 C_1 street_2 192 73 32.5 B+
S_1 C_1 street_2 167 81 80.4 B-
S_1 C_1 street_4 159 64 84.8 B+
S_1 C_2 street_4 176 94 63.5 B-
S_1 C_2 street_5 162 63 33.8 B
S_1 C_2 street_6 167 63 68.4 B-
S_1 C_3 street_1 175 57 87.7 A-
S_1 C_3 street_5 187 69 61.7 B-
S_2 C_1 street_6 161 61 50.6 B+
S_2 C_1 street_5 159 97 72.2 B+
S_2 C_2 street_7 194 77 68.5 B+
S_2 C_2 street_7 183 76 85.4 B
S_2 C_3 street_4 157 78 72.3 B+
S_2 C_3 street_7 190 99 65.9 C
S_2 C_3 street_6 164 81 95.5 A-
S_2 C_4 street_2 192 62 45.3 A
S_2 C_4 street_6 158 60 59.7 B+
S_2 C_4 street_2 160 84 67.7 B
S_2 C_4 street_6 193 54 47.6 B
M
Class Address Height Weight Math Physics
Gender School
M S_1 C_1 street_1 173 63 34.0 A+
S_1 C_1 street_2 186 82 87.2 B+
S_1 C_2 street_5 188 68 97.0 A-
S_1 C_2 street_6 160 53 58.8 A+
S_1 C_3 street_4 161 68 31.5 B+
S_1 C_3 street_7 188 82 49.7 B
S_1 C_3 street_2 195 70 85.2 A
S_2 C_1 street_7 174 84 83.3 C
S_2 C_1 street_4 157 61 52.5 B-
S_2 C_1 street_4 170 81 34.2 A
S_2 C_2 street_5 193 100 39.1 B
S_2 C_2 street_4 155 91 73.8 A+
S_2 C_2 street_1 175 74 47.2 B-
S_2 C_3 street_5 171 88 32.7 A
S_2 C_3 street_4 187 73 48.9 B
S_2 C_4 street_7 166 82 48.7 B
4.2 groupby对象的特点
4.2.1 可用的方法
print([attr for attr in dir(grouped_single) if not attr.startswith('_')])
['Address', 'Class', 'Gender', 'Height', 'Math', 'Physics', 'School', 'Weight', 'agg',
'aggregate', 'all', 'any', 'apply', 'backfill', 'bfill', 'boxplot', 'corr', 'corrwith',
'count', 'cov', 'cumcount', 'cummax', 'cummin', 'cumprod', 'cumsum', 'describe', 'diff',
'dtypes', 'expanding', 'ffill', 'fillna', 'filter', 'first', 'get_group', 'groups', 'head',
'hist', 'idxmax', 'idxmin', 'indices', 'last', 'mad', 'max', 'mean', 'median', 'min',
'ndim', 'ngroup', 'ngroups', 'nth', 'nunique', 'ohlc', 'pad', 'pct_change', 'pipe', 'plot',
'prod', 'quantile', 'rank', 'resample', 'rolling', 'sem', 'shift', 'size', 'skew', 'std',
'sum', 'tail', 'take', 'transform', 'tshift', 'var']
4.2.2 head()方法和firtst()方法
head()方法会输出每一组的前几条数据,first()方法会输出每一组的第一条数据。
print(grouped_single.head(2))
School Class Gender Address Height Weight Math Physics
ID
1101 S_1 C_1 M street_1 173 63 34.0 A+
1102 S_1 C_1 F street_2 192 73 32.5 B+
2101 S_2 C_1 M street_7 174 84 83.3 C
2102 S_2 C_1 F street_6 161 61 50.6 B+
print(grouped_single.first())
Class Gender Address Height Weight Math Physics
School
S_1 C_1 M street_1 173 63 34.0 A+
S_2 C_1 M street_7 174 84 83.3 C
4.2.3 自定义分组依据
groupby()分组的依据是非常自由的,只要是与数据框长度相同的列表即可。同时它也支持函数型分组。函数型分组传入的参数是索引,如果是多级索引则传入的是各级索引构成的元组。
# 相当于将np.random.choice(['a','b','c'],df.shape[0])当做新的一列进行分组
print(df.groupby(np.random.choice(['a', 'b', 'c'], \
df.shape[0])).get_group('a').head())
School Class Gender Address Height Weight Math Physics
ID
1301 S_1 C_3 M street_4 161 68 31.5 B+
1302 S_1 C_3 F street_1 175 57 87.7 A-
2102 S_2 C_1 F street_6 161 61 50.6 B+
2103 S_2 C_1 M street_4 157 61 52.5 B-
2204 S_2 C_2 M street_1 175 74 47.2 B-
print(df[:10].groupby(lambda x: print(x)).head())
1101
1102
1103
1104
1105
1201
1202
1203
1204
1205
School Class Gender Address Height Weight Math Physics
ID
1101 S_1 C_1 M street_1 173 63 34.0 A+
1102 S_1 C_1 F street_2 192 73 32.5 B+
1103 S_1 C_1 M street_2 186 82 87.2 B+
1104 S_1 C_1 F street_2 167 81 80.4 B-
1105 S_1 C_1 F street_4 159 64 84.8 B+
df.groupby(lambda x: '奇数行' if not df.index.get_loc(x) % 2 == 1 else '偶数行').groups
{'偶数行': Int64Index([1102, 1104, 1201, 1203, 1205, 1302, 1304, 2101, 2103, 2105, 2202,
2204, 2301, 2303, 2305, 2402, 2404],
dtype='int64', name='ID'),
'奇数行': Int64Index([1101, 1103, 1105, 1202, 1204, 1301, 1303, 1305, 2102, 2104, 2201,
2203, 2205, 2302, 2304, 2401, 2403, 2405],
dtype='int64', name='ID')}
# 此处只是演示groupby的用法,实际操作不会这样写
math_score = df.set_index(['Gender', 'School'])['Math'].sort_index()
grouped_score = df.set_index(['Gender', 'School']) \
.sort_index().groupby(lambda x: (x, '均分及格' \
if math_score[x].mean() >= 60 else '均分不及格'))
for name, group in grouped_score:
print(name)
(('F', 'S_1'), '均分及格')
(('F', 'S_2'), '均分及格')
(('M', 'S_1'), '均分及格')
(('M', 'S_2'), '均分不及格')
4.2.4 groupby()的[]操作
print(df.groupby(['Gender','School'])['Math'].mean() >= 60)
Gender School
F S_1 True
S_2 True
M S_1 True
S_2 False
Name: Math, dtype: bool
print(df.groupby(['Gender','School'])[['Math','Height']].mean())
Math Height
Gender School
F S_1 64.100000 173.125000
S_2 66.427273 173.727273
M S_1 63.342857 178.714286
S_2 51.155556 172.000000
4.2.5 用groupby()对连续型变量进行分组
bins = [0, 40, 60, 80, 90, 100]
cuts = pd.cut(df['Math'], bins = bins)
df.groupby(cuts).size()
Math
(0, 40] 7
(40, 60] 10
(60, 80] 9
(80, 90] 7
(90, 100] 2
dtype: int64
5. groupby()的小伙伴们:agg(), filter(), transform()和apply()方法
5.1 聚合方法agg()
所谓聚合就是把一堆数,变成一个标量。因此,mean/sum/size/count/std/var/sem/describe/first/last/nth/min/max都是聚合函数。
实际上,计算各个统计量的过程就是一种聚合。
5.1.1 同时使用多个聚合函数
group_m = grouped_single['Math']
print(group_m.agg(['sum', 'mean', 'std']))
sum mean std
School
S_1 956.2 63.746667 23.077474
S_2 1191.1 59.555000 17.589305
5.1.2 对聚合结果的列重命名
print(group_m.agg([('rename_sum', 'sum'), ('rename_mean', 'mean')]))
rename_sum rename_mean
School
S_1 956.2 63.746667
S_2 1191.1 59.555000
5.1.3 指定哪些列做哪种聚合
print(grouped_mul.agg({'Math':['mean', 'max'], 'Height':'var'}))
Math Height
mean max var
School Class
S_1 C_1 63.78 87.2 183.3
C_2 64.30 97.0 132.8
C_3 63.16 87.7 179.2
S_2 C_1 58.56 83.3 54.7
C_2 62.80 85.4 256.0
C_3 63.06 95.5 205.7
C_4 53.80 67.7 300.2
5.1.4 自定义聚合函数
agg()方法参数的传入是分组逐列进行的。有了这个特性就可以做许多事情。
grouped_single['Math'].agg(lambda x: print(x.head(), '间隔'))
1101 34.0
1102 32.5
1103 87.2
1104 80.4
1105 84.8
Name: Math, dtype: float64 间隔
2101 83.3
2102 50.6
2103 52.5
2104 72.2
2105 34.2
Name: Math, dtype: float64 间隔
School
S_1 None
S_2 None
Name: Math, dtype: object
print(grouped_single['Math'].agg(lambda x: x.max() - x.min()))
School
S_1 65.5
S_2 62.8
Name: Math, dtype: float64
def R1(x):
return x.max() - x.min()
def R2(x):
return x.max() - x.median()
print(grouped_single['Math'].agg(min_score1 = pd.NamedAgg(column = 'col1', \
aggfunc = R1),
max_score1 = pd.NamedAgg(column = 'col2', \
aggfunc ='max'),
range_score2 = pd.NamedAgg(column = 'col3', \
aggfunc = R2)).head())
min_score1 max_score1 range_score2
School
S_1 65.5 97.0 33.5
S_2 62.8 95.5 39.4
def f(s, low, high):
return s.between(low, high).max()
print(grouped_single['Math'].agg(f, 50, 52))
School
S_1 False
S_2 True
Name: Math, dtype: bool
def f_test(s, low, high):
return s.between(low, high).max()
def agg_f(f_mul,name, *args, **kwargs):
def wrapper(x):
return f_mul(x, *args, **kwargs)
wrapper.__name__ = name
return wrapper
new_f = agg_f(f_test, 'at_least_one_in_50_52', 50, 52)
print(grouped_single['Math'].agg([new_f, 'mean']).head())
at_least_one_in_50_52 mean
School
S_1 False 63.746667
S_2 True 59.555000
5.2 过滤方法filter()
filter()方法是用来筛选某些组的(务必记住结果是组的全体),因此传入的值应当是布尔标量。
print(grouped_single[['Math','Physics']].filter(lambda x: \
(x['Math'] > 32).all()).head())
Math Physics
ID
2101 83.3 C
2102 50.6 B+
2103 52.5 B-
2104 72.2 B+
2105 34.2 A
5.3 变换方法transform()
5.3.1 传入参数
transform()方法传入的参数是组内的列,并且返回值需要与列长完全一致。如果返回了标量值,那么组内的所有元素会被广播为这个值。
print(grouped_single[['Math', 'Height']].head())
print(grouped_single[['Math', 'Height']].transform(lambda x: x - x.min()).head())
Math Height
ID
1101 34.0 173
1102 32.5 192
1103 87.2 186
1104 80.4 167
1105 84.8 159
2101 83.3 174
2102 50.6 161
2103 52.5 157
2104 72.2 159
2105 34.2 170
Math Height
ID
1101 2.5 14
1102 1.0 33
1103 55.7 27
1104 48.9 8
1105 53.3 0
print(grouped_single[['Math', 'Height']].transform(lambda x: x.mean()).head())
Math Height
ID
1101 63.746667 175.733333
1102 63.746667 175.733333
1103 63.746667 175.733333
1104 63.746667 175.733333
1105 63.746667 175.733333
5.3.2 利用变换进行组内标准化
print(grouped_single[['Math','Height']].transform(lambda x: \
(x - x.mean()) / x.std()).head())
Math Height
ID
1101 -1.288991 -0.214991
1102 -1.353990 1.279460
1103 1.016287 0.807528
1104 0.721627 -0.686923
1105 0.912289 -1.316166
5.3.3 利用变换进行组内缺失值处理
df_nan = df[['Math','School']].copy().reset_index()
df_nan.loc[np.random.randint(0, df.shape[0], 25),['Math']] = np.nan
print(df_nan.head())
ID Math School
0 1101 34.0 S_1
1 1102 32.5 S_1
2 1103 NaN S_1
3 1104 NaN S_1
4 1105 84.8 S_1
print(df_nan.groupby('School'). \
transform(lambda x: x.fillna(x.mean())).join(df.reset_index()['School']).head())
ID Math School
0 1101 34.0 S_1
1 1102 32.5 S_1
2 1103 60.0 S_1
3 1104 60.0 S_1
4 1105 84.8 S_1
5.4 应用方法apply()
5.4.1 apply()方法的灵活性
apply()方法应该是最常见的分组方法,这都要得益于它的灵活性。apply函数的灵活性很大程度来源于其返回值的多样性。对于传入值而言,它是以分组后的表传入的(注意不是列,而是表)。
df.groupby('School').apply(lambda x: print(x.head(1)))
School Class Gender Address Height Weight Math Physics
ID
1101 S_1 C_1 M street_1 173 63 34.0 A+
School Class Gender Address Height Weight Math Physics
ID
2101 S_2 C_1 M street_7 174 84 83.3 C
print(df[['School', 'Math', 'Height']].groupby('School').apply(lambda x: x.max()))
School Math Height
School
S_1 S_1 97.0 195
S_2 S_2 95.5 194
print(df[['School', 'Math', 'Height']]. \
groupby('School').apply(lambda x: x - x.min()).head())
Math Height
ID
1101 2.5 14.0
1102 1.0 33.0
1103 55.7 27.0
1104 48.9 8.0
1105 53.3 0.0
print(df[['School','Math','Height']].groupby('School')\
.apply(lambda x:pd.DataFrame({'col1':x['Math']-x['Math'].max(),
'col2':x['Math']-x['Math'].min(),
'col3':x['Height']-x['Height'].max(),
'col4':x['Height']-x['Height'].min()})).head())
col1 col2 col3 col4
ID
1101 -63.0 2.5 -22 14
1102 -64.5 1.0 -3 33
1103 -9.8 55.7 -9 27
1104 -16.6 48.9 -28 8
1105 -12.2 53.3 -36 0
5.4.2 用apply()方法同时统计多个统计量
此处可以借助OrderedDict工具进行快捷的统计。
from collections import OrderedDict
def f(df):
data = OrderedDict()
data['M_sum'] = df['Math'].sum()
data['W_var'] = df['Weight'].var()
data['H_mean'] = df['Height'].mean()
return pd.Series(data)
print(grouped_single.apply(f))
M_sum W_var H_mean
School
S_1 956.2 117.428571 175.733333
S_2 1191.1 181.081579 172.950000
6. 问题与练习
6.1 问题
【问题一】 什么是fillna的前向/后向填充,如何实现?
答案见[1]。
【问题二】 下面的代码实现了什么功能?请仿照设计一个它的groupby版本。
s = pd.Series ([0, 1, 1, 0, 1, 1, 1, 0])
s1 = s.cumsum()
result = s.mul(s1).diff().where(lambda x: x < 0).ffill().add(s1,fill_value = 0)
print(result)
0 0.0
1 1.0
2 2.0
3 0.0
4 1.0
5 2.0
6 3.0
7 0.0
dtype: float64
实现了对累计连续为1的数量的统计。
s1 = (-s+1).cumsum()
result = s.groupby(s1).cumsum()
result
【问题三】 如何计算组内0.25分位数与0.75分位数?要求显示在同一张表上。
def R1(x):
return np.percentile(x, 25)
def R2(x):
return np.percentile(x, 75)
print(grouped_single.agg(percent_25 = pd.NamedAgg(column = 'Math', aggfunc = R1),
percent_75 = pd.NamedAgg(column = 'Math', aggfunc = R2)))
【问题四】 既然索引已经能够选出某些符合条件的子集,那么filter函数的设计有什么意义?
filter()方法可以筛选满足条件的组,而符合条件的数据未必是同一组的。
【问题五】 整合、变换、过滤三者在输入输出和功能上有何异同?
聚合输入每一个组的多个列并对每个列做不同的聚合操作,输出多是一个标量;
变换输入每一个组的多个列并对每个列做同一个变换操作,输出是一个与输入的组等长的序列或者表格;
过滤输入由每一个组的多个列得到的布尔函数(变量),输出一个与输入的组等长的布尔序列。
【问题六】 在带参数的多函数聚合时,有办法能够绕过wrap技巧实现同样功能吗?
改写多参数函数为单参数函数,将原参数设置为全局变量。
6.2 练习
略。