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

文章目录

  • 前言
  • 一、应用:通用拆分-应用-联合
    • 1.1 压缩分组键
    • 1.2 分位数与桶分析
    • 1.3 示例:使用指定分组值填充缺失值
    • 1.4 示例:随机采样与排列
    • 1.5 示例:分组加权平均和相关性
    • 1.6 示例:逐组线性回归
  • 二、数据透视表与交叉表
    • 2.1 交叉表:crosstab
  • 总结


前言

根据上一篇文章对数据集的拆分-应用-联合,具体讲解了写拆分和联合用到的细则,这篇将围绕应用这块进行描述。


一、应用:通用拆分-应用-联合

GroupBy方法最常见的目的是apply(应用),这将是本节的内容。apply将对象拆分成多块,然后在每一块上调用传递函数,之后尝试将每一块拼接到一起。

回到之前的小费数据集,假设你想要按组选出小费百分比(tip_pct)最高的五组。首先,写一个可以在特定列中选出最大值所在行的函数:

def top(df,n=5,column='tip_pct'):
    return df.sort_values(by=column)[-n:]
a = top(tips,n=6)
print(a)
---------------------------------------------------------
     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,我们会得到以下结果:

            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传递函数,还传递其他参数或关键字的话,你可以把这些放在函数后进行传递:

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

之前我们有在GroupBy对象上调用describe方法,在GroupBy对象的内部,当你调用像describe这样的方法时,实际上是以下代码的简写:

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

1.1 压缩分组键

在之前的例子中,你可以看到所得到的对象具有分组键所形成的分层索引以及每个原始对象的索引。你可以通过向groupby传递group_keys=False来禁用这个功能:

a = tips.groupby('smoker',group_keys=False).apply(top)
print(a)
------------------------------------------------------------------------
     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


1.2 分位数与桶分析

在之前的第七章中,pandas有一些工具,尤其是cut和qcut,用于将数据按照你选择的箱位或样本分位数进行分桶。与groupby方法一起使用这些函数可以对数据集更方便地进行分桶或分位分析。考虑一个简单的随机数据集和一个使用cut的等长桶分类:

frame = pd.DataFrame({'data1':np.random.randn(1000),
                      'data2':np.random.randn(1000)})
quartiles=pd.cut(frame.data1,4)
print(quartiles)
-----------------------------------------------------------------------------
0      (-1.487, 0.0725]
1      (-1.487, 0.0725]
2      (-1.487, 0.0725]
3       (0.0725, 1.632]
4      (-1.487, 0.0725]
             ...       
995    (-3.053, -1.487]
996      (1.632, 3.192]
997     (0.0725, 1.632]
998    (-1.487, 0.0725]
999    (-1.487, 0.0725]
Name: data1, Length: 1000, dtype: category
Categories (4, interval[float64, right]): [(-3.053, -1.487] < (-1.487, 0.0725] < (0.0725, 1.632] <
                                           (1.632, 3.192]]

cut返回的Categories对象可以直接传递给groupby。所以我们可以计算出data2列的一个统计值集合:

def get_stats(group):
    return {'min': group.min(), 'max': group.max(),
            'count': group.count(), 'mean': group.mean()}


grouped = frame.data2.groupby(quartiles)
a = grouped.apply(get_stats).unstack()
print(a)
-------------------------------------------------------------
                       min       max  count      mean
data1                                                
(-3.053, -1.487] -1.929778  2.387990   58.0 -0.067484
(-1.487, 0.0725] -3.116310  3.202132  474.0  0.054767
(0.0725, 1.632]  -3.019290  2.734140  415.0 -0.118471
(1.632, 3.192]   -3.065552  2.069831   53.0 -0.246630

为了根据样本分位数计算出等大小的桶,则需要qcut。我们将传递labels=False来获得分位数:

grouping = pd.qcut(frame.data1,10,labels=False)
grouped = frame.data2.groupby(grouping)
a = grouped.apply(get_stats).unstack()
print(a)
---------------------------------------------------------------
            min       max  count      mean
