《利用Python进行数据分析》第十章---数据聚合与分组操作1

文章目录

  • 前言
  • 一、GroupBy机制
    • 1.1 遍历各分组
    • 1.2 选择一列或所有列的子集
    • 1.3 使用字典和Series分组
    • 1.4 使用函数分组
    • 1.5 根据索引层级分组
  • 二、数据聚合
    • 2.1 逐列及多函数应用
    • 2.2 返回不含行索引的聚合数据
  • 总结


前言

对数据集进行分类,并在每一组上应用一个聚合函数或转换函数,这通常是数据分析工作流中的一个重要部分。在载入、合并、准备数据集之后,你可能需要计算分组统计或者数据透视表用于报告或可视化的目的。pandas提供一个灵活的groupby接口,允许你以一种自然发方式对数据集进行切片、切块和总结。

我们通过Python和pandas的表达,我们可以使用pandas对象或NumPy数组执行相当复杂的组操作。在本章,我们将了解到:

  • 使用一个或多个键(以函数、数组或DataFrame列名的形式)将pandas对象拆分为多块
  • 计算组汇总统计信息,如计数、平均值或标准偏差或用户定义的函数
  • 应用组内变换或其他操作,如标准化、线性回归、排位或子集选择
  • 计算数据透视表和交叉表
  • 执行分位数分析和其他统计组分析

一、GroupBy机制

描述组操作的术语为拆分-应用-联合。在操作的第一步,数据包含在pandas对象中,可以是Series、DataFrame或其他数据结构,之后根据你提供的一个或多个键分离到各个组中。分离操作是在数据对象的特定轴向上进行的。例如,DataFrame可以在它的行方向(axis=0)或列方向(axis=1)进行分组。分组操作后,一个函数就可以应用到各个组去,产生新的值,最终,所以函数的应用结果会联合为一个结果对象。结果对象的形式通常取决于对数据进行的操作:

分组键可是多种形式的,并且键不一定是完全相同的类型:

  • 与需要分组的轴向长度一致的值列表或值数组
  • DataFrame的列名的值
  • 可以将分组轴向上的值和分组名称相匹配的字典或Series
  • 可以在轴索引或索引中的单个标签上调用的函数

在下面的DataFrame当中,我们将介绍的三个方法是可以产生用于分隔对象的值数组的快捷方式:

import pandas as pd
import numpy as np

df = pd.DataFrame({'key1':['a','a','b','b','a'],
                   'key2':['one','two','one','two','one'],
                   'data1':np.random.randn(5),
                   'data2':np.random.randn(5)})
print(df)
---------------------------------------------------------------------
  key1 key2     data1     data2
0    a  one  1.548485 -0.219585
1    a  two  0.447387 -0.100247
2    b  one  0.134934 -1.013706
3    b  two -1.090257  0.071942
4    a  one -1.650373 -0.496783

假设你想要根据key1标签计算data1列的均值,有多种方法可以实现。其中一种是访问data1并使用key1列(它是一个Series)调用groupby方法:

grouped = df['data1'].groupby(df['key1'])
print(grouped)
<pandas.core.groupby.generic.SeriesGroupBy object at 0x0000021CCB493100>

grouped变量现在是一个GroupBy对象。除了一些关于分组键df[‘key1’]的一些中间数据之外,它实际上还没有进行任何计算。这个对象拥有所有必需的信息,之后可以在每一个分组上应用一些操作。例如,为了计算分组的均值我们可以调用GroupBy的mean方法:

print(grouped.mean())
key1
a    0.115166
b   -0.477662
Name: data1, dtype: float64

数据(一个Series)根据分组键进行了聚合,并产生了新的Series,这个Series使用key1列的唯一值作为索引。由于DataFrame的列df[‘key1’],结果中的索引名称是’key1‘。

如果我们将多个数组作为列表传入,则我们会得到一些不同的结果:

