查看原文
其他

《利用Python进行数据分析·第2版》第10章(中) 数据聚合与分组运算

SeanCheney Python爱好者社区 2019-04-07

作者:SeanCheney   Python爱好者社区专栏作者

简书专栏:https://www.jianshu.com/u/130f76596b02


前文传送门:

【翻译】《利用Python进行数据分析·第2版》第1章 准备工作

【翻译】《利用Python进行数据分析·第2版》第2章(上)Python语法基础,IPython和Jupyter

【翻译】《利用Python进行数据分析·第2版》第2章(中)Python语法基础,IPython和Jupyter

【翻译】《利用Python进行数据分析·第2版》第2章(下)Python语法基础,IPython和Jupyter

【翻译】《利用Python进行数据分析·第2版》第3章(上)Python的数据结构、函数和文件

【翻译】《利用Python进行数据分析·第2版》第3章(中)Python的数据结构、函数和文件

【翻译】《利用Python进行数据分析·第2版》第3章(下)Python的数据结构、函数和文件

【翻译】《利用Python进行数据分析·第2版》第4章(上)NumPy基础:数组和矢量计算

【翻译】《利用Python进行数据分析·第2版》第4章(中)NumPy基础:数组和矢量计算

【翻译】《利用Python进行数据分析·第2版》第4章(下)NumPy基础:数组和矢量计算

【翻译】《利用Python进行数据分析·第2版》第5章(上)pandas入门

【翻译】《利用Python进行数据分析·第2版》第5章(中)pandas入门

【翻译】《利用Python进行数据分析·第2版》第5章(下)pandas入门

【翻译】《利用Python进行数据分析·第2版》第6章(上) 数据加载、存储与文件格式

【翻译】《利用Python进行数据分析·第2版》第6章(中) 数据加载、存储与文件格式

【翻译】《利用Python进行数据分析·第2版》第6章(下) 数据加载、存储与文件格式

【翻译】《利用Python进行数据分析·第2版》第7章(上)数据清洗和准备

【翻译】《利用Python进行数据分析·第2版》第7章(中) 数据清洗和准备

【翻译】《利用Python进行数据分析·第2版》第7章(下) 数据清洗和准备

【翻译】《利用Python进行数据分析·第2版》第8章(上) 数据规整:聚合、合并和重塑

【翻译】《利用Python进行数据分析·第2版》第8章(中) 数据规整:聚合、合并和重塑

【翻译】《利用Python进行数据分析·第2版》第8章(下) 数据规整:聚合、合并和重塑

【翻译】《利用Python进行数据分析·第2版》第9章(上) 绘图和可视化

【翻译】《利用Python进行数据分析·第2版》第9章(中) 绘图和可视化

【翻译】《利用Python进行数据分析·第2版》第9章(下) 绘图和可视化

【翻译】《利用Python进行数据分析·第2版》第10章(上) 数据聚合与分组运算


根据索引级别分组


层次化索引数据集最方便的地方就在于它能够根据轴索引的一个级别进行聚合:

In [47]: columns = pd.MultiIndex.from_arrays([['US', 'US', 'US', 'JP', 'JP'],   ....:                                     [1, 3, 5, 1, 3]],   ....:                                     names=['cty', 'tenor']) In [48]: hier_df = pd.DataFrame(np.random.randn(4, 5), columns=columns) In [49]: hier_df Out[49]: cty          US                            JP           tenor         1         3         5         1         3 0      0.560145 -1.265934  0.119827 -1.063512  0.332883 1     -2.359419 -0.199543 -1.541996 -0.970736 -1.307030 2      0.286350  0.377984 -0.753887  0.331286  1.349742 3      0.069877  0.246674 -0.011862  1.004812  1.327195

要根据级别分组,使用level关键字传递级别序号或名字:

In [50]: hier_df.groupby(level='cty', axis=1).count() Out[50]: cty  JP  US 0     2   3 1     2   3 2     2   3 3     2   3

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),最后将这些结果组装成最终结果:

In [51]: df Out[51]:      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 [52]: grouped = df.groupby('key1') In [53]: grouped['data1'].quantile(0.9) Out[53]: key1 a    1.668413 b   -0.523068 Name: data1, dtype: float64

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

In [54]: def peak_to_peak(arr):   ....:     return arr.max() - arr.min() In [55]: grouped.agg(peak_to_peak) Out[55]:         data1     data2 key1                     a     2.170488  1.300498 b     0.036292  0.487276

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

In [56]: grouped.describe() Out[56]:     data1                                                              \     count      mean       std       min       25%       50%       75%   key1                                                                     a      3.0  0.746672  1.109736 -0.204708  0.137118  0.478943  1.222362   b      2.0 -0.537585  0.025662 -0.555730 -0.546657 -0.537585 -0.528512                 data2                                                    \ max count      mean       std       min       25%       50%   key1                                                                     a     1.965781   3.0  0.910916  0.712217  0.092908  0.669671  1.246435   b    -0.519439   2.0  0.525384  0.344556  0.281746  0.403565  0.525384             75%       max   key1                       a     1.319920  1.393406   b     0.647203  0.769023

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


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


面向列的多函数应用


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

In [57]: tips = pd.read_csv('examples/tips.csv') # Add tip percentage of total bill In [58]: tips['tip_pct'] = tips['tip'] / tips['total_bill'] In [59]: tips[:6] Out[59]:   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

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

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

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

