Pandas 的 groupby 功能是資料分析的利器,但內建的聚合函式有時無法滿足特定需求。例如,計算眾數時,若遇到多個眾數,需要自行定義處理方式,例如傳回列表、選擇單一值或重複索引。本文提供的 scalar_or_list_mode 和 scalar_or_bust_mode 函式分別示範了傳回列表和單一值的處理方式,並說明如何將這些自定義函式應用於 groupby 物件的 agg 方法。除了聚合函式,Pandas 的 apply 方法提供更彈性的分組操作,可以根據需求自定義處理邏輯。在時間序列分析中,rolling 和 expanding 函式則能有效計算移動平均、累計總和等指標,方便分析資料的趨勢變化。例如,可以計算股票的移動平均線或年初至今的漲跌幅。最後,文章也示範瞭如何使用 idxmax 或排序 combined with groupby 找出每組中的最大值,例如選出每年評分最高的電影。
使用pandas進行分組資料分析的挑戰與自定義聚合函式的應用
在資料分析過程中,經常需要對資料進行分組並計算各類別統計量。pandas函式庫提供了強大的groupby功能,讓使用者能夠輕鬆地對資料進行分組並套用不同的聚合函式。然而,在某些情況下,內建的聚合函式可能無法滿足特定的需求,需要自定義聚合函式來解決問題。
自定義眾數(Mode)計算的挑戰
以計算分組資料的眾數為例,pandas的mode函式可能會遇到多個眾數的情況。對於group_a這組資料,如果其眾數有兩個(例如42和555),該如何處理?主要有三種可能的期望結果:
傳回列表或序列:傳回包含所有眾數的列表或序列,如
[42, 555]。這樣做的缺點是傳回的資料型別會變成object,可能會導致後續處理上的複雜性。選擇單一值:從多個眾數中選擇一個值傳回。然而,這引發瞭如何選擇的問題,究竟應該選擇42還是555?
重複索引:傳回結果中,重複使用相同的索引標籤(如兩次
group_a),但這種做法與其他聚合函式的行為不一致。
程式碼範例:自定義眾數計算函式
def scalar_or_list_mode(ser: pd.Series):
"""計算眾數,若有多個眾數則傳回列表,否則傳回單一值"""
result = ser.mode()
if len(result) > 1:
return result.tolist()
elif len(result) == 1:
return result.iloc[0]
return pd.NA
def scalar_or_bust_mode(ser: pd.Series):
"""計算眾數,若無眾數則傳回NA,否則傳回第一個眾數"""
result = ser.mode()
if len(result) == 0:
return pd.NA
return result.iloc[0]
內容解密:
scalar_or_list_mode函式首先計算輸入序列ser的眾數。如果有多個眾數,則將它們轉換為列表傳回;如果只有一個眾數,則直接傳回該值;如果沒有眾數,則傳回pd.NA。scalar_or_bust_mode函式同樣計算眾數,但如果沒有眾數,它會傳回pd.NA;否則,它傳回第一個眾數。
使用自定義聚合函式
定義好自定義聚合函式後,可以將它們套用於groupby物件的agg方法中:
df.groupby("group").agg(
scalar_or_list=pd.NamedAgg(column="value", aggfunc=scalar_or_list_mode),
scalar_or_bust=pd.NamedAgg(column="value", aggfunc=scalar_or_bust_mode),
)
內容解密:
- 這段程式碼首先對
df按照"group"欄位進行分組,然後套用兩種不同的自定義聚合函式。 scalar_or_list_mode和scalar_or_bust_mode分別被套用於"value"欄位,計算每個分組的眾數。
使用apply方法進行更靈活的操作
在某些情況下,使用者可能需要進行比聚合或轉換更靈活的操作。pandas提供了apply方法,可以對每個分組進行任意的操作:
def mode_for_apply(df: pd.DataFrame):
"""對每個分組計算眾數"""
return df["value"].mode()
df.groupby("group").apply(mode_for_apply, include_groups=False)
內容解密:
mode_for_apply函式接收一個DataFrame,計算其"value"欄位的眾數。include_groups=False引數用於避免在未來版本的pandas中出現棄用警告。
使用Pandas進行資料分組與視窗操作
在資料分析中,經常需要對資料進行分組或套用視窗函式以計算匯總值或滑動平均等指標。Pandas提供了強大的groupby和視窗操作功能,能夠高效地處理這些需求。
使用groupby進行資料分組
groupby操作允許我們根據一個或多個欄位將資料分成多個群組,並對每個群組套用特定的函式進行運算。Pandas中的DataFrameGroupBy.apply方法能夠將使用者定義的函式套用到每個群組的資料上。
重點解析
DataFrameGroupBy.apply會將每個群組的資料(不含分組依據的欄位)傳遞給使用者定義的函式。- 根據函式的回傳型別,Pandas會嘗試推斷最合適的輸出結構。
- 當函式回傳一個
pd.Series時,輸出的結果會是一個具有多層索引(pd.MultiIndex)的Series。
groupby與apply的陷阱
雖然DataFrameGroupBy.apply非常靈活,但它可能被誤用於聚合操作。當套用的函式能夠簡化為單一值時,Pandas會嘗試將結果轉換為聚合形式。然而,這種行為依賴於實作細節,可能導致效能損失或在不同版本的Pandas中出現相容性問題。因此,若確定函式會回傳單一值,應優先使用DataFrameGroupBy.agg而非DataFrameGroupBy.apply。
視窗操作
視窗操作允許我們在資料的滑動區間(或稱“視窗”)上計算值。常見的應用包括計算“滾動90天平均值”等指標。
滾動視窗
滾動視窗操作可透過pd.Series.rolling方法實作。需要指定視窗大小(n),Pandas會從每個元素開始,向前查詢n-1筆記錄以形成視窗。
ser = pd.Series([0, 1, 2, 4, 8, 16], dtype=pd.Int64Dtype())
result = ser.rolling(2).sum().astype(pd.Int64Dtype())
內容解密:
ser.rolling(2)指定了視窗大小為2,意味著每次計算會考慮當前元素和前一個元素。.sum()對每個視窗內的資料進行求和。- 由於原始Series使用了
Int64Dtype,而滾動操作後結果轉換為float64,因此使用.astype(pd.Int64Dtype())轉回適當的資料型別。 - 第一個元素由於無法形成大小為2的視窗,因此結果為
<NA>。
中心對齊的滾動視窗
可以透過設定center=True使滾動視窗中心對齊,即同時考慮前後的元素。
ser.rolling(3, center=True).sum().astype(pd.Int64Dtype())
內容解密:
center=True使視窗中心對齊當前元素。- 當視窗大小為3時,會考慮當前元素、前一個元素和後一個元素進行計算。
- 首尾元素由於無法滿足視窗大小要求,因此結果為
<NA>。
擴充套件視窗
擴充套件視窗會考慮所有之前的資料,可用於計算累計值。
ser.expanding().mean().astype(pd.Float64Dtype())
內容解密:
expanding()建立擴充套件視窗,從第一個元素開始逐漸擴大至所有元素。.mean()計算每個擴充套件視窗內的平均值。- 結果展示了從第一個元素到當前元素的累計平均值。
實際應用:計算滾動平均與累計指標
在處理時間序列資料時,滾動平均和擴充套件視窗運算特別有用。例如,使用Nvidia股票資料集可以輕鬆計算“N日移動平均”或“年初至今”的指標。
df = pd.read_csv(
"data/NVDA.csv",
usecols=["Date", "Close"],
parse_dates=["Date"],
dtype_backend="numpy_nullable",
).set_index("Date")
圖表翻譯:
此段程式碼讀取Nvidia股票收盤價資料,並將日期設為索引,便於進行時間序列分析。
使用滾動視窗函式和擴充套件視窗函式進行時間序列分析
在處理時間序列資料時,經常需要計算移動平均值、最大值、最小值等統計指標。Pandas 提供了 rolling 和 expanding 函式來實作這些功能。
計算移動平均值
首先,我們可以使用 rolling 函式來計算移動平均值。以下是一個示例:
import matplotlib.pyplot as plt
plt.ion()
df.assign(
ma30=df["Close"].rolling(30).mean().astype(pd.Float64Dtype()),
ma60=df["Close"].rolling(60).mean().astype(pd.Float64Dtype()),
ma90=df["Close"].rolling(90).mean().astype(pd.Float64Dtype()),
).plot()
內容解密:
- 使用
rolling函式計算Close列的 30、60 和 90 天移動平均值。 - 使用
assign方法將計算出的移動平均值新增到原始 DataFrame 中。 - 使用
plot方法繪製結果。
計算年初至今和季度至今的統計指標
接下來,我們可以使用 groupby 和 expanding 函式來計算年初至今和季度至今的統計指標。以下是一個示例:
df.groupby(pd.Grouper(freq="YS")).expanding().agg(["min", "max", "mean"])
內容解密:
- 使用
groupby函式按年份對資料進行分組。 - 使用
expanding函式計算每個組的擴充套件視窗統計指標。 - 使用
agg方法計算最小值、最大值和平均值。
視覺化結果
為了更好地理解結果,我們可以將其視覺化:
df.groupby(pd.Grouper(freq="YS")).expanding().agg(["min", "max", "mean"]).droplevel(axis=1, level=0).reset_index(level=0, drop=True).plot()
內容解密:
- 使用
droplevel方法刪除多餘的索引層級。 - 使用
reset_index方法重置索引。 - 使用
plot方法繪製結果。
選擇每年的最高評分電影
在資料分析中,經常需要選擇每個組中具有最大值的行。以下是一個示例,展示如何選擇每年的最高評分電影:
df = pd.read_csv("data/movie.csv", usecols=["movie_title", "title_year", "imdb_score"], dtype_backend="numpy_nullable")
df["title_year"] = df["title_year"].astype(pd.Int16Dtype())
df.sort_values(["title_year", "imdb_score"]).groupby("title_year")[["movie_title"]].agg(top_rated_movie=pd.NamedAgg("movie_title", "last"))
內容解密:
- 使用
sort_values方法按title_year和imdb_score對資料進行排序。 - 使用
groupby方法按title_year對資料進行分組。 - 使用
agg方法選擇每個組中最後一行(即最高評分電影)。
或者,可以使用 idxmax 方法來選擇每個組中具有最大值的行:
df.set_index("movie_title").groupby("title_year").agg(top_rated_movie=pd.NamedAgg("imdb_score", "idxmax"))
內容解密:
- 使用
set_index方法將movie_title列設定為索引。 - 使用
groupby方法按title_year對資料進行分組。 - 使用
agg方法選擇每個組中具有最大imdb_score的行索引。
資料分組與聚合分析
在資料分析中,分組與聚合是非常重要的技術。透過分組,可以將資料依據特定的欄位進行分類別,並對每組資料進行聚合運算,以獲得有意義的統計結果。
電影評分資料分析
首先,我們來分析電影評分的資料。假設我們有一個包含電影標題、年份和IMDB評分的資料集。我們希望找出每年評分最高的電影。
import pandas as pd
# 假設df是我們的電影資料DataFrame
df = pd.read_csv("movie_data.csv")
# 使用groupby找出每年評分最高的電影
def top_rated(df: pd.DataFrame):
top_rating = df["imdb_score"].max()
top_rated = df[df["imdb_score"] == top_rating]["movie_title"].unique()
if len(top_rated) == 1:
return top_rated[0]
else:
return top_rated
result = df.groupby("title_year").apply(top_rated, include_groups=False).to_frame().rename(columns={0: "top_rated_movie(s)"})
print(result)
內容解密:
top_rated函式:接受一個DataFrame,找出該DataFrame中IMDB評分最高的電影。如果只有一部電影評分最高,則傳回該電影的標題;如果有多部電影評分相同且最高,則傳回這些電影的標題陣列。groupby操作:按照title_year欄位對資料進行分組,然後對每組應用top_rated函式。apply方法:將top_rated函式應用於每組資料,並透過include_groups=False引數確保結果中不包含分組鍵。to_frame和rename方法:將結果轉換為DataFrame,並重新命名欄位為top_rated_movie(s)。
棒球打擊率分析
接下來,我們分析棒球選手的打擊率。打擊率是棒球統計中的一個重要指標,計算方式為選手的安打數除以打數。
# 讀取棒球比賽資料
df = pd.read_parquet("data/mlb_batting_lines.parquet")
# 計算每年每個選手的總打數和總安打數
player_stats = df.groupby(["year", "id"]).agg(
total_ab=pd.NamedAgg(column="ab", aggfunc="sum"),
total_h=pd.NamedAgg(column="h", aggfunc="sum"),
)
# 計算打擊率
player_stats = player_stats.assign(avg=lambda x: x["total_h"] / x["total_ab"]).drop(columns=["total_ab", "total_h"])
print(player_stats)
內容解密:
groupby操作:按照year和id欄位對資料進行分組,分別計算每個選手每年的總打數(total_ab)和總安打數(total_h)。agg方法:對每組資料進行聚合運算,使用NamedAgg來指定聚合後的欄位名稱和聚合函式。assign方法:計算打擊率(avg),即總安打數除以總打數。drop方法:刪除不再需要的total_ab和total_h欄位。
棒球資料分析:探索打擊率的變化趨勢
在棒球資料分析中,打擊率是一項重要的指標,用於評估球員的表現。本章節將探討如何利用資料分析技術來理解打擊率的變化趨勢,並比較不同球季中頂尖球員的表現。
資料品質問題與處理
在計算打擊率時,首先需要考慮資料品質問題。某些球員可能只在特定情況下出場,因此其打擊機會有限。若直接計算打擊率,可能會遇到除以零的情況,導致結果為 NaN。此外,樣本數量過少也可能導致結果偏差。
為解決此問題,可以設定至少 400 次打擊機會作為門檻,以確保樣本數量足夠。利用 pandas 的 groupby 功能,可以按照球季和球員 ID 分組,並計算總打擊次數和總安打數。然後,篩選出總打擊次數大於 400 的球員,並計算其打擊率。
(
df.groupby(["year", "id"]).agg(
total_ab=pd.NamedAgg(column="ab", aggfunc="sum"),
total_h=pd.NamedAgg(column="h", aggfunc="sum")
)
.loc[lambda df: df["total_ab"] > 400]
.assign(avg=lambda x: x["total_h"] / x["total_ab"])
.drop(columns=["total_ab", "total_h"])
)
內容解密:
- 資料分組:使用
groupby將資料按照year和id分組。 - 匯總計算:計算每個球員在每個球季中的總打擊次數 (
total_ab) 和總安打數 (total_h)。 - 篩選資料:僅保留總打擊次數大於 400 的球員資料,以確保樣本數量足夠。
- 計算打擊率:將總安打數除以總打擊次數,得到每個球員的打擊率 (
avg)。 - 刪除中間結果:刪除
total_ab和total_h欄位,以簡化資料。
分析打擊率的變化趨勢
進一步分析可以發現,各球季的平均打擊率存在波動。利用 groupby 功能,可以計算每個球季的平均打擊率、最高打擊率以及最佳打擊者的身份。
averages.groupby("year").agg(
league_mean_avg=pd.NamedAgg(column="avg", aggfunc="mean"),
league_max_avg=pd.NamedAgg(column="avg", aggfunc="max"),
batting_champion=pd.NamedAgg(column="avg", aggfunc="idxmax"),
)
內容解密:
- 分組匯總:按照
year分組,並計算每個球季的平均打擊率 (league_mean_avg)、最高打擊率 (league_max_avg) 和最佳打擊者 (batting_champion)。 idxmax功能:利用idxmax找出每個球季中打擊率最高的球員。
視覺化打擊率的分佈
為了更好地理解各球季打擊率的分佈情況,可以使用小提琴圖(violin plot)進行視覺化。首先,需要將資料轉換為 seaborn 可接受的格式,並將年份轉換為分類別資料型別。
sns_df = averages.reset_index()
years = sns_df["year"].unique()
cat = pd.CategoricalDtype(sorted(years), ordered=True)
sns_df["year"] = sns_df["year"].astype(cat)
mask = (sns_df["year"] >= 2000) & (sns_df["year"] < 2010)
fig, ax = plt.subplots()
sns.violinplot(
data=sns_df[mask],
ax=ax,
x="avg",
y="year",
order=sns_df.loc[mask, "year"].unique(),
)
ax.set_xlim(0.15, 0.4)
plt.show()
圖表翻譯:
- 小提琴圖:用於展示不同年份打擊率的分佈情況。
- X 軸限制:設定 x 軸範圍為 0.15 至 0.4,以保持不同年份圖表的可比性。
正規化打擊率
透過觀察不同年份的打擊率分佈,可以發現某些年份的資料存在偏差。因此,可以對資料進行正規化處理,以比較不同年份頂尖球員的表現。這種方法不是直接比較絕對的打擊率,而是評估球員相對於當年平均水平的表現。
本章節透過對棒球資料的深入分析,展示瞭如何利用資料分析技術來理解打擊率的變化趨勢,並比較不同球季中頂尖球員的表現。未來,可以進一步探索其他相關指標,以全面評估球員的表現。