天天看點

Datawhale Pandas Task04 分組一、分組模式及其對象二、聚合函數三、變換和過濾四、跨列分組

import pandas as pd
import numpy as np
           

一、分組模式及其對象

1. 分組的一般模式

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

  • 依據 性别 分組,統計全國人口 壽命 的 平均值
  • 依據 季節 分組,對每一個季節的 溫度 進行 組内标準化
  • 依據 班級 分組,篩選出組内 數學分數 的 平均值超過80分的班級

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

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

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

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

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

df = pd.read_csv('../data/learn_pandas.csv')
df.groupby('Gender')['Height'].median()

Gender
Female    159.6
Male      173.4
Name: Height, dtype: float64
           

2. 分組依據的本質

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

df.groupby(['School', 'Gender'])['Height'].mean()

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 的分組依據都是直接可以從列中按照名字擷取的,那如果希望通過一定的複雜邏輯來分組,例如根據學生體重是否超過總體均值來分組,同樣還是計算身高的均值。

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

condition = df.Weight > df.Weight.mean()
           

然後将其傳入 groupby 中:

df.groupby(condition)['Height'].mean()

Weight
False    159.034646
True     172.705357
Name: Height, dtype: float64
           

請根據上下四分位數分割,将體重分為high、normal、low三組,統計身高的均值。

condition1 = ["high" if x>df.Weight.quantile(0.75) else "medium" if x>df.Weight.quantile(0.25) else "low" for x in df.Weight ]
df.groupby(condition1)["Height"].mean()

high      174.935714
low       155.891071
medium    162.255294
Name: Height, dtype: float64
           

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

item = np.random.choice(list('abc'), df.shape[0])
df.groupby(item)['Height'].mean()

a    162.440000
b    163.813846
c    163.441509
Name: Height, dtype: float64
           

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

df.groupby([condition, item])['Height'].mean()

Weight   
False   a    159.160417
        b    159.179545
        c    158.680000
True    a    171.700000
        b    173.523810
        c    172.700000
Name: Height, dtype: float64
           

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

df[['School', 'Gender']].drop_duplicates()
           
Datawhale Pandas Task04 分組一、分組模式及其對象二、聚合函數三、變換和過濾四、跨列分組
df.groupby([df['School'], df['Gender']])['Height'].mean()

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 對象,這個對象上定義了許多方法,也具有一些友善的屬性。

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

<pandas.core.groupby.generic.DataFrameGroupBy object at 0x00000210149CA648>
           

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

gb.ngroups
16
           

通過 groups 屬性,可以傳回從 組名 映射到 組索引清單 的字典:

res = gb.groups
res.keys()  # 字典的值由于是索引,元素個數過多,此處隻展示字典的鍵

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')])
           

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

exercise = df.groupby(["School","Gender"])
res = exercise.groups.keys()
pd.DataFrame(res, columns=["School", "Gender"])
           
Datawhale Pandas Task04 分組一、分組模式及其對象二、聚合函數三、變換和過濾四、跨列分組

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

gb.size()

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 方法可以直接擷取所在組對應的行,此時必須知道組的具體名字:

gb.get_group(('Fudan University', 'Freshman'))
           
Datawhale Pandas Task04 分組一、分組模式及其對象二、聚合函數三、變換和過濾四、跨列分組

這裡列出了2個屬性和2個方法,而先前的 mean 、 median 都是 groupby 對象上的方法,這些函數和許多其他函數的操作具有高度相似性,将在之後的小節進行專門介紹。

4. 分組的三大操作

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

  • 第一個例子中,每一個組傳回一個标量值,可以是平均值、中位數、組容量 size 等
  • 第二個例子中,做了原序列的标準化處理,也就是說每組傳回的是一個 Series 類型
  • 第三個例子中,既不是标量也不是序列,傳回的整個組所在行的本身,即傳回了 DataFrame 類型

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

二、聚合函數

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('Gender')['Height']
gb.idxmin()

Gender
Female    143
Male      199
Name: Height, dtype: int64
           
gb.quantile(0.95)

Gender
Female    166.8
Male      185.9
Name: Height, dtype: float64
           

請查閱文檔,明确 all/any/mad/skew/sem/prod 函數的含義。

  • all:傳回是否所有元素都為真
  • any:傳回是否有元素為真
  • mad:mean absolute deviation 平均絕對離差
  • skew:偏度
  • sem:standard error of the mean 均值的标準誤差
  • prod:product of values 乘積

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

gb = df.groupby('Gender')[['Height', 'Weight']]
gb.max()
           
Datawhale Pandas Task04 分組一、分組模式及其對象二、聚合函數三、變換和過濾四、跨列分組

2. agg方法

雖然在 groupby 對象上定義了許多友善的函數,但仍然有以下不便之處:

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

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

【a】使用多個函數

當使用多個聚合函數時,需要用清單的形式把内置聚合函數對應的字元串傳入,先前提到的所有字元串都是合法的。

gb.agg(['sum', 'idxmax', 'skew'])
           
Datawhale Pandas Task04 分組一、分組模式及其對象二、聚合函數三、變換和過濾四、跨列分組

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

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

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

gb.agg({'Height':['mean','max'], 'Weight':'count'})
           
Datawhale Pandas Task04 分組一、分組模式及其對象二、聚合函數三、變換和過濾四、跨列分組

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

gb.agg({"Height":["sum","idxmax","skew"],"Weight":["sum","idxmax","skew"]})
           

【c】使用自定義函數

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

gb.agg(lambda x: x.mean()-x.min())
           
Datawhale Pandas Task04 分組一、分組模式及其對象二、聚合函數三、變換和過濾四、跨列分組

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