data1                                     
0     -2.452613  2.387990  100.0  0.009122
1     -2.711972  1.814960  100.0  0.012730
2     -2.489099  3.202132  100.0  0.032057
3     -3.116310  2.554327  100.0  0.094443
4     -1.919159  2.781821  100.0  0.071050
5     -2.370437  2.393702  100.0 -0.021028
6     -2.903778  2.734140  100.0 -0.327223
7     -2.969580  1.735026  100.0 -0.041452
8     -2.050720  2.268590  100.0  0.004044
9     -3.065552  2.069831  100.0 -0.235658

1.3 示例:使用指定分组值填充缺失值

在清楚缺失值时,有时你会使用dronpa来去除缺失值,但是有时你可能想要使用修正值或来自于其他数据的值来输入(填充)到null值(NA)。fillna是一个可以使用的正确工具,例如这里我们使用平均值来填充NA值:

s = pd.Series(np.random.randn(6))
s[::2] = np.nan
print(s)
a = s.fillna(s.mean())
print(a)
---------------------------------------------------------------------------
0         NaN
1    0.882770
2         NaN
3   -0.086084
4         NaN
5   -0.221206
dtype: float64

0    0.191826
1    0.882770
2    0.191826
3   -0.086084
4    0.191826
5   -0.221206
dtype: float64

假设你需要填充值按组变化。一个方法是对数据分组后使用apply和一个在每个数据块上都调用fillna的函数:

states = ['Ohio','New York','Vermont','Florida','Oregon','Nevada','California','Idaho']
group_key = ['East']*4+['West']*4
print(group_key)
s = pd.Series(np.random.randn(8),index=states)
print(s)

--------------------------------------------------------------------------------
['East', 'East', 'East', 'East', 'West', 'West', 'West', 'West']

Ohio         -1.150053
New York      0.041887
Vermont       0.975530
Florida      -0.153980
Oregon       -0.531615
Nevada       -0.720547
California    1.093888
Idaho         1.278055
dtype: float64

然后我们将数据中的一些值设置为缺失值:

s[['Vermont','Nevada','Idaho']]=np.nan
print(s)
a = s.groupby(group_key).mean()
print(a)
---------------------------------------------------------------
Ohio         -1.150053
New York      0.041887
Vermont            NaN
Florida      -0.153980
Oregon       -0.531615
Nevada             NaN
California    1.093888
Idaho              NaN
dtype: float64

East   -0.420715
West    0.281137
dtype: float64

之后我们使用分组的平均值来填充NA值:

fill_mean = lambda g:g.fillna(g.mean())
a = s.groupby(group_key).apply(fill_mean)
print(a)
---------------------------------------------------------------
Ohio         -1.150053
New York      0.041887
Vermont      -0.420715
Florida      -0.153980
Oregon       -0.531615
Nevada        0.281137
California    1.093888
Idaho         0.281137
dtype: float64

如果你已经在代码中为每个分组预定义了填充值。由于每个分组都有一个内置的name属性,就可以这样使用 :

fill_values = {'East':0.5,'West':-1}
fill_func = lambda g: g.fillna(fill_values[g.name])
a = s.groupby(group_key).apply(fill_func)
print(a)
-------------------------------------------------------------
Ohio         -1.150053
New York      0.041887
Vermont       0.500000
Florida      -0.153980
Oregon       -0.531615
Nevada       -1.000000
California    1.093888
Idaho        -1.000000
dtype: float64

1.4 示例:随机采样与排列

假设你想从大数据集中抽取随机样本(有或没有替换)以用于蒙特卡罗模拟目的或某些其他应用程序。有很多方法来执行“抽取”,这里我们使用Series的sample方法:
为了演示,这里讲解一种构造一副英式扑克牌的方法:

# 红桃、黑桃、梅花、方块
suits = ['H', 'S', 'C', 'D']
card_val = (list(range(1,11))+[10]*3)*4
base_names = ['A']+ list(range(2,11))+['J','K','Q']
cards =  []
for suit in suits:
    cards.extend(str(num)+suit for num in base_names)
