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 #果然有錢人的年齡普遍比較大<.<.
從索引可以看出,其實最後產生的結果就是按照條件列表中元素的值(此處是 True
和 False
)來分組,下面用隨機傳入字母序列來驗證這一想法:
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個方法,而先前的 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('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
物件方法,否則在效能上會存在較大的差距。同時,在使用聚合函式和變換函式時,也應當優先使用內建函式,它們經過了高度的效能優化,一般而言在速度上都會快於用自定義函式來實現。