1. 程式人生 > 其它 >pandas之分組groupby學習筆記

pandas之分組groupby學習筆記

技術標籤:pandas問題python

分組

In [1]: import numpy as np

In [2]: import pandas as pd

一、分組模式及其物件

1. 分組的一般模式

分組操作在日常生活中使用極其廣泛,例如:

依據 性別 分組,統計全國人口 壽命平均值

依據 季節 分組,對每一個季節的 溫度 進行 組內標準化

依據 班級 分組,篩選出組內 數學分數平均值超過80分的班級

從上述的幾個例子中不難看出,想要實現分組操作,必須明確三個要素:分組依據 、 資料來源 、 操作及其返回結果 。同時從充分性的角度來說,如果明確了這三方面,就能確定一個分組操作,從而分組程式碼的一般模式即:

df.groupby(分組依據)[資料來源].使用操作

例如第一個例子中的程式碼就應該如下:

df.groupby('Gender')['Longevity'].mean()

現在返回到學生體測的資料集上,如果想要按照性別統計身高中位數,就可以如下寫出:

In [3]: df = pd.read_csv('data/learn_pandas.csv')

In [4]: df.groupby('Gender')['Height'].median()
Out[4]: 
Gender
Female    159.6
Male      173.4
Name: Height, dtype: float64

2. 分組依據的本質

前面提到的若干例子都是以單一維度進行分組的,比如根據性別,如果現在需要根據多個維度進行分組,該如何做?事實上,只需在 groupby 中傳入相應列名構成的列表即可。例如,現希望根據學校和性別進行分組,統計身高的均值就可以如下寫出:

In [5]: df.groupby(['School', 'Gender'])['Height'].mean()
Out[5]: 
School                         Gender
Fudan University               Female    158.776923
                               Male      174.212500
Peking University Female 158.666667 Male 172.030000 Shanghai Jiao Tong University Female 159.122500 Male 176.760000 Tsinghua University Female 159.753333 Male 171.638889 Name: Height, dtype: float64

目前為止, groupby 的分組依據都是直接可以從列中按照名字獲取的,那如果希望通過一定的複雜邏輯來分組,例如根據學生體重是否超過總體均值來分組,同樣還是計算身高的均值。

首先應該先寫出分組條件:

In [6]: condition = df.Weight > df.Weight.mean()

然後將其傳入 groupby 中:

In [7]: df.groupby(condition)['Height'].mean()
Out[7]: 
Weight
False    159.034646
True     172.705357
Name: Height, dtype: float64

之前傳入列名只是一種簡便的記號,事實上等價於傳入的是一個或多個列,最後分組的依據來自於資料來源組合的unique 值,通過 drop_duplicates 就能知道具體的組類別:

In [11]: df[['School', 'Gender']].drop_duplicates()
Out[11]: 
                           School  Gender
0   Shanghai Jiao Tong University  Female
1               Peking University    Male
2   Shanghai Jiao Tong University    Male
3                Fudan University  Female
4                Fudan University    Male
5             Tsinghua University  Female
9               Peking University  Female
16            Tsinghua University    Male

In [12]: df.groupby([df['School'], df['Gender']])['Height'].mean()
Out[12]: 
School                         Gender
Fudan University               Female    158.776923
                               Male      174.212500
Peking University              Female    158.666667
                               Male      172.030000
Shanghai Jiao Tong University  Female    159.122500
                               Male      176.760000
Tsinghua University            Female    159.753333
                               Male      171.638889
Name: Height, dtype: float64

3. Groupby物件

能夠注意到,最終具體做分組操作時,所呼叫的方法都來自於 pandas 中的 groupby 物件,這個物件上定義了許多方法,也具有一些方便的屬性。

In [13]: gb = df.groupby(['School', 'Grade'])

In [14]: gb
Out[14]: <pandas.core.groupby.generic.DataFrameGroupBy object at 0x000001EBFBAC5F48>

通過 ngroups 屬性,可以得到分組個數:

In [15]: gb.ngroups
Out[15]: 16

通過groups 屬性,可以返回從 組名 對映到 組索引列表 的字典:

In [16]: res = gb.groups

In [17]: res.keys() # 字典的值由於是索引,元素個數過多,此處只展示字典的鍵
Out[17]: dict_keys([('Fudan University', 'Freshman'), ('Fudan University', 'Junior'), ('Fudan University', 'Senior'), ('Fudan University', 'Sophomore'), ('Peking University', 'Freshman'), ('Peking University', 'Junior'), ('Peking University', 'Senior'), ('Peking University', 'Sophomore'), ('Shanghai Jiao Tong University', 'Freshman'), ('Shanghai Jiao Tong University', 'Junior'), ('Shanghai Jiao Tong University', 'Senior'), ('Shanghai Jiao Tong University', 'Sophomore'), ('Tsinghua University', 'Freshman'), ('Tsinghua University', 'Junior'), ('Tsinghua University', 'Senior'), ('Tsinghua University', 'Sophomore')])

