返回文章列表

Pandas資料處理技巧提升程式碼可讀性

本文介紹如何使用 pandas 的 pipe 方法串接多個資料處理步驟,提升程式碼可讀性。pipe 方法可以將多個函式呼叫鏈式串接,避免巢狀呼叫或冗長的變數指定,使程式碼更清晰易懂。此外,文章還示範瞭如何使用 nlargest 和 nsmallest 方法進行 Top N 分析,例如找出評分最高的電影中預算最

資料科學 程式開發

在資料處理過程中,經常需要對 DataFrame 進行多個步驟的操作,傳統方法容易造成程式碼冗長或難以閱讀。Pandas 提供的 pipe 方法能有效改善這個問題,它允許將多個處理步驟組合成清晰的管道,提升程式碼可讀性和維護性。搭配 assign 方法可以更簡潔地修改 DataFrame 欄位。此外,nlargestnsmallest 方法則提供了方便的 Top N 分析功能,例如快速找出資料集中最大或最小的 N 筆資料,這在實際資料分析中非常實用。這些技巧都能有效提升 Pandas 程式碼的效率和可讀性。

資料處理流程的最佳實踐:使用 pandas 管道(pipe)提升程式碼可讀性

在資料分析過程中,我們經常需要對 DataFrame 進行多步驟的處理。傳統的做法是將每個步驟的結果指定給一個新的變數,或是將函式呼叫巢狀在一起。然而,這些方法會使程式碼變得冗長或難以閱讀。pandas 提供的 pipe 方法可以將多個處理步驟組合成一個清晰的管道(pipeline),提升程式碼的可讀性和維護性。

使用 pipe 方法串接資料處理步驟

首先,我們定義幾個簡單的函式來修改 DataFrame 中的資料欄位。每個函式都接受一個 DataFrame 並傳回一個新的 DataFrame。

import pandas as pd

def change_col1(df: pd.DataFrame) -> pd.DataFrame:
    return df.assign(col1=pd.Series([4, 5, 6], dtype=pd.Int64Dtype()))

def change_col2(df: pd.DataFrame, str_case: str = "upper") -> pd.DataFrame:
    if str_case == "upper":
        values = ["X", "Y", "Z"]
    else:
        values = ["x", "y", "z"]
    return df.assign(col2=pd.Series(values, dtype=pd.StringDtype()))

內容解密:

  1. change_col1 函式使用 assign 方法新增或修改 col1 欄位,並賦予新的數值。
  2. change_col2 函式同樣使用 assign 方法修改 col2 欄位,並根據 str_case 引數決定新值的大小寫。

傳統的做法是將每個步驟的結果指定給一個新的變數:

df = pd.DataFrame({'col1': [1, 2, 3], 'col2': ['a', 'b', 'c']})
df2 = change_col1(df)
df3 = change_col2(df2)
print(df3)

或者巢狀呼叫函式:

print(change_col2(change_col1(df)))

然而,這些方法要麼使程式碼冗長,要麼難以閱讀。使用 pipe 方法可以解決這些問題:

df.pipe(change_col1).pipe(change_col2, str_case="lower")

內容解密:

  1. pipe 方法將 DataFrame 傳遞給指定的函式,並傳回處理後的結果。
  2. 可以鏈式呼叫多個 pipe 方法,將多個處理步驟串接起來。
  3. 在呼叫 pipe 方法時,可以傳遞額外的引數給目標函式,如 str_case="lower"

使用 nlargestnsmallest 進行 Top N 分析

在實際的資料分析中,Top N 分析是一種常見的需求。pandas 提供了 nlargestnsmallest 方法來方便地進行這種分析。

範例:找出評分最高的 100 部電影中預算最低的 5 部

首先,讀取電影資料集並選擇需要的欄位:

df = pd.read_csv(
    "data/movie.csv",
    usecols=["movie_title", "imdb_score", "budget", "gross"],
    dtype_backend="numpy_nullable",
)

使用 nlargest 方法選出評分最高的 100 部電影:

top_100_movies = df.nlargest(100, "imdb_score")

再使用 nsmallest 方法從這 100 部電影中找出預算最低的 5 部:

lowest_budget_movies = top_100_movies.nsmallest(5, "budget")
print(lowest_budget_movies)

內容解密:

  1. nlargest 方法根據指定的欄位選出最大的 N 筆資料。
  2. nsmallest 方法根據指定的欄位選出最小的 N 筆資料。
  3. 可以鏈式呼叫這兩個方法來進行更複雜的分析,如先選出評分最高的 100 部電影,再從中找出預算最低的 5 部。

使用 Pandas 進行股票交易策略分析:計算追蹤停損訂單價格

