阅读笔记:利用Python进行数据分析第2版——第10章 数据聚合与分组运算

对数据集进行分组并对各组应用一个函数(无论是聚合还是转换),通常是数据分析工作中的重要环节。在将数据集加载、融合、准备好之后,通常就是计算分组统计或生成透视表。pandas提供了一个灵活高效的gruopby`功能,它使你能以一种自然的方式对数据集进行切片、切块、摘要等操作。
在本章中,你将会学到:

  • 使用一个或多个键(形式可以是函数、数组或DataFrame列名)分割pandas对象。
  • 计算分组的概述统计,比如数量、平均值或标准差,或是用户定义的函数。
  • 应用组内转换或其他运算,如规格化、线性回归、排名或选取子集等。
  • 计算透视表或交叉表。
  • 执行分位数分析以及其它统计分组分析。

笔记:对时间序列数据的聚合(groupby的特殊用法之一)也称作重采样(resampling),本书将在第11章中单独对其进行讲解。

一、GroupBy机制

  1. 分组键可以有多种形式,且类型不必相同:
  • 列表或数组,其长度与待分组的轴一样。
  • 表示DataFrame某个列名的值。
  • 字典或Series,给出待分组轴上的值与分组名之间的对应关系。
  • 函数,用于处理轴索引或索引中的各个标签。

注意,后三种都只是快捷方式而已,其最终目的仍然是产生一组用于拆分对象的值。

  1. 传入一列:
grouped = df['data1'].groupby(df['key1'])  # 访问data1,并根据key1调用groupby
grouped.mean()  # 基于分组结果求平均值
grouped.sum()  # 基于分组结果求和
  1. 如果我们一次传入多个数组的列表,就会得到不同的结果:
In[15]: means = df['data1'].groupby([df['key1'], df['key2']]).mean()
In[16]: means  # 得到的Series具有一个层次化索引
Out[16]:
key1  key2
a     one     0.880536
      two     0.478943
b     one    -0.519439
      two    -0.555730
Name: data1, dtype: float64
  1. 实际上,分组键可以是任何长度适当的数组:
In[18]: states = np.array(['Ohio','California','California','Ohio','Ohio'])
In[19]: years = np.array([2005,2005,2006,2005,2006])
In[20]: df['data1'].groupby([states, years]).mean()
Out[20]:
California  2005    0.071615
            2006   -1.660034
Ohio        2005    0.919267
            2006   -0.949231
Name: data1, dtype: float64
  1. 通常,分组信息就位于相同的要处理DataFrame中。因此可以将列名(可以是字符串、数字或其他Python对象)用作分组键:
In[21]: df.groupby('key1').mean()  # key2不是数值列,被从结果中排除
Out[21]:
         data1     data2
key1
a     0.7466720.910916
b    -0.5375850.525384
In[22]: df.groupby(['key1','key2']).mean()
Out[22]:
              data1     data2
key1 key2                    
a    one   0.8805361.319920
     two   0.4789430.092908
b    one  -0.5194390.281746
     two  -0.5557300.769023
  1. 可以用GroupBysize方法,获取大小信息:df.groupby(['key1','key2']).size()

注意,任何分组关键词中的缺失值,都会被从结果中除去。

  1. GroupBy对象支持迭代,可以产生一组二元元组(由分组名和数据块组成)。
for name, group in df.groupby('key1'):
    print(name)
    print(group)

对于多重键的情况,元组的第一个元素将会是由键值组成的元组。
8. 将这些数据片段做成一个字典可能会比较有用:pieces = dict(list(df.groupby('key1')))
9. groupby默认是在axis=0上进行分组的,通过设置也可以在其他任何轴上进行分组。
10. 对于由DataFrame产生的GroupBy对象,如果用一个(单个字符串)或一组(字符串数组)列名对其进行索引,就能实现选取部分列进行聚合的目的。

df.groupby('key1')['data1']  # 是以下代码的语法糖
df['data1'].groupby(df['key1'])
  1. 通过字典或Series进行分组
people = pd.DataFrame(np.random.randn(5,5), columns=['a','b','c','d','e'], index=['Joe','Steve','Wes','Jim','Travis'])
people.iloc[2:3, [1,2]] = np.nan
mapping ={'a':'red','b':'red','c':'blue', 'd':'blue','e':'red','f':'orange'}  # 存在未使用的分组键是可以的
by_col = people.groupby(mapping, axis=1)  # 通过字典分组
by_col.sum()
	        blue	     red
   Joe	0.440924	0.236276
 Steve	-2.023552	2.156495
   Wes	1.714252	0.495760
   Jim	4.370667	-0.784421
Travis	1.526930	1.694027

map_series = pd.Series(mapping)  # 将字典转换为Series
people.groupby(map_series, axis=1).count()  # 通过Series分组
  1. 通过函数进行分组:任何被当做分组键的函数都会在各个**索引值(index)**上被调用一次,其返回值就会被用作分组名称。
people.groupby(len).sum()  # 索引值为人的名字
	       a	        b	       c	        d	        e
3	2.668444	-1.767265	 3.785625	 2.740218	-0.953565
5	0.764664	 0.779645	-1.521141	-0.502411	 0.612186
6	0.051568	 1.038692	 0.474233	 1.052698	 0.603767
  1. 将函数跟数组、列表、字典、Series混合使用也不是问题,因为任何东西在内部都会被转换为数组:
key_list =['one','one','one','two','two']
people.groupby([len, key_list]).min()
  1. 根据索引级别分组:层次化索引数据集最方便的地方就在于它能够根据轴索引的一个级别进行聚合:
columns = pd.MultiIndex.from_arrays([['US','US','US','JP','JP'], [1,3,5,1,3]], names=['cty','tenor'])
hier_df = pd.DataFrame(np.random.randn(4,5), columns=columns)
hier_df.groupby(level='cty', axis=1).count()  # 要根据级别分组,使用level关键字传递级别序号或名字

cty	JP	US
0	2	3
1	2	3
2	2	3
3	2	3

二、数据聚合

  1. 聚合指的是任何能够从数组产生标量值的数据转换过程。比如meancountmin以及sum等。
    常见的聚合运算
    除了这些方法,可以使用自己发明的聚合运算,还可以调用分组对象上已经定义好的任何方法。例如,quantile可以计算SeriesDataFrame列的样本分位数。
  • 实际上,GroupBy会高效地对Series进行切片,然后对各片调用piece.quantile(0.9),最后将这些结果组装成最终结果:
grouped = df.groupby('key1')
grouped['data1'].quantile(0.9)
  • 如果要使用你自己的聚合函数,只需将其传入aggregateagg方法即可:
def peak_to_peak(arr):
     return arr.max() - arr.min()
grouped.agg(peak_to_peak)
  1. 有些方法(如describe)也可以用在这里,即使严格来讲,它们并非聚合运算:grouped.describe()

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

  1. 面向列的多函数应用:有时我们可能希望对不同的列使用不同的聚合函数,或一次应用多个函数,下面通过一些示例来进行讲解。
  • 可以将函数名以字符串的形式传入:
grouped = tips.groupby(['day', 'smoker'])  # 根据天和smoker对tips进行分组
grouped_pct = grouped['tip_pct']
grouped_pct.agg('mean')
  • 如果传入一组函数或函数名,得到的DataFrame的列就会以相应的函数命名:
grouped_pct.agg(['mean', 'std', peak_to_peak])

                mean	     std	peak_to_peak
day	smoker			
Fri	    No	0.179740	0.039458	0.094263
       Yes	0.216293	0.077530	0.242219
Sat	    No	0.190412	0.058626	0.352192
       Yes	0.179833	0.089496	0.446137
Sun	    No	0.193617	0.060302	0.274897
       Yes	0.322021	0.538061	2.382107
Thur	No	0.193424	0.056065	0.284273
       Yes	0.198508	0.057170	0.219047
  • 如果传入的是一个由(name,function)元组组成的列表,则各元组的第一个元素就会被用作DataFrame的列名(可以将这种二元元组列表看做一个有序映射)
grouped_pct.agg([('foo', 'mean'), ('bar', np.std)])
  • 对tip_pct和total_bill列计算三个统计信息
functions = ['count', 'mean', 'max']
result = grouped['tip_pct', 'total_bill'].agg(functions)  # 结果DataFrame拥有层次化的列,这相当于分别对各列进行聚合,然后用concat将结果组装到一起
  • 对一个列或不同的列应用不同的函数,具体的办法是向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'})

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

  1. 以“没有行索引”的形式返回聚合数据:
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方法可以避免一些不必要的计算。

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

最通用的GroupBy方法是apply,本节剩余部分将重点讲解它。

  1. 如下图所示,apply会将待处理的对象拆分成多个片段,然后对各片段调用传入的函数,最后尝试将各片段组合到一起。
    在这里插入图片描述

  2. 假设你想要根据分组选出最高的5个tip_pct值:
    top函数在DataFrame的各个片段上调用,然后结果由pandas.concat组装到一起,并以分组名称进行了标记。于是,最终结果就有了一个层次化索引,其内层索引值来自原DataFrame

def top(df, n=5, column='tip_pct'):
    return df.sort_values(by=column)[-n:]
tips.groupby('smoker').apply(top)
            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
  1. 如果传给apply的函数能够接受其他参数或关键字,则可以将这些内容放在函数名后面一并传入:
tips.groupby(['smoker', 'day']).apply(top, n=1, column='total_bill')

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

  1. GroupBy中,当你调用诸如describe之类的方法时,实际上只是应用了下面两条代码的快捷方式而已:
f = lambda x: x.describe()
grouped.apply(f)
  1. 从上面的例子中可以看出,分组键会跟原始对象的索引共同构成结果对象中的层次化索引。将group_keys=False传入groupby即可禁止该效果:
tips.groupby('smoker', group_keys=False).apply(top)

注意,group_keysas_index的功能有一定区别。as_index是限制显示分组键,会以数字序号替代,但group_keys则是不将分组键作为索引,只显示之前的行索引。
在这里插入图片描述

  1. 分位数和桶分析
frame = pd.DataFrame({'data1': np.random.randn(1000), 'data2': np.random.randn(1000)})
quartiles = pd.cut(frame.data1, 4)  # 利用cut将frame 装入长度相等的桶中
def get_stats(group):
    return {'count': group.count(), 'mean': group.mean(), 'max': group.max(), 'min': group.min()}
# 由cut返回的Categorical对象可直接传递到groupby
grouped = frame.data2.groupby(quartiles)
grouped.apply(get_stats).unstack()
Out[87]: 
                 count       max      mean       min
data1                                               
(-2.956, -1.23]   95.0  1.670835 -0.039521 -3.399312
(-1.23, 0.489]   598.0  3.260383 -0.002051 -2.989741
(0.489, 2.208]   297.0  2.954439  0.081822 -3.745356
(2.208, 3.928]    10.0  1.765640  0.024750 -1.929776
  1. 对于缺失数据的清理工作,有时你会用dropna将其替换掉,而有时则可能会希望用一个固定值或由数据集本身所衍生出来的值去填充NA值。这时就得使用fillna这个工具了。
s = pd.Series(np.random.randn(6))
s[::2] = np.nan
s.fillna(s.mean()
  1. 假设需要对不同的分组填充不同的值。一种方法是将数据分组,并使用apply和一个能够对各数据块调用fillna的函数即可。
states = ['Ohio', 'New York', 'Vermont', 'Florida', 'Oregon', 'Nevada', 'California', 'Idaho']
group_key = ['East'] * 4 + ['West'] * 4
data = pd.Series(np.random.randn(8), index=states)
data[['Vermont', 'Nevada', 'Idaho']] = np.nan
fill_mean = lambda g: g.fillna(g.mean())  # 用分组平均值去填充NA值
data.groupby(group_key).apply(fill_mean)
# 也可以在代码中预定义各组的填充值
fill_values = {'East': 0.5, 'West': -1}
fill_func = lambda g: g.fillna(fill_values[g.name])
data.groupby(group_key).apply(fill_func)

四、透视表和交叉表

  1. 透视表(pivot table)是各种电子表格程序和其他数据分析软件中一种常见的数据汇总工具。它根据一个或多个键对数据进行聚合,并根据行和列上的分组键将数据分配到各个矩形区域中。
  2. DataFrame有一个pivot_table方法,此外还有一个顶级的pandas.pivot_table函数。除能为groupby提供便利之外,pivot_table还可以添加分项小计,也叫做margins
  3. 回到小费数据集,假设我想要根据day和smoker计算分组平均数(pivot_table的默认聚合类型),并将day和smoker放到行上:
In [130]: tips.pivot_table(index=['day', 'smoker'])
Out[130]: 
                 size       tip   tip_pct  total_bill
day  smoker                                          
Fri  No      2.250000  2.812500  0.151650   18.420000
     Yes     2.066667  2.714000  0.174783   16.813333
Sat  No      2.555556  3.102889  0.158048   19.661778
     Yes     2.476190  2.875476  0.147906   21.276667
Sun  No      2.929825  3.167895  0.160113   20.506667
     Yes     2.578947  3.516842  0.187250   24.120000
Thur No      2.488889  2.673778  0.160298   17.113111
     Yes     2.352941  3.030000  0.163863   19.190588
  1. 假设我们只想聚合tip_pct和size,而且想根据time进行分组。我将smoker放到列上,把day放到行上:
In [131]: tips.pivot_table(['tip_pct', 'size'], index=['time', 'day'], columns='smoker')
Out[131]: 
                 size             tip_pct          
smoker             No       Yes        No       Yes
time   day                                         
Dinner Fri   2.000000  2.222222  0.139622  0.165347
       Sat   2.555556  2.476190  0.158048  0.147906
       Sun   2.929825  2.578947  0.160113  0.187250
       Thur  2.000000       NaN  0.159744       NaN
Lunch  Fri   3.000000  1.833333  0.187735  0.188937
       Thur  2.500000  2.352941  0.160311  0.163863
  1. 传入margins=True添加分项小计。这将会添加标签为All行和列,其值对应于单个等级中所有数据的分组统计:
In [132]: tips.pivot_table(['tip_pct', 'size'], index=['time', 'day'], columns='smoker', margins=True)
Out[132]: 
                 size                       tip_pct                    
smoker             No       Yes       All        No       Yes       All
time   day                                                             
Dinner Fri   2.000000  2.222222  2.166667  0.139622  0.165347  0.158916
       Sat   2.555556  2.476190  2.517241  0.158048  0.147906  0.153152
       Sun   2.929825  2.578947  2.842105  0.160113  0.187250  0.166897
       Thur  2.000000       NaN  2.000000  0.159744       NaN  0.159744
Lunch  Fri   3.000000  1.833333  2.000000  0.187735  0.188937  0.188765
       Thur  2.500000  2.352941  2.459016  0.160311  0.163863  0.161301
All          2.668874  2.408602  2.569672  0.159328  0.163196  0.160803

这里,All值为平均数:不单独考虑烟民与非烟民(All列),不单独考虑行分组两个级别中的任何单项(All行)。

  1. 要使用其他的聚合函数,将其传给aggfunc即可。例如,使用countlen可以得到有关分组大小的交叉表(计数或频率):
tips.pivot_table('tip_pct', index=['time', 'smoker'], columns='day', aggfunc=len, margins=True)

pivot_table函数的参数

  1. 交叉表(cross-tabulation,简称crosstab)是一种用于计算分组频率的特殊透视表。
    crosstab的前两个参数可以是数组或Series,或是数组列表。就像小费数据:
In [140]: pd.crosstab([tips.time, tips.day], tips.smoker, margins=True)
Out[140]: 
smoker        No  Yes  All
time   day                
Dinner Fri     3    9   12
       Sat    45   42   87
       Sun    57   19   76
       Thur    1    0    1
Lunch  Fri     1    6    7
       Thur   44   17   61
All          151   93  244
# 用pivot_table函数也能实现上述功能,但所有有区别
tips.pivot_table(['tip'], index=['time', 'day'], columns=['smoker'], aggfunc='count', margins=True, fill_value=0)

在这里插入图片描述


以上,欢迎交流。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
学习数据分析是当代信息时代的一项重要技能,Python作为一种强大的编程语言,是数据分析的热门工具之一。以下是我在学习利用Python进行数据分析过程中的一些笔记。 首先,学习Python的基础知识是必不可少的。了解Python的基本语法、数据类型、循环和条件语句等知识对于数据分析的学习非常重要。我通过自学网课和阅读相关书籍,逐渐掌握了Python的基础知识。 其次,学习使用Python数据分析库。在Python中,有很多强大的数据分析库,例如NumPy、Pandas和Matplotlib等。我通过学习这些库的用法,掌握了数据的处理、清洗、分析和可视化的技巧。我学习了如何使用NumPy进行矩阵运算和数值计算,如何使用Pandas进行数据处理和数据操作,以及如何使用Matplotlib进行数据可视化。 除了数据分析库,学习Python的机器学习库也是必不可少的。机器学习在数据分析中扮演着重要角色,Python中有很多优秀的机器学习库,例如Scikit-learn和TensorFlow。我通过学习这些库的使用,了解了机器学习的基本概念和常用算法,例如回归、分类和聚类等。我也学习了如何使用这些库来构建和训练机器学习模型。 最后,实践是学习的关键。在学习的过程中,我通过实践项目来巩固所学知识。我选择了一些真实的数据集,并运用Python数据分析技术进行数据处理、分析和可视化。通过实践,我不仅掌握了数据分析的具体步骤和方法,还锻炼了自己解决实际问题的能力。 总的来说,学习利用Python进行数据分析需要掌握Python的基础知识、数据分析库和机器学习库的使用,同时也需要通过实践项目来巩固所学知识。这个过程需要持续不断的学习和实践,但是通过不断的努力和实践,我相信能够掌握Python进行数据分析的技能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值