10.2 数据聚合

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

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

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

    1. ....: return arr.max() - arr.min()
    2. In [55]: grouped.agg(peak_to_peak)
    3. Out[55]:
    4. data1 data2
    5. key1
    6. a 2.170488 1.300498
    7. 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节,我将详细说明这到底是怎么回事。

    回到前面小费的例子。使用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进行分组:

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

    1. In [61]: grouped_pct = grouped['tip_pct']
    2. Out[62]:
    3. Fri No 0.151650
    4. Yes 0.174783
    5. Sat No 0.158048
    6. Yes 0.147906
    7. Sun No 0.160113
    8. Yes 0.187250
    9. Thur No 0.160298
    10. Yes 0.163863
    11. 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拥有层次化的列,这相当于分别对各列进行聚合,然后用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. Thur No 45 0.160298 0.266312
    10. 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. Yes 0.090014 0.241255 0.163863 0.039389 40

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

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

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

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