deck = pd.Series(card_val,index=cards)
print(deck[:13])
----------------------------------------------------------------------------
AH      1
2H      2
3H      3
4H      4
5H      5
6H      6
7H      7
8H      8
9H      9
10H    10
JH     10
KH     10
QH     10
dtype: int64

利用sample方法,从这副牌中拿出五张牌可以写成:

def draw(deck,n=5):
    return deck.sample(n)
a = draw(deck)
print(a)
------------------------------------------------------
JD    10
7D     7
5C     5
9C     9
3D     3
dtype: int64

假设你想要从每个花色中随机抽取两张牌。由于花色是牌名的最后两个字符,我们可以基于这点进行分组,并使用apply:

get_suit = lambda card : card[-1]
a = deck.groupby(get_suit).apply(draw,n=2)
print(a)
------------------------------------------------------------------------
dtype: int64
C  KC    10
   5C     5
D  3D     3
   AD     1
H  3H     3
   9H     9
S  QS    10
   8S     8
dtype: int64

或者我们也可以写成:

a = deck.groupby(get_suit,group_keys=False).apply(draw,n=2)
print(a)
--------------------------------------------------------------------
8C     8
4C     4
5D     5
6D     6
7H     7
6H     6
9S     9
JS    10
dtype: int64

1.5 示例:分组加权平均和相关性

在groupby的拆分-应用-联合的范式下,DataFrame的列间操作或两个Series之间的操作,例如分组加权平均是可以做到的。作为一个例子,我们使用一个包含分组键和权重值的数据集:

df = pd.DataFrame({'category':['a','a','a','a','b','b','b','b'],
                   'data':np.random.randn(8),
                   'weights':np.random.rand(8)})
print(df)
------------------------------------------------------------------------
  category      data   weights
0        a  0.410634  0.040028
1        a -0.788315  0.213055
2        a -0.654367  0.570789
3        a -0.157310  0.199540
4        b  0.411850  0.656720
5        b  0.165850  0.358905
6        b  1.337180  0.206716
7        b -0.517227  0.329182

通过category进行分组加权平均如下:

grouped = df.groupby('category')
get_wavg = lambda g :np.average(g['data'],weights=g['weights'])
a = grouped.apply(get_wavg)
print(a)
-------------------------------------------------------
category
a   -0.543685
b    0.281110
dtype: float64

作为另一个例子,是一个从雅虎财经上获得的数据集,该数据集包含一些标普500(SPX符号)和股票的收盘价:

close_px = pd.read_csv('D:\浏览器下载\pydata-book-2nd-edition\pydata-book-2nd-edition\examples/stock_px_2.csv',parse_dates=True,index_col=0)
close_px.info()
print(close_px[-4:])
--------------------------------------------------------------------------------------------
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 2214 entries, 2003-01-02 to 2011-10-14
Data columns (total 4 columns):
 #   Column  Non-Null Count  Dtype  
---  ------  --------------  -----  
 0   AAPL    2214 non-null   float64
 1   MSFT    2214 non-null   float64
 2   XOM     2214 non-null   float64
 3   SPX     2214 non-null   float64
dtypes: float64(4)
memory usage: 86.5 KB

               Unnamed: 0    AAPL   MSFT    XOM      SPX
2210  2011-10-11 00:00:00  400.29  27.00  76.27  1195.54
2211  2011-10-12 00:00:00  402.19  26.96  77.16  1207.25
2212  2011-10-13 00:00:00  408.43  27.18  76.37  1203.66
2213  2011-10-14 00:00:00  422.00  27.27  78.11  1224.58

计算一个DataFrame,它包含标普指数(spx)每日收益的年度相关性(通过百分比变化计算)。作为实现的一种方式,我们首先创建一个计算每列与’SPX’列成关联的函数:

spx_corr = lambda x:x.corrwith(x['SPX'])

之后,我们使用pct_change计算close_px百分比的变化:

rets = close_px.pct_change().dropna()

