10.2 数据聚合

聚合指的是任何能够从数组产生标量值的数据转换过程。之前的例子已经用过一些,比如mean、count、min以及sum等。你可能想知道在GroupBy对象上调用mean()时究竟发生了什么。许多常见的聚合运算(如表10-1所示)都有进行优化。然而,除了这些方法,你还可以使用其它的。

表10-1 经过优化的groupby方法

你可以使用自己发明的聚合运算,还可以调用分组对象上已经定义好的任何方法。例如,quantile可以计算Series或DataFrame列的样本分位数。

虽然quantile并没有明确地实现于GroupBy,但它是一个Series方法,所以这里是能用的。实际上,GroupBy会高效地对Series进行切片,然后对各片调用piece.quantile(0.9),最后将这些结果组装成最终结果:

  1. In [51]: df
  2. Out[51]:
  3. data1 data2 key1 key2
  4. 0 -0.204708 1.393406 a one
  5. 1 0.478943 0.092908 a two
  6. 2 -0.519439 0.281746 b one
  7. 3 -0.555730 0.769023 b two
  8. 4 1.965781 1.246435 a one
  9. In [52]: grouped = df.groupby('key1')
  10. In [53]: grouped['data1'].quantile(0.9)
  11. Out[53]:
  12. key1
  13. a 1.668413
  14. b -0.523068
  15. Name: data1, dtype: float64

如果要使用你自己的聚合函数,只需将其传入aggregate或agg方法即可:

  1. In [54]: def peak_to_peak(arr):
  2. ....: return arr.max() - arr.min()
  3. In [55]: grouped.agg(peak_to_peak)
  4. Out[55]:
  5. data1 data2
  6. key1
  7. a 2.170488 1.300498
  8. b 0.036292 0.487276

你可能注意到注意,有些方法(如describe)也是可以用在这里的,即使严格来讲,它们并非聚合运算:

  1. In [56]: grouped.describe()
  2. Out[56]:
  3. data1 \
  4. count mean std min 25% 50% 75%
  5. key1
  6. a 3.0 0.746672 1.109736 -0.204708 0.137118 0.478943 1.222362
  7. b 2.0 -0.537585 0.025662 -0.555730 -0.546657 -0.537585 -0.528512
  8. data2 \
  9. max count mean std min 25% 50%
  10. key1
  11. a 1.965781 3.0 0.910916 0.712217 0.092908 0.669671 1.246435
  12. b -0.519439 2.0 0.525384 0.344556 0.281746 0.403565 0.525384
  13. 75% max
  14. key1
  15. a 1.319920 1.393406
  16. b 0.647203 0.769023

在后面的10.3节,我将详细说明这到底是怎么回事。

笔记:自定义聚合函数要比表10-1中那些经过优化的函数慢得多。这是因为在构造中间分组数据块时存在非常大的开销(函数调用、数据重排等)。

面向列的多函数应用

回到前面小费的例子。使用read_csv导入数据之后,我们添加了一个小费百分比的列tip_pct:

  1. In [57]: tips = pd.read_csv('examples/tips.csv')
  2. # Add tip percentage of total bill
  3. In [58]: tips['tip_pct'] = tips['tip'] / tips['total_bill']
  4. In [59]: tips[:6]
  5. Out[59]:
  6. total_bill tip smoker day time size tip_pct
  7. 0 16.99 1.01 No Sun Dinner 2 0.059447
  8. 1 10.34 1.66 No Sun Dinner 3 0.160542
  9. 2 21.01 3.50 No Sun Dinner 3 0.166587
  10. 3 23.68 3.31 No Sun Dinner 2 0.139780
  11. 4 24.59 3.61 No Sun Dinner 4 0.146808
  12. 5 25.29 4.71 No Sun Dinner 4 0.186240