means = df['data1'].groupby([df['key1'],df['key2']]).mean()
print(means)
-----------------------------------------------------------------
key1  key2
a     one    -0.050944
      two     0.447387
b     one     0.134934
      two    -1.090257
Name: data1, dtype: float64

这里我们使用了两个键对数据进行分组,并且结果Series现在拥有一个包含唯一键对的多层索引:

print(means.unstack())
---------------------------------------------------------------------
key2       one       two
key1                    
a    -0.050944  0.447387
b     0.134934 -1.090257

在这个例子中,分组键都是Series,尽管分组键也可以是正确长度的任何数组:

states = np.array(['Ohio','California','California','Ohio','Ohio'])
years = np.array([2005,2005,2006,2005,2006])
a = df['data1'].groupby([states,years]).mean()
print(a)
--------------------------------------------------------------------------
California  2005    0.447387
            2006    0.134934
Ohio        2005    0.229114
            2006   -1.650373
Name: data1, dtype: float64

分组信息作为你想要继续处理的数据,通常包含在同一个DataFrame中。在这种情况下,你可以传递列名作为分组键:

a = df.groupby('key1').mean()
print(a)
a = df.groupby(['key1','key2']).mean()
print(a)
---------------------------------------------------------
         data1     data2
key1                    
a     0.115166 -0.272205
b    -0.477662 -0.470882

              data1     data2
key1 key2                    
a    one  -0.050944 -0.358184
     two   0.447387 -0.100247
b    one   0.134934 -1.013706
	 two  -1.090257  0.071942

第一行代码中df.grouby(‘key1’).mean()的结果里并没有key2列。这是因为df[‘key2’]并不是数值数据,即df[‘key2’]是一个冗余列,因此被排除在结果之外。默认情况下,所有的数值列都可以聚合。

如果不在意使用grouby的目的,通用的GroupBy方法是size,size返回一个包含组大小信息的Series:

a = df.groupby(['key1','key2']).size()
print(a)
-------------------------------------------------------------------
key1  key2
a     one     2
      two     1
b     one     1
      two     1
dtype: int64

1.1 遍历各分组

GroupBy对象支持迭代,会生成一个包含组名和数据块的2维元组序列。

for name,group in df.groupby('key1'):
    print(name)
    print(group)
----------------------------------------------------------
a
  key1 key2     data1     data2
0    a  one  1.548485 -0.219585
1    a  two  0.447387 -0.100247
4    a  one -1.650373 -0.496783
b
  key1 key2     data1     data2
2    b  one  0.134934 -1.013706
3    b  two -1.090257  0.071942

在多个分组键的情况下,元组中的第一个元素是键值的元组:

for name,group in df.groupby(['key1','key2']):
    print(name)
    print(group)
-----------------------------------------------------------
('a', 'one')
  key1 key2     data1     data2
0    a  one  1.548485 -0.219585
4    a  one -1.650373 -0.496783
('a', 'two')
  key1 key2     data1     data2
1    a  two  0.447387 -0.100247
('b', 'one')
  key1 key2     data1     data2
2    b  one  0.134934 -1.013706
('b', 'two')
  key1 key2     data1     data2
3    b  two -1.090257  0.071942

当然,你可以选择在任何一块数据上进行你想要的操作。使用一行代码计算出数据块的字典可能会对你有用:

a = dict(list(df.groupby('key1')))
print(a['b'])
------------------------------------------------------
  key1 key2     data1     data2
2    b  one  0.134934 -1.013706
3    b  two -1.090257  0.071942

默认情况下,grouby在axis=0的轴向(行向)上分组,但你也可以在其他任意轴向上进行分组。例如,我们可以像以下代码一样,根据dtype对我们示例df的列进行分组:

a = df.dtypes
print(a)
------------------------------------------------------
key1      object
key2      object
data1    float64
data2    float64
dtype: object
grouped = df.groupby(df.dtypes,axis=1)
for dtypes,group in grouped:
    print(dtypes)
    print(group)
