数据分组聚合
分组
基本使用
>>> df = pd.DataFrame({'key1' : ['a', 'a', 'b', 'b', 'a'],
'key2' : ['one', 'two', 'one', 'two', 'one'],
'data1' : np.random.randn(5),
'data2' : np.random.randn(5)})
>>> df
key1 key2 data1 data2
0 a one -0.606091 -1.181332
1 a two -0.866639 0.300651
2 b one -1.218590 1.054789
3 b two -1.984462 -1.148834
4 a one 0.187622 0.857184
按 key1 进行分组,并计算 data1 列的平均值。
>>> grouped = df['data1'].groupby(df['key1'])
>>> grouped
<pandas.core.groupby.generic.SeriesGroupBy object at 0x7f1fea017130>
变量 grouped 是一个 GroupBy 对象。它实际上还没有进行任何计算,只是含有一些有关分组键df['key1'] 的中间数据而已。换句话说,该对象已经有了接下来对各分组执行运算所需的一切信息。
例如,我们可以调用 GroupBy 的 mean 方法来计算分组平均值:
>>> grouped.mean()
key1
a -0.428369
b -1.601526
Name: data1, dtype: float64
按照多个 key 进行分组,得到结果包含层次化索引:
>>> means = df['data1'].groupby([df['key1'], df['key2']]).mean()
>>> means
key1 key2
a one -0.209234
two -0.866639
b one -1.218590
two -1.984462
Name: data1, dtype: float64
>>> means.unstack()
key2 one two
key1
a -0.209234 -0.866639
b -1.218590 -1.984462
上面的例子里选取的分组键为 DataFrame 中的列,实际上可以选择任意的数组作为分组键,但数组长度要和 DataFrame 一致:
>>> states = np.array(['Ohio', 'California', 'California', 'Ohio', 'Ohio'])
>>> years = np.array([2005, 2005, 2006, 2005, 2006])
>>> df['data1'].groupby([states, years]).mean()
California 2005 -0.866639
2006 -1.218590
Ohio 2005 -1.295276
2006 0.187622
Name: data1, dtype: float64
通常,可以将 DataFrame 中的列名直接作为分组键:
>>> df.groupby('key1').mean()
data1 data2
key1
a -0.428369 -0.007833
b -1.601526 -0.047023
>>> df.groupby(['key1', 'key2']).mean()
data1 data2
key1 key2
a one -0.209234 -0.162074
two -0.866639 0.300651
b one -1.218590 1.054789
two -1.984462 -1.148834
第一个例子在执行 df.groupby('key1').mean() 时,结果中没有 key2 列,这是因为 df['key2'] 不是数值数据(俗称“麻烦列”),所以被从结果中排除了。
无论你准备拿 groupby 做什么,都有可能会用到 GroupBy 的 size 方法,它可以返回一个含有分组大小的 Series:
>>> df.groupby(['key1', 'key2']).size()
key1 key2
a one 2
two 1
b one 1
two 1
dtype: int64
对分组进行迭代
GroupBy 对象支持迭代,可以产生一组二元元组(由分组名和数据块组成)。
>>> for name, group in df.groupby('key1'):
... print(name)
... print(group)
a
key1 key2 data1 data2
0 a one -0.606091 -1.181332
1 a two -0.866639 0.300651
4 a one 0.187622 0.857184
b
key1 key2 data1 data2
2 b one -1.218590 1.054789
3 b two -1.984462 -1.148834
对于多重键的情况,元组的第一个元素将会是由键值组成的元组:
>>> for (k1, k2), group in df.groupby(['key1', 'key2']):
... print((k1, k2))
... print(group)
('a', 'one')
key1 key2 data1 data2
0 a one -0.606091 -1.181332
4 a one 0.187622 0.857184
('a', 'two')
key1 key2 data1 data2
1 a two -0.866639 0.300651
('b', 'one')
key1 key2 data1 data2
2 b one -1.21859 1.054789
('b', 'two')
key1 key2 data1 data2
3 b two -1.984462 -1.148834
分组数据可转化为字典:
>>> pieces = dict(list(df.groupby('key1')))
>>> pieces
{'a': key1 key2 data1 data2
0 a one -0.606091 -1.181332
1 a two -0.866639 0.300651
4 a one 0.187622 0.857184,
'b': key1 key2 data1 data2
2 b one -1.218590 1.054789
3 b two -1.984462 -1.148834}
groupby 默认是在 axis=0 上进行分组的,通过设置也可以在其他任何轴上进行分组。例如可以根据 dtype 对列进行分组:
>>> df.dtypes
key1 object
key2 object
data1 float64
data2 float64
dtype: object
>>> grouped = df.groupby(df.dtypes, axis=1)
>>> for dtype, group in grouped:
... print(dtype)
... print(group)
float64
data1 data2
0 -0.606091 -1.181332
1 -0.866639 0.300651
2 -1.218590 1.054789
3 -1.984462 -1.148834
4 0.187622 0.857184
object
key1 key2
0 a one
1 a two
2 b one
3 b two
4 a one
选取一列或列的子集
对于由 DataFrame 产生的 GroupBy 对象,如果用一个(单个字符串)或一组(字符串数组)列名对其进行索引,就能实现选取部分列进行聚合的目的。也就是说:
>>> df.groupby('key1')['data1']
>>> df.groupby('key1')[['data2']]
是以下代码的语法糖:
>>> df['data1'].groupby(df['key1'])
>>> df[['data2']].groupby(df['key1'])
尤其对于大数据集,很可能只需要对部分列进行聚合。例如,在前面那个数据集中,如果只需计算data2 列的平均值并以 DataFrame 形式得到结果,可以这样写:
>>> df.groupby(['key1', 'key2'])[['data2']].mean()
data2
key1 key2
a one -0.162074
two 0.300651
b one 1.054789
two -1.148834
如果传入的是列表或数组,操作返回的对象是一个已分组的 DataFrame;如果传入的是单个列名,操作返回的是已分组的 Series。
>>> s_grouped = df.groupby(['key1', 'key2'])['data2']
>>> s_grouped.mean()
key1 key2
a one -0.162074
two 0.300651
b one 1.054789
two -1.148834
Name: data2, dtype: float64
通过映射进行分组
>>> people = pd.DataFrame(np.random.randn(5, 5),
columns=['a', 'b', 'c', 'd', 'e'],
index=['Joe', 'Steve', 'Wes', 'Jim', 'Travis'])
>>> people.iloc[2:3, [1, 2]] = np.nan
>>> people
a b c d e
Joe -0.107592 -0.112039 1.446168 0.343354 0.279940
Steve -0.906023 -0.052071 -1.422474 -0.492246 0.626603
Wes 0.026861 NaN NaN 1.002414 -0.736593
Jim 0.193563 -1.654297 -0.932921 -0.618760 0.237360
Travis 0.710881 0.189908 -0.677636 0.513925 1.178200
现在,假设已知列的分组关系,并希望根据分组计算列的和:
>>> mapping = {'a': 'red', 'b': 'red', 'c': 'blue','d': 'blue', 'e': 'red', 'f' : 'orange'}
>>> by_column = people.groupby(mapping, axis=1)
>>> by_column.sum()
blue red
Joe 1.789522 0.060309
Steve -1.914719 -0.331491
Wes 1.002414 -0.709732
Jim -1.551681 -1.223375
Travis -0.163711 2.078988
Series 也可以作为映射 mapping :
>>> map_series = pd.Series(mapping)
>>> map_series
a red
b red
c blue
d blue
e red
f orange
dtype: object
>>> people.groupby(map_series, axis=1).count()
blue red
Joe 2 3
Steve 2 3
Wes 1 2
Jim 2 3
Travis 2 3
通过函数进行分组
比起使用字典或 Series,使用 Python 函数是一种更原生的方法定义分组映射。
>>> people.groupby(len).sum()
a b c d e
3 0.112831 -1.766336 0.513247 0.727009 -0.219293
5 -0.906023 -0.052071 -1.422474 -0.492246 0.626603
6 0.710881 0.189908 -0.677636 0.513925 1.178200
任何被当做分组键的函数都会在各个索引值上被调用一次,其返回值就会被用作分组名称。
将函数跟数组、列表、字典、Series 混合使用也不是问题,因为任何东西在内部都会被转换为数组:
>>> key_list = ['one', 'one', 'one', 'two', 'two']
>>> people.groupby([len, key_list]).min()
a b c d e
3 one -0.107592 -0.112039 1.446168 0.343354 -0.736593
two 0.193563 -1.654297 -0.932921 -0.618760 0.237360
5 one -0.906023 -0.052071 -1.422474 -0.492246 0.626603
6 two 0.710881 0.189908 -0.677636 0.513925 1.178200
根据索引级别分组
层次化索引数据集最方便的地方就在于它能够根据轴索引的一个级别进行聚合:
>>> columns = pd.MultiIndex.from_arrays([['US', 'US', 'US', 'JP', 'JP'],
[1, 3, 5, 1, 3]],
names=['cty', 'tenor'])
>>> hier_df = pd.DataFrame(np.random.randn(4, 5), columns=columns)
>>> hier_df
cty US JP
tenor 1 3 5 1 3
0 1.075830 -0.301233 0.624428 -1.761233 2.263103
1 -0.445027 -0.564059 -0.475415 -0.914696 -1.403103
2 -0.865851 -0.008406 0.938678 1.165307 -0.164514
3 -0.012107 0.098953 2.749072 -0.707615 -0.395376
要根据级别分组,使用 level 关键字传递级别序号或名字:
>>> hier_df.groupby(level='cty', axis=1).count()
cty JP US
0 2 3
1 2 3
2 2 3
3 2 3
聚合
基本使用
聚合指的是任何能够从数组产生标量值的数据转换过程。之前的例子已经用过一些,比如 mean、count、min 以及 sum 等。
函数名 | 说明 |
---|---|
count | 分组中非 NA 值的数量 |
sum | 非 NA 值的和 |
mean | 非 NA 值的平均值 |
median | 非 NA 值的算术中位数 |
std、var | 无偏(分母为 n-1)标准差和方差 |
min、max | 非 NA 值的最小值和最大值 |
prod | 非 NA 值的积 |
first、last | 第一个和最后一个非 NA 值 |
如果要使用自己定义的聚合函数,只需将其传入 aggregate 或 agg 方法即可:
>>> df
key1 key2 data1 data2
0 a one -0.606091 -1.181332
1 a two -0.866639 0.300651
2 b one -1.218590 1.054789
3 b two -1.984462 -1.148834
4 a one 0.187622 0.857184
>>> grouped = df.groupby('key1')
>>> def peak_to_peak(arr):
... return arr.max() - arr.min()
>>> grouped.agg(peak_to_peak)
data1 data2
key1
a 1.054261 2.038516
b 0.765872 2.203622
>>> grouped.describe()
data1 ... data2
count mean std min 25% ... min 25% 50% 75% max
key1 ...
a 3.0 -0.428369 0.549141 -0.866639 -0.736365 ... -1.181332 -0.440341 0.300651 0.578917 0.857184
b 2.0 -1.601526 0.541553 -1.984462 -1.792994 ... -1.148834 -0.597928 -0.047023 0.503883 1.054789
[2 rows x 16 columns]
不同列使用不同的函数
>>> tips = pd.read_csv('examples/tips.csv')
>>> tips['tip_pct'] = tips['tip'] / tips['total_bill']
>>> tips[:6]
total_bill tip smoker day time size tip_pct
0 16.99 1.01 No Sun Dinner 2 0.059447
1 10.34 1.66 No Sun Dinner 3 0.160542
2 21.01 3.50 No Sun Dinner 3 0.166587
3 23.68 3.31 No Sun Dinner 2 0.139780
4 24.59 3.61 No Sun Dinner 4 0.146808
5 25.29 4.71 No Sun Dinner 4 0.186240
首先,根据 day 和 smoker 对 tips 进行分组:
>>> grouped = tips.groupby(['day', 'smoker'])
对于常见聚合函数,可以将函数名以字符串的形式传入。
>>> grouped_pct = grouped['tip_pct']
>>> grouped_pct.agg('mean')
day smoker
Fri No 0.151650
Yes 0.174783
Sat No 0.158048
Yes 0.147906
Sun No 0.160113
Yes 0.187250
Thur No 0.160298
Yes 0.163863
Name: tip_pct, dtype: float64
如果传入一组函数或函数名,得到的 DataFrame 的列就会以相应的函数命名:
>>> grouped_pct.agg(['mean', 'std', peak_to_peak])
mean std peak_to_peak
day smoker
Fri No 0.151650 0.028123 0.067349
Yes 0.174783 0.051293 0.159925
Sat No 0.158048 0.039767 0.235193
Yes 0.147906 0.061375 0.290095
Sun No 0.160113 0.042347 0.193226
Yes 0.187250 0.154134 0.644685
Thur No 0.160298 0.038774 0.193350
Yes 0.163863 0.039389 0.151240
你并非一定要接受 GroupBy 自动给出的那些列名,特别是 lambda 函数。因此,如果传入的是一个由 (name,function) 元组组成的列表,则各元组的第一个元素就会被用作 DataFrame 的列名(可以将这种二元元组列表看做一个有序映射)。
>>> grouped_pct.agg([('foo', 'mean'), ('bar', np.std)])
foo bar
day smoker
Fri No 0.151650 0.028123
Yes 0.174783 0.051293
Sat No 0.158048 0.039767
Yes 0.147906 0.061375
Sun No 0.160113 0.042347
Yes 0.187250 0.154134
Thur No 0.160298 0.038774
Yes 0.163863 0.039389
对于分组为 DataFrame 时,你可以定义一组应用于全部列的一组函数,或不同的列应用不同的函数。假设我们想要对 tip_pct 和 total_bill 列计算三个统计信息:
>>> functions = ['count', 'mean', 'max']
>>> result = grouped[['tip_pct', 'total_bill']].agg(functions)
>>> result
tip_pct total_bill
count mean max count mean max
day smoker
Fri No 4 0.151650 0.187735 4 18.420000 22.75
Yes 15 0.174783 0.263480 15 16.813333 40.17
Sat No 45 0.158048 0.291990 45 19.661778 48.33
Yes 42 0.147906 0.325733 42 21.276667 50.81
Sun No 57 0.160113 0.252672 57 20.506667 48.17
Yes 19 0.187250 0.710345 19 24.120000 45.35
Thur No 45 0.160298 0.266312 45 17.113111 41.19
Yes 17 0.163863 0.241255 17 19.190588 43.11
也可以传入带有自定义名称的一组元组:
>>> ftuples = [('Durchschnitt', 'mean'),('Abweichung', np.var)]
>>> grouped[['tip_pct', 'total_bill']].agg(ftuples)
tip_pct total_bill
Durchschnitt Abweichung Durchschnitt Abweichung
day smoker
Fri No 0.151650 0.000791 18.420000 25.596333
Yes 0.174783 0.002631 16.813333 82.562438
Sat No 0.158048 0.001581 19.661778 79.908965
Yes 0.147906 0.003767 21.276667 101.387535
Sun No 0.160113 0.001793 20.506667 66.099980
Yes 0.187250 0.023757 24.120000 109.046044
Thur No 0.160298 0.001503 17.113111 59.625081
Yes 0.163863 0.001551 19.190588 69.808518
如果想要对一个列或不同的列应用不同的函数,则向 agg 传入一个从列名映射到函数的字典:
>>> grouped.agg({'tip' : np.max, 'size' : 'sum'})
tip size
day smoker
Fri No 3.50 9
Yes 4.73 31
Sat No 9.00 115
Yes 10.00 104
Sun No 6.00 167
Yes 6.50 49
Thur No 6.70 112
Yes 5.00 40
>>> grouped.agg({'tip_pct' : ['min', 'max', 'mean', 'std'],'size' : 'sum'})
tip_pct size
min max mean std sum
day smoker
Fri No 0.120385 0.187735 0.151650 0.028123 9
Yes 0.103555 0.263480 0.174783 0.051293 31
Sat No 0.056797 0.291990 0.158048 0.039767 115
Yes 0.035638 0.325733 0.147906 0.061375 104
Sun No 0.059447 0.252672 0.160113 0.042347 167
Yes 0.065660 0.710345 0.187250 0.154134 49
Thur No 0.072961 0.266312 0.160298 0.038774 112
Yes 0.090014 0.241255 0.163863 0.039389 40
到目前为止,所有示例中的聚合数据都有由唯一的分组键组成的索引(可能还是层次化的)。由于并不总是需要如此,所以你可以向 groupby 传入 as_index=False 以禁用该功能:
>>> tips.groupby(['day', 'smoker'], as_index=False).mean()
day smoker total_bill tip size tip_pct
0 Fri No 18.420000 2.812500 2.250000 0.151650
1 Fri Yes 16.813333 2.714000 2.066667 0.174783
2 Sat No 19.661778 3.102889 2.555556 0.158048
3 Sat Yes 21.276667 2.875476 2.476190 0.147906
4 Sun No 20.506667 3.167895 2.929825 0.160113
5 Sun Yes 24.120000 3.516842 2.578947 0.187250
6 Thur No 17.113111 2.673778 2.488889 0.160298
7 Thur Yes 19.190588 3.030000 2.352941 0.163863
apply
回到之前那个小费数据集,假设你想要根据分组选出最高的 5 个 tip_pct 值。首先,编写一个选取指定列具有最大值的行的函数:
>>> def top(df, n=5, column='tip_pct'):
... return df.sort_values(by=column)[-n:]
...
>>> top(tips, n=6)
total_bill tip smoker day time size tip_pct
109 14.31 4.00 Yes Sat Dinner 2 0.279525
183 23.17 6.50 Yes Sun Dinner 4 0.280535
232 11.61 3.39 No Sat Dinner 2 0.291990
67 3.07 1.00 Yes Sat Dinner 1 0.325733
178 9.60 4.00 Yes Sun Dinner 2 0.416667
172 7.25 5.15 Yes Sun Dinner 2 0.710345
现在,如果对 smoker 分组并用该函数调用 apply,就会得到:
>>> tips.groupby('smoker').apply(top)
total_bill tip smoker day time size tip_pct
smoker
No 88 24.71 5.85 No Thur Lunch 2 0.236746
185 20.69 5.00 No Sun Dinner 5 0.241663
51 10.29 2.60 No Sun Dinner 2 0.252672
149 7.51 2.00 No Thur Lunch 2 0.266312
232 11.61 3.39 No Sat Dinner 2 0.291990
Yes 109 14.31 4.00 Yes Sat Dinner 2 0.279525
183 23.17 6.50 Yes Sun Dinner 4 0.280535
67 3.07 1.00 Yes Sat Dinner 1 0.325733
178 9.60 4.00 Yes Sun Dinner 2 0.416667
172 7.25 5.15 Yes Sun Dinner 2 0.710345
top 函数在 DataFrame 的各个片段上调用,然后结果由 pandas.concat 组装到一起,并以分组名称进行了标记。于是,最终结果就有了一个层次化索引,其内层索引值来自原 DataFrame。
如果传给 apply 的函数能够接受其他参数或关键字,则可以将这些内容放在函数名后面一并传入:
>>> tips.groupby(['smoker', 'day']).apply(top, n=1, column='total_bill')
total_bill tip smoker day time size tip_pct
smoker day
No Fri 94 22.75 3.25 No Fri Dinner 2 0.142857
Sat 212 48.33 9.00 No Sat Dinner 4 0.186220
Sun 156 48.17 5.00 No Sun Dinner 6 0.103799
Thur 142 41.19 5.00 No Thur Lunch 5 0.121389
Yes Fri 95 40.17 4.73 Yes Fri Dinner 4 0.117750
Sat 170 50.81 10.00 Yes Sat Dinner 3 0.196812
Sun 182 45.35 3.50 Yes Sun Dinner 3 0.077178
Thur 197 43.11 5.00 Yes Thur Lunch 4 0.115982