《利用Python进行数据分析·第2版》第10章(中) 数据聚合与分组运算
作者: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入门视频课程!!!
崔老师爬虫实战案例免费学习视频。
丘老师数据科学入门指导免费学习视频。
陈老师数据分析报告制作免费学习视频。
玩转大数据分析!Spark2.X+Python 精华实战课程免费学习视频。
丘老师Python网络爬虫实战免费学习视频。