1. 程式人生 > 實用技巧 >pandas學習筆記(四)

pandas學習筆記(四)

pandas學習筆記(四) ——分組

分組模式及其物件

1.分組的一般模式

分組操作的三要素:分組依據資料來源操作及其返回結果

同時從充分性的角度來說,如果明確了這三方面,就能確定一個分組操作,從而分組程式碼的一般模式即:

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

例:按照性別統計年齡的中位數,可以如下寫出:

df.groupby('Sex')['Age'].median()
Out[10]: 
Sex
female    27.0
male      29.0
Name: Age, dtype: float64
2.分組依據的本質

前面提到的若干例子都是以單一維度進行分組的,比如根據性別,如果現在需要根據多個維度進行分組,該如何做?事實上,只需在 groupby

中傳入相應列名構成的列表即可。

例:根據乘客的目的港口和性別進行分組,統計年齡的均值,就可以如下寫出:

df.groupby(['Embarked','Sex'])['Age'].mean()
Out[11]: 
Embarked  Sex   
C         female    28.344262
          male      32.998841
Q         female    24.291667
          male      30.937500
S         female    27.771505
          male      30.291440
Name: Age, dtype: float64

前為止, groupby 的分組依據都是直接可以從列中按照名字獲取的,那如果希望通過一定的複雜邏輯來分組。

:根據乘客票價是否超過平均值來分組,同樣還是計算年齡的均值。

condition = df.Fare > df.Fare.mean()
df.groupby(condition)['Age'].mean()
Out[13]: 
Fare
False    28.554143
True     33.021421
Name: Age, dtype: float64

練一練

根據上下四分位數分割,將票價分為high、normal、low三組,統計年齡的均值。

data = df.copy()
def fenzhu(x):
  ...:     if x <= df.Fare.quantile(0.25):
  ...:         return('low')
  ...:     if x >= df.Fare.quantile(0.75):
  ...:         return('high')
  ...:     else:
  ...:         return('normal')
  ...:     
condition = df.Fare.apply(fenzhu)
data.groupby(condition)['Age'].mean()
Out[10]: 
Fare
high      32.030204
low       28.328671
normal    29.003333
Name: Age, dtype: float64
#果然有錢人的年齡普遍比較大<.<.

從索引可以看出,其實最後產生的結果就是按照條件列表中元素的值(此處是 TrueFalse )來分組,下面用隨機傳入字母序列來驗證這一想法:

item = np.random.choice(list('abc'),df.shape[0])
df.groupby(item)['Age'].mean()
Out[14]: 
a    29.659553
b    29.627702
c    29.823864
Name: Age, dtype: float64

此處的索引就是原先item中的元素,如果傳入多個序列進入 groupby ,那麼最後分組的依據就是這兩個序列對應行的唯一組合:

df.groupby([condition,item])['Age'].mean()
Out[18]: 
Fare     
high    a    31.666667
        b    32.347937
        c    32.155172
low     a    28.105769
        b    26.936170
        c    30.079545
normal  a    29.073529
        b    29.302536
        c    28.582627
Name: Age, dtype: float64

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

df[['Embarked','Sex']].drop_duplicates()
Out[19]: 
   Embarked     Sex
0         S    male
1         C  female
2         S  female
5         Q    male
22        Q  female
26        C    male
61      NaN  female
df.groupby([df['Embarked'],df['Sex']])['Age'].mean()
Out[20]: 
Embarked  Sex   
C         female    28.344262
          male      32.998841
Q         female    24.291667
          male      30.937500
S         female    27.771505
          male      30.291440
Name: Age, dtype: float64
3.Groupby物件

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

gb = df.groupby(['Embarked','Sex'])
gb
Out[22]: <pandas.core.groupby.generic.DataFrameGroupBy object at 0x000001E218071610>

gb.ngroups
Out[23]: 6          #通過ngroups屬性,得到分組個數

res = gb.groups
res.keys()          #字典的值由於是索引,元素個數過多,此處只展示字典的鍵
Out[25]: dict_keys([('S', 'male'), ('C', 'female'), ('S', 'female'), ('Q', 'male'), ('Q', 'female'), ('C', 'male'), (nan, 'female')])

練一練

上一小節介紹了可以通過 drop_duplicates 得到具體的組類別,現請用 groups 屬性完成類似的功能。

df.groupby(['Embarked','Sex']).groups.keys()
Out[26]: dict_keys([('S', 'male'), ('C', 'female'), ('S', 'female'), ('Q', 'male'), ('Q', 'female'), ('C', 'male'), (nan, 'female')])

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

gb.size()
Out[27]: 
Embarked  Sex   
C         female     73
          male       95
Q         female     36
          male       41
S         female    203
          male      441
dtype: int64

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