你已经看到,对Series或DataFrame列的聚合运算其实就是使用aggregate(使用自定义函数)或调用诸如mean、std之类的方法。然而,你可能希望对不同的列使用不同的聚合函数,或一次应用多个函数。其实这也好办,我将通过一些示例来进行讲解。首先,我根据天和smoker对tips进行分组:

  1. In [60]: grouped = tips.groupby(['day', 'smoker'])

注意,对于表10-1中的那些描述统计,可以将函数名以字符串的形式传入:

  1. In [61]: grouped_pct = grouped['tip_pct']
  2. In [62]: grouped_pct.agg('mean')
  3. Out[62]:
  4. day smoker
  5. Fri No 0.151650
  6. Yes 0.174783
  7. Sat No 0.158048
  8. Yes 0.147906
  9. Sun No 0.160113
  10. Yes 0.187250
  11. Thur No 0.160298
  12. Yes 0.163863
  13. Name: tip_pct, dtype: float64

如果传入一组函数或函数名,得到的DataFrame的列就会以相应的函数命名:

  1. In [63]: grouped_pct.agg(['mean', 'std', peak_to_peak])
  2. Out[63]:
  3. mean std peak_to_peak
  4. day smoker
  5. Fri No 0.151650 0.028123 0.067349
  6. Yes 0.174783 0.051293 0.159925
  7. Sat No 0.158048 0.039767 0.235193
  8. Yes 0.147906 0.061375 0.290095
  9. Sun No 0.160113 0.042347 0.193226
  10. Yes 0.187250 0.154134 0.644685
  11. Thur No 0.160298 0.038774 0.193350
  12. Yes 0.163863 0.039389 0.151240

这里,我们传递了一组聚合函数进行聚合,独立对数据分组进行评估。

你并非一定要接受GroupBy自动给出的那些列名,特别是lambda函数,它们的名称是’‘,这样的辨识度就很低了(通过函数的name属性看看就知道了)。因此,如果传入的是一个由(name,function)元组组成的列表,则各元组的第一个元素就会被用作DataFrame的列名(可以将这种二元元组列表看做一个有序映射):

  1. In [64]: grouped_pct.agg([('foo', 'mean'), ('bar', np.std)])
  2. Out[64]:
  3. foo bar
  4. day smoker
  5. Fri No 0.151650 0.028123
  6. Yes 0.174783 0.051293
  7. Sat No 0.158048 0.039767
  8. Yes 0.147906 0.061375
  9. Sun No 0.160113 0.042347
  10. Yes 0.187250 0.154134
  11. Thur No 0.160298 0.038774
  12. Yes 0.163863 0.039389

对于DataFrame,你还有更多选择,你可以定义一组应用于全部列的一组函数,或不同的列应用不同的函数。假设我们想要对tip_pct和total_bill列计算三个统计信息:

  1. In [65]: functions = ['count', 'mean', 'max']
  2. In [66]: result = grouped['tip_pct', 'total_bill'].agg(functions)
  3. In [67]: result
  4. Out[67]:
  5. tip_pct total_bill
  6. count mean max count mean max
  7. day smoker
  8. Fri No 4 0.151650 0.187735 4 18.420000 22.75
  9. Yes 15 0.174783 0.263480 15 16.813333 40.17
  10. Sat No 45 0.158048 0.291990 45 19.661778 48.33
  11. Yes 42 0.147906 0.325733 42 21.276667 50.81
  12. Sun No 57 0.160113 0.252672 57 20.506667 48.17
  13. Yes 19 0.187250 0.710345 19 24.120000 45.35
  14. Thur No 45 0.160298 0.266312 45 17.113111 41.19
  15. Yes 17 0.163863 0.241255 17 19.190588 43.11

如你所见,结果DataFrame拥有层次化的列,这相当于分别对各列进行聚合,然后用concat将结果组装到一起,使用列名用作keys参数:

  1. In [68]: result['tip_pct']
  2. Out[68]:
  3. count mean max
  4. day smoker
  5. Fri No 4 0.151650 0.187735
  6. Yes 15 0.174783 0.263480
  7. Sat No 45 0.158048 0.291990
  8. Yes 42 0.147906 0.325733
  9. Sun No 57 0.160113 0.252672
  10. Yes 19 0.187250 0.710345
  11. Thur No 45 0.160298 0.266312
  12. Yes 17 0.163863 0.241255