gb.agg(["count", "mean", "std", "min",(0.25,lambda x:x.quantile(0.25)), (0.5,lambda x:x.quantile(0.5)), (0.75,lambda x:x.quantile(0.75)), "max"])
           
Datawhale Pandas Task04 分組一、分組模式及其對象二、聚合函數三、變換和過濾四、跨列分組

由于傳入的是序列,是以序列上的方法和屬性都是可以在函數中使用的,隻需保證傳回值是标量即可。下面的例子是指,如果組的名額均值,超過該名額的總體均值,傳回High,否則傳回Low。

def my_func(s):
    res = 'High'
    if s.mean() <= df[s.name].mean():
        res = 'Low'
    return res
gb.agg(my_func)
           
Datawhale Pandas Task04 分組一、分組模式及其對象二、聚合函數三、變換和過濾四、跨列分組

【d】聚合結果重命名

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

gb.agg([('range', lambda x: x.max()-x.min()), ('my_sum', 'sum')])
           
Datawhale Pandas Task04 分組一、分組模式及其對象二、聚合函數三、變換和過濾四、跨列分組
gb.agg({'Height': [('my_func', my_func), 'sum'], 'Weight': lambda x:x.max()})
           
Datawhale Pandas Task04 分組一、分組模式及其對象二、聚合函數三、變換和過濾四、跨列分組

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

gb.agg([('my_sum', 'sum')])
           
Datawhale Pandas Task04 分組一、分組模式及其對象二、聚合函數三、變換和過濾四、跨列分組
gb.agg({'Height': [('my_func', my_func), 'sum'], 'Weight': [('range', lambda x:x.max())]})
           
Datawhale Pandas Task04 分組一、分組模式及其對象二、聚合函數三、變換和過濾四、跨列分組

三、變換和過濾

1. 變換函數與transform方法

變換函數的傳回值為同長度的序列,最常用的内置變換函數是累計函數: cumcount/cumsum/cumprod/cummax/cummin ,它們的使用方式和聚合函數類似,隻不過完成的是組内累計操作。此外在 groupby 對象上還定義了填充類和滑窗類的變換函數,這些函數的一般形式将會分别在第七章和第十章中讨論,此處略過。

gb.cummax().head()
           
Datawhale Pandas Task04 分組一、分組模式及其對象二、聚合函數三、變換和過濾四、跨列分組

當用自定義變換時需要使用 transform 方法,被調用的自定義函數, 其傳入值為資料源的序列 ,與 agg 的傳入類型是一緻的,其最後的傳回結果是行列索引與資料源一緻的 DataFrame 。

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

gb.transform(lambda x: (x-x.mean())/x.std()).head()
           
Datawhale Pandas Task04 分組一、分組模式及其對象二、聚合函數三、變換和過濾四、跨列分組

對于 transform 方法無法像 agg 一樣,通過傳入字典來對指定列使用特定的變換,如果需要在一次 transform 的調用中實作這種功能,請給出解決方案。

對不同的列進行分别處理,if-else模式

gb.transform(lambda x:x.cummin() if x.name=='Height' else x.rank()).head()
           

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

gb.transform('mean').head() # 傳入傳回标量的函數也是可以的
           
Datawhale Pandas Task04 分組一、分組模式及其對象二、聚合函數三、變換和過濾四、跨列分組

2. 組索引與過濾

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

過濾在分組中是對于組的過濾,而索引是對于行的過濾,在第二章中的傳回值,無論是布爾清單還是元素清單或者位置清單,本質上都是對于行的篩選,即如果符合篩選條件的則選入結果表,否則不選入。

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

在 groupby 對象中,定義了 filter 方法進行組的篩選,其中自定義函數的輸入參數為資料源構成的 DataFrame 本身,在之前例子中定義的 groupby 對象中,傳入的就是 df[[‘Height’, ‘Weight’]] ,是以所有表方法和屬性都可以在自定義函數中相應地使用,同時隻需保證自定義函數的傳回為布爾值即可。

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

gb.filter(lambda x: x.shape[0] > 100).head()
           
Datawhale Pandas Task04 分組一、分組模式及其對象二、聚合函數三、變換和過濾四、跨列分組

從概念上說,索引功能是組過濾功能的子集,請使用 filter 函數完成 loc[.] 的功能,這裡假設 ” . “是元素清單。

四、跨列分組

1. apply的引入

之前幾節介紹了三大分組操作,但事實上還有一種常見的分組場景,無法用前面介紹的任何一種方法處理,例如現在如下定義身體品質指數BMI:

Datawhale Pandas Task04 分組一、分組模式及其對象二、聚合函數三、變換和過濾四、跨列分組

其中體重和身高的機關分别為千克和米,需要分組計算組BMI的均值。

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

2. apply的使用

在設計上, apply 的自定義函數傳入參數與 filter 完全一緻,隻不過後者隻允許傳回布爾值。現如下解決上述計算問題:

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

gb.apply(BMI)

Gender
Female    18.860930
Male      24.318654
dtype: float64
           

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

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

gb = df.groupby(['Gender','Test_Number'])[['Height','Weight']]
gb.apply(lambda x: 0)

Gender  Test_Number
Female  1              0
        2              0
        3              0
Male    1              0
        2              0
        3              0
dtype: int64

gb.apply(lambda x: [0, 0]) # 雖然是清單,但是作為傳回值仍然看作标量
Gender  Test_Number
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']))
           
Datawhale Pandas Task04 分組一、分組模式及其對象二、聚合函數三、變換和過濾四、跨列分組

【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')])))
           
Datawhale Pandas Task04 分組一、分組模式及其對象二、聚合函數三、變換和過濾四、跨列分組

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