pandas之分組groupby學習筆記
分組
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 型別
由此,引申出分組的三大操作:聚合、變換和過濾
,分別對應了三個例子的操作,下面就要分別介紹相應的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
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