gb.get_group(('C','male')).iloc[:3,:3]
Out[28]: 
    PassengerId  Survived  Pclass
26           27         0       3
30           31         0       1
34           35         0       1

這裡列出了2個屬性和2個方法,而先前的 meanmedian 都是 groupby 物件上的方法,這些函式和許多其他函式的操作具有高度相似性,將在之後的小節進行專門介紹。

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

gb = df.groupby('Sex')['Age']
gb.idxmax()
Out[30]: 
Sex
female    275
male      630
Name: Age, dtype: int64

gb.quantile(0.95)
Out[31]: 
Sex
female    53.0
male      59.0
Name: Age, dtype: float64

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

gb = df.groupby('Sex')[['Age','Fare']]
gb.max()
Out[33]: 
         Age      Fare
Sex                   
female  63.0  512.3292
male    80.0  512.3292
2.agg方法

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

  • 無法同時使用多個函式
  • 無法對特定的列使用特定的聚合函式
  • 無法使用自定義的聚合函式
  • 無法直接對結果的列名在聚合前進行自定義命名

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

【a】使用多個函式

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

gb.agg(['sum','idxmax','skew'])
Out[35]: 
             Age                         Fare                 
             sum idxmax      skew         sum idxmax      skew
Sex                                                           
female   7286.00    275  0.206097  13966.6628    258  3.277623
male    13919.17    630  0.475318  14727.2865    679  6.621515

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

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

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

gb.agg({'Age':['mean','max'],'Fare':'count'})
Out[36]: 
              Age        Fare
             mean   max count
Sex                          
female  27.915709  63.0   314
male    30.726645  80.0   577

練一練

請使用【b】中的傳入字典的方法完成【a】中等價的聚合任務。

gb.agg({'Age':['sum','idxmax','skew'],'Fare':['sum','idxmax','skew']})
Out[37]: 
             Age                         Fare                 
             sum idxmax      skew         sum idxmax      skew
Sex                                                           
female   7286.00    275  0.206097  13966.6628    258  3.277623
male    13919.17    630  0.475318  14727.2865    679  6.621515

【c】使用自定義函式

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

gb.agg(lambda x : x.mean()-x.min())
Out[38]: 
              Age       Fare
Sex                         
female  27.165709  37.729818
male    30.306645  25.523893

練一練

groupby物件中可以使用describe方法進行統計資訊彙總,請同時使用多個聚合函式,完成與該方法相同的功能。

gb.describe()
Out[39]: 
          Age                              ...       Fare                       
        count       mean        std   min  ...        25%   50%    75%       max
Sex                                        ...                                  
female  261.0  27.915709  14.110146  0.75  ...  12.071875  23.0  55.00  512.3292
male    453.0  30.726645  14.678201  0.42  ...   7.895800  10.5  26.55  512.3292
[2 rows x 16 columns]
gb.agg(['count','mean','std','min',('25%',lambda x:x.quantile(0.25)),('50%','quantile'),('75%',lambda x:x.quantile(0.75)),'max'])
Out[40]: 
         Age                              ...       Fare                       
       count       mean        std   min  ...        25%   50%    75%       max
Sex                                       ...                                  
female   261  27.915709  14.110146  0.75  ...  12.071875  23.0  55.00  512.3292
male     453  30.726645  14.678201  0.42  ...   7.895800  10.5  26.55  512.3292
[2 rows x 16 columns]

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

def my_func(s):
   ...:     res = 'Age'
   ...:     if s.mean() <= df[s.name].mean():
   ...:         res = 'Low'
   ...:         return res
   ...:     
gb.agg(my_func)
Out[42]: 
         Age  Fare
Sex               
female   Low  None
male    None   Low

【d】聚合結果重新命名

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

gb.agg([('range', lambda x: x.max()-x.min()), ('my_sum', 'sum')])
Out[43]: 
          Age                Fare            
        range    my_sum     range      my_sum
Sex                                          
female  62.25   7286.00  505.5792  13966.6628
male    79.58  13919.17  512.3292  14727.2865

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

gb.agg([('my_sum', 'sum')])
Out[45]: 
             Age        Fare
          my_sum      my_sum
Sex                         
female   7286.00  13966.6628
male    13919.17  14727.2865

交換和過濾

1.交換函式與transform方法

變換函式的返回值為同長度的序列,最常用的內建變換函式是累計函式: cumcount/cumsum/cumprod/cummax/cummin ,它們的使用方式和聚合函式類似,只不過完成的是組內累計操作。此外在 groupby 物件上還定義了填充類和滑窗類的變換函式,這些函式的一般形式將會分別在第七章和第十章中討論,此處略過。

gb.cummax().head()
Out[46]: 
    Age     Fare