在股票交易中,停損訂單是一種常見的風險管理策略。當股票價格達到某一特定水平時,停損訂單會自動觸發買入或賣出操作,以限制損失或保護收益。本文將介紹如何使用 Pandas 計算追蹤停損訂單價格,並探討相關的交易策略。

什麼是追蹤停損訂單?

追蹤停損訂單是一種動態的停損策略,它會根據股票價格的變化調整停損價格。當股票價格上漲時,停損價格也會隨之上調,但當股票價格下跌時,停損價格保持不變。這種策略可以幫助投資者在股票價格上漲時鎖定收益,同時在價格下跌時限制損失。

使用 Pandas 計算追蹤停損訂單價格

首先,我們需要載入 Nvidia(NVDA)股票的歷史價格資料,並將其轉換為 Pandas 的 DataFrame 或 Series 物件。以下是範例程式碼:

import pandas as pd

# 載入 Nvidia 股票歷史價格資料
df = pd.read_csv(
    "data/NVDA.csv",
    usecols=["Date", "Close"],
    parse_dates=["Date"],
    index_col=["Date"],
).convert_dtypes(dtype_backend="numpy_nullable")

# 將 DataFrame 轉換為 Series
ser = df.squeeze()

# 計算追蹤停損訂單價格(假設初始購買價格為第一天的收盤價)
ser_cummax = ser.cummax()
stop_prices = ser_cummax.mul(0.9)  # 10% 的停損比例

# 顯示前五天的追蹤停損訂單價格
print(stop_prices.head())

程式碼解析:

  1. pd.read_csv: 載入 Nvidia 股票的歷史價格資料,並指定日期欄位為索引。
  2. df.squeeze(): 將 DataFrame 轉換為 Series,以便進行後續的運算。
  3. ser.cummax(): 計算每一天的累計最高收盤價。
  4. ser_cummax.mul(0.9): 將累計最高收盤價乘以 0.9,計算出 10% 的追蹤停損訂單價格。

分析追蹤停損訂單的觸發條件

當股票價格跌破追蹤停損訂單價格時,停損訂單會被觸發。我們可以使用以下程式碼找出觸發停損訂單的交易日:

# 找出股票價格低於追蹤停損訂單價格的交易日
trigger_days = ser[ser <= stop_prices]

# 顯示第一個觸發停損訂單的交易日
print((ser <= stop_prices).idxmax())

程式碼解析:

  1. ser <= stop_prices: 建立一個布林 Series,標示出股票價格低於追蹤停損訂單價格的交易日。
  2. (ser <= stop_prices).idxmax(): 找出第一個 True 值對應的索引,即第一個觸發停損訂單的交易日。

棒球資料分析:找出最佳球員與最佳打擊順序

找出表現最佳的棒球球員

美國的棒球運動長期以來一直是深入分析研究的物件,資料收集可以追溯到20世紀初。對於美國職業棒球大聯盟(MLB)的球隊來說,先進的資料分析有助於回答諸如「我應該為X球員支付多少薪水?」和「根據目前的比賽狀態,我應該怎麼做?」等問題。對於球迷來說,同樣的資料可以用來進行無盡的辯論,比如「誰是史上最偉大的球員?」。

本篇食譜將使用從retrosheet.org收集的資料。根據Retrosheet的許可要求,您應該瞭解以下法律免責宣告:

本文中使用的資訊是免費獲得的,但受Retrosheet版權保護。有興趣的各方可以聯絡Retrosheet,網址為www.retrosheet.org。

如何實作

首先,讓我們讀入總結的資料,並將id列(代表唯一的球員)設定為索引:

df = pd.read_parquet("data/mlb_batting_summaries.parquet").set_index("id")
df

輸出結果如下:

           ab    r    h   hr
id                           
abadf001    0    0    0    0
abboa001    0    0    0    0
abboc001    3    0    1    0
abrac001  847  116  208   20
abrea001    0    0    0    0
...       ...  ...  ...  ...
zimmk001    0    0    0    0
zimmr001  255   27   62   14
zubet001    1    0    0    0
zunig001    0    0    0    0
zunim001  572   82  111   41
2183 rows × 4 columns

在棒球運動中,很少有球員能夠在所有統計類別中都表現出色。通常,一個擊出很多全壘打(hr)的球員可能更強大,能夠將球擊得更遠,但可能不如專門收集很多安打(h)的球員那樣頻繁地做到這一點。使用pandas,我們很幸運地不需要逐一檢視每個指標;簡單地呼叫pd.DataFrame.idxmax就可以檢視每一列,找到最大值,並傳回與該最大值相關的行索引值:

df.idxmax()

輸出結果如下:

ab      semim001
r       freef001
h       freef001
hr      judga001
dtype: string

由此可見,球員semim001(Marcus Semien)擁有最多的打數,freef001(Freddie Freeman)擁有最多的得分和安打,而judga001(Aaron Judge)則擊出了最多的全壘打。

更深入的分析

如果您想進一步瞭解這些優秀球員在所有類別中的表現,可以取pd.DataFrame.idxmax的輸出結果,隨後呼叫pd.Series.unique來取得唯一值,並將其用作整體DataFrame的掩碼:

best_players = df.idxmax().unique()
mask = df.index.isin(best_players)
df[mask]

輸出結果如下:

          ab    r    h   hr
id                           
freef001 1849  368  590   81
judga001 1487  301  433  138
semim001 1979  338  521  100

程式碼解密:

  1. df.idxmax():找出每個欄位的最大值索引。
  2. pd.Series.unique:取得唯一值,用於建立掩碼。
  3. df.index.isin(best_players):建立掩碼以篩選出最佳球員。
  4. df[mask]:使用掩碼篩選出最佳球員的資料。

圖表呈現

為了更好地視覺化這些資料,可以使用pd.DataFrame.style.highlight_max來突出顯示這些球員在哪些類別中表現最佳:

df[mask].style.highlight_max()

此圖示呈現了最佳球員在不同類別中的表現。

圖表翻譯: 此圖表顯示了最佳球員在不同統計類別中的表現,其中突出顯示了每個類別的最大值。

瞭解哪個打擊順序位置得分最多

在棒球比賽中,球隊允許9名擊球手按照一定的順序上場,1代表第一個上場的擊球手,9代表最後一個。通常,球隊會將一些最好的擊球手放在「打擊順序的前面」(即較低的數字位置),以最大限度地增加他們上場並得分的機會。然而,這並不總是意味著第一個上場的擊球手總是第一個得分。

如何實作

與前面的食譜類別似,我們將使用從retrosheet.org取得的資料。對於這個特定的資料集,我們將設定yearteam列作為行索引,剩下的列則顯示擊球順序中的位置:

df = pd.read_parquet("data/runs_scored_by_team.parquet").set_index(["year", "team"])
df

輸出結果如下:

               1    2    3 ...    7    8    9
year team                           
2000 ANA     124  107  100 ...   77   76   54
     ARI     110  106  109 ...   72   68   40
     ATL     113  125  124 ...   77   74   39
     BAL     ... 

使用pd.DataFrame.idxmax,我們可以檢視每個年份和球隊哪個位置得分最多。然而,在這個資料集中,我們希望pd.DataFrame.idxmax識別的索引標籤實際上是在列中,而不是在行中。幸運的是,pandas仍然可以透過axis=1引數輕鬆計算出這一點:

df.idxmax(axis=1)

輸出結果如下:

year team   
2000 ANA      1
     ARI      ...
2023 SLN      ...
Length: ...

從那裡,我們可以使用pd.Series.value_counts來瞭解某個位置在擊球順序中代表最多得分的次數。我們還將使用normalize=True引數,這將給我們一個頻率而不是總數:

df.idxmax(axis=1).value_counts(normalize=True)

輸出結果如下:

1    ...
2    ...
...
Name: proportion, dtype: float64

不出所料,第一棒擊球手得分最多,佔據了48%的比例。

更深入的分析

我們可能想要進一步探索並回答這個問題:「對於第一棒擊球手得分最多的球隊來說,第二多的得分者是誰?」

為了計算這一點,我們可以建立一個掩碼來過濾第一棒擊球手得分最多的球隊,從我們的資料集中刪除該列,然後重複使用相同的pd.DataFrame.idxmax方法來識別下一個位置:

mask = df.idxmax(axis=1).eq("1")
df[mask].drop(columns=["1"]).idxmax(axis=1).value_counts(normalize=True)

輸出結果如下:

2    ...
3    ...
...
Name: proportion, dtype: float64

正如您所看到的,如果一個球隊的第一棒擊球手沒有領先該隊得分,那麼第二棒擊球手幾乎有50%的機率成為領先者。

程式碼解密:

  1. df.idxmax(axis=1):找出每個年份和球隊哪個位置得分最多。
  2. pd.Series.value_counts(normalize=True):計算每個位置得分最多的頻率。
  3. mask = df.idxmax(axis=1).eq("1"):建立掩碼以篩選出第一棒擊球手得分最多的球隊。
  4. df[mask].drop(columns=["1"]).idxmax(axis=1):找出這些球隊中第二多的得分者。