iT邦幫忙

2024 iThome 鐵人賽

DAY 3
0

今天我們來說明如何使用Pandas的Fluent API來操作DataFrame(註1)。其原理就是使用向量化計算來提高速度,也就是說要以欄為思考中心並少用迴圈,且盡量不要mutate DataFrame。

我將針對下面三個主題說明:

  • 欄:新增及修改欄位(DataFrame.assign())。
  • 行:給定條件來選擇行數(DataFrame.query())。
  • pipe:當無法再fluent下去的最後一招(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().

這是運算的先後次序不明確所致,可以在每次邏輯運算前後以()分開。

pipe

DataFrame.pipe()的原理是傳入一個函數,該函數接收一個DataFrame且會再回傳一個DataFrame(一般來說)。

舉例來說,假如想使用Fluent API完成下面三項工作:

  • 新增一欄「"a"」欄位,其值為「"nrs"」欄位加1。
  • 變更「"a"」欄位中第一及第三行的值為100。
  • 新增一欄「"b"」欄位,其值為「"a"」欄位加1。

可以這麼寫:

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都有一套自己的心法,這裡小弟只是展示個人習慣的用法。

Code

本日程式碼傳送門


上一篇
[Day02] - 表格基本介紹
下一篇
[Day04] - Polars的Context與Expression
系列文
眾裏尋它:Python表格利器Great Tables30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言