0  22.0   7.2500
1  38.0  71.2833
2  38.0  71.2833
3  38.0  71.2833
4  35.0   8.0500

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

現對年齡和票價進行分組標準化,即減去組均值後除以組的標準差:

gb.transform(lambda x: (x-x.mean())/x.std()).head()
Out[49]: 
        Age      Fare
0 -0.594531 -0.423612
1  0.714684  0.462147
2 -0.135768 -0.630280
3  0.502071  0.148630
4  0.291136 -0.405067

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

gb.transform('mean').head()
Out[50]: 
         Age       Fare
0  30.726645  25.523893
1  27.915709  44.479818
2  27.915709  44.479818
3  27.915709  44.479818
4  30.726645  25.523893
2.組索引與過濾

在上一章中介紹了索引的用法,那麼索引和過濾有什麼區別呢?

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

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

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

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

gb.filter(lambda x : x.shape[0] > 100).head()
Out[51]: 
    Age     Fare
0  22.0   7.2500
1  38.0  71.2833
2  26.0   7.9250
3  35.0  53.1000
4  35.0   8.0500

跨列分組

1.apply的引入

之前幾節介紹了三大分組操作,但事實上還有一種常見的分組場景,無法用前面介紹的任何一種方法處理,例如現在如下定義身體質量指數BMI:
$$
BMI=
Weight
/
Height^2​
$$
其中體重和身高的單位分別為千克和米,需要分組計算組BMI的均值。

首先,這顯然不是過濾操作,因此 filter 不符合要求;其次,返回的均值是標量而不是序列,因此 transform 不符合要求;最後,似乎使用 agg 函式能夠處理,但是之前強調過聚合函式是逐列處理的,而不能夠 多列資料同時處理 。由此,引出了 apply 函式來解決這一問題。

2.apply的使用

在設計上, apply 的自定義函式傳入引數與 filter 完全一致,只不過後者只允許返回布林值。現如下解決上述計算問題:

In [38]: def BMI(x):
   ....:     Height = x['Height']/100
   ....:     Weight = x['Weight']
   ....:     BMI_value = Weight/Height**2
   ....:     return BMI_value.mean()
   ....: 

In [39]: gb.apply(BMI)
Out[39]: 
Gender
Female    18.860930
Male      24.318654
dtype: float64

除了返回標量之外, apply 方法還可以返回一維 Series 和二維 DataFrame ,但它們產生的資料框維數和多級索引的層數應當如何變化?下面舉三組例子就非常容易明白結果是如何生成的:

【a】標量情況:結果得到的是 Series ,索引與 agg 的結果一致

gb = df.groupby(['Sex','Pclass'])[['Age','Fare']]
gb.apply(lambda x:0)
Out[54]: 
Sex     Pclass
female  1         0
        2         0
        3         0
male    1         0
        2         0
        3         0
dtype: int64
    
gb.apply(lambda x : [0,0])
Out[55]: 
Sex     Pclass
female  1         [0, 0]
        2         [0, 0]
        3         [0, 0]
male    1         [0, 0]
        2         [0, 0]
        3         [0, 0]
dtype: object

【b】 Series 情況:得到的是 DataFrame ,行索引與標量情況一致,列索引為 Series 的索引

gb.apply(lambda x: pd.Series([0,0],index=['a','b']))
Out[56]: 
               a  b
Sex    Pclass      
female 1       0  0
       2       0  0
       3       0  0
male   1       0  0
       2       0  0
       3       0  0

【c】 DataFrame 情況:得到的是 DataFrame ,行索引最內層在每個組原先 agg 的結果索引上,再加一層返回的 DataFrame 行索引,同時分組結果 DataFrame 的列索引和返回的 DataFrame 列索引一致。

gb.apply(lambda x: pd.DataFrame(np.ones((2,2)),
   ...:    ....:                                 index = ['a','b'],
   ...:    ....:                                 columns=pd.Index([('w','x'),('y','z')])))
Out[57]: 
                   w    y
                   x    z
Sex    Pclass            
female 1      a  1.0  1.0
              b  1.0  1.0
       2      a  1.0  1.0
              b  1.0  1.0
       3      a  1.0  1.0
              b  1.0  1.0
male   1      a  1.0  1.0
              b  1.0  1.0
       2      a  1.0  1.0
              b  1.0  1.0
       3      a  1.0  1.0
              b  1.0  1.0

最後需要強調的是, apply 函式的靈活性是以犧牲一定效能為代價換得的,除非需要使用跨列處理的分組處理,否則應當使用其他專門設計的 groupby 物件方法,否則在效能上會存在較大的差距。同時,在使用聚合函式和變換函式時,也應當優先使用內建函式,它們經過了高度的效能優化,一般而言在速度上都會快於用自定義函式來實現。