缺失填補的範例說明
# Pandas 填補缺失資料範例與技巧
在 Pandas 中,缺失資料通常會以以下形式出現:
- `NaN`
- `None`
- `NaT`:日期時間欄位的缺失值
- `pd.NA`
常用函式包括:
```python
isna()
notna()
fillna()
dropna()
interpolate()
ffill()
bfill()
```
---
## 1. 建立範例資料
```python
import pandas as pd
import numpy as np
df = pd.DataFrame({
"姓名": ["小明", "小華", "小美", "小強"],
"年齡": [20, np.nan, 22, None],
"城市": ["台北", "台中", None, "台南"],
"分數": [80, 90, np.nan, 70]
})
print(df)
```
輸出:
```text
姓名 年齡 城市 分數
0 小明 20.0 台北 80.0
1 小華 NaN 台中 90.0
2 小美 22.0 None NaN
3 小強 NaN 台南 70.0
```
---
# 2. 查看缺失資料
## 判斷每個儲存格是否缺失
```python
df.isna()
```
結果會是布林值:
```text
姓名 年齡 城市 分數
0 False False False False
1 False True False False
2 False False True True
3 False True False False
```
`isnull()` 和 `isna()` 功能相同:
```python
df.isnull()
```
---
## 計算每個欄位的缺失數量
```python
df.isna().sum()
```
輸出:
```text
姓名 0
年齡 2
城市 1
分數 1
dtype: int64
```
---
## 計算每個欄位的缺失比例
```python
missing_ratio = df.isna().mean()
print(missing_ratio)
```
例如:
```text
姓名 0.00
年齡 0.50
城市 0.25
分數 0.25
dtype: float64
```
若要以百分比呈現:
```python
print(df.isna().mean() * 100)
```
---
# 3. 使用固定值填補
## 整個 DataFrame 使用同一個值
```python
df_filled = df.fillna(0)
```
所有缺失值都會被替換為 `0`。
但這種方式不一定適合所有欄位,例如:
- 年齡填 `0` 可能合理或不合理
- 城市填 `0` 通常不合理
- 分數填 `0` 可能代表真的考零分,也可能代表資料缺失
因此通常會針對不同欄位指定不同值。
---
## 各欄位使用不同填充值
```python
df_filled = df.fillna({
"年齡": df["年齡"].median(),
"城市": "未知",
"分數": df["分數"].mean()
})
print(df_filled)
```
此範例的做法:
- `年齡`:使用中位數填補
- `城市`:使用文字 `"未知"`
- `分數`:使用平均數填補
---
# 4. 使用平均數、中位數或眾數填補
## 平均數 Mean
```python
df["分數"] = df["分數"].fillna(df["分數"].mean())
```
平均數適合:
- 數值資料
- 分布沒有太多極端值的情況
例如分數:
```python
mean_score = df["分數"].mean()
df["分數"] = df["分數"].fillna(mean_score)
```
### 注意
如果資料中有極端值,平均數可能會被拉高或拉低。
```python
[50, 60, 70, 1000]
```
此時平均數通常不太能代表一般資料。
---
## 中位數 Median
```python
df["年齡"] = df["年齡"].fillna(df["年齡"].median())
```
中位數對極端值比較不敏感,通常適合:
- 收入
- 房價
- 年齡
- 交易金額
- 有明顯偏態分布的數值欄位
例如:
```python
[30000, 35000, 40000, 1000000]
```
此時使用中位數通常比平均數穩定。
---
## 眾數 Mode
文字欄位常使用眾數填補:
```python
most_common_city = df["城市"].mode()[0]
df["城市"] = df["城市"].fillna(most_common_city)
```
也可以直接寫:
```python
df["城市"] = df["城市"].fillna(df["城市"].mode()[0])
```
眾數代表出現次數最多的值,常用於:
- 城市
- 類別
- 性別
- 商品種類
- 是否通過等欄位
---
# 5. 前一筆與後一筆資料填補
## 使用前一筆資料:`ffill`
```python
df["分數"] = df["分數"].ffill()
```
若資料如下:
```text
80
90
NaN
70
```
使用 `ffill()` 後:
```text
80
90
90
70
```
也可以寫成:
```python
df["分數"].fillna(method="ffill")
```
不過新版 Pandas 建議直接使用:
```python
df["分數"].ffill()
```
---
## 使用下一筆資料:`bfill`
```python
df["分數"] = df["分數"].bfill()
```
結果:
```text
80
90
70
70
```
`bfill()` 適合使用下一筆已知資料填補前面的缺失值。
---
## 限制連續填補筆數
假設:
```python
s = pd.Series([10, np.nan, np.nan, np.nan, 20])
```
只填補最多一筆:
```python
s.ffill(limit=1)
```
結果:
```text
0 10.0
1 10.0
2 NaN
3 NaN
4 20.0
dtype: float64
```
這可以避免長時間缺失資料被不合理地全部延用。
---
# 6. 線性插值 `interpolate()`
對具有順序性的數值資料,可以使用插值法。
```python
s = pd.Series([10, np.nan, np.nan, 40])
print(s.interpolate())
```
結果:
```text
0 10.0
1 20.0
2 30.0
3 40.0
dtype: float64
```
Pandas 會在 `10` 和 `40` 之間平均推估:
- 第一個缺失值:20
- 第二個缺失值:30
---
## 應用於 DataFrame
```python
df["分數"] = df["分數"].interpolate()
```
適合的情境:
- 溫度紀錄
- 感測器資料
- 股價
- 時間序列
- 連續測量值
不適合用於:
- 城市
- 姓名
- 類別
- 性別
- 商品類型
---
## 使用時間索引進行插值
```python
dates = pd.date_range("2024-01-01", periods=5)
temperature = pd.Series(
[20, np.nan, 24, np.nan, 28],
index=dates
)
temperature_filled = temperature.interpolate(method="time")
print(temperature_filled)
```
`method="time"` 會根據時間間隔進行插值,適合日期間隔不完全相同的資料。
---
# 7. 依群組填補缺失值
假設每個城市的資料應該使用該城市自己的平均數填補:
```python
df = pd.DataFrame({
"城市": ["台北", "台北", "台中", "台中"],
"分數": [80, np.nan, 70, np.nan]
})
df["分數"] = df["分數"].fillna(
df.groupby("城市")["分數"].transform("mean")
)
print(df)
```
結果:
```text
城市 分數
0 台北 80.0
1 台北 80.0
2 台中 70.0
3 台中 70.0
```
這比直接使用全體平均數更合理,因為不同群組可能有不同的分布。
---
## 依群組使用中位數
```python
df["分數"] = df["分數"].fillna(
df.groupby("城市")["分數"].transform("median")
)
```
使用 `transform()` 的好處是,結果會保留原本 DataFrame 的索引與列數,能直接指定回欄位。
---
# 8. 時間序列的缺失值填補
先建立時間序列:
```python
df = pd.DataFrame({
"日期": pd.to_datetime([
"2024-01-01",
"2024-01-02",
"2024-01-03",
"2024-01-04"
]),
"銷售額": [100, np.nan, 140, 160]
})
df = df.set_index("日期")
```
## 使用前後值插值
```python
df["銷售額"] = df["銷售額"].interpolate()
```
## 使用前一日資料
```python
df["銷售額"] = df["銷售額"].ffill()
```
## 使用後一日資料
```python
df["銷售額"] = df["銷售額"].bfill()
```
一般而言:
- 感測器連續數值:常用 `interpolate()`
- 狀態或標籤:可考慮 `ffill()`
- 預測資料:需小心避免使用未來資訊
---
# 9. 依列或依欄填補
`fillna()` 預設是針對每個欄位處理,也可以指定方向。
## 依欄位填補
```python
df.fillna(0, axis="columns")
```
## 依列填補
```python
df.fillna(0, axis="rows")
```
不過實務上最常見的做法是直接對指定欄位操作,例如:
```python
df["年齡"] = df["年齡"].fillna(df["年齡"].median())
```
這樣比較清楚,也比較不容易誤填其他欄位。
---
# 10. 直接刪除缺失資料
有時候不適合填補,可以選擇刪除。
## 刪除包含任何缺失值的列
```python
df_clean = df.dropna()
```
## 只有整列全部都是缺失值才刪除
```python
df_clean = df.dropna(how="all")
```
## 指定欄位缺失時才刪除
```python
df_clean = df.dropna(subset=["年齡", "分數"])
```
## 保留至少有兩個非缺失值的列
```python
df_clean = df.dropna(thresh=2)
```
`thresh=2` 表示該列至少要有兩個非缺失值。
---
# 11. 缺失值過多的欄位
如果某個欄位缺失比例很高,填補可能沒有意義。
例如刪除缺失比例超過 50% 的欄位:
```python
threshold = 0.5
df = df.loc[:, df.isna().mean() <= threshold]
```
也可以計算缺失比例後自行判斷:
```python
missing_ratio = df.isna().mean()
print(missing_ratio[missing_ratio > 0.5])
```
通常需要考慮:
- 該欄位是否重要
- 缺失是否具有特殊意義
- 是否有替代欄位
- 填補後是否會造成偏誤
---
# 12. 將特殊文字轉換成缺失值
有些資料不會使用 `NaN`,而是使用:
- `""`
- `"N/A"`
- `"NA"`
- `"未知"`
- `"-"`
- `"null"`
讀取 CSV 時可以指定:
```python
df = pd.read_csv(
"data.csv",
na_values=["", "N/A", "NA", "-", "null"]
)
```
如果資料已經讀入,也可以替換:
```python
df = df.replace(["N/A", "NA", "-", "null"], np.nan)
```
對空字串處理:
```python
df = df.replace(r"^\s*$", np.nan, regex=True)
```
這可將空字串或只包含空白的內容轉為 `NaN`。
---
# 13. 使用 `pd.NA` 與資料型別
Pandas 中的缺失值可能影響資料型別:
```python
s = pd.Series([1, 2, None])
print(s.dtype)
```
通常會轉成:
```text
float64
```
因為傳統 NumPy 的整數型別無法容納 `NaN`。
可以使用 Pandas 的可空整數型別:
```python
s = pd.Series([1, 2, None], dtype="Int64")
print(s)
```
結果:
```text
0 1
1 2
2 <NA>
dtype: Int64
```
其他可空型別包括:
```python
"Int64"
"boolean"
"string"
```
例如:
```python
df["年齡"] = df["年齡"].astype("Int64")
```
---
# 14. 避免資料洩漏:訓練集與測試集分開處理
在機器學習中,不應該先把訓練集和測試集合併後再計算平均數或中位數,否則會造成資料洩漏。
錯誤概念:
```python
all_data = pd.concat([train, test])
median = all_data["年齡"].median()
```
正確做法:
```python
median_age = train["年齡"].median()
train["年齡"] = train["年齡"].fillna(median_age)
test["年齡"] = test["年齡"].fillna(median_age)
```
填補值應該只從訓練資料計算,再套用到測試資料。
使用 Scikit-learn 時,可以使用:
```python
from sklearn.impute import SimpleImputer
imputer = SimpleImputer(strategy="median")
train[["年齡"]] = imputer.fit_transform(train[["年齡"]])
test[["年齡"]] = imputer.transform(test[["年齡"]])
```
重點是:
- 訓練集使用 `fit_transform()`
- 測試集只使用 `transform()`
---
# 15. 常見實務範例
## 數值欄位使用中位數,類別欄位使用眾數
```python
numeric_cols = ["年齡", "分數"]
category_cols = ["城市"]
for col in numeric_cols:
df[col] = df[col].fillna(df[col].median())
for col in category_cols:
df[col] = df[col].fillna(df[col].mode()[0])
```
---
## 自訂缺失值標籤
```python
df["城市"] = df["城市"].fillna("未提供")
```
如果缺失本身具有意義,使用明確文字通常比填入眾數更好。
例如:
- 沒有填寫城市:`"未提供"`
- 沒有購買紀錄:`"無購買紀錄"`
- 尚未評分:`"尚未評分"`
---
## 填補完成後檢查
```python
print(df.isna().sum())
```
若想確認整個 DataFrame 是否仍有缺失值:
```python
print(df.isna().any().any())
```
結果:
- `True`:仍存在缺失值
- `False`:沒有缺失值
也可以查看缺失位置:
```python
print(df[df.isna().any(axis=1)])
```
---
# 16. 如何選擇填補方法?
| 資料類型或情境 | 建議方法 |
|---|---|
| 一般數值欄位,分布穩定 | 平均數 |
| 有極端值的數值欄位 | 中位數 |
| 類別欄位 | 眾數或自訂類別 |
| 缺失具有特殊意義 | `"未知"`、`"未提供"` 等標籤 |
| 連續時間序列 | `interpolate()` |
| 狀態延續的時間資料 | `ffill()` |
| 要使用後續觀測值 | `bfill()` |
| 缺失比例非常高 | 考慮刪除欄位 |
| 關鍵欄位缺失且無法合理推估 | `dropna()` |
| 不同群組有不同分布 | `groupby()` 後分組填補 |
---
# 17. 一個完整的小範例
```python
import pandas as pd
import numpy as np
df = pd.DataFrame({
"部門": ["A", "A", "B", "B", "B"],
"年齡": [25, np.nan, 30, 35, np.nan],
"薪資": [40000, 42000, np.nan, 50000, np.nan],
"職位": ["工程師", None, "主管", "工程師", None]
})
# 數值欄位:使用各部門中位數填補
df["年齡"] = df["年齡"].fillna(
df.groupby("部門")["年齡"].transform("median")
)
df["薪資"] = df["薪資"].fillna(
df.groupby("部門")["薪資"].transform("median")
)
# 類別欄位:使用眾數填補
df["職位"] = df["職位"].fillna(df["職位"].mode()[0])
print(df)
print("\n缺失值統計:")
print(df.isna().sum())
```
---
# 18. 使用 `inplace=True` 的注意事項
以下寫法可以直接修改原始資料:
```python
df["年齡"].fillna(df["年齡"].median(), inplace=True)
```
但在部分 Pandas 版本中,對欄位切片使用 `inplace=True` 可能產生警告或不如預期。較推薦明確重新指定:
```python
df["年齡"] = df["年齡"].fillna(df["年齡"].median())
```
這種寫法:
- 可讀性較高
- 較不容易遇到 chained assignment 問題
- 在不同 Pandas 版本中通常更穩定
---
## 總結
Pandas 填補缺失資料時,不應只是一律使用 `0` 或平均數,而應根據資料特性選擇方法:
1. 先用 `isna().sum()` 了解缺失狀況。
2. 數值資料可考慮平均數、中位數或插值。
3. 類別資料可使用眾數或明確的缺失標籤。
4. 時間序列可使用 `ffill()`、`bfill()` 或 `interpolate()`。
5. 不同群組的資料,最好分組後分別填補。
6. 缺失比例過高時,應評估刪除欄位。
7. 機器學習情境中,填補統計值應只從訓練集計算。
相關學習地圖、教學課程
Python 資料工程