----------------------------------------------------
float64
      data1     data2
0  1.548485 -0.219585
1  0.447387 -0.100247
2  0.134934 -1.013706
3 -1.090257  0.071942
4 -1.650373 -0.496783
object
  key1 key2
0    a  one
1    a  two
2    b  one
3    b  two
4    a  one

1.2 选择一列或所有列的子集

将从DataFrame创建的GrouBy对象用列名称或列名称数组进行索引时,会产生聚合的列子集的效果。例如:

df.groupby('key1')['data1']
df.groupby('key1')[['data2']]

与下面的语句是等价的:

df['data1'].groupby(df['key1'])
df[['data2']].groupby(df['key1'])

当我们要计算data2列的均值,并获得DataFrame形式的结果,我们可以写:

a = df.groupby(['key1','key2'])[['data2']].mean()
print(a)
------------------------------------------------------------------
              data2
key1 key2          
a    one  -0.358184
     two  -0.100247
b    one  -1.013706
     two   0.071942

如果传递的是列表或数组,则此索引操作返回的对象是分组的DataFrame;如果只有单个列名作为标量传递,则为分组的Series:

a = df.groupby(['key1','key2'])['data2'].mean()
print(a)
-------------------------------------------------------------
key1  key2
a     one    -0.358184
      two    -0.100247
b     one    -1.013706
      two     0.071942
Name: data2, dtype: float64

1.3 使用字典和Series分组

分组信息可能会以非数组形式存在。让我们考虑另一个示例DataFrame:

df = pd.DataFrame(np.random.randn(5,5),
                  columns=['a','b','c','d','e'],
                  index=['Joe','Steve','Wes','Jim','Travis'])

df.iloc[2:3,[1,2]]=np.nan
print(df)
-----------------------------------------------------------------
               a         b         c         d         e
Joe     1.388421 -1.129679  0.075746 -1.894284 -0.715854
Steve  -0.298438 -1.859657 -0.085582 -0.971794 -1.143098
Wes    -0.124365       NaN       NaN -1.528739 -0.409837
Jim     0.100342 -0.423482  0.909694  0.256412 -0.399447
Travis -1.620090  1.258952 -0.051295  1.331768  0.640878

现在我们假设拥有各列的分组对应关系,并且想把各列按组累加:

mapping = {'a':'red','b':'red','c':'blue','d':'blue','e':'red'}
by_column = df.groupby(mapping,axis=1)
print(by_column.sum())
----------------------------------------------------------------
            blue       red
Joe    -1.818538 -0.457112
Steve  -1.057376 -3.301192
Wes    -1.528739 -0.534203
Jim     1.166107 -0.722587
Travis  1.280473  0.279740

Series也有相同的功能,可以视为固定大小的映射:

map_series = pd.Series(mapping)
a = df.groupby(map_series,axis=1).count()
print(a)
---------------------------------------------------------
        blue  red
Joe        2    3
Steve      2    3
Wes        1    2
Jim        2    3
Travis     2    3


1.4 使用函数分组

与使用字典或Series相比,使用Python函数是定义分组关系的一种更为通用的方式。作为分组键传递的函数将会按照每个索引值调用一次,同时返回值会被用作分组名称。更具体的来说,考虑上一节中的示例DataFrame,其中人的名字作为索引值。假设你想根据名字长度来进行分组。虽然你可以计算出字符串长度的数组,但传递len函数更为简单:

a = df.groupby(len).sum()
print(a)
------------------------------------------------------------------
          a         b         c         d         e
3  1.364397 -1.553161  0.985440 -3.166611 -1.525138
5 -0.298438 -1.859657 -0.085582 -0.971794 -1.143098
6 -1.620090  1.258952 -0.051295  1.331768  0.640878

将函数与数组、字典或Series进行混合并不困难,所有对象都会在内部转换为数组:

key_list=['one','one','one','two','two']
a = df.groupby([len,key_list]).min()
print(a)
------------------------------------------------------------
              a         b         c         d         e
