数据聚合

对于聚合,我指的是任何能够从数组产生标量值的数据转换过程。之前的例子中我已经用过一些,比如mean、count、min以及sum等。你可能想知道在GroupBy对象上调用mean()时究竟发生了什么。许多常见的聚合运算(如表9-1所示)都有就地计算数据集统计信息的优化实现。然而,并不是只能使用这些方法。你可以使用自己发明的聚合运算,还可以调用分组对象上已经定义好的任何方法。例如,quantile可以计算Series或DataFrame列的样本分位数译注2

  1. In [54]: df
  2. Out[54]:
  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.  
  10. In [55]: grouped = df.groupby('key1')
  11.  
  12. In [56]: grouped['data1'].quantile(0.9)
  13. Out[56]:
  14. key1
  15. a 1.668413
  16. b -0.523068

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

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

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

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

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

在后面关于分组级运算译注3和转换的那一节中,我将详细说明这到底是怎么回事。

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

数据聚合 - 图1

译注4:这里应该是“经过优化的GroupBy的方法”,原文有误。

为了说明一些更高级的聚合功能,我将使用一个有关餐馆小费的数据集。我是在R语言的reshape2包中得到该数据集的(可以在本书的GitHub库中找到)。它最初出现于Bryant和Smith在1995年编写的一本有关商业统计的书中。通过read_csv将其加载之后,我添加了一个表示小费比例的列tip_pct。

  1. In [60]: tips = pd.read_csv('ch08/tips.csv')
  2.  
  3. # 添加“小费占总额百分比”的列
  4. In [61]: tips['tip_pct'] = tips['tip'] / tips['total_bill']
  5.  
  6. In [62]: tips[:6]
  7. Out[62]:
  8. total_bill tip sex smoker day time size tip_pct
  9. 0 16.99 1.01 Female False Sun Dinner 2 0.059447
  10. 1 10.34 1.66 Male False Sun Dinner 3 0.160542
  11. 2 21.01 3.50 Male False Sun Dinner 3 0.166587
  12. 3 23.68 3.31 Male False Sun Dinner 2 0.139780
  13. 4 24.59 3.61 Female False Sun Dinner 4 0.146808
  14. 5 25.29 4.71 Male False Sun Dinner 4 0.186240

面向列的多函数应用

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

  1. In [63]: grouped = tips.groupby(['sex', 'smoker'])

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

  1. In [64]: grouped_pct = grouped['tip_pct']
  2.  
  3. In [65]: grouped_pct.agg('mean')
  4. Out[65]:
  5. sex smoker
  6. Female False 0.156921
  7. True 0.182150
  8. Male False 0.160669
  9. True 0.152771
  10. Name: tip_pct

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

  1. In [66]: grouped_pct.agg(['mean', 'std', peak_to_peak])
  2. Out[66]:
  3. mean std peak_to_peak
  4. sex smoker
  5. Female False 0.156921 0.036421 0.195876
  6. True 0.182150 0.071595 0.360233
  7. Male False 0.160669 0.041849 0.220186
  8. True 0.152771 0.090588 0.674707

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

  1. In [67]: grouped_pct.agg([('foo', 'mean'), ('bar', np.std)])
  2. Out[67]:
  3. foo bar
  4. sex smoker
  5. Female False 0.156921 0.036421
  6. True 0.182150 0.071595
  7. Male False 0.160669 0.041849
  8. True 0.152771 0.090588

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

  1. In [68]: functions = ['count', 'mean', 'max']
  2.  
  3. In [69]: result = grouped['tip_pct', 'total_bill'].agg(functions)
  4.  
  5. In [70]: result
  6. Out[70]:
  7. tip_pct total_bill
  8. count mean max count mean max
  9. sex smoker
  10. Female False 54 0.156921 0.252672 54 18.105185 35.83
  11. True 33 0.182150 0.416667 33 17.977879 44.30
  12. Male False 97 0.160669 0.291990 97 19.791237 48.33
  13. True 60 0.152771 0.710345 60 22.284500 50.81

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

  1. In [71]: result['tip_pct']
  2. Out[71]:
  3. count mean max
  4. sex smoker
  5. Female False 54 0.156921 0.252672
  6. True 33 0.182150 0.416667
  7. Male False 97 0.160669 0.291990
  8. True 60 0.152771 0.710345

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

  1. In [72]: ftuples = [('Durchschnitt', 'mean'), ('Abweichung', np.var)]
  2.  
  3. In [73]: grouped['tip_pct', 'total_bill'].agg(ftuples)
  4. Out[73]:
  5. tip_pct total_bill
  6. Durchschnitt Abweichung Durchschnitt Abweichung
  7. sex smoker
  8. Female False 0.156921 0.001327 18.105185 53.092422
  9. True 0.182150 0.005126 17.977879 84.451517
  10. Male False 0.160669 0.001751 19.791237 76.152961
  11. True 0.152771 0.008206 22.284500 98.244673

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

  1. In [74]: grouped.agg({'tip' : np.max, 'size' : 'sum'})
  2. Out[74]:
  3. size tip
  4. sex smoker
  5. Female False 140 5.2
  6. True 74 6.5
  7. Male False 263 9.0
  8. True 150 10.0
  9.  
  10. In [75]: grouped.agg({'tip_pct' : ['min', 'max', 'mean', 'std'],
  11. ...: 'size' : 'sum'})
  12. Out[75]:
  13. tip_pct size
  14. min max mean std sum
  15. sex smoker
  16. Female False 0.056797 0.252672 0.156921 0.036421 140
  17. True 0.056433 0.416667 0.182150 0.071595 74
  18. Male False 0.071804 0.291990 0.160669 0.041849 263
  19. True 0.035638 0.710345 0.152771 0.090588 150

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

以“无索引”的形式返回聚合数据

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

  1. In [76]: tips.groupby(['sex', 'smoker'], as_index=False).mean()
  2. Out[76]:
  3. sex smoker total_bill tip size tip_pct
  4. 0 Female False 18.105185 2.773519 2.592593 0.156921
  5. 1 Female True 17.977879 2.931515 2.242424 0.182150
  6. 2 Male False 19.791237 3.113402 2.711340 0.160669
  7. 3 Male True 22.284500 3.051167 2.500000 0.152771

当然,对结果调用reset_index也能得到这种形式的结果。

警告: groupby的这种用法比较缺乏灵活性。

译注2:注意,如果传入的百分位上没有值,则quantile会进行线性插值。

译注3:也就是“面向分组”的计算。