Pandas 提供了強大的時間序列處理能力,resample 函式是其中的核心工具,可以靈活地調整時間序列的頻率。升取樣和降取樣過程中,缺失值的處理至關重要,ffill、bfill 和 interpolate 等方法提供了多種填充策略。理解不同匯總函式(如 sum、mean)對缺失值的影響,選擇合適的函式才能得到準確的分析結果。
在實際應用中,讀取不同格式的時間序列資料(如 Parquet),並正確設定索引和時區至關重要。針對不同粒度(日、小時、分鐘)的重取樣和缺失值處理,可以更精細地分析資料趨勢。此外,使用 pd.StringDtype 取代 object 處理字串資料,能有效提升效能並確保資料型別一致性。
時間序列重取樣與分析
在處理時間序列資料時,經常需要將資料從一個頻率轉換到另一個頻率。Pandas 提供了 resample 方法來實作這一功能。本章節將介紹如何使用 resample 進行降取樣(downsampling)和升取樣(upsampling),並展示如何處理缺失值。
降取樣
降取樣是指將資料從較高的頻率轉換到較低的頻率。例如,將每日資料轉換為每週資料。
程式碼範例:
import pandas as pd
# 建立一個每日的時間序列
index = pd.date_range(start="2024-01-01", periods=10, freq="D")
ser = pd.Series(range(10), index=index, dtype=pd.Int64Dtype())
# 將每日資料降取樣為每週資料
weekly_ser = ser.resample("W-SAT").sum()
print(weekly_ser)
內容解密:
pd.date_range用於生成一個日期範圍,作為時間序列的索引。ser.resample("W-SAT").sum()將每日資料降取樣為每週資料,以星期六為一週的最後一天,並計算每週的總和。
升取樣
升取樣是指將資料從較低的頻率轉換到較高的頻率。例如,將每日資料轉換為每12小時的資料。
程式碼範例:
# 將每日資料升取樣為每12小時的資料
upsampled_ser = ser.resample("12h").asfreq().iloc[:5]
print(upsampled_ser)
內容解密:
ser.resample("12h").asfreq()將每日資料升取樣為每12小時的資料,並使用asfreq方法來保持原始資料的頻率特性。iloc[:5]用於顯示前5行資料。
處理缺失值
在升取樣過程中,可能會產生缺失值。Pandas 提供了多種方法來處理這些缺失值,包括向前填充(forward fill)、向後填充(backward fill)和插值(interpolation)。
程式碼範例:
# 向前填充缺失值
ffilled_ser = ser.resample("12h").asfreq().ffill().iloc[:6]
print(ffilled_ser)
# 向後填充缺失值
bfilled_ser = ser.resample("12h").asfreq().bfill().iloc[:6]
print(bfilled_ser)
# 線性插值
interpolated_ser = ser.resample("12h").asfreq().interpolate().iloc[:6]
print(interpolated_ser)
內容解密:
ffill()方法用於向前填充缺失值,即使用前一個非缺失值來填充。bfill()方法用於向後填充缺失值,即使用後一個非缺失值來填充。interpolate()方法用於線性插值,即根據前後非缺失值的趨勢來估計缺失值。
應使用案例項:分析丹佛犯罪資料
本文將展示如何使用 resample 方法來分析丹佛犯罪資料。
程式碼範例:
# 讀取丹佛犯罪資料
df = pd.read_parquet("data/crime.parquet").set_index("REPORTED_DATE")
# 將資料按週重取樣,並計算每週的犯罪數量
weekly_crime_count = df.resample("W").size()
print(weekly_crime_count.head())
內容解密:
pd.read_parquet用於讀取 Parquet 格式的資料檔案。set_index("REPORTED_DATE")將REPORTED_DATE列設定為索引。resample("W").size()將資料按週重取樣,並計算每週的犯罪數量。
時間資料型別與演算法
計算犯罪類別的年度變化率
在分析時間序列資料時,使用者經常想了解年度或季度之間的變化率。儘管這類別問題頻繁出現,但撰寫演算法來回答它們可能相當複雜且耗時。幸運的是,pandas提供了許多開箱即用的功能,使這項工作變得簡單。
實作步驟
首先,讀取犯罪資料集,但這次不將REPORTED_DATE設定為索引:
df = pd.read_parquet("data/crime.parquet")
df.head()
接下來,利用pd.DataFrame.groupby與pd.Grouper的組合來表達按OFFENSE_CATEGORY_ID和REPORTED_DATE分組的資料:
df.groupby([
"OFFENSE_CATEGORY_ID",
pd.Grouper(key="REPORTED_DATE", freq="YS"),
], observed=True).agg(
total_crime=pd.NamedAgg(column="IS_CRIME", aggfunc="sum"),
)
內容解密:
- 使用
pd.Grouper進行時間分組:pd.Grouper允許我們根據特定的時間頻率(如年、季度等)對資料進行分組。這裡我們使用freq="YS"來表示按年初(Year Start)進行分組。 observed=True引數的作用:在pandas 2.x版本中,使用分類別資料型別進行分組時會出現警告,設定observed=True可以抑制這個警告。未來版本的pandas可能會預設這個引數。total_crime的計算:我們使用pd.NamedAgg來計算每個分組內的犯罪總數,將結果命名為total_crime。
為了計算年度變化率,我們可以使用pd.Series.pct_change方法。但是,直接使用這個方法會將當前行的值與前一行的值進行比較,無論它們是否屬於同一類別。因此,我們需要再次使用groupby方法,並指定level=0來確保比較是在同一類別內進行的:
yoy_crime = df.groupby([
"OFFENSE_CATEGORY_ID",
pd.Grouper(key="REPORTED_DATE", freq="YS"),
], observed=True).agg(
total_crime=pd.NamedAgg(column="IS_CRIME", aggfunc="sum"),
).assign(
yoy_change=lambda x: x.groupby(level=0, observed=True).pct_change().astype(pd.Float64Dtype())
)
yoy_crime.head(10)
內容解密:
yoy_change的計算:首先,我們計算每個類別內的年度犯罪總數。然後,使用assign方法新增一個欄位yoy_change,用於計算年度變化率。groupby(level=0)的作用:這裡再次使用groupby方法,並指定level=0,確保年度變化率的計算是在每個OFFENSE_CATEGORY_ID內部進行的。astype(pd.Float64Dtype())的作用:將結果轉換為可空的浮點數型別,以處理可能的缺失值。
最後,為了更好地視覺化結果,我們可以繪製每個犯罪類別的總犯罪數和年度變化率:
crimes = tuple(("aggravated-assault", "arson", "auto-theft"))
fig, axes = plt.subplots(nrows=len(crimes), ncols=2, sharex=True)
for idx, crime in enumerate(crimes):
crime_df = yoy_crime.loc[crime]
ax0 = axes[idx][0]
ax1 = axes[idx][1]
crime_df.plot(kind="bar", y="total_crime", ax=ax0, legend=False)
crime_df.plot(kind="bar", y="yoy_change", ax=ax1, legend=False)
xlabels = [x.year for x in crime_df.index]
ax0.set_xticklabels(xlabels)
圖表翻譯:
此圖表呈現了不同犯罪類別在不同年份的總犯罪數和年度變化率。左側柱狀圖表示總犯罪數,右側柱狀圖表示年度變化率。透過比較這些圖表,可以更直觀地瞭解各類別犯罪的年度變化趨勢。
時間資料型別與演算法的實際應用
在處理時間序列資料時,正確的資料型別和演算法選擇至關重要。本篇文章將探討如何使用 pandas 來處理時間資料,並分析來自芝加哥資料入口網站的智慧綠色基礎設施監測感測器歷史資料集。
資料載入與初步處理
首先,我們使用 pd.read_parquet 函式載入資料集,並檢查資料的前幾行。
df = pd.read_parquet("data/sgi_monitoring.parquet", dtype_backend="numpy_nullable")
df.head()
內容解密:
pd.read_parquet用於讀取 Parquet 格式的檔案,這是一種高效的列式儲存格式。dtype_backend="numpy_nullable"引數確保 pandas 正確處理可空的資料型別。
時間資料型別的轉換
檢查 Measurement Time 列的資料型別後,發現它被識別為字串型別。因此,我們需要將其轉換為 datetime 型別。
df["Measurement Time"] = pd.to_datetime(df["Measurement Time"]).dt.tz_localize("US/Central")
df["Measurement Time"]
內容解密:
pd.to_datetime將字串轉換為 datetime 型別。.dt.tz_localize("US/Central")將 datetime 資料本地化到芝加哥時區。
資料過濾與索引設定
為了專注於特定的感測器資料,我們過濾出 Measurement Description 為 “TM1 Temp Sensor” 且 Data Stream ID 為 39176 的資料,並將 Measurement Time 設定為索引。
mask = (df["Measurement Description"] == "TM1 Temp Sensor") & (df["Data Stream ID"] == 39176)
df = df[mask].set_index("Measurement Time").sort_index()
內容解密:
- 使用布林遮罩
mask過濾資料。 .set_index("Measurement Time")將Measurement Time列設定為 DataFrame 的索引。.sort_index()對索引進行排序,以確保時間順序的正確性。
資料重取樣與缺失值分析
透過對 Measurement Value 列進行日級別的重取樣和平均值聚合,我們可以初步瞭解資料的趨勢和缺失情況。
df.resample("D")["Measurement Value"].mean().plot()
內容解密:
.resample("D")將資料重取樣到日級別。["Measurement Value"].mean()對Measurement Value列計算平均值。
進一步檢查特定日期範圍內的資料,可以發現缺失值的情況。
df.loc["2017-07-24":"2017-08-01"].resample("D")["Measurement Value"].mean()
內容解密:
.loc["2017-07-24":"2017-08-01"]篩選出指定日期範圍內的資料。- 重取樣和平均值計算同上。
時間序列資料中的缺失值處理
在處理時間序列資料時,缺失值的存在會對資料分析和視覺化產生重大影響。本章節將探討如何使用 pandas 函式庫有效地處理時間序列資料中的缺失值。
重取樣與缺失值
當我們對資料進行重取樣時,缺失值的出現可能會導致資料不完整或不準確。例如,在每日重取樣中,如果某一天的資料完全缺失,則該天的匯總值將為空或零。
程式碼範例:每日重取樣與缺失值
df.resample("D")["Measurement Value"].mean().plot()
內容解密:
此程式碼對資料進行每日重取樣,並計算每天的平均測量值。然而,如果某一天的資料缺失,則該天的平均值將為空,導致圖表中出現空白。
使用插值法填補缺失值
為瞭解決缺失值的問題,我們可以使用 pd.Series.interpolate() 方法來填補缺失值。此方法會根據前後的值進行插值,從而填補缺失的資料點。
程式碼範例:插值法填補缺失值
df.resample("D")["Measurement Value"].mean().interpolate().plot()
內容解密:
此程式碼首先對資料進行每日重取樣並計算平均值,然後使用插值法填補缺失值。最後,繪製填補後的資料圖表。這樣可以確保圖表中的資料是連續的,從而提供更準確的視覺化結果。
不同匯總函式的影響
不同的匯總函式對缺失值的處理方式不同。例如,平均值匯總函式可以容忍一些缺失值,但總和匯總函式則對缺失值更敏感。
程式碼範例:總和匯總與缺失值
df.resample("D")["Measurement Value"].sum().plot()
內容解密:
此程式碼對資料進行每日重取樣並計算每天的總和。然而,由於某些天的資料缺失,圖表中出現了明顯的下降趨勢。
分析資料收集頻率
為了更好地理解資料的收集頻率,我們可以對資料進行小時重取樣並計算每個小時的事件數量。
程式碼範例:小時重取樣與事件數量
df.resample("h").size().plot()
內容解密:
此程式碼對資料進行小時重取樣並計算每個小時的事件數量。結果顯示,大多數小時的事件數量接近 60,但實際上只有一個小時的事件數量恰好為 60。
分鐘級別的重取樣與插值
為了進一步提高資料的精確度,我們可以對資料進行分鐘級別的重取樣並使用插值法填補缺失值。
程式碼範例:分鐘級別重取樣與插值
interpolated = df.resample("min")["Measurement Value"].sum(min_count=1).interpolate()
內容解密:
此程式碼首先對資料進行分鐘級別的重取樣並計算每分鐘的總和,然後使用插值法填補缺失值。其中,min_count=1 引數確保至少有一個非缺失值才會計算總和。
結果驗證
最後,我們可以驗證插值後的資料是否符合預期,例如檢查每個小時的事件數量是否為 60。
程式碼範例:驗證每小時事件數量
interpolated.resample("h").size().plot()
內容解密:
此程式碼對插值後的資料進行小時重取樣並計算每個小時的事件數量。結果顯示,每個小時的事件數量現在均為 60,表明插值後的資料品質有所提高。
一般使用與效能提示
在深入瞭解 pandas 函式庫的大部分內容並透過範例應用來強化良好的使用方法之後,您現在已經準備好進入現實世界,並開始將所學的一切應用於您的資料分析問題中。
本章將提供一些您獨立工作時應牢記的提示和技巧。本章介紹的配方是所有經驗級別的 pandas 使用者都會犯的常見錯誤。雖然出於好意,但不當使用 pandas 結構可能會浪費很多效能。當您的資料集較小時,這可能不是什麼大問題,但資料往往會增長,而不是縮小。使用正確的慣用語並避免維護低效率程式碼所帶來的負擔,可以為您的組織節省大量的時間和金錢。
避免使用 dtype=object
在 pandas 中使用 dtype=object 來儲存字串是最容易出錯和低效率的事情之一。不幸的是,長期以來,dtype=object 是處理字串資料的唯一方法;直到 1.0 版本才得到「解決」。
如何實作
讓我們建立兩個具有相同資料的 pd.Series 物件,一個使用 object 資料型別,另一個使用 pd.StringDtype:
ser_obj = pd.Series(["foo", "bar", "baz"] * 10_000, dtype=object)
ser_str = pd.Series(["foo", "bar", "baz"] * 10_000, dtype=pd.StringDtype())
嘗試將非字串值賦給 ser_str 將會失敗:
ser_str.iloc[0] = False
內容解密:
此錯誤是因為 pd.StringDtype() 只允許儲存字串,而不允許儲存其他資料型別,如布林值。這種限制有助於保持資料的一致性和正確性。
相反,使用 object 資料型別的 pd.Series 將欣然接受我們的布林值:
ser_obj.iloc[0] = False
這可能會混淆資料中可能出現的問題。使用 pd.StringDtype 時,當我們嘗試賦非字串資料時,失敗點非常明顯。使用 object 資料型別時,您可能直到稍後在程式碼中嘗試進行一些字串操作(如大寫)時才會發現問題:
ser_obj.str.capitalize().head()
輸出結果:
0 NaN
1 Bar
2 Baz
3 Foo
4 Bar
dtype: object
內容解密:
這裡,pandas 只是將第一行的 False 條目設定為缺失值。這不是我們想要的行為,但使用 object 資料型別,您會失去對資料品質的控制。
如果您使用的是 pandas 3.0 及更高版本,並且安裝了 PyArrow,您將會看到 pd.StringDtype 的效能顯著提高。讓我們重新建立我們的 pd.Series 物件來衡量這一點:
ser_obj = pd.Series(["foo", "bar", "baz"] * 10_000, dtype=object)
ser_str = pd.Series(["foo", "bar", "baz"] * 10_000, dtype=pd.StringDtype())
使用標準函式庫中的 timeit 模組進行快速計時比較:
import timeit
timeit.timeit(ser_obj.str.upper, number=1000)
timeit.timeit(ser_str.str.upper, number=1000)
內容解密:
在 pandas 3.0 及更高版本中,使用 pd.StringDtype 的效能優於使用 object 資料型別。這是因為 pd.StringDtype 針對字串操作進行了最佳化。
那麼,避免使用 dtype=object 的最簡單方法是什麼?如果您幸運地使用 pandas 3.0 及更高版本,您將自然不會經常遇到這種資料型別。即使對於仍在使用 pandas 2.x 系列的使用者,我建議在使用 I/O 方法時使用 dtype_backend="numpy_nullable" 引數:
import io
data = io.StringIO("int_col,string_col\n0,foo\n1,bar\n2,baz")
data.seek(0)
pd.read_csv(data, dtype_backend="numpy_nullable").dtypes
輸出結果:
int_col Int64
string_col string[python]
dtype: object
如果您手動建構 pd.DataFrame,您可以使用 pd.DataFrame.convert_dtypes 配契約樣的 dtype_backend="numpy_nullable" 引數:
df = pd.DataFrame([
[0, "foo"],
[1, "bar"],
[2, "baz"],
])
df = df.convert_dtypes(dtype_backend="numpy_nullable")