3 one -0.124365 -1.129679  0.075746 -1.894284 -0.715854
  two  0.100342 -0.423482  0.909694  0.256412 -0.399447
5 one -0.298438 -1.859657 -0.085582 -0.971794 -1.143098
6 two -1.620090  1.258952 -0.051295  1.331768  0.640878

1.5 根据索引层级分组

分层索引的数据集有一个非常方便的地方,就是能够在轴索引的某个层级上进行聚合。

columns=pd.MultiIndex.from_arrays([['US','US','US','JP','JP'],[1,3,5,1,3]],names=['cty','tenor'])
df = pd.DataFrame(np.random.randn(4,5),
                  columns=columns)


print(df)
-----------------------------------------------------------------------------
cty          US                            JP          
tenor         1         3         5         1         3
0      0.763263 -0.125198  0.072028 -0.670269  1.275435
1     -0.954808 -0.401308 -0.061292 -1.294746 -0.479677
2     -0.404107  0.014756 -2.206899  2.188720  0.594605
3     -1.047688 -0.753724  0.086409  0.652343 -0.438745

根据层级分组时,将层级数值或层级名称传递给level关键字:

a = df.groupby(level='cty',axis=1).count()
print(a)
------------------------------------------------------------
cty  JP  US
0     2   3
1     2   3
2     2   3
3     2   3

二、数据聚合

聚合是指所有根据数组产生标量值的数据转换过程。之前的例子已经使用了一些聚合操作,包括mean、count、min和sum等。很多常见的聚合,例如下表中的操作都有了优化实现。然而,你要用的可能并不局限于下面这个方法集。

函数名 描述
count 分组中非NA值数量
sum 非NA值的累和
mean 非NA值的均值
median 非NA值的算术中位数
std、var 无偏的(n-1分母)标准差和方差
min、max 非NA值的最小值、最大值
prod 非NA值的乘积
first、last 非NA值的第一个和最后一个值

也可以使用自行制定的聚合,并再调用已经在分组对象上定义好的方法。例如,你可能还记得quantile可以计算Series或DataFrame列的样本分位数。尽管quantile并不是显式地为GroupBy对象实现的,但它是Series的方法,因此也可以用于聚合。在内部,GroupBy有效地对Series进行切片,为每一块调用piece.quantile(0.9),然后将这些结果一起组装到结果对象中:

print(df)
a = df.groupby('key1')['data1'].quantile(0.9)
print(a)
------------------------------------------------------
  key1 key2     data1     data2
0    a  one  1.548485 -0.219585
1    a  two  0.447387 -0.100247
2    b  one  0.134934 -1.013706
3    b  two -1.090257  0.071942
4    a  one -1.650373 -0.496783

key1
a    1.328265
b    0.012415
Name: data1, dtype: float64

要使用你自己的聚合函数,需要将函数传递给aggregateagg方法

def peak_to_peak(arr):
    return arr.max()-arr.min()
a = df.groupby('key1').agg(peak_to_peak)
print(a)
-----------------------------------------------
         data1     data2
key1                    
a     3.198858  0.396536
b     1.225191  1.085648

你可能会注意到一些方法,比如describe也是有效的,尽管严格来说它们并不是聚合函数:

a = df.groupby('key1').describe()
print(a)
-----------------------------------------------------
     data1                      ...     data2                    
     count      mean       std  ...       50%       75%       max
key1                            ...                              
a      3.0  0.115166  1.625100  ... -0.219585 -0.159916 -0.100247
b      2.0 -0.477662  0.866341  ... -0.470882 -0.199470  0.071942

2.1 逐列及多函数应用

让我们回到上一章中的小费数据集。在使用read_csv载入数据集后,我们增加一个小费比例列tip_pct:

