All Revision Notes

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

  1. dropna(thresh=N):保留至少有 N 個 non-null 值嘅 row(或 axis=1 時嘅 column)——N 係「要保留嘅最少 non-null 數」,唔係「容許嘅 missing 數」。
  2. One-to-one mapping 判斷:兩 columns 嘅組合數(df[[a,b]].value_counts() 長度)== 各自嘅 nunique()。
  3. duplicated(keep='first') 第一次出現標 False、其餘 True;drop_duplicates(keep='first'|'last'|False, ignore_index=True);446 → 434(keep first)/ 422(keep=False)。
  4. Mean vs median:有強烈 outliers / skewed 用 median;用 mean 填唔會改變 mean。Categorical 用 mode(most frequent)。
  5. 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)
⭐ Key Takeaways for Data Cleaning(slide 原文)
  • 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 valuesvalue_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 planet may have only one value
  • problem 2:column country_name may be identical to column Country Name(slide 寫 "country_code",但內容上指 Country Name)
  • problem 3:column Country Code may be one-to-one mapped to column country_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 輸出

原 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 個組合
⭐ 考試重點:one-to-one 判斷規則

有 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()

#missing rows of a column=total rows−non-null rows\text{\#missing rows of a column} = \text{total rows} - \text{non-null rows}

例: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:(25.5−999+15.75+30−999)/5=−1926.75/5=−385.35(25.5 - 999 + 15.75 + 30 - 999)/5 = -1926.75/5 = -385.35 ✔。

💡 直覺

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()
⭐ 考試重點:axis 方向

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"。
⭐ 考試重點:thresh 換算
keep row  ⟺  #non-null≥N  ⟺  #missing≤(total columns−N)\text{keep row} \iff \#\text{non-null} \ge N \iff \#\text{missing} \le (\text{total columns} - N)

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 小錯

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%」,但用上面數字計:(446−117)/446≈73.8%(446-117)/446 \approx 73.8\%。35.2% 對應 157 行(446 × 0.352 ≈ 157),估計係 cell 亂序執行時 wdi 處於另一個狀態。考試以公式為準:dropped %=(rows before−rows after)/rows before×100\text{dropped \%} = (\text{rows before} - \text{rows after}) / \text{rows before} \times 100。重點係: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 名對號入座。
⭐ 考試重點:Mean or median?
  • 有 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」。

💡 點解 median 會變成等於 mean?

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 細節

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)

⭐ 考試重點:三種 Scikit-Learn Imputers
Imputer類型做法
SimpleImputerUnivariate(basic)用 mean、median、most_frequent(mode)或 constant 填;numerical 同 categorical 都用得
IterativeImputerMultivariate(advanced)將每個有 missing 嘅 feature 當成其他 features 嘅 function,round-robin 咁逐個建模預測(BayesianRidge、decision trees 等 estimators)
KNNImputerMultivariate用 k-nearest neighbors 搵相似 samples,按 feature similarity 估計 missing

8.1 SimpleImputer:三個步驟

  1. Setup:from sklearn.impute import SimpleImputer;SimpleImputer(strategy=..., fill_value=...)。strategy='mean'(default),其他:'constant'、'median'、'most_frequent';fill_value = constant 策略用嘅數字 / string。
  2. Fit and transform:SimpleImputer.fit_transform(),輸出係 NumPy array。
  3. 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
💡 點解要 indicator?

「係咪 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:default BayesianRidge()——一個加咗 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)
💡 Round-robin 直覺

想像有 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.

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
💡 Age 用 mean 定 median?(Q7.4 要你講理由)

年齡通常有少數老年乘客令分佈 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?

保留 non-null≥7\text{non-null} \ge 7 嘅 rows,即 missing≤10−7=3\text{missing} \le 10 - 7 = 3。所以會 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.ndarray
Q8. 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 相似樣本估計。