跟前面一样,这里也可以传入带有自定义名称的一组元组:

  1. In [69]: ftuples = [('Durchschnitt', 'mean'),('Abweichung', np.var)]
  2. In [70]: grouped['tip_pct', 'total_bill'].agg(ftuples)
  3. Out[70]:
  4. tip_pct total_bill
  5. Durchschnitt Abweichung Durchschnitt Abweichung
  6. day smoker
  7. Fri No 0.151650 0.000791 18.420000 25.596333
  8. Yes 0.174783 0.002631 16.813333 82.562438
  9. Sat No 0.158048 0.001581 19.661778 79.908965
  10. Yes 0.147906 0.003767 21.276667 101.387535
  11. Sun No 0.160113 0.001793 20.506667 66.099980
  12. Yes 0.187250 0.023757 24.120000 109.046044
  13. Thur No 0.160298 0.001503 17.113111 59.625081
  14. Yes 0.163863 0.001551 19.190588 69.808518

现在,假设你想要对一个列或不同的列应用不同的函数。具体的办法是向agg传入一个从列名映射到函数的字典:

  1. In [71]: grouped.agg({'tip' : np.max, 'size' : 'sum'})
  2. Out[71]:
  3. tip size
  4. day smoker
  5. Fri No 3.50 9
  6. Yes 4.73 31
  7. Sat No 9.00 115
  8. Yes 10.00 104
  9. Sun No 6.00 167
  10. Yes 6.50 49
  11. Thur No 6.70 112
  12. Yes 5.00 40
  13. In [72]: grouped.agg({'tip_pct' : ['min', 'max', 'mean', 'std'],
  14. ....: 'size' : 'sum'})
  15. Out[72]:
  16. tip_pct size
  17. min max mean std sum
  18. day smoker
  19. Fri No 0.120385 0.187735 0.151650 0.028123 9
  20. Yes 0.103555 0.263480 0.174783 0.051293 31
  21. Sat No 0.056797 0.291990 0.158048 0.039767 115
  22. Yes 0.035638 0.325733 0.147906 0.061375 104
  23. Sun No 0.059447 0.252672 0.160113 0.042347 167
  24. Yes 0.065660 0.710345 0.187250 0.154134 49
  25. Thur No 0.072961 0.266312 0.160298 0.038774 112
  26. Yes 0.090014 0.241255 0.163863 0.039389 40

只有将多个函数应用到至少一列时,DataFrame才会拥有层次化的列。

以“没有行索引”的形式返回聚合数据

到目前为止,所有示例中的聚合数据都有由唯一的分组键组成的索引(可能还是层次化的)。由于并不总是需要如此,所以你可以向groupby传入as_index=False以禁用该功能:

  1. In [73]: tips.groupby(['day', 'smoker'], as_index=False).mean()
  2. Out[73]:
  3. day smoker total_bill tip size tip_pct
  4. 0 Fri No 18.420000 2.812500 2.250000 0.151650
  5. 1 Fri Yes 16.813333 2.714000 2.066667 0.174783
  6. 2 Sat No 19.661778 3.102889 2.555556 0.158048
  7. 3 Sat Yes 21.276667 2.875476 2.476190 0.147906
  8. 4 Sun No 20.506667 3.167895 2.929825 0.160113
  9. 5 Sun Yes 24.120000 3.516842 2.578947 0.187250
  10. 6 Thur No 17.113111 2.673778 2.488889 0.160298
  11. 7 Thur Yes 19.190588 3.030000 2.352941 0.163863

当然,对结果调用reset_index也能得到这种形式的结果。使用as_index=False方法可以避免一些不必要的计算。