Day 18 Pandas 資料清理
引言
真實世界的資料幾乎都不乾淨:可能缺值、可能有重複列、可能有格式不一致、可能有離群值。資料科學家常說「資料清理佔了 80% 的時間」,剩下的 20% 才是建模與分析。Pandas 提供一整套清理工具,今天要學會的正是這些日常必備:缺失值處理、條件篩選、排序、去重、型別轉換,以及最實用的 groupby 分組聚合。這些動作不需要寫複雜的演算法,但每一招都得做扎實,模型訓練的成果才會穩定。
學完今天的內容,你會具備處理一份中等規模的真實表格資料的能力。從明天起,我們會進入視覺化階段,用 Matplotlib 與 Seaborn 把清理後的資料畫成圖表。資料清理與視覺化是資料分析的兩條腿,先把資料整理好,畫出來的圖才有意義。
缺失值處理:isnull、fillna、dropna
Pandas 用 NaN(Not a Number)表示缺失值。可以用 isnull() 或 notnull() 判斷位置,再用 fillna() 填補、dropna() 丟棄。實務上建議先檢查缺失比例,決定要補還是刪:缺失很少就刪,缺失有規律或占比偏高就補。判斷缺失型態也是清理的關鍵:是「完全隨機缺漏」還是「某個族群特別容易漏」,處理方式會很不一樣。如果是後者,直接補平均反而會掩蓋問題,這時通常會加上「是否缺值」的衍生欄位給模型參考。
import pandas as pd
import numpy as np
df = pd.DataFrame({
"姓名": ["小明", "小華", "小美", "小李"],
"數學": [90, np.nan, 95, 70],
"英文": [78, 80, np.nan, 75],
})
print(df.isnull()) # 標出每個位置是否缺失
print(df.isnull().sum()) # 每欄缺失值數量
# 刪除有缺失的列
df_drop = df.dropna()
# 填補:固定值
df_fill_const = df.fillna(0)
# 填補:每欄平均(實務上很常用)
df_fill_mean = df.fillna(df.mean(numeric_only=True))
fillna() 還能接受字典,對不同欄位給不同填補策略:df.fillna({"數學": 0, "英文": df["英文"].mean()})。當資料有時間順序時,常用前一筆或後一筆填補(method="ffill" 或 "bfill"),這個技巧在處理感測器紀錄時特別好用。從 Pandas 2.1 起,也可以用 method="nearest" 找最近的非缺失值做填補,這對間距均勻的時間序列很實用。
資料篩選與排序
昨天學過布林索引,今天把它套用到 DataFrame 上。常見的篩選條件包括「數值大於某門檻」、「字串包含某關鍵字」、「日期在區間內」等。query() 方法則提供更接近 SQL 的寫法,對熟悉資料庫的人很友善。布林索引的回傳結果是原 DataFrame 的子集(複製),因此修改子集不會影響原資料,這點和 NumPy 的 view 不同。
import pandas as pd
df = pd.DataFrame({
"姓名": ["小明", "小華", "小美", "小李"],
"城市": ["台北", "台中", "台北", "高雄"],
"消費": [1200, 850, 2300, 600],
})
# 布林索引
high_spend = df[df["消費"] >= 1000]
# 多條件
taipei_high = df[(df["城市"] == "台北") & (df["消費"] >= 1000)]
# 用 isin() 篩選多個城市
multi_city = df[df["城市"].isin(["台北", "高雄"])]
# 用 query() 寫法
result = df.query("消費 >= 1000 and 城市 == '台北'")
# 排序
df_sorted = df.sort_values("消費", ascending=False)
df_sorted_multi = df.sort_values(["城市", "消費"])
isin() 對於「多選一」的篩選特別方便,例如篩選多個會員等級、多個產品類別。query() 則把條件寫成字串,可讀性更高,但要注意字串中的引號與運算子的優先順序,必要時加括號。sort_values() 是排序的主力,能依一個或多個欄位排,也能指定遞增或遞減。如果想要穩定排序(相同值保持原本順序),可以加上 kind="stable" 參數。處理大型資料時,先用 sort_values() 排序再 drop_duplicates(),常常比直接刪重更省事,因為排序後的索引會比較有規律。
處理重複資料
實務上常會遇到完全重複或部分欄位重複的列,例如系統匯入時不小心多按幾次送出。duplicated() 會回傳布林 Series,標出每列是否為重複;drop_duplicates() 則直接刪除重複列。判斷重複時常常是看某幾個關鍵欄位(例如訂單編號、會員 ID),而不是看整列是否一致,這時 subset 參數就是關鍵。
import pandas as pd
df = pd.DataFrame({
"顧客ID": [1, 2, 1, 3, 2],
"消費": [1200, 850, 1200, 600, 850],
})
print(df.duplicated()) # 整列比對
print(df.duplicated(subset=["顧客ID"])) # 只看顧客ID是否重複
df_unique = df.drop_duplicates() # 預設保留第一次出現
df_last = df.drop_duplicates(subset=["顧客ID"], keep="last") # 保留最後一次
subset 參數讓我們只看特定欄位是否重複,搭配 keep 可以決定要保留第一筆(first)、最後一筆(last),或全部丟掉(False)。處理交易資料時,常常會希望保留「最新一筆」,這時 keep="last" 就很實用。如果想依時間欄位判斷最新一筆,可以先用 sort_values() 排序再 drop_duplicates(),順序寫對就不會出錯。對於「同一個人同一天的多筆紀錄」這類常見場景,先用日期切、再對其他關鍵欄位去重是比較乾淨的做法。
apply 與型別轉換
有時候欄位裡的數字其實是字串(例如 "1,200"),或是需要把多欄位合併出新欄位。Pandas 提供 astype() 轉型、apply() 套用自訂函式、map() 對 Series 做映射。apply() 是最泛用的工具,可以處理複雜的邏輯。學會這幾招,面對格式不一的資料就能有條理地處理。
import pandas as pd
df = pd.DataFrame({
"金額字串": ["1,200", "850", "2,300"],
"顧客ID": ["A01", "A02", "A03"],
})
# 型別轉換:先去掉逗號再轉 int
df["金額"] = df["金額字串"].str.replace(",", "").astype(int)
# 用 map 做對應
city_map = {"A01": "台北", "A02": "台中", "A03": "高雄"}
df["城市"] = df["顧客ID"].map(city_map)
# 用 apply 套用自訂函式
def price_level(amount):
if amount >= 2000:
return "高"
elif amount >= 1000:
return "中"
return "低"
df["消費等級"] = df["金額"].apply(price_level)
apply() 可以對整個 DataFrame 走訪(搭配 axis=1 對每列處理),但大量使用會比較慢,建議優先考慮向量化寫法或 NumPy 的 ufunc。只有在邏輯複雜、無法向量化時才用 apply。如果想用更複雜的鏈式操作,可以考慮 .pipe() 與 .assign() 讓程式碼讀起來更接近資料流。實務上可以先把 df["金額"].to_numpy() 轉成 NumPy 陣列運算,再寫回欄位,這樣會比 apply 快上好幾倍。
groupby:分組聚合
groupby() 是 Pandas 分析的靈魂:把資料依某個欄位分成多組,再對每組做彙總(例如加總、平均、計數)。寫法類似 SQL 的 GROUP BY,核心概念是「split-apply-combine」:先切、再算、最後合併。學會 groupby 之後,Excel 的樞紐分析能做的事幾乎都能用兩三行 Python 完成。
import pandas as pd
df = pd.DataFrame({
"城市": ["台北", "台北", "台中", "台中", "高雄"],
"通路": ["線上", "門市", "線上", "門市", "門市"],
"金額": [1200, 800, 1500, 700, 900],
})
# 依城市分組,計算總金額
by_city = df.groupby("城市")["金額"].sum()
# 依城市分組,計算平均與筆數
by_city_stats = df.groupby("城市")["金額"].agg(["mean", "count"])
# 多欄分組
by_city_channel = df.groupby(["城市", "通路"])["金額"].sum()
# 對每組套用自訂函式
def top_amount(s):
return s.max() - s.min()
spread = df.groupby("城市")["金額"].apply(top_amount)
groupby() 的回傳結果是 SeriesGroupBy 或 DataFrameGroupBy,需要呼叫彙總函式(sum、mean、count 等)才會真正計算。agg() 可以同時算多個統計量,apply() 則能套任意函式。學會 groupby 之後,很多 Excel 的樞紐分析都能用兩三行程式碼完成。做完 groupby 後如果想重置索引回一般 DataFrame,呼叫 reset_index() 即可,這對後續繪圖或輸出都很方便。想要更細的「每組前 N 名」可以用 groupby + head() 組合,這在電商、賽事報表很常見。
結語
今天把 Pandas 資料清理的核心工具都走了一遍:缺失值用 isnull / fillna / dropna、條件篩選用布林索引與 query、排序用 sort_values、重複用 duplicated / drop_duplicates、型別與衍生欄位用 astype / apply / map,分組彙總則用 groupby。這些工具的組合,足以應付 80% 日常的資料清理需求。建議找一份過去工作中曾經處理過的 CSV,把今天的工具實際用一次,看看能省下多少時間。
明天,我們會進入 Pandas 的合併與時間序列:學會 merge()、concat() 處理多份資料的整合,並用 to_datetime() 與 resample() 處理日期時間資料。這些是處理真實世界資料的進階工具,學會之後就能應付常見的多來源、跨期資料分析需求。
留言
張貼留言