In [61]: grouped_pct = grouped['tip_pct'] In [62]: grouped_pct.agg('mean') Out[62]: 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的列就会以相应的函数命名:

In [63]: grouped_pct.agg(['mean', 'std', peak_to_peak]) Out[63]:                 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函数,它们的名称是'<lambda>',这样的辨识度就很低了(通过函数的name属性看看就知道了)。因此,如果传入的是一个由(name,function)元组组成的列表,则各元组的第一个元素就会被用作DataFrame的列名(可以将这种二元元组列表看做一个有序映射):

In [64]: grouped_pct.agg([('foo', 'mean'), ('bar', np.std)]) Out[64]:                  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列计算三个统计信息:

In [65]: functions = ['count', 'mean', 'max'] In [66]: result = grouped['tip_pct', 'total_bill'].agg(functions) In [67]: result Out[67]:            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

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

In [68]: result['tip_pct'] Out[68]:             count      mean       max day  smoker                           Fri  No          4  0.151650  0.187735     Yes        15  0.174783  0.263480 Sat  No         45  0.158048  0.291990     Yes        42  0.147906  0.325733 Sun  No         57  0.160113  0.252672     Yes        19  0.187250  0.710345 Thur No         45  0.160298  0.266312     Yes        17  0.163863  0.241255

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

In [69]: ftuples = [('Durchschnitt', 'mean'),('Abweichung', np.var)] In [70]: grouped['tip_pct', 'total_bill'].agg(ftuples) Out[70]:                 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传入一个从列名映射到函数的字典:

In [71]: grouped.agg({'tip' : np.max, 'size' : 'sum'}) Out[71]:               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 In [72]: grouped.agg({'tip_pct' : ['min', 'max', 'mean', 'std'],   ....:              'size' : 'sum'}) Out[72]:              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

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


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


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

In [73]: tips.groupby(['day', 'smoker'], as_index=False).mean() Out[73]:    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

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


10.3 apply:一般性的“拆分-应用-合并”


最通用的GroupBy方法是apply,本节剩余部分将重点讲解它。如图10-2所示,apply会将待处理的对象拆分成多个片段,然后对各片段调用传入的函数,最后尝试将各片段组合到一起。


图10-2 分组聚合示例


回到之前那个小费数据集,假设你想要根据分组选出最高的5个tip_pct值。首先,编写一个选取指定列具有最大值的行的函数:

In [74]: def top(df, n=5, column='tip_pct'):   ....:     return df.sort_values(by=column)[-n:] In [75]: top(tips, n=6) Out[75]:     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,就会得到:

In [76]: tips.groupby('smoker').apply(top) Out[76]:            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的函数能够接受其他参数或关键字,则可以将这些内容放在函数名后面一并传入:

In [77]: tips.groupby(['smoker', 'day']).apply(top, n=1, column='total_bill') Out[77]:                 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

笔记:除这些基本用法之外,能否充分发挥apply的威力很大程度上取决于你的创造力。传入的那个函数能做什么全由你说了算,它只需返回一个pandas对象或标量值即可。本章后续部分的示例主要用于讲解如何利用groupby解决各种各样的问题。


可能你已经想起来了,之前我在GroupBy对象上调用过describe:

In [78]: result = tips.groupby('smoker')['tip_pct'].describe() In [79]: result Out[79]:        count      mean       std       min       25%       50%       75%  \ smoker                                                                       No      151.0  0.159328  0.039910  0.056797  0.136906  0.155625  0.185014   Yes      93.0  0.163196  0.085119  0.035638  0.106771  0.153846  0.195059               max   smoker No      0.291990   Yes     0.710345   In [80]: result.unstack('smoker') Out[80]:       smoker count  No        151.000000       Yes        93.000000 mean   No          0.159328       Yes         0.163196 std    No          0.039910       Yes         0.085119 min    No          0.056797       Yes         0.035638 25%    No          0.136906       Yes         0.106771 50%    No          0.155625       Yes         0.153846 75%    No          0.185014       Yes         0.195059 max    No          0.291990       Yes         0.710345 dtype: float64

在GroupBy中,当你调用诸如describe之类的方法时,实际上只是应用了下面两条代码的快捷方式而已:

f = lambda x: x.describe() grouped.apply(f)

禁止分组键


从上面的例子中可以看出,分组键会跟原始对象的索引共同构成结果对象中的层次化索引。将group_keys=False传入groupby即可禁止该效果:

In [81]: tips.groupby('smoker', group_keys=False).apply(top) Out[81]:     total_bill   tip smoker   day    time  size   tip_pct 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 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

赞赏作者

Python爱好者社区历史文章大合集

Python爱好者社区历史文章列表(每周append更新一次)

福利:文末扫码立刻关注公众号,“Python爱好者社区”,开始学习Python课程:

关注后在公众号内回复“课程”即可获取:

小编的Python入门视频课程!!!

崔老师爬虫实战案例免费学习视频。

丘老师数据科学入门指导免费学习视频。

陈老师数据分析报告制作免费学习视频。

玩转大数据分析!Spark2.X+Python 精华实战课程免费学习视频。

丘老师Python网络爬虫实战免费学习视频。


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存