最后,我们按年对百分比进行分组,可以使用单行函数从每个标签中提取每个datetime标签的year属性:

get_year =lambda x:x.year
by_year =rets.groupby(get_year)
a = by_year.apply(spx_corr)
print(a)
------------------------------------------------------------------
          AAPL      MSFT       XOM  SPX
2003  0.541124  0.745174  0.661265  1.0
2004  0.374283  0.588531  0.557742  1.0
2005  0.467540  0.562374  0.631010  1.0
2006  0.428267  0.406126  0.518514  1.0
2007  0.508118  0.658770  0.786264  1.0
2008  0.681434  0.804626  0.828303  1.0
2009  0.707103  0.654902  0.797921  1.0
2010  0.710105  0.730118  0.839057  1.0
2011  0.691931  0.800996  0.859975  1.0

你也可以计算内部列相关性:

a = by_year.apply(lambda g:g['AAPL'].corr(g['MSFT']))
print(a)
-------------------------------------------------------------------
2003    0.480868
2004    0.259024
2005    0.300093
2006    0.161735
2007    0.417738
2008    0.611901
2009    0.432738
2010    0.571946
2011    0.581987
dtype: float64

1.6 示例:逐组线性回归

该示例中,我们可以定义以下regress(回归)函数,该函数对每个数据块执行普通最小二乘(OLS)回归:

import statsmodels.api as sm
def regress(data,yvar,xvars):
    Y=data[yvar]
    X=data[xvars]
    X['intercept']=1.
    result = sm.OLS(Y,X).fit()
    return result.params

现在,要计算AAPL在SPX回报上的年度线性回归,执行以下代码:

a = by_year.apply(regress,'AAPL',['SPX'])
print(a)
-----------------------------------------------------------------------
           SPX  intercept
2003  1.195406   0.000710
2004  1.363463   0.004201
2005  1.766415   0.003246
2006  1.645496   0.000080
2007  1.198761   0.003438
2008  0.968016  -0.001110
2009  0.879103   0.002954
2010  1.052608   0.001261
2011  0.806605   0.001514

二、数据透视表与交叉表

数据透视表是电子表格程序和其他数据分析软件中常见的数据汇总工具。它根据一个或多个键聚合一张表的数据,将数据在矩形格式中排列,其中一些分组键是沿着行的,另一些是沿着列的。Python中的pandas透视表是通过本章介绍的groupby工具以及使用分层索引的重塑操作实现。DataFrame拥有一个pivot_table方法,并且还有一个顶层的pandas.pivot_table函数。除了为groupby提供一个方便接口,pivot_table还可以添加部分设计,也称作边距。

回到小费数据集,假设你想要计算一张在行方向上按day和smoker排列的分组平均值(默认的pivot_table聚合类型)的表:

a = tips.pivot_table(index=['day','smoker'])
print(a)
-----------------------------------------------------------------------
                 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

现在,假设我们只想在tip_pct和size上进行聚合,并根据time分组。我将把smoker放入表列,而将day放入表的行:

a = tips.pivot_table(['tip_pct','size'],index=['time','day'],
                     columns='smoker')
print(a)
-----------------------------------------------------------------------------
                 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

我们可以通过传递margins=True来扩充这个表包含部分总计。这会添加ALL行和列标签,其中相应的值是单层中所有数据的分组统计值:

a = tips.pivot_table(['tip_pct','size'],index=['time','day'],
                     columns='smoker',margins=True)
print(a)
------------------------------------------------------------------------------
                 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行)。
要使用不同的聚合函数时,将函数传递给aggfunc。例如,’count‘或者len将给出一张分组大小的交叉表(计数或出现频率):

a = tips.pivot_table('tip_pct',index=['time','smoker'],
                     columns='day',aggfunc=len,margins=True)
print(a)
--------------------------------------------------------------------------------------------------
day             Fri   Sat   Sun  Thur  All
time   smoker                             
Dinner No       3.0  45.0  57.0   1.0  106
       Yes      9.0  42.0  19.0   NaN   70
