今天我們來說明如何使用Pandas的Fluent API來操作DataFrame(註1)。其原理就是使用向量化計算來提高速度,也就是說要以欄為思考中心並少用迴圈,且盡量不要mutate
DataFrame。
我將針對下面三個主題說明:
DataFrame.assign()
)。DataFrame.query()
)。DataFrame.pipe()
)。以下這個名為df
的Pandas Dataframe將作為今天的範例:
import numpy as np
import pandas as pd
np.random.seed(42)
data = {"nrs": [1, 2, 3, 4, 5], "random": np.random.rand(5)}
df = pd.DataFrame(data)
nrs random
0 1 0.374540
1 2 0.950714
2 3 0.731994
3 4 0.598658
4 5 0.156019
DataFrame.assign()可以新增欄位。
舉例來說,如果想新增一欄「"a"」欄位:
(
df.assign(a=[6, 7, 8, 9, 10])
)
nrs random a
0 1 0.374540 6
1 2 0.950714 7
2 3 0.731994 8
3 4 0.598658 9
4 5 0.156019 10
如果欄位名稱不符合Python命名原則的話,可以這麼寫:
(
df.assign(**{"*a*": [6, 7, 8, 9, 10]})
)
nrs random *a*
0 1 0.374540 6
1 2 0.950714 7
2 3 0.731994 8
3 4 0.598658 9
4 5 0.156019 10
如果新增欄位需要用到既有欄位資訊的話,例如想新增一欄「"a"」欄位,其值為「"nrs"」欄位加1:
(
df.assign(a=lambda df_: df_.nrs.add(1))
)
nrs random a
0 1 0.374540 2
1 2 0.950714 3
2 3 0.731994 4
3 4 0.598658 5
4 5 0.156019 6
我們可以指定一個函數給「"a"」,該函數接收「當前這一刻的DataFrame」為參數(這邊以df_
代表),並需回傳新欄位的值,回傳型態多為Series。
由於DataFrame.assign()
並非平行運算,所以在同一個DataFrame.assign()
中靠後面的運算,可以參考前方的計算結果。舉例來說,如果想新增一欄「"a"」欄位,其值為「"nrs"」欄位加1,且想再新增一欄「"b"」欄位,其值為「"a"」欄位加1:
(
df.assign(a=lambda df_: df_.nrs.add(1),
b=lambda df_: df_.a.add(1))
)
nrs random a b
0 1 0.374540 2 3
1 2 0.950714 3 4
2 3 0.731994 4 5
3 4 0.598658 5 6
4 5 0.156019 6 7
第一個lambda中的df_
其實與df
是一樣的,但第二個lambda中的df_
則是經過df.assign(a=lambda df_: df_.nrs.add(1))
運算後的DataFrame(新增了「"a"」欄位),所以可以使用「"a"」欄位的資訊。
在需要不斷參考欄位進行運算的情況下,我建議使用傳入函數或使用lambda來新增欄位,但是如果只是需要簡單的運算,也可以直接引用最開頭的df
:
(
df.assign(a=df.nrs.add(1))
)
nrs random a
0 1 0.374540 2
1 2 0.950714 3
2 3 0.731994 4
3 4 0.598658 5
4 5 0.156019 6
這裡我們直接使用df.nrs.add(1)
取得Series指定給「"a"」欄位。
此外,DataFrame.assign()
也可以用來修改欄位,例如將「"nrs"」欄位每行都減1:
(
df.assign(nrs=lambda df_:df_.nrs.sub(1))
)
nrs random
0 0 0.374540
1 1 0.950714
2 2 0.731994
3 3 0.598658
4 4 0.156019
甚至直接將整欄換成另一個型別,例如:
(
df.assign(nrs=list("abcde"))
)
nrs random
0 a 0.374540
1 b 0.950714
2 c 0.731994
3 d 0.598658
4 e 0.156019
最後,對於取得欄位的寫法,您可以使用像是df.nrs
或是df_["nrs"]
兩種方式,也就是說下面兩種寫法,結果其實是相同的:
(
df.assign(a=lambda df_: df_.nrs.add(1))
)
(
df.assign(a=lambda df_: df_["nrs"].add(1))
)
nrs random a
0 1 0.374540 2
1 2 0.950714 3
2 3 0.731994 4
3 4 0.598658 5
4 5 0.156019 6
主要分別是:
df_.nrs
在IDE中可以享有提示,就像是取得DataFrame的attribute。但是需小心如果欄位名稱與DataFrame中attribute一樣的話,該attribute將優先被取得。由於DataFrame中有相當多的attribute,這其實是一個很容易發生的情況。舉例來說:data = {"T": [1, 2, 3, 4, 5], "size": [4, 5, 6, 7, 8]}
df2 = pd.DataFrame(data)
print(df2.T) # transpose
print(df2.size) # 10
這個df2.T
並不會取得「"T"」欄位的Series,而是得到df2
的轉置。同理,df2.size
不會取得「"size"」欄位的Series,而是得到df2
的長度。
df_["nrs"]
無法在IDE中享有提示,但可以用來存取欄位名稱不符合Python命名原則的欄位,且可以避免與DataFrame attribute混淆。DataFrame.query()可以讓我們給定條件來選擇行數。
舉例來說,如果只想選取「"nrs"」欄位中大於「"random"」乘10的行:
(
df.query("nrs > random*10")
)
nrs random
4 5 0.156019
在DataFrame.query()
中可以直接使用欄位名稱來取得該欄位,並進行計算。
如果是需要引用到環境變數的話,可以使用@
,例如只想選取「"nrs"」欄位大於target的行:
target = 3
(
df.query("nrs > @target")
)
nrs random
3 4 0.598658
4 5 0.156019
在DataFrame.query()
中也可以進行邏輯運算,像是只想選取「"nrs"」欄位大於target或是「"nrs"」欄位等於1的行:
(
df.query("nrs > @target | nrs==1")
)
nrs random
0 1 0.374540
3 4 0.598658
4 5 0.156019
或許您熟悉的是下面這種使用df[]
的寫法:
(
df.loc[df["nrs"] > df["random"]*10]
)
(
df.loc[df["nrs"] > target]
)
(
df.loc[(df["nrs"] > target) | (df["nrs"] == 1)]
)
這樣的寫法往往使程式顯得冗長,充滿[]
及()
,因為:
df[]
來引用欄位。ValueError
:(
df.loc[df["nrs"] > target | df["nrs"] == 1]
)
ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().
這是運算的先後次序不明確所致,可以在每次邏輯運算前後以()
分開。
DataFrame.pipe()的原理是傳入一個函數,該函數接收一個DataFrame且會再回傳一個DataFrame(一般來說)。
舉例來說,假如想使用Fluent API完成下面三項工作:
可以這麼寫:
def change_value(df_, col_name, value):
df_.loc[[0, 2], col_name] = value
return df_
(
df.assign(a=lambda df_: df_.nrs.add(1))
.pipe(change_value, "a", 100)
.assign(b=lambda df_:df_.a.add(1))
)
nrs random a b
0 1 0.374540 100 101
1 2 0.950714 3 4
2 3 0.731994 100 101
3 4 0.598658 5 6
4 5 0.156019 6 7
這裡我們將df_.loc[]
進行指定的工作包在change_value
函數之中,且可以由DataFrame.pipe()
傳入change_value
所需的參數。
DataFrame.pipe()
這種DataFrame in DataFrame out的寫法,讓我們可以在面臨困境時維持fluent的寫法。由於fluent的寫法很難遇到接不下去的情況,當遇到時,十之八九是想mutate
當前的DataFrame,此時應該特別小心,確認這是您真正想進行的操作。
註1:相信大家對於怎麼操作DataFrame都有一套自己的心法,這裡小弟只是展示個人習慣的用法。