size 作為 DataFrame 的屬性時,返回的是表長乘以表寬的大小,但在 groupby 物件上表示統計每個組的元素個數:

In [18]: gb.size()
Out[18]: 
School                         Grade    
Fudan University               Freshman      9
                               Junior       12
                               Senior       11
                               Sophomore     8
Peking University              Freshman     13
                               Junior        8
                               Senior        8
                               Sophomore     5
Shanghai Jiao Tong University  Freshman     13
                               Junior       17
                               Senior       22
                               Sophomore     5
Tsinghua University            Freshman     17
                               Junior       22
                               Senior       14
                               Sophomore    16
dtype: int64

通過 get_group 方法可以直接獲取所在組對應的行,此時必須知道組的具體名字:

In [19]: gb.get_group(('Fudan University', 'Freshman')).iloc[:3, :3] # 展示一部分
Out[19]: 
              School     Grade             Name
15  Fudan University  Freshman  Changqiang Yang
28  Fudan University  Freshman     Gaoqiang Qin
63  Fudan University  Freshman     Gaofeng Zhao

4. 分組的三大操作

熟悉了一些分組的基本知識後,重新回到開頭舉的三個例子,可能會發現一些端倪,即這三種類型分組返回的資料型態並不一樣:

第一個例子中,每一個組返回一個標量值,可以是平均值、中位數、組容量 size 等

第二個例子中,做了原序列的標準化處理,也就是說每組返回的是一個 Series 型別

第三個例子中,既不是標量也不是序列,返回的整個組所在行的本身,即返回了 DataFrame 型別

由此,引申出分組的三大操作:聚合、變換和過濾 ,分別對應了三個例子的操作,下面就要分別介紹相應的aggtransformfilter 函式及其操作。

二、聚合函式

1. 內建聚合函式

在介紹agg之前,首先要了解一些直接定義在groupby 物件的聚合函式,因為它的速度基本都會經過內部的優化,使用功能時應當優先考慮。根據返回標量值的原則,包括如下函式: max/min/mean/median/count/all/any/idxmax/idxmin/mad/nunique/skew/quantile/sum/std/var/sem/size/prod

In [20]: gb = df.groupby('Gender')['Height']

In [21]: gb.idxmin()
Out[21]: 
Gender
Female    143
Male      199
Name: Height, dtype: int64

In [22]: gb.quantile(0.95)
Out[22]: 
Gender
Female    166.8
Male      185.9
Name: Height, dtype: float64

這些聚合函式當傳入的資料來源包含多個列時,將按照列進行迭代計算:

In [23]: gb = df.groupby('Gender')[['Height', 'Weight']]

In [24]: gb.max()
Out[24]: 
        Height  Weight
Gender                
Female   170.2    63.0
Male     193.9    89.0

2. agg方法

雖然在 groupby 物件上定義了許多方便的函式,但仍然有以下不便之處:

無法同時使用多個函式

無法對特定的列使用特定的聚合函式

無法使用自定義的聚合函式

無法直接對結果的列名在聚合前進行自定義命名

下面說明如何通過 agg 函式解決這四類問題:

【a】使用多個函式

當使用多個聚合函式時,需要用列表的形式把內建聚合函式對應的字串傳入,先前提到的所有字串都是合法的。

In [25]: gb.agg(['sum', 'idxmax', 'skew'])
Out[25]: 
         Height                   Weight                 
            sum idxmax      skew     sum idxmax      skew
Gender                                                   
Female  21014.0     28 -0.219253  6469.0     28 -0.268482
Male     8854.9    193  0.437535  3929.0      2 -0.332393

從結果看,此時的列索引為多級索引,第一層為資料來源,第二層為使用的聚合方法,分別逐一對列使用聚合,因此結果為6列。

【b】對特定的列使用特定的聚合函式

對於方法和列的特殊對應,可以通過構造字典傳入 agg 中實現,其中字典以列名為鍵,以聚合字串或字串列表為值。

In [26]: gb.agg({'Height':['mean','max'], 'Weight':'count'})
Out[26]: 
           Height        Weight
             mean    max  count
Gender                         
Female  159.19697  170.2    135
Male    173.62549  193.9     54

【c】使用自定義函式

在 agg 中可以使用具體的自定義函式, 需要注意傳入函式的引數是之前資料來源中的列,逐列進行計算 。下面分組計算身高和體重的極差:

In [27]: gb.agg(lambda x: x.mean()-x.min())
Out[27]: 
          Height     Weight
Gender                     
Female  13.79697  13.918519
Male    17.92549  21.759259

