数据聚合
对于聚合,我指的是任何能够从数组产生标量值的数据转换过程。之前的例子中我已经用过一些,比如mean、count、min以及sum等。你可能想知道在GroupBy对象上调用mean()时究竟发生了什么。许多常见的聚合运算(如表9-1所示)都有就地计算数据集统计信息的优化实现。然而,并不是只能使用这些方法。你可以使用自己发明的聚合运算,还可以调用分组对象上已经定义好的任何方法。例如,quantile可以计算Series或DataFrame列的样本分位数译注2:
- In [54]: df
- Out[54]:
- data1 data2 key1 key2
- 0 -0.204708 1.393406 a one
- 1 0.478943 0.092908 a two
- 2 -0.519439 0.281746 b one
- 3 -0.555730 0.769023 b two
- 4 1.965781 1.246435 a one
- In [55]: grouped = df.groupby('key1')
- In [56]: grouped['data1'].quantile(0.9)
- Out[56]:
- key1
- a 1.668413
- b -0.523068
虽然quantile并没有明确地实现于GroupBy,但它是一个Series方法,所以这里是能用的。实际上,GroupBy会高效地对Series进行切片,然后对各片调用piece.quantile(0.9),最后将这些结果组装成最终结果。
如果要使用你自己的聚合函数,只需将其传入aggregate或agg方法即可:
- In [57]: def peak_to_peak(arr):
- ...: return arr.max() - arr.min()
- In [58]: grouped.agg(peak_to_peak)
- Out[58]:
- data1 data2
- key1
- a 2.170488 1.300498
- b 0.036292 0.487276
注意,有些方法(如describe)也是可以用在这里的,即使严格来讲,它们并非聚合运算:
- In [59]: grouped.describe()
- Out[59]:
- data1 data2
- key1
- a count 3.000000 3.000000
- mean 0.746672 0.910916
- std 1.109736 0.712217
- min -0.204708 0.092908
- 25% 0.137118 0.669671
- 50% 0.478943 1.246435
- 75% 1.222362 1.319920
- max 1.965781 1.393406
- b count 2.000000 2.000000
- mean -0.537585 0.525384
- std 0.025662 0.344556
- min -0.555730 0.281746
- 25% -0.546657 0.403565
- 50% -0.537585 0.525384
- 75% -0.528512 0.647203
- max -0.519439 0.769023
在后面关于分组级运算译注3和转换的那一节中,我将详细说明这到底是怎么回事。
注意: 可能你已经注意到了,自定义聚合函数要比表9-1中那些经过优化的函数慢得多。这是因为在构造中间分组数据块时存在非常大的开销(函数调用、数据重排等)。
译注4:这里应该是“经过优化的GroupBy的方法”,原文有误。
为了说明一些更高级的聚合功能,我将使用一个有关餐馆小费的数据集。我是在R语言的reshape2包中得到该数据集的(可以在本书的GitHub库中找到)。它最初出现于Bryant和Smith在1995年编写的一本有关商业统计的书中。通过read_csv将其加载之后,我添加了一个表示小费比例的列tip_pct。
- In [60]: tips = pd.read_csv('ch08/tips.csv')
- # 添加“小费占总额百分比”的列
- In [61]: tips['tip_pct'] = tips['tip'] / tips['total_bill']
- In [62]: tips[:6]
- Out[62]:
- total_bill tip sex smoker day time size tip_pct
- 0 16.99 1.01 Female False Sun Dinner 2 0.059447
- 1 10.34 1.66 Male False Sun Dinner 3 0.160542
- 2 21.01 3.50 Male False Sun Dinner 3 0.166587
- 3 23.68 3.31 Male False Sun Dinner 2 0.139780
- 4 24.59 3.61 Female False Sun Dinner 4 0.146808
- 5 25.29 4.71 Male False Sun Dinner 4 0.186240
面向列的多函数应用
我们已经看到,对Series或DataFrame列的聚合运算其实就是使用aggregate(使用自定义函数)或调用诸如mean、std之类的方法。然而,你可能希望对不同的列使用不同的聚合函数,或一次应用多个函数。其实这事也好办,我将通过一些示例来进行讲解。首先,我根据sex和smoker对tips进行分组:
- In [63]: grouped = tips.groupby(['sex', 'smoker'])
注意,对于表9-1中的那些描述统计,可以将函数名以字符串的形式传入:
- In [64]: grouped_pct = grouped['tip_pct']
- In [65]: grouped_pct.agg('mean')
- Out[65]:
- sex smoker
- Female False 0.156921
- True 0.182150
- Male False 0.160669
- True 0.152771
- Name: tip_pct
如果传入一组函数或函数名,得到的DataFrame的列就会以相应的函数命名:
- In [66]: grouped_pct.agg(['mean', 'std', peak_to_peak])
- Out[66]:
- mean std peak_to_peak
- sex smoker
- Female False 0.156921 0.036421 0.195876
- True 0.182150 0.071595 0.360233
- Male False 0.160669 0.041849 0.220186
- True 0.152771 0.090588 0.674707
你并非一定要接受GroupBy自动给出的那些列名,特别是lambda函数,它们的名称是'<lambda>',这样的辨识度就很低了(通过函数的name属性看看就知道了)。如果传入的是一个由(name,function)元组组成的列表,则各元组的第一个元素就会被用作DataFrame的列名(可以将这种二元元组列表看做一个有序映射):
- In [67]: grouped_pct.agg([('foo', 'mean'), ('bar', np.std)])
- Out[67]:
- foo bar
- sex smoker
- Female False 0.156921 0.036421
- True 0.182150 0.071595
- Male False 0.160669 0.041849
- True 0.152771 0.090588
对于DataFrame,你还可以定义一组应用于全部列的函数,或不同的列应用不同的函数。假设我们想要对tip_pct和total_bill列计算三个统计信息:
- In [68]: functions = ['count', 'mean', 'max']
- In [69]: result = grouped['tip_pct', 'total_bill'].agg(functions)
- In [70]: result
- Out[70]:
- tip_pct total_bill
- count mean max count mean max
- sex smoker
- Female False 54 0.156921 0.252672 54 18.105185 35.83
- True 33 0.182150 0.416667 33 17.977879 44.30
- Male False 97 0.160669 0.291990 97 19.791237 48.33
- True 60 0.152771 0.710345 60 22.284500 50.81
如你所见,结果DataFrame拥有层次化的列,这相当于分别对各列进行聚合,然后用concat将结果组装到一起(列名用作keys参数)。
- In [71]: result['tip_pct']
- Out[71]:
- count mean max
- sex smoker
- Female False 54 0.156921 0.252672
- True 33 0.182150 0.416667
- Male False 97 0.160669 0.291990
- True 60 0.152771 0.710345
跟前面一样,这里也可以传入带有自定义名称的元组列表:
- In [72]: ftuples = [('Durchschnitt', 'mean'), ('Abweichung', np.var)]
- In [73]: grouped['tip_pct', 'total_bill'].agg(ftuples)
- Out[73]:
- tip_pct total_bill
- Durchschnitt Abweichung Durchschnitt Abweichung
- sex smoker
- Female False 0.156921 0.001327 18.105185 53.092422
- True 0.182150 0.005126 17.977879 84.451517
- Male False 0.160669 0.001751 19.791237 76.152961
- True 0.152771 0.008206 22.284500 98.244673
现在,假设你想要对不同的列应用不同的函数。具体的办法是向agg传入一个从列名映射到函数的字典:
- In [74]: grouped.agg({'tip' : np.max, 'size' : 'sum'})
- Out[74]:
- size tip
- sex smoker
- Female False 140 5.2
- True 74 6.5
- Male False 263 9.0
- True 150 10.0
- In [75]: grouped.agg({'tip_pct' : ['min', 'max', 'mean', 'std'],
- ...: 'size' : 'sum'})
- Out[75]:
- tip_pct size
- min max mean std sum
- sex smoker
- Female False 0.056797 0.252672 0.156921 0.036421 140
- True 0.056433 0.416667 0.182150 0.071595 74
- Male False 0.071804 0.291990 0.160669 0.041849 263
- True 0.035638 0.710345 0.152771 0.090588 150
只有将多个函数应用到至少一列时,DataFrame才会拥有层次化的列。
以“无索引”的形式返回聚合数据
到目前为止,所有示例中的聚合数据都有由唯一的分组键组成的索引(可能还是层次化的)。由于并不总是需要如此,所以你可以向groupby传入as_index=False以禁用该功能:
- In [76]: tips.groupby(['sex', 'smoker'], as_index=False).mean()
- Out[76]:
- sex smoker total_bill tip size tip_pct
- 0 Female False 18.105185 2.773519 2.592593 0.156921
- 1 Female True 17.977879 2.931515 2.242424 0.182150
- 2 Male False 19.791237 3.113402 2.711340 0.160669
- 3 Male True 22.284500 3.051167 2.500000 0.152771
当然,对结果调用reset_index也能得到这种形式的结果。
警告: groupby的这种用法比较缺乏灵活性。
译注2:注意,如果传入的百分位上没有值,则quantile会进行线性插值。
译注3:也就是“面向分组”的计算。