Lunch  No       1.0   NaN   NaN  44.0   45
       Yes      6.0   NaN   NaN  17.0   23
All            19.0  87.0  76.0  62.0  244

如果某些情况下产生了空值(或者NA),你想要传递一个fill_value:

a = tips.pivot_table('tip_pct',index=['time','size','smoker'],
                     columns='day',aggfunc='mean',fill_value=0)
print(a)
---------------------------------------------------------------------------------------------------
day                      Fri       Sat       Sun      Thur
time   size smoker                                        
Dinner 1    No      0.000000  0.137931  0.000000  0.000000
            Yes     0.000000  0.325733  0.000000  0.000000
       2    No      0.139622  0.162705  0.168859  0.159744
            Yes     0.171297  0.148668  0.207893  0.000000
       3    No      0.000000  0.154661  0.152663  0.000000
            Yes     0.000000  0.144995  0.152660  0.000000
       4    No      0.000000  0.150096  0.148143  0.000000
            Yes     0.117750  0.124515  0.193370  0.000000
       5    No      0.000000  0.000000  0.206928  0.000000
            Yes     0.000000  0.106572  0.065660  0.000000
       6    No      0.000000  0.000000  0.103799  0.000000
Lunch  1    No      0.000000  0.000000  0.000000  0.181728
            Yes     0.223776  0.000000  0.000000  0.000000
       2    No      0.000000  0.000000  0.000000  0.166005
            Yes     0.181969  0.000000  0.000000  0.158843
       3    No      0.187735  0.000000  0.000000  0.084246
            Yes     0.000000  0.000000  0.000000  0.204952
       4    No      0.000000  0.000000  0.000000  0.138919
            Yes     0.000000  0.000000  0.000000  0.155410
       5    No      0.000000  0.000000  0.000000  0.121389
       6    No      0.000000  0.000000  0.000000  0.173706

下表是pivot_table选项表:

选项名 描述
values 需要聚合的列名;默认情况下聚合所有数值型的列
index 在结果透视表的行上进行分组的列名或其他分组键
columns 在结果透视表的列上进行分组的列名或其他分组键
aggfunc 聚合函数或函数列表(默认情况下是‘mean’);可是groupby上下文的任意有效函数
fill_value 在结果表中替换缺失值的值
dropna 如果为True,将不含所有条目均为NA的列
margins 添加行/列小计和总和(默认为False)

2.1 交叉表:crosstab

交叉表是数据透视表的一个特殊情况,计算的是分组中的频率,如下:

df = pd.DataFrame({'Sample':np.arange(1,11),
                   'Nationality':['USA','Japan','USA','Japan','Japan','Japan','USA','USA','Japan','USA'],
                   'Handedness':['Right-handed','left-handed','Right-handed','Right-handed','left-handed','Right-handed','Right-handed','left-handed','Right-handed','Right-handed']})
print(df)
-------------------------------------------------------------------------------------------------
   Sample Nationality    Handedness
0       1         USA  Right-handed
1       2       Japan   left-handed
2       3         USA  Right-handed
3       4       Japan  Right-handed
4       5       Japan   left-handed
5       6       Japan  Right-handed
6       7         USA  Right-handed
7       8         USA   left-handed
8       9       Japan  Right-handed
9      10         USA  Right-handed

作为研究分析的一部分,我们可能想按照国籍和惯用性来总结这些数据。你可以使用pivot_table来说实现这个功能,但是pandas.crosstab函数更为方便:

a = pd.crosstab(df.Nationality,df.Handedness,margins=True)
print(a)
----------------------------------------------------------------------------------------------------
Handedness   Right-handed  left-handed  All
Nationality                                
Japan                   3            2    5
USA                     4            1    5
All                     7            3   10

总结

以上就是今天要讲的内容,本文如何充分利用apply函数对数据集进行应用:通用拆分-应用-联合并且列举了相关的示例供参考,还学习了数据透视表和交叉表。精通pandas的数据分组工具既可以帮助我们清洗数据,也对建模或统计分析工作有益。

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