Day 19 Pandas 合併與時間序列
引言
真實世界的資料很少只放在一張表格裡。訂單資料有客戶基本資料、銷售資料、產品資料,當我們要分析「哪個區域、哪個年齡層買了哪些商品」,就得把多張表合併在一起。此外,許多分析都跟時間有關:每日銷售量、月活躍使用者、股價走勢,這些都屬於時間序列。
今天要學習的 Pandas 合併與時間序列處理,正是把零散資料整合成可用資訊的關鍵技能。我們會介紹 concat()、merge() 與 join() 三種合併方式,再學會把字串轉成日期、用日期做索引,最後用 resample() 做不同時間尺度的彙總。學完後,你就能處理「跨表格」與「跨時間」這兩類最常見的資料分析情境。
這一篇會延續 Day 17、Day 18 學過的 Pandas 基礎。如果你對 DataFrame、groupby()、缺失值處理還不熟悉,可以先回去複習。今天的重點放在「如何把多份資料拼起來」與「如何用日期做分析」這兩件事上。
使用 concat() 串接資料表
concat() 是最直觀的合併方式:把多個 DataFrame 沿著某一個軸(列或欄)疊在一起。常見的情境是把同一個欄位結構的資料,例如各月份、各分店報表,垂直串成一張大表。concat() 的運作方式類似「疊紙牌」,依預設會以列方向(垂直)堆疊,並保留原本的索引。
import pandas as pd
jan = pd.DataFrame({
"product": ["A", "B", "C"],
"sales": [120, 230, 180]
})
feb = pd.DataFrame({
"product": ["A", "B", "C"],
"sales": [150, 200, 210]
})
result = pd.concat([jan, feb], ignore_index=True)
print(result)
輸出:
product sales
0 A 120
1 B 230
2 C 180
3 A 150
4 B 200
5 C 210
ignore_index=True 會重新編排索引,讓結果的索引是 0 到 5,而不是各自保留原本的 0、1、2。當我們要疊加多筆相同結構的資料時,這個參數幾乎都會用上。如果想維持原本的索引並看出資料來源,可以改成多層索引:pd.concat([jan, feb], keys=["1月", "2月"])。
使用 merge() 合併兩張表
merge() 的概念類似資料庫的 JOIN:透過一個或多個共用欄位(鍵),把兩張表的對應列拼在一起。這是合併「客戶資料」與「訂單資料」這類有對應關係的表時的主力。與 concat() 不同的是,merge() 是依「鍵的值」對應合併,而不是單純疊在一起。
customers = pd.DataFrame({
"customer_id": [1, 2, 3],
"name": ["Alice", "Bob", "Charlie"]
})
orders = pd.DataFrame({
"order_id": [101, 102, 103, 104],
"customer_id": [1, 1, 2, 3],
"amount": [250, 180, 320, 150]
})
merged = pd.merge(customers, orders, on="customer_id")
print(merged)
輸出:
customer_id name order_id amount
0 1 Alice 101 250
1 1 Alice 102 180
2 2 Bob 103 320
3 3 Charlie 104 150
on="customer_id" 指定要用哪個欄位當作合併鍵;如果兩張表的鍵名稱不同,可以用 left_on 與 right_on 分別指定。merge() 預設是 inner join,只保留兩邊都有的鍵;如果要保留某一邊全部的紀錄,可以改用 how="left"、how="right" 或 how="outer"。merge 也支援多個鍵,例如 on=["year", "month"] 同時用兩個欄位對應。
處理合併後的缺失值
合併之後常常會冒出 NaN,例如某些客戶沒有訂單、或某些訂單對不到客戶資料。Day 18 學過的處理方式在這裡一樣適用:可以用 fillna(0) 補 0,或用 dropna() 直接拿掉。選擇哪一種,要看那筆缺失在分析上有沒有意義。
all_customers = pd.DataFrame({
"customer_id": [1, 2, 3, 4],
"name": ["Alice", "Bob", "Charlie", "David"]
})
left_join = pd.merge(all_customers, orders, on="customer_id", how="left")
left_join["amount"] = left_join["amount"].fillna(0)
print(left_join)
輸出:
customer_id name order_id amount
0 1 Alice 101.0 250.0
1 1 Alice 102.0 180.0
2 2 Bob 103.0 320.0
3 3 Charlie 104.0 150.0
4 4 David NaN 0.0
David 沒有任何訂單,但我們用 fillna(0) 把金額補成 0,這樣在算總和時就不會把他誤算成「缺資料」。如果是要計算「沒有下單的客戶比例」這類指標,則要保留 NaN,才能正確反映實際情況。合併後的缺失值處理,往往決定了分析的結論能不能站得住腳。
日期時間處理:to_datetime 與 dt 存取器
時間序列分析的第一步,是讓 Pandas 認得我們手上的日期字串。pd.to_datetime() 可以把字串轉成 datetime64 型別,之後就能用 dt 存取器取得年、月、日、星期幾等資訊。這個步驟看似簡單,但很多新手會卡在「資料看起來像日期,其實還是字串」的陷阱。
df = pd.DataFrame({
"date": ["2024-01-15", "2024-02-20", "2024-03-10"],
"sales": [200, 340, 280]
})
df["date"] = pd.to_datetime(df["date"])
df["year"] = df["date"].dt.year
df["month"] = df["date"].dt.month
df["weekday"] = df["date"].dt.day_name()
print(df)
輸出:
date sales year month weekday
0 2024-01-15 200 2024 1 Monday
1 2024-02-20 340 2024 2 Tuesday
2 2024-03-10 280 2024 3 Sunday
轉成 datetime 之後,這個欄位就支援加減天數、比較先後,也能直接丟進繪圖函式,省下不少前置處理功夫。to_datetime() 對常見的 "YYYY-MM-DD"、"YYYY/MM/DD"、甚至中英文混雜的日期格式都能自動判斷。對於不規則的格式,可以加上 format="%Y年%m月%d日" 明確指定。實務上從資料庫或 API 拉回的時間欄位常常是「Unix 時間戳」(例如 1704153600),這時候可以用 unit="s" 讓 Pandas 自動轉成人類可讀的日期。
如果只想取出日期、不要時間部分,可以用 df["date"].dt.date;只想取時間則用 df["date"].dt.time。這些小工具在處理「同一天不同時段」的資料時特別有用,例如把訂單時間轉成「日期+小時」兩個欄位,再依小時做分組分析。
時間索引與 resample() 重取樣
把日期欄位設成索引後,就能用 resample() 把資料重新彙總到不同時間尺度,例如把日資料變成月資料或季資料。這在做趨勢分析時非常方便,能把雜訊平滑掉,看出真正的走向。resample() 的概念類似 groupby(),只是它依時間頻率分組。
import numpy as np
dates = pd.date_range("2024-01-01", periods=90, freq="D")
daily = pd.DataFrame({
"date": dates,
"sales": np.random.randint(100, 400, size=90)
})
daily = daily.set_index("date")
monthly = daily.resample("ME").sum()
print(monthly)
輸出:
sales
date
2024-01-31 7820
2024-02-29 6540
2024-03-30 7010
"ME" 代表月底頻率;如果是 "W" 就是週、"QE" 就是季。除了 sum(),也可以換成 mean()、max()、min(),看分析需求而定。需要注意的是,resample() 會把每個時段的數字彙總起來,若該時段沒有資料,預設會用 NaN 填補,這時可以用 fillna(method="ffill") 把前一筆的值補上。另一個常見需求是「同月份跨年度比較」,這時候可以用 df.groupby(df.index.month).mean() 把資料依月份重新分組,得到「1 月平均、2 月平均」這類季節性指標。
計算滾動平均
另一個常用的時間序列技巧是「滾動平均」(rolling mean),用鄰近幾筆資料的平均值來平滑短期波動。當我們想看長期趨勢、又不想丟掉原始資料時,這個做法很實用。例如把 7 日滾動平均疊在原始銷售曲線上,就能看出「整體上升,但日資料波動很大」的趨勢。
daily["rolling_7d"] = daily["sales"].rolling(window=7).mean()
print(daily.head(10))
輸出:
sales rolling_7d
date
2024-01-01 312 NaN
2024-01-02 185 NaN
2024-01-03 256 NaN
2024-01-04 298 NaN
2024-01-05 341 NaN
2024-01-06 120 NaN
2024-01-07 275 255.29
2024-01-08 388 266.14
2024-01-09 152 261.43
2024-01-10 266 263.71
前 6 天因為資料筆數不足,會顯示 NaN;從第 7 天開始就是前 7 筆的移動平均。視覺化時把原始銷售與 7 日滾動平均疊在一起,最能看出趨勢。window 參數決定視窗大小,數字越大曲線越平滑,但也越延遲。常見的選擇是 7(日)、30(月)、90(季),依資料的時間尺度與分析目的調整。
結語
今天把 Pandas 的合併與時間序列兩個主題一次補齊。concat() 適合疊加同結構的資料,merge() 適合處理有鍵對應的多張表;把日期欄位轉成 datetime 並設成索引後,就能用 resample() 與 rolling() 做出不同尺度的彙總與平滑。掌握這些技巧,分析「跨表」與「跨時間」的資料就不再是難事。
明天,我們會進入 Matplotlib,從最基礎的折線圖開始,學習怎麼把資料轉成清楚又專業的圖表。
留言
張貼留言