由於傳入的是序列,因此序列上的方法和屬性都是可以在函式中使用的,只需保證返回值是標量即可。下面的例子是指,如果組的指標均值,超過該指標的總體均值,返回High,否則返回Low。

In [28]: def my_func(s):
   ....:     res = 'High'
   ....:     if s.mean() <= df[s.name].mean():
   ....:         res = 'Low'
   ....:     return res
   ....: 

In [29]: gb.agg(my_func)
Out[29]: 
       Height Weight
Gender              
Female    Low    Low
Male     High   High

【d】聚合結果重新命名
如果想要對聚合結果的列名進行重新命名,只需要將上述函式的位置改寫成元組,元組的第一個元素為新的名字,第二個位置為原來的函式,包括聚合字串和自定義函式,現舉若干例子說明:

In [30]: gb.agg([('range', lambda x: x.max()-x.min()), ('my_sum', 'sum')])
Out[30]: 
       Height          Weight        
        range   my_sum  range  my_sum
Gender                               
Female   24.8  21014.0   29.0  6469.0
Male     38.2   8854.9   38.0  3929.0
In [31]: gb.agg({'Height': [('my_func', my_func), 'sum'],
   ....:         'Weight': lambda x:x.max()})
   ....: 
Out[31]: 
        Height            Weight
       my_func      sum <lambda>
Gender                          
Female     Low  21014.0     63.0
Male      High   8854.9     89.0

另外需要注意,使用對一個或者多個列使用單個聚合的時候,重新命名需要加方括號,否則就不知道是新的名字還是手誤輸錯的內建函式字串:

In [32]: gb.agg([('my_sum', 'sum')])
Out[32]: 
         Height  Weight
         my_sum  my_sum
Gender                 
Female  21014.0  6469.0
Male     8854.9  3929.0
In [33]: gb.agg({'Height': [('my_func', my_func), 'sum'],
   ....:         'Weight': [('range', lambda x:x.max())]})
   ....: 
Out[33]: 
        Height          Weight
       my_func      sum  range
Gender                        
Female     Low  21014.0   63.0
Male      High   8854.9   89.0

三、變換和過濾

1. 變換函式與transform方法

變換函式的返回值為同長度的序列,最常用的內建變換函式是累計函式: cumcount/cumsum/cumprod/cummax/cummin ,它們的使用方式和聚合函式類似,只不過完成的是組內累計操作。

In [34]: gb.cummax().head()
Out[34]: 
   Height  Weight
0   158.9    46.0
1   166.5    70.0
2   188.9    89.0
3     NaN    46.0
4   188.9    89.0

當用自定義變換時需要使用 transform 方法,被呼叫的自定義函式, 其傳入值為資料來源的序列 ,與agg 的傳入型別是一致的,其最後的返回結果是行列索引與資料來源一致的 DataFrame

現對身高和體重進行分組標準化,即減去組均值後除以組的標準差:

In [35]: gb.transform(lambda x: (x-x.mean())/x.std()).head()
Out[35]: 
     Height    Weight
0 -0.058760 -0.354888
1 -1.010925 -0.355000
2  2.167063  2.089498
3       NaN -1.279789
4  0.053133  0.159631

前面提到了 transform 只能返回同長度的序列,但事實上還可以返回一個標量,這會使得結果被廣播到其所在的整個組,這種 標量廣播 的技巧在特徵工程中是非常常見的。例如,構造兩列新特徵來分別表示樣本所在性別組的身高均值和體重均值:

In [36]: gb.transform('mean').head() # 傳入返回標量的函式也是可以的
Out[36]: 
      Height     Weight
0  159.19697  47.918519
1  173.62549  72.759259
2  173.62549  72.759259
3  159.19697  47.918519
4  173.62549  72.759259

2. 組索引與過濾

過濾在分組中是對於組的過濾,而索引是對於行的過濾,無論是布林列表還是元素列表或者位置列表,本質上都是對於行的篩選,即如果符合篩選條件的則選入結果表,否則不選入。

組過濾作為行過濾的推廣,指的是如果對一個組的全體所在行進行統計的結果返回 True 則會被保留, False 則該組會被過濾,最後把所有未被過濾的組其對應的所在行拼接起來作為 DataFrame 返回。

groupby 物件中,定義了 filter 方法進行組的篩選,其中自定義函式的輸入引數為資料來源構成的 DataFrame 本身,在之前例子中定義的 groupby 物件中,傳入的就是 df[['Height', 'Weight']] ,因此所有表方法和屬性都可以在自定義函式中相應地使用,同時只需保證自定義函式的返回為布林值即可。

例如,在原表中通過過濾得到所有容量大於100的組:

In [37]: gb.filter(lambda x: x.shape[0] > 100).head()
Out[37]: 
   Height  Weight
0   158.9    46.0
3     NaN    41.0
5   158.0    51.0
6   162.5    52.0
7   161.9    50.0