COM6001 Computing with Big Data · Week 6
Data Cleaning (I): Unnecessary Data & Missing Values
COM6001 · Week 6 · Source: Lecture06_Data_cleaning_Part_1.pdf(Jupyter notebook export, 43 pages)· Lecturer: Dr. Aileen Hou
Contents · 13 sections
🎯 本講重點 (Learning outcomes)
- Key takeaways(slide 原文):Know the steps to inspect the data and identify the problems; Know the methods to handle the problems.
- 識別 unnecessary data:constant values、duplicated columns、one-to-one mapped columns、duplicated rows;用
drop()、drop_duplicates()刪除。 - 理解 missing values 嘅成因(missing at random vs missing with a reason)同 4 種搵 missing values 嘅方法(
info()、describe()、isna()、value_counts(dropna=False))。 - 處理 missing values:
dropna()(thresh、axis)、fillna()constants / mean / median / mode。 - 用 scikit-learn:
SimpleImputer(mean / median / most_frequent / constant、add_indicator)同IterativeImputer(model-based)。
⭐ 考試必溫 Top-5
dropna(thresh=N):保留至少有 N 個 non-null 值嘅 row(或axis=1時嘅 column)——N 係「要保留嘅最少 non-null 數」,唔係「容許嘅 missing 數」。- One-to-one mapping 判斷:兩 columns 嘅組合數(
df[[a,b]].value_counts()長度)== 各自嘅nunique()。 duplicated(keep='first')第一次出現標 False、其餘 True;drop_duplicates(keep='first'|'last'|False, ignore_index=True);446 → 434(keep first)/ 422(keep=False)。- Mean vs median:有強烈 outliers / skewed 用 median;用 mean 填唔會改變 mean。Categorical 用 mode(most frequent)。
- SimpleImputer 三步:setup(strategy, fill_value)→
fit_transform()得 NumPy array → assign 返 DataFrame columns;IterativeImputer 用 BayesianRidge round-robin 預測,用min_value/max_value設界。
1. What is data cleaning(複習)
ExamData cleansing or data cleaning is the process of detecting and correcting (or removing) corrupt or inaccurate records from a record set, table, or dataset and refers to identifying incomplete, incorrect, inaccurate or irrelevant parts of the data and then replacing, modifying, or deleting the dirty or coarse data.
- Why it is needed? Garbage in, Garbage out
- Data analysis procedures:Data collection → Data cleaning → Data analysis → Insights
- Data Audit(每個 dataset 問題都唔同):Inconsistent values、Unnecessary data、Missing data、Incorrect encodings
- Removing unnecessary data (dropping):Repeated columns、Duplicated rows、Constant values
- Handling the missing data:Dropping、Imputing using constants、Imputing using statistics、Imputing using models
- Handling the outliers(Week 7)
- Know the steps to inspect the data and identify the problems
- Know the methods to handle the problems
即係考試通常會俾你一個 dataset 描述/輸出,要你 (1) 講出有咩問題、用咩 method 檢查;(2) 寫 code 處理。
2. Handling unnecessary data
工具總覽:
| 問題 | 檢查方法 | 處理 |
|---|---|---|
| Constant values | value_counts()、nunique() | drop(columns=...) |
| One-to-one mapped data(冇額外資訊) | value_counts()、nunique() | drop(columns=...) |
| Duplicated data(columns / rows) | compare()、duplicated() | drop()、drop_duplicates() |
import pandas as pd
dir_path = "./Datasets/week_6/"
wdi = pd.read_csv(dir_path + 'world_development_indicators.csv')
wdi.info(verbose=True)
# RangeIndex: 446 entries, 0 to 445;22 columns
# atms_per_100000 417 non-null;compulsory_education_years 424;health_expenditure_pct_of_gdp 439
# population_density 444;alcohol_consumption_per_capita 220;country_category 289;其餘 446
# dtypes: float64(15), int64(2), str(5)
檢查 wdi[['country_name', 'planet', 'Country Name', 'Country Code']] 後,notebook 列出三個懷疑問題:
- problem 1:column
planetmay have only one value - problem 2:column
country_namemay be identical to columnCountry Name(slide 寫 "country_code",但內容上指 Country Name) - problem 3:column
Country Codemay be one-to-one mapped to columncountry_name
2.1 Constant values
DataFrame.nunique():每 column 嘅 unique values 數。dropna=False將 NaN 當一個 unique value;dropna=True只數 non-NaN。Series.value_counts():每個值嘅次數。normalize=True輸出比例;normalize=False輸出絕對次數。
wdi.nunique(dropna=True)
# country_name 217, ..., year 2, Country Name 217, Country Code 217,
# country_category 2, is_region 2, planet 1
wdi.nunique(dropna=False) - wdi.nunique(dropna=True)
# 1 → 該 column 有 NaN:atms_per_100000, compulsory_education_years,
# health_expenditure_pct_of_gdp, population_density,
# alcohol_consumption_per_capita, country_category
wdi['planet'].value_counts(normalize=True) # Earth 1.0
wdi['planet'].value_counts() # Earth 446
Conclusion:用 nunique() 或 value_counts() 檢查有冇 column 只得一個值(planet)。
2.2 Duplicated columns
wdi['country_name'] == wdi['Country Name'] # 446 個 True(太長,唔好用嚟肉眼睇)
wdi['country_name'].compare(wdi['Country Name']) # 空(self / other 冇 rows)→ 完全相同
(wdi['country_name'] == wdi['Country Name']).value_counts() # True 446
Q1(Homework)要求寫 assert_single_value_result(col1, col2, return_type):bool 版本全部相同回傳 True;int 版本全部相同回傳 0、否則回傳 >0。
def assert_single_value_result(col1, col2, return_type):
if return_type == 'bool':
return (col1 == col2).all() # np.True_
return (col1 != col2).sum() # np.int64(0)
原 notebook 呢格只有 function header 冇 body,所以輸出 SyntaxError: incomplete input——呢個係留空俾學生作答,唔係概念錯誤。
2.3 Confirming one-to-one mapped data
有啲 columns 內容唔同,但係一一對應(例如 Country Code ↔ Country Name:AFG ↔ Afghanistan)。保留兩個等於重複資訊。點樣用程式檢查?
test = pd.DataFrame({'label': ['a','b','c','a'], 'value': [1,2,3,2]})
# label value
# 0 a 1
# 1 b 2
# 2 c 3
# 3 a 2
test.nunique() # label 3, value 3 ← nunique() 唔會數組合
test.value_counts() # DataFrame 上用 → 數「組合」
# (a,1) 1;(b,2) 1;(c,3) 1;(a,2) 1 → 4 個組合
有 4 個組合,但每個 column 只有 3 個 unique values,4 > 3 → 一定有某個 label 對應多過一個 value(呢度 a 對應 1 同 2)→ 唔係 one-to-one。
規則:如果 nunique(col1) == nunique(col2) == 兩 column 組合數,就係 one-to-one mapped。
# Q2:Country Name 同 Country Code
wdi[['Country Name', 'Country Code']].value_counts().shape[0] # 組合數 → 217
wdi[['Country Name', 'Country Code']].nunique() # 217, 217
# 答:因為組合數 217 等於兩個 column 各自嘅 unique 數 217,所以 values 係 one-to-one mapped。如果每個 label 只配一個 value、每個 value 只配一個 label,咁組合數 = label 數 = value 數。只要有人「一腳踏兩船」,組合數就會大過其中一邊嘅 unique 數。
2.4 Duplicated rows
DataFrame.duplicated() 逐行標示係咪重複。keep 參數(default 'first'):第一次出現標 False(唔當重複),其他相同嘅 rows 標 True。
wdi.duplicated(keep='first') # 0–4 False ... 441–445 True
wdi.duplicated(keep='last').sum() # np.int64(12) → 有 12 行重複
wdi[wdi['country_name'] == 'Comoros']
# country_name access_to_electricity_pct atms_per_100000
# 38 Comoros 79.419693 5.100094
# 255 Comoros 81.922752 4.967858 ← 唔同年份,唔係重複
# 445 Comoros 79.419693 5.100094 ← 同 38 一模一樣
keep 影響「邊個被標 True」,唔影響數目一對重複 rows(38 同 445):keep='first' → 38 False、445 True;keep='last' → 38 True、445 False;keep=False → 兩個都 True。所以 keep='first' 或 'last' 嘅 .sum() 都係 12,但 keep=False 會係 24。
3. Remove the duplicated data
3.1 Drop duplicated columns
# 用 df.drop(),設定 columns=[...],要 assign 返先生效
wdi = wdi.drop(columns=['Country Name', 'Country Code', 'planet'])
wdi.info() # 19 columns;dtypes: float64(15), int64(2), str(2)
3.2 Drop duplicated rows
DataFrame.drop_duplicates():keep= 決定保留第一個、最後一個,或者全部唔留(keep=False);ignore_index=True 重新編 index(0, 1, 2, …)。同樣要 assign 返。
wdi.drop_duplicates(keep='first', ignore_index=True) # 434 rows × 19 columns(每組保留第一個)
wdi.drop_duplicates(keep=False, ignore_index=True) # 422 rows × 19 columns(重複嘅全部刪晒)
446 rows,有 12 組「一對」重複 → keep='first':446 − 12 = 434(12 duplicated rows removed);keep=False:446 − 2×12 = 422(連「原本」嗰個都刪)。考試可能俾你 duplicated().sum() 嘅結果叫你推算 drop 之後幾多行。
4. Missing values:成因與尋找
Missing value = 某 observation(row)喺某 feature(column)冇儲存任何資料。非常常見:
- Missing at random:建立 dataset 時打錯字;hardware / sensor malfunctions、communication errors。
- Missing with a reason:text / categorical 值因 data 性質而缺(呢個 feature 本身就應該係空)。例如 wdi 嘅
country_category(DEVELOPING/DEVELOPED)對「Arab World」呢類 region 本身就唔適用。
通常顯示為 NaN、None、Null 或 empty string。注意:NaN 唔等於 string "NaN"。我哋冇可能永遠知道係 random 定有原因,只能盡量檢查;但 missing values 會顯著影響分析或 modelling 結果。
4.1 Method 1:.info()
例:alcohol_consumption_per_capita:446 − 220 = 226 個 missing。空白資料讀入時會變成 NaN(Not a Number)。
pandas 預設會將某啲 strings 自動識別為 NaN,例如 empty strings、'#N/A'、'#N/A N/A'、'NA'、'null' 等。亦可以用 na_values 自訂:
# test.csv
# ID,Product,Price,Quantity,Notes
# 1,Widget,25.50,10,N/A
# 2,Gadget,-999,5,In stock
# 3,Thingy,15.75,-999,Unknown
# 4,Doodad,30.00,8,Available
# 5,Whatsit,-999,-999,N/A
test = pd.read_csv(dir_path + 'test.csv')
# Notes 嘅 'N/A' 自動變 NaN;但 Price/Quantity 嘅 -999 仍然係數字
test = pd.read_csv(dir_path + 'test.csv', na_values=[-999])
# ID Product Price Quantity Notes
# 0 1 Widget 25.50 10.0 NaN
# 1 2 Gadget NaN 5.0 In stock
# 2 3 Thingy 15.75 NaN Unknown
# 3 4 Doodad 30.00 8.0 Available
# 4 5 Whatsit NaN NaN NaN
留意 Quantity 由 int 變成 10.0 等 float——因為 NaN 本身係 float,含 NaN 嘅 numeric column 會被 upcast 成 float64。
4.2 Method 2:describe() 搵 placeholder 值
好多 dataset 會用正常範圍以外嘅值(例如 -999、-1)代表 special / missing values,info() 唔會當佢哋係 null。用 df.describe()(或 .describe().T 轉置,rows ↔ columns)睇 five-number summary:
test = pd.read_csv(dir_path + 'test.csv')
test.describe().T
# count mean std min 25% 50% 75% max
# ID 5.0 3.00 1.581139 1.0 2.0 3.00 4.0 5.0
# Price 5.0 -385.35 560.206926 -999.0 -999.0 15.75 25.5 30.0
# Quantity 5.0 -395.00 551.376913 -999.0 -999.0 5.00 8.0 10.0
Price 嘅 min = −999、mean 係負數——價錢冇可能負,即刻知道 −999 係 placeholder。驗算 Price mean: ✔。
Placeholder 值會嚴重拉歪 mean / std。如果唔處理,後面用 mean 填 missing 或者做 modelling 都會錯晒。所以 audit 時,min / max 有冇不合理係必睇項目。
4.3 Method 3:isna() / notna()
df.isna():逐個 cell 輸出 True(missing)/ False。df.notna():相反。- 加
.sum()統計 True 數:axis=0(default)逐 column 加;axis=1逐 row 加。
test.isna().sum(axis=0) # 每個 column:ID 0, Product 0, Price 0, Quantity 0, Notes 2
test.isna().sum(axis=1) # 每個 row:0→1, 1→0, 2→0, 3→0, 4→1
# (呢度 test 係冇用 na_values 讀,所以只有 Notes 嘅兩個 N/A 係 NaN)
# Q3.1 wdi:
wdi.isna().sum() # 每 column 嘅 missing 數
wdi.isna().sum(axis=1) # 每 row 嘅 missing 數
# Q3.3(homework):有至少一個 missing value 嘅 row 數
wdi[wdi.isna().sum(axis=1) > 0].shape[0]
# 或者:wdi.isna().any(axis=1).sum()
sum(axis=0) = 沿住 rows 向下加 → 每個 column 一個結果;sum(axis=1) = 沿住 columns 橫向加 → 每個 row 一個結果。dropna(axis=0) 刪 rows、axis=1 刪 columns,邏輯一致:axis 係「被消去/被操作嘅方向」。
4.4 Method 4:value_counts(dropna=False)
dropna=False 會將 NaN 都計入,輸出會有一行 index 係 NaN。要直接攞 NaN 嘅數目:s.value_counts(dropna=False)[np.nan]。
import numpy as np
wdi['alcohol_consumption_per_capita'].value_counts(dropna=False)
# NaN 226, 0.69 3, 0.019 2, 11.45 2, ... Length: 205
wdi['alcohol_consumption_per_capita'].value_counts(dropna=False)[np.nan] # np.int64(226)
# Q3.2
wdi['atms_per_100000'].value_counts(dropna=False)[np.nan] # 446 − 417 = 29
5. Handling missing values:Dropping
三大策略:
- Drop 個 column 或 row(如果 missing 太多)
- Fill(如果只得少量 missing):special constant(−999、0)、statistical value(mean、median、mode)、predicted value(model-based)
- Keep 佢哋,用接受 missing values 嘅 methods
5.1 dropna() 同 thresh
df.dropna(thresh, axis=0):drop row(default)或 column(axis=1)。df.dropna(thresh=N):如果某 row/column 嘅 non-null values 少過 N 就 drop。- 即係:你想保留「唔係太多 missing」嘅 row,所以指定 row 要有 N 個 non-null 先保留。N = "the minimum number of non-null values you want to have after dropping"。
wdi 有 19 columns,thresh=18 → 每行最多容許 19 − 18 = 1 個 missing → 「drop a row if there is more than 1 missing value」。
wdi.dropna() # 任何 missing 就 drop row → 只剩 117 rows(drop 咗 329 行)
wdi.dropna(thresh=18) # 最多 1 個 missing → 保留 338 rows
wdi.dropna(axis=1) # 任何 missing 就 drop column → 446 rows × 13 columns
wdi.dropna(axis=1, thresh=300) # column 要有 ≥300 non-null 先保留 → 17 columns
# drop 咗 alcohol_consumption_per_capita(220)同 country_category(289)
驗證 dropna(axis=1):有 missing 嘅 columns 共 6 個(atms、compulsory_education、health_expenditure、population_density、alcohol、country_category),19 − 6 = 13 ✔。
Notebook comment 寫「you can tolerate 447-300=147 missing values」,正確應為 446 − 300 = 146(總共 446 rows)。另外 comment「drop a column if there is more than 300 non-null values」意思應該係「drop a column if there are fewer than 300 non-null values」。
5.2 How much data was dropped
用 df.nunique() 比較 drop 前後嘅 unique 值,或者用 df.shape[0] 計少咗幾多行:
wdi.nunique()['country_name'] # 217 個國家/地區
wdi.dropna().nunique()['country_name'] # 只剩 115 個!year 亦只剩 1 個值
print(f'There are {100*(wdi.shape[0] - wdi.dropna().shape[0])/wdi.shape[0]}% of the rows were dropped')
Notebook 印出「35.20%」,但用上面數字計:。35.2% 對應 157 行(446 × 0.352 ≈ 157),估計係 cell 亂序執行時 wdi 處於另一個狀態。考試以公式為準:。重點係:dropna() 一刀切會失去大量資料(國家數由 217 跌到 115)。
5.3 Q4(homework):dropna 練習
import pandas as pd
from numpy import nan as NA
data = pd.DataFrame([[1.0, 2.0, 3, 4, 5 ],
[2.0, 3.5, 3., 4.2, NA],
[2., 3., 2.7, NA, NA],
[NA, NA, NA, NA, NA],
[2.5, 6.0, 3.0, NA, NA]])
# non-null 數:row0=5, row1=4, row2=3, row3=0, row4=3
data.dropna() # Q4.1 有任何 missing 就 drop → 只剩 row 0
data.dropna(thresh=1) # Q4.2 全部 missing 先 drop(= 至少 1 個值就保留)→ 刪 row 3
data.dropna(thresh=1, axis=1) # Q4.3 全部 missing 嘅 column 先 drop → 冇 column 全空,全部保留
data.dropna(thresh=4) # Q4.4 多過 1 個 missing 就 drop(= 至少 4 個值)→ row 0, row 1
how='all'Q4.2 亦可以寫 data.dropna(how='all')(全部 NaN 先 drop);default 係 how='any'。Slide 用 thresh=1 表達同一意思。
5.4 Summary on Dropping
- Drop 好簡單:要麼失去 rows(observations),要麼失去成個 column(feature)。
thresh有用,避免 drop 太多。- 如果 missing 好少、唔影響分析 → 可以 drop;如果好多 rows/columns 有 missing 或者佢哋有價值資訊 → drop 唔理想。
- 更好嘅策略係 impute(assign a value to something by inference)。
6. Imputing with constant values
6.1 先分開 numerical 同 categorical columns
通常 numerical 同 categorical features 要用唔同 constants 填,所以第一步係分開:Step 1 取得兩類 column labels 嘅 list;Step 2 用 list 分別 select 同 update。
numeric_cols = wdi.select_dtypes(include='number').columns
# 17 個:access_to_electricity_pct, ..., year, is_region
cat_cols = wdi.select_dtypes(exclude='number').columns
# Index(['country_name', 'country_category'])
wdi.select_dtypes(exclude='number') # 只係回傳另一個 DataFrame;要改原 df 要用 column labels
6.2 fillna(value)
wdi[numeric_cols] = wdi[numeric_cols].fillna(-999) # 一定要 assign 返原 columns
wdi.info() # 所有 numeric columns 都 446 non-null;country_category 仍然 289
# Q5:categorical 用 'MISSING' 填
wdi[cat_cols] = wdi[cat_cols].fillna('MISSING') # 只影響 country_category
wdi.info() # 一句確認:全部 446 non-null
# 或者:wdi.isna().sum().sum() → 0
7. Statistical imputing
用 constant 好多時唔理想;可以用反映現有數據整體統計嘅值:
- Numerical columns:
df.mean()或df.median() - Categorical columns:
df.mode() - 語法:
DataFrame.fillna(series of statistic values)——multi-column 嘅mean()回傳「index(column 名)+ mean value」嘅 Series,fillna會按 column 名對號入座。
- 有 strong outliers(或 skewed)→ 用 median(median 對極端值 robust)。
- 冇 outliers → mean 或 median 都冇乜分別;而且用 mean 填唔會改變 data 嘅 mean。
- 判斷 skewness:median < mean → right-skewed(右尾長,有少數大值拉高 mean)。
wdi = pd.read_csv(dir_path + 'world_development_indicators.csv')
wdi = wdi.drop(columns=['Country Name', 'Country Code', 'planet'])
numeric_cols = wdi.select_dtypes(include='number').columns
cat_cols = wdi.select_dtypes(exclude='number').columns
wdi[numeric_cols].mean() # e.g. alcohol 5.969568, gdp_per_capita_usd 13468.14, population 3.686e+08
wdi[numeric_cols].median() # e.g. alcohol 5.767258, gdp_per_capita_usd 5659.05, population 1.617e+07
# alcohol:median (5.77) < mean (5.97) → right-skewed
留意 gdp_per_capita_usd(mean 13468 vs median 5659)同 population(mean 3.7 億 vs median 1600 萬)差距更大——因為 dataset 入面有 World、regions 等「超大」rows,係極端 right-skewed,用 median 會更合理。
7.1 Filling with the mean values
wdi[numeric_cols] = wdi[numeric_cols].fillna(wdi[numeric_cols].mean())
print(f"median is {wdi['alcohol_consumption_per_capita'].median()}") # 5.969568370579796
print(f"mean is {wdi['alcohol_consumption_per_capita'].mean()}") # 5.969568370579797
Slide 結論:「The filled data becomes more symmetric, where its mean = its median」。
alcohol 有 226 / 446 ≈ 50.7% 係 missing,全部用 mean(5.9696)填之後,超過一半嘅值都等於 5.9696,所以排序後中間位置一定落喺呢堆值入面 → median = mean。Mean 本身冇變(填 mean 唔改 mean)。但要知道:咁做會令 distribution 喺 mean 位置出現一條極高嘅柱,std 被人為縮細——missing 比例高時,mean imputation 會扭曲 distribution。
PDF 第 25 頁(填之前)同第 26 頁(填之後)嘅 wdi['alcohol_consumption_per_capita'].hist() histogram 係圖片,文字擷取不到;請對照原 PDF。
7.2 Filling the most frequent value for categorical columns
兩個 function 可以攞 categorical column 嘅 most frequent value:
describe()(categorical column) | mode() | |
|---|---|---|
| 輸出 | count、unique、top(最常見值)、freq(次數);冇 five-number summary | 所有並列最高次數嘅值(可能多過一個) |
| 並列時 | 從並列者中任意(arbitrarily)揀一個 | 全部列出 |
wdi['country_name'].value_counts()
# Belarus 3, Comoros 3, Euro area 3, IDA total 3, Indonesia 3, ..., Zimbabwe 2 Length: 217
wdi[cat_cols].mode()
# country_name country_category
# 0 Belarus DEVELOPING
# 1 Comoros NaN
# ... (共 12 個國家並列 3 次;country_category 只有 1 個 mode,其餘用 NaN 補位)
# 11 United Arab Emirates NaN
wdi[cat_cols].describe()
# country_name country_category
# count 446 289
# unique 217 2
# top Euro area DEVELOPING ← 任意揀咗其中一個
# freq 3 231
7.3 Q6:點樣由 describe() 同 mode() 抽出 most frequent values?
wdi[cat_cols].mode().loc[0] # 第 0 行 → country_name Belarus, country_category DEVELOPING
wdi[cat_cols].describe().loc['top'] # 'top' 行 → (notebook 該次執行) Belarus, DEVELOPING
PDF 第 28 頁 mode and top.png 圖示 mode() 同 describe() 結果嘅位置,文字擷取不到;請對照原 PDF。
同一個 notebook 入面 describe() 嘅 top 一次係 Euro area、一次係 Belarus——正正示範「arbitrarily chosen」。如果需要穩定結果,用 mode().loc[0](mode 輸出係排好序嘅)。
7.4 Impute categorical columns with most frequent values
# step 1:中間變數,裝住「column 名 → most frequent value」
most_frequent_values = wdi[cat_cols].mode().loc[0]
# 或者 most_frequent_values = wdi[cat_cols].describe().loc['top']
# step 2:fillna
wdi[cat_cols] = wdi[cat_cols].fillna(most_frequent_values)
# Arab World, United Arab Emirates 等 country_category → DEVELOPING
wdi[cat_cols].info() # 兩個 columns 都 446 non-null
Notebook 嗰格 wdi[cat_cols].fillna(most_frequent_values) 冇 assign 返 wdi[cat_cols] = ...,所以只係預覽;要真正改 wdi 必須 assign。(之後 info() 顯示 446 non-null,估計係另外執行過 assignment。)另外,將 region(例如 Arab World)填成 DEVELOPING 未必合理——呢個係「missing with a reason」,填 'MISSING' 或另開一類可能更恰當。
8. Imputing using scikit-learn
scikit-learn:comprehensive machine learning library,以一致嘅 Python interface 提供大量 supervised / unsupervised algorithms;統一 APIs(fit / predict / transform)令建立、評估、比較 models 好方便;包含完整 ML workflow 工具:data preprocessing、feature selection、model evaluation、hyperparameter tuning、practice datasets。(安裝:%pip install scikit-learn)
| Imputer | 類型 | 做法 |
|---|---|---|
SimpleImputer | Univariate(basic) | 用 mean、median、most_frequent(mode)或 constant 填;numerical 同 categorical 都用得 |
IterativeImputer | Multivariate(advanced) | 將每個有 missing 嘅 feature 當成其他 features 嘅 function,round-robin 咁逐個建模預測(BayesianRidge、decision trees 等 estimators) |
KNNImputer | Multivariate | 用 k-nearest neighbors 搵相似 samples,按 feature similarity 估計 missing |
8.1 SimpleImputer:三個步驟
- Setup:
from sklearn.impute import SimpleImputer;SimpleImputer(strategy=..., fill_value=...)。strategy='mean'(default),其他:'constant'、'median'、'most_frequent';fill_value= constant 策略用嘅數字 / string。 - Fit and transform:
SimpleImputer.fit_transform(),輸出係 NumPy array。 - Assign back:將 array assign 返原 pandas DataFrame 嘅 columns。
from sklearn.impute import SimpleImputer
wdi = pd.read_csv(dir_path + 'world_development_indicators.csv')
wdi = wdi.drop(columns=['Country Name', 'Country Code', 'planet'])
numeric_cols = wdi.select_dtypes(include='number').columns
cat_cols = wdi.select_dtypes(exclude='number').columns
# (a) Constant -999 填 numeric
simple_imput_num = SimpleImputer(strategy='constant', fill_value=-999)
imputed_num = simple_imput_num.fit_transform(wdi[numeric_cols])
type(imputed_num) # numpy.ndarray,shape=(446, 17)
wdi[numeric_cols] = imputed_num
wdi.info() # numeric 全部 446 non-null;dtypes: float64(17), str(2)
# ← year、is_region 由 int64 變成 float64(array 統一 dtype)
# (b) Mean 填 numeric
simple_imput_mean = SimpleImputer(strategy="mean")
wdi[numeric_cols] = simple_imput_mean.fit_transform(wdi[numeric_cols])
# (c) Most frequent 填 categorical(唔使自己由 describe() 抽 'top')
simple_imput_mostfreq = SimpleImputer(strategy='most_frequent')
wdi[cat_cols] = simple_imput_mostfreq.fit_transform(wdi[cat_cols])
wdi['country_category'].value_counts() # DEVELOPING / DEVELOPED
Notebook 中 most_frequent 嗰格冇執行編號(In [ ]),之後 describe() 仍顯示 country_category count 289、value_counts DEVELOPING 231 / DEVELOPED 58——即係顯示嘅係填之前嘅狀態。真正執行後 count 應為 446,DEVELOPING 會變成 231 + 157 = 388。
- 參數係
strategy(slide 有一處打錯成stratery)。 fit_transform回傳 NumPy array,冇 column 名;要 assign 落wdi[numeric_cols](column 數同次序要對得上)。strategy='mean'/'median'只可以用喺 numeric columns;categorical 要用'most_frequent'或'constant'。
8.2 Imputing with indicator columns(optional)
設 add_indicator=True,SimpleImputer 會額外輸出 indicator columns(1 = 呢格原本係 missing、已被填;0 = 原本有值)。
simple_imput_ind = SimpleImputer(strategy="mean", add_indicator=True)
wdi[numeric_cols].shape # (446, 17)
simple_imput_ind.fit_transform(wdi[numeric_cols]).shape # (446, 22) → 多咗 5 個 indicator columns
點解係 5 個?因為 17 個 numeric columns 入面只有 5 個有 missing(atms_per_100000、compulsory_education_years、health_expenditure_pct_of_gdp、population_density、alcohol_consumption_per_capita);indicator 只會為有 missing 嘅 features 建立。頭 5 行最後一個 indicator 都係 1,對應 alcohol 嘅值都係 5.96956837(即 mean,原本 missing)。
wdi_ind = wdi[['alcohol_consumption_per_capita']].copy() # 雙括號 → 一 column 嘅 DataFrame
simple_imput_ind = SimpleImputer(strategy="mean", add_indicator=True)
wdi_ind[['alcohol_consumption_per_capita', 'missing_mask']] = simple_imput_ind.fit_transform(wdi_ind)
wdi_ind.tail(10)
# alcohol_consumption_per_capita missing_mask
# 441 8.320000 0.0
# 442 5.969568 1.0 ← 原本 missing,填咗 mean
# 443 0.570000 0.0
「係咪 missing」本身可能有資訊(missing with a reason)。保留 missing_mask 可以令之後嘅 model 知道邊啲值係估出嚟,而唔係真實觀察。
9. Imputing with models:IterativeImputer
IterativeImputer 係 advanced multivariate imputer:將每個有 missing 嘅 feature 當成其他 features 嘅 function,round-robin(輪流)咁建模,用 BayesianRidge、decision trees 或其他 estimators。
class sklearn.impute.IterativeImputer(
estimator=None, *, missing_values=nan, sample_posterior=False,
max_iter=10, tol=0.001, n_nearest_features=None, initial_strategy='mean',
imputation_order='ascending', skip_complete=False, min_value=-inf,
max_value=inf, verbose=0, random_state=None, add_indicator=False)
estimator:defaultBayesianRidge()——一個加咗 Bayesian regularization 嘅 linear regression model,根據 features 之間嘅關係估計 missing values。min_value,max_value:通常會設上下限,令估計值唔會超出範圍。可以用原 data 嘅df.min()同df.max()——佢哋回傳兩個 Series(index = column 名、value = bound),可以逐 column 設界。- 其他:
max_iter=10(最多輪數)、tol(收斂條件)、initial_strategy='mean'(第一輪先用 mean 臨時填)。
from sklearn.experimental import enable_iterative_imputer # 必須先 import 呢行(experimental feature)
from sklearn.impute import IterativeImputer
wdi[numeric_cols].min() # e.g. alcohol 0.003, access_to_electricity_pct 9.3
wdi[numeric_cols].max() # e.g. alcohol 15.09, access_to_electricity_pct 100
iter_imput = IterativeImputer(min_value=wdi[numeric_cols].min(),
max_value=wdi[numeric_cols].max())
iter_imput.fit_transform(wdi[numeric_cols]).shape # (446, 17)
wdi[numeric_cols] = iter_imput.fit_transform(wdi[numeric_cols])
wdi[numeric_cols].info() # 全部 446 non-null
wdi['alcohol_consumption_per_capita']
# 0 3.968605 1 6.295583 2 1.498436 ... ← 每行唔同嘅預測值(唔再全部係 5.97)
想像有 5 個 columns 有 missing。第一輪先用 mean 臨時填晒。然後輪流:用其他 columns 做 X、atms 做 y,train 一個 regression,預測 atms 嘅 missing 位置;再到 compulsory_education_years……如此類推,一輪完再嚟一輪,直到結果穩定(tol)或者到 max_iter。好處:每個國家得到「按佢其他特徵估計」嘅值(例如 GDP 高、life expectancy 高嘅國家,ATM 數估計會較高),比全部填同一個 mean 合理得多。
ExamFor categorical columns, we cannot use regression filling. We stick to the most-frequent filling using the SimpleImputer.
BayesianRidge 本質係 linear regression(COM6005 後段 Linear regression 會詳細講);KNNImputer 用嘅 k-nearest neighbors 同 COM6004 Classification 嘅 nearest-neighbor classifier 係同一個概念。COM6004 Chapter 2 嘅 data preprocessing(handling missing values:eliminate data objects / estimate missing values / ignore during analysis)正正對應今堂嘅 drop / impute / keep 三大策略。
10. Q7:Clean the Titanic data(綜合練習)
df_titanic = pd.read_csv(dir_path + 'train_to_be_cleaned.csv')
df_titanic.info()
# 893 entries;Age 715 non-null、Cabin 205 non-null、Embarked 891 non-null,其餘 893
# Q7.1 移除重複乘客(PassengerId 唔同、其他 columns 一樣);keep first、re-index
cols = df_titanic.columns.drop('PassengerId')
df_titanic.duplicated(subset=cols).sum() # 檢查
df_titanic = df_titanic.drop_duplicates(subset=cols, keep='first', ignore_index=True)
# Q7.2 Drop 同預測 Survived 無關嘅 columns(例如 PassengerId、Name、Ticket;Cabin 大部分 missing)
df_titanic = df_titanic.drop(columns=['PassengerId', 'Name', 'Ticket', 'Cabin'])
# Q7.3 有 missing 嘅 columns
df_titanic.isna().sum() # Age、Embarked(Cabin 若未 drop 亦有)
# Q7.4 Age:notebook 預期 177 個 missing;用 median 填
print(df_titanic['Age'].isna().sum()) # 177
df_titanic['Age'] = df_titanic['Age'].fillna(df_titanic['Age'].median())
print(df_titanic['Age'].isna().sum()) # 0
# Q7.5 Embarked:2 個 missing;用最常見嘅登船港填
print(df_titanic['Embarked'].isna().sum()) # 2
most_popular = df_titanic['Embarked'].mode()[0]
df_titanic['Embarked'] = df_titanic['Embarked'].fillna(most_popular)
print(df_titanic['Embarked'].isna().sum()) # 0
年齡通常有少數老年乘客令分佈 right-skewed,而且有極端值;median 唔受極端值影響,所以傾向用 median。如果 describe() 顯示 mean ≈ median 又冇明顯 outliers,用 mean 亦可。答案重點係根據 skewness / outliers 作判斷。Q7.2 嘅「無關 columns」係判斷題——要講出理由(ID、名字、票號屬唯一識別,冇預測規律)。
11. Take away points
- How to confirm one-to-one mapped columns?
- Duplicated columns and rows,by
compare()、duplicated() - Drop the unnecessary data:
drop()、drop_duplicates() - Locate the missing values:
isna()、notna() - Imputing with constant or statistical values:
fillna() - Imputing with iterative models:sklearn
IterativeImputer
自我測驗 Self-test
Q1. 一個 DataFrame 有 10 columns。df.dropna(thresh=7) 會 drop 咩 rows?
保留 嘅 rows,即 。所以會 drop 有 4 個或以上 missing values 嘅 rows。
Q2. 點樣判斷 column city 同 city_code 係咪 one-to-one mapped?寫 code 同判斷準則。
n_comb = df[['city', 'city_code']].value_counts().shape[0]
n_city, n_code = df['city'].nunique(), df['city_code'].nunique()
n_comb == n_city == n_code # True → one-to-one如果組合數大過任何一邊嘅 unique 數,代表有一個值對應多個值,唔係 one-to-one。
Q3. 一個 dataset 有 200 rows,df.duplicated().sum() 係 8,而每組重複都只係「一對」。drop_duplicates() 同 drop_duplicates(keep=False) 之後各剩幾多 rows?
keep='first':200 − 8 = 192。keep=False:每對兩行都刪,200 − 16 = 184。
Q4. 某 column 值為 [2, 3, 3, 4, 100, NaN, NaN]。用 mean 同 median 填,各填咩值?邊個較好?
Non-null:2,3,3,4,100。Mean = 112/5 = 22.4;Median = 3。100 係明顯 outlier,mean 被拉高到 22.4,完全唔代表典型值,所以用 median 較好。另外 median (3) < mean (22.4) → right-skewed。
Q5. 點解 info() 顯示 Price column 冇 missing,但 data 其實有 missing?點處理?
因為 missing 用 placeholder(例如 −999)表示,pandas 只當佢係普通數字。可以用 describe() 發現 min = −999 或 mean 異常;然後 pd.read_csv(..., na_values=[-999]) 重新讀,或者 df['Price'] = df['Price'].replace(-999, np.nan)。
Q6. mode() 同 describe() 喺攞 most frequent value 有咩分別?各自點抽出嚟?
mode() 列出所有並列最高次數嘅值(DataFrame,多行),用 .loc[0] 攞第一個;describe() 喺並列時任意揀一個放喺 top,用 .loc['top'] 攞。
Q7. 寫出用 SimpleImputer 以 median 填 numeric columns 嘅完整 code。輸出係咩 type?
from sklearn.impute import SimpleImputer
num_cols = df.select_dtypes(include='number').columns
imp = SimpleImputer(strategy='median')
df[num_cols] = imp.fit_transform(df[num_cols]) # fit_transform 輸出 numpy.ndarrayQ8. SimpleImputer(strategy='mean', add_indicator=True) 應用喺 6 個 numeric columns(其中 2 個有 missing),輸出 shape 嘅 columns 數係幾多?
6 + 2 = 8 columns。Indicator 只為有 missing 嘅 features 建立(wdi 例子:17 + 5 = 22)。
Q9. IterativeImputer 同 SimpleImputer(mean) 最大分別係咩?點解要設 min_value/max_value?Categorical 可唔可以用?
SimpleImputer 係 univariate,每個 missing 都填同一個統計值;IterativeImputer 係 multivariate,用其他 features 以 regression(default BayesianRidge)round-robin 預測,每行嘅填補值唔同。設 min/max 係避免預測值超出合理範圍(例如百分比 >100 或人口為負)。Categorical 唔可以用 regression filling,要用 SimpleImputer most_frequent。另外要先 from sklearn.experimental import enable_iterative_imputer。
Q10. df.isna().sum(axis=1) 同 df.isna().sum() 分別回傳咩?點計「至少有一個 missing 嘅 rows 數」?
sum(axis=1):每行嘅 missing 數(長度 = rows 數);sum()(axis=0):每 column 嘅 missing 數。至少一個 missing 嘅 rows 數:(df.isna().sum(axis=1) > 0).sum() 或 df[df.isna().sum(axis=1) > 0].shape[0]。
一頁總結 Cheat sheet
- Data cleaning 定義 + Garbage in, Garbage out;Key takeaways:inspect & identify problems → handle problems。
- Constant column:
nunique()==1、value_counts(normalize=True)= 1.0。 - Duplicated columns:
a.compare(b)空 /(a==b).value_counts()全 True。 - One-to-one:
df[[a,b]].value_counts()組合數 == 各自nunique()。 - Duplicated rows:
duplicated(keep='first')(首次 False);drop_duplicates(keep='first'|'last'|False, ignore_index=True);446→434/422。 - Drop columns:
df = df.drop(columns=[...])(記得 assign)。 - Missing:at random(typo、sensor)vs with a reason;NaN ≠ "NaN"。
- 搵 missing:①
info()(total − non-null)②describe()(−999 placeholder)③isna().sum(axis=0/1)④value_counts(dropna=False)[np.nan];read_csv(na_values=[-999,'N/A'])。 dropna()任何 NaN 就 drop row;thresh=N= 至少 N 個 non-null 先保留;axis=1對 columns。- 分 columns:
select_dtypes(include='number')/exclude='number'。 fillna(-999)/fillna('MISSING')/fillna(df[num].mean())/fillna(df[cat].mode().loc[0])。- Outliers / skewed → median;median < mean → right-skewed;填 mean 唔改 mean。
- SimpleImputer:
strategy='mean'|'median'|'most_frequent'|'constant',fill_value;fit_transform→ ndarray → assign 返;add_indicator=True加 missing mask。 - IterativeImputer:
enable_iterative_imputer;BayesianRidge round-robin;min_value=df.min(), max_value=df.max();只限 numeric。 - KNNImputer:k-nearest neighbors 相似樣本估計。