tips = pd.read_csv('D:\浏览器下载\pydata-book-2nd-edition\pydata-book-2nd-edition\examples/tips.csv')
tips['tip_pct'] = tips['tip']/tips['total_bill']
print(tips[:6])
--------------------------------------------------------------------------------
   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这种方法的。然而,你可能想根据各列同时使用多个函数进行聚合。这是可以做到的。首先,得将根据day和smoker来对tips进行分组:

grouped = tips.groupby(['day','smoker'])
grouped_pct = grouped['tip_pct']
a = grouped_pct.agg('mean')
print(a)
------------------------------------------------------------------------
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

如果你传递的是函数或函数名的列表,你会获得一个列名是这些函数名的DataFrame:

a = grouped_pct.agg(['mean', 'std', peak_to_peak])
print(a)
-------------------------------------------------------------------------
                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

这里我们传递了聚合函数的列表agg方法,这些函数会各自运用于数据分组。

如果你传递的是(name,function)元组的列表,每个元组的第一个元素将作为DataFrame的列名:

a = grouped_pct.agg([('foo','mean'),('bar',np.std)])
print(a)
-------------------------------------------------------------------
                  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列的三个相同的统计值:

functions = ['count','max','min']
result = grouped['tip_pct','total_bill'].agg(functions)
print(result)
-----------------------------------------------------------------
            tip_pct                     total_bill              
              count       max       min      count    max    min
day  smoker                                                     
Fri  No           4  0.187735  0.120385          4  22.75  12.46
     Yes         15  0.263480  0.103555         15  40.17   5.75
Sat  No          45  0.291990  0.056797         45  48.33   7.25
     Yes         42  0.325733  0.035638         42  50.81   3.07
Sun  No          57  0.252672  0.059447         57  48.17   8.77
     Yes         19  0.710345  0.065660         19  45.35   7.25
Thur No          45  0.266312  0.072961         45  41.19   7.51
     Yes         17  0.241255  0.090014         17  43.11  10.34

如你所见,产生的DataFrame拥有分层列,与分别聚合每一列,再以列名作为keys参数使用concat将结果拼接在一起的结果相同:

和之前一样,可以传递具有自定义名称的元组列表:

ftuples = [('Magnum','mean'),('Boost',np.var)]
a = grouped['tip_pct','total_bill'].agg(ftuples)
print(a)
-------------------------------------------------------
              tip_pct           total_bill            
               Magnum     Boost     Magnum       Boost
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:

a = grouped.agg({'tip':np.max,'size':'sum'})
print(a)
a = grouped.agg({'tip_pct':['min','max','mean','std'],
                 'size':'sum'})
print(a)
-------------------------------------------------------------------------
               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
     
              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才具有分层列。

2.2 返回不含行索引的聚合数据

在前面所有的列子中,聚合数据返回时都是带有索引的,有时索引是分层的,由唯一的分组键联合形成的,你也可以通过向groupby传递ax_index = False 来禁用分组键作为索引的行为:

a = tips.groupby(['day','smoker']).mean()
print(a)
a = tips.groupby(['day','smoker'],as_index=False).mean()
print(a)
--------------------------------------------------------------------------
             total_bill       tip      size   tip_pct
day  smoker                                          
Fri  No       18.420000  2.812500  2.250000  0.151650
     Yes      16.813333  2.714000  2.066667  0.174783
Sat  No       19.661778  3.102889  2.555556  0.158048
     Yes      21.276667  2.875476  2.476190  0.147906
Sun  No       20.506667  3.167895  2.929825  0.160113
     Yes      24.120000  3.516842  2.578947  0.187250
Thur No       17.113111  2.673778  2.488889  0.160298
     Yes      19.190588  3.030000  2.352941  0.163863

    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

总结

以上就是今天要讲的内容,本文仅仅简单介绍了利用GroupBy的机制对数据集进行拆分-应用-联合。而这两节分别介绍了拆分时和聚合时可以做到的功能和细则。下一节将围绕应用进行学习。

你可能感兴趣的:(python,数据分析,pandas)