All Revision Notes

COM6001 Computing with Big Data · Week 7

Data Cleaning (II): Outliers, Datetime, Encoding & Binning

COM6001 · Week 7 · Source: Lecture07_notebook.pdf(Jupyter notebook export, 42 pages)· Lecturer: Dr. Aileen Hou

Contents · 10 sections

🎯 本講重點 (Learning outcomes)

  • Outliers:定義、成因;用 five-number summary(describe())、percentiles(quantile())、IQR method、histogram、boxplot 偵測;用 drop 或 capping(例如 99th percentile)處理。
  • Date & time:Python datetime module 各 classes;parse_dates / pd.to_datetime();.dt accessor 抽 year/month/day/hour…;Timedelta 運算;用日期做 Boolean filtering。
  • Categorical → numerical:nominal vs ordinal;binary、one-hot encoding(pd.get_dummies())、ordinal encoding(sklearn OrdinalEncoder)。
  • Numerical → categorical:binning,pd.cut()(equal-width / custom edges)vs pd.qcut()(equal-sized / quantile-based)。

⭐ 考試必溫 Top-5

  1. IQR=Q3−Q1IQR = Q_3 - Q_1;Upper bound=Q3+1.5×IQR\text{Upper bound} = Q_3 + 1.5 \times IQR;Lower bound=Q1−1.5×IQR\text{Lower bound} = Q_1 - 1.5 \times IQR;超出即 potential outlier。識由 quantile(0.25)、quantile(0.75) 計同數 outliers。
  2. Capping:df.loc[df[col] > p99, col] = p99——保留 data points 但限制極端值。
  3. Nominal(無次序:red/green)→ one-hot(get_dummies);Ordinal(有次序:High/Mid/Low)→ ordinal encoding(OrdinalEncoder(categories=[...]),default 跟字母排序)。
  4. pd.cut(bins=N) 將值域切成等寬區間(每組人數可以好唔平均);pd.qcut(q=M) 按 quantiles 切成人數相近嘅組。Salary 例子:cut → 16/2/1/1;qcut → 5/5/5/5。
  5. Datetime:pd.to_datetime()、.dt.year/.month/.day/.hour/.quarter/.dayofweek/.day_name();Timestamp − Timestamp = Timedelta;Timedelta / Timedelta = float;Timedelta / number = Timedelta。

0. Data preparation:沿用 Week 6 嘅 imputation

Notebook 開頭將 Week 6 嘅步驟包成一個 function:drop 多餘 columns → numeric 用 IterativeImputer(有 min/max 界限)→ categorical 用 SimpleImputer most_frequent → save 成 pickle。

import pandas as pd
path = "./Datasets/week_7/"

def data_prep_week7(path):
    wdi = pd.read_csv(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
    # numerical columns:model-based imputation
    from sklearn.experimental import enable_iterative_imputer
    from sklearn.impute import IterativeImputer
    iter_imput = IterativeImputer(min_value=wdi[numeric_cols].min(),
                                  max_value=wdi[numeric_cols].max())
    wdi[numeric_cols] = iter_imput.fit_transform(wdi[numeric_cols])
    # categorical columns:most frequent
    from sklearn.impute import SimpleImputer
    simple_imput_cat = SimpleImputer(strategy="most_frequent")
    wdi[cat_cols] = simple_imput_cat.fit_transform(wdi[cat_cols])
    pd.to_pickle(wdi, path + "wdi_imp.pkl")     # save
    return wdi

wdi = data_prep_week7(path)
wdi.info()      # 446 entries,19 columns 全部 446 non-null;float64(17), object(2)
wdi_imp = pd.read_pickle(path + 'wdi_imp.pkl')
💡 點解包成 function?

Data cleaning pipeline 要可以重複執行(reproducible):新數據來、或者 notebook restart 之後,一句 data_prep_week7(path) 就重建同一個 clean dataset,唔使逐格再 run。

Review:Data cleaning 大綱(今堂新增 Transforming features)

  • Removing unnecessary data:Constant-value columns、Repeated columns、One-to-one mapped columns、Duplicated rows
  • Handling the missing data:Dropping、Imputing using constants / statistics / models
  • Handling the outliers(今堂)
  • Transforming features(今堂):Date and time、Feature encoding、Binning、Feature scaling
ℹ️ 範圍

大綱列咗 Feature scaling,但本 notebook 冇對應內容(可能下一講)。

1. Handling the Outliers

ExamOutliers:unusual observations that are distinct from most of the others。通常係 aberrations,未必反映我哋分析緊嘅真實現象,可能導致誤導性嘅分析結果。

Main reasons:

  • Rare cases(真實數據,你可能想保留!)
  • Measurement or data input errors
  • Mix-up of data from different sources/categories(例如 wdi 入面「World」「regions」同國家混埋一齊)

Outlier 冇統一定義,要 case-by-case study。偵測方法(只適用於 numerical data):

Statistical methodsData visualization methods
Min, max, mean, percentiles
Interquartile range (IQR) method
Histogram
Boxplot
🖼️ 圖片內容

PDF 第 3–4 頁 outlier.png 同 five number.png(five-number summary 圖解)係圖片,文字擷取不到;請對照原 PDF。

1.1 用 describe() 偵測

DataFrame.describe() 顯示 count、mean、std、min、25%、50%、75%、max(categorical columns 自動被排除)。判斷原則:

  • Range(max − min)越大,越有可能有 outliers。
  • Max 或 min 距離其他 percentiles 越遠,越有可能。

1.2 Percentiles 同 quartiles

Percentile:data 由細到大排序,X% percentile 係「有 X% 數據 ≤ 呢個值」嘅值。

  • 50% percentile = median(Q2)
  • 25% percentile = first quartile Q1
  • 75% percentile = third quartile Q3

用 Series.quantile(percentile) 攞某 percentile,參數範圍係 [0, 1](例如 0.33,唔係 33)。

from numpy import random
value = random.randint(101, size=20)   # 20 個 0–100 嘅隨機整數
value.sort()
# [ 0 11 13 35 44 45 47 52 52 52 58 60 68 71 82 83 84 85 95 99]
df = pd.DataFrame(value)
df.describe().T
#    count  mean       std   min    25%   50%    75%   max
# 0   20.0  56.8  27.740527  0.0  44.75  55.0  82.25  99.0
⭐ Worked example:pandas 點計 percentile(linear interpolation)

n=20n = 20,位置 = (n−1)×p(n-1) \times p(由 0 開始數):

  • 25%:19×0.25=4.7519 \times 0.25 = 4.75 → 第 4 個 (44) 同第 5 個 (45) 之間 → 44+0.75×(45−44)=44.7544 + 0.75 \times (45-44) = 44.75 ✔
  • 50%:19×0.5=9.519 \times 0.5 = 9.5 → 52 同 58 之間 → 52+0.5×6=55.052 + 0.5 \times 6 = 55.0 ✔
  • 75%:19×0.75=14.2519 \times 0.75 = 14.25 → 82 同 83 之間 → 82+0.25×1=82.2582 + 0.25 \times 1 = 82.25 ✔

IQR=82.25−44.75=37.5IQR = 82.25 - 44.75 = 37.5;1.5×IQR=56.251.5 \times IQR = 56.25 → Upper = 138.5、Lower = −11.5。所有值都喺 0–99 之內 → 冇 outliers。

1.3 wdi_imp 嘅 describe 同「normalized distance」

des_wdi = wdi_imp.describe()
#        access_to_electricity_pct  atms_per_100000  compulsory_education_years
# mean   84.396673                  46.582851        9.650608
# 25%    71.693660                  11.894731        9.000000
# 50%    98.849995                  38.034455       10.000000
# 75%   100.000000                  64.213716       10.875000
# max   100.000000                 272.592801       16.000000

(des_wdi.loc['max'] - des_wdi.loc['75%']) / des_wdi.loc['75%']
# access_to_electricity_pct 0.0, atms_per_100000 3.245, gdp_per_capita_usd 6.58,
# population_density 57.66, population 71.19, year 0.0, is_region inf
normalized distance=max⁡−Q3Q3\text{normalized distance} = \frac{\max - Q_3}{Q_3}

數值越大,max 離「大部分數據」越遠:population(71 倍)、population_density(58 倍)最可疑。is_region 出 inf 係因為 75% = 0(大部分 rows 係國家,is_region=0),除以 0。

1.4 Histogram

  • df.hist():為每個 numerical column 畫 histogram。
  • figsize=(w, h) 控制圖嘅大小;bins=N 用 N 個等寬 bins。
  • 由 histogram 可以快速睇到 outliers(例如大部分值擠喺左邊,右邊有零星柱)。
wdi_imp.hist(figsize=(20, 20));          # 分號:唔顯示返回嘅 Axes object 文字
wdi_imp['population'].hist(bins=50)
🖼️ 圖片內容

PDF 第 7、8、10 頁嘅 histograms(全部 columns、population、wdi_country_only population)係圖片,文字擷取不到;請對照原 PDF。

1.5 Q1:Outlier detection(含答案)

# Q1.1 人口最多嘅「國家」
wdi_imp[['country_name', 'is_region', 'population']].sort_values('population', ascending=False)
# 431 World 1.0 7.591945e+09;214 World;303 IDA & IBRD total;335 Low & middle income ...
# → 最大嘅全部係 regions(is_region = 1),唔係國家!

# Q1.2 睇 is_region == 1 嘅名
wdi_imp.loc[wdi_imp['is_region'] == 1, 'country_name']
# Arab World, Caribbean small states, ..., World, Euro area, IDA total   Length: 95
wdi_country_only = wdi_imp[wdi_imp['is_region'] == 0]     # 只保留國家
wdi_country_only['population'].hist()

# Q1.3 method 1:人口 > 1.2 billion
wdi_country_only[wdi_country_only['population'] > 1.2e9]   # China ×2, India ×2(兩個年份)
# Q1.3 method 2:5 個最大
wdi_country_only.nlargest(5, 'population')[['country_name', 'population']]
# China 1.392730e+09, China 1.386395e+09, India 1.352617e+09, India 1.338659e+09, United States 3.266875e+08
⚠️ 題目字眼

Q1.2 原文寫「drop the rows with is_region == 0」,但 DataFrame 名叫 wdi_country_only、目的係只留國家,所以應該係 drop is_region == 1(regions),保留 is_region == 0。

💡 呢個例子嘅教訓

「World」嘅人口係 outlier,但唔係錯誤數據——係 mix-up of data from different categories(regions 同 countries 混埋)。正確處理唔係 cap,而係將兩類分開。而 China / India 就算分開之後仍然係 outliers,但佢哋係 rare cases(真實數據),應該保留。

2. IQR-based outlier detection

Quartiles 係將 data 分成四等份嘅特殊 percentiles:Q1 = 25th percentile;Q2 = 50th percentile(median);Q3 = 75th percentile。

⭐ 考試重點:IQR 公式
IQR=Q3−Q1IQR = Q_3 - Q_1
Upper bound=Q3+1.5×IQR,Lower bound=Q1−1.5×IQR\text{Upper bound} = Q_3 + 1.5 \times IQR, \qquad \text{Lower bound} = Q_1 - 1.5 \times IQR

大過 upper bound 或細過 lower bound 嘅值 = potential outliers。Slide 補充:如果冇 data 高過 Q3+1.5IQRQ_3 + 1.5 IQR,upper bound 就用 data 嘅 max(lower 同理用 min)——即係 boxplot whisker 嘅畫法:whisker 伸到「界限內最遠嘅真實數據點」。

# Q2(homework):33% percentile
wdi_imp['atms_per_100000'].quantile(0.33)     # np.float64(24.32170064268077)

# Boxplot(根據 IQR 畫,盒外嘅點 = outliers)
wdi_imp.boxplot(column='atms_per_100000', figsize=(3, 5))

Q1 = wdi_imp['atms_per_100000'].quantile(0.25)   # 11.894731154404873
Q3 = wdi_imp['atms_per_100000'].quantile(0.75)   # 64.21371556643965
🖼️ 圖片內容

PDF 第 12 頁 boxplot 同第 13 頁 IQR detection.png 圖解係圖片,文字擷取不到;請對照原 PDF。

2.1 Q3:計 bounds 同數 outliers

IQR = Q3 - Q1                       # 52.3190
upper_bound = Q3 + 1.5 * IQR        # 142.6921921844918
lower_bound = Q1 - 1.5 * IQR        # -66.58374546364729
print(upper_bound); print(lower_bound)

large_outliers = wdi_imp[wdi_imp['atms_per_100000'] > upper_bound]
len(large_outliers)                 # 15
# 全部 outliers(一行):((wdi_imp['atms_per_100000'] > upper_bound) | (wdi_imp['atms_per_100000'] < lower_bound)).sum()

驗算:IQR=64.2137−11.8947=52.3190IQR = 64.2137 - 11.8947 = 52.3190;1.5×52.3190=78.47851.5 \times 52.3190 = 78.4785;Upper = 64.2137+78.4785=142.69264.2137 + 78.4785 = 142.692 ✔;Lower = 11.8947−78.4785=−66.58411.8947 - 78.4785 = -66.584 ✔。Lower bound 係負數而 ATM 數冇可能負,所以只有 large outliers。

⚠️ 常見錯誤
  • 寫成 quantile(25)——參數要喺 [0,1],應為 quantile(0.25)。
  • 將 1.5 乘錯對象:係 1.5×IQR1.5 \times IQR 加落 Q3Q_3,唔係 1.5×Q31.5 \times Q_3。
  • Lower bound 用 Q3、upper bound 用 Q1 調轉咗。

3. Treating the outliers by capping

IQR 方法搵到 15 個超出範圍嘅值(共 217 個國家/地區)——全部當 outliers drop 走就太多。改為睇 99th percentile(99% 嘅值細過佢),揀出真正極端嘅:

p99 = wdi_imp['atms_per_100000'].quantile(0.99)    # 220.007919399819
wdi_imp.sort_values(by='atms_per_100000', ascending=False).head(10)
#      country_name   atms_per_100000
# 103  Korea, Rep.    272.592801
# 320  Korea, Rep.    266.967032
# 30   Canada         228.429181
# 148  North America  228.429181
# 247  Canada         220.007919   ← = p99
# 365  North America  220.007919
# 225  Austria        169.405219 ...
# → 有 4 個值大過 p99

ExamCapping:將極端值限制喺某個 threshold(例如 99th percentile)。好處:減低極端值對分析嘅影響,但唔使將 data points 整個刪走。用 df.loc[row_indices, col_indices] = p99:

wdi_imp.loc[wdi_imp['atms_per_100000'] > p99, 'atms_per_100000'] = p99
wdi_imp.sort_values(by='atms_per_100000', ascending=False).head(10)
# 頭 6 行(Canada, North America, Korea ×2, Canada, North America)全部變成 220.007919
# Austria 169.405219 不變
💡 直覺

Capping(又叫 winsorizing)好似話:「你好高,我承認你係最高嗰班,但我唔俾你高到拉歪成個平均」。Korea 由 272.6 被壓到 220.0,佢仍然排第一梯隊,但對 mean / regression 嘅影響細咗。

3.1 Q4(homework):gdp_per_capita_usd capping

wdi_imp.boxplot(column='gdp_per_capita_usd')
p99 = wdi_imp['gdp_per_capita_usd'].quantile(0.99)       # np.float64(79627.04955097953)
(wdi_imp['gdp_per_capita_usd'] > p99).sum()              # np.int64(5)
wdi_imp.loc[wdi_imp['gdp_per_capita_usd'] > p99, 'gdp_per_capita_usd'] = p99   # capping
(wdi_imp['gdp_per_capita_usd'] > p99).sum()              # np.int64(0)
🖼️ 圖片內容

PDF 第 15 頁 gdp_per_capita_usd 嘅 boxplot 係圖片,文字擷取不到。

3.2 Advices for dealing with the outliers

⭐ 考試重點:處理任何 column 嘅 guideline(slide 原文要點)
  1. Plot your data to identify if you have outliers.
  2. Handle outliers accordingly:keep them、drop them 或 capping them。
  3. 如果冇 outliers 而且 data 係 normal distribution → 用 mean 同 standard deviation 描述,並報告 data are normally distributed。
  4. 如果 data skewed 或有 outliers → 用 five number summary(min, Q1, median, Q3, max)描述,並報告 outliers。
  5. 唔肯定係咪 normal → 可以用 normal quantile plots 或者 Kolmogorov-Smirnov test(超出本科範圍,fun fact)。

4. Feature Transformations:Date and time data

Why?Date/time data 經常有各種 formats,需要 standardize;正確處理先可以做準確嘅時間計算、比較同 feature engineering。

4.1 Python datetime module

提供操作日期時間嘅 classes;支援 date/time arithmetic,但重點係 efficient attribute extraction(for output formatting and manipulation)。

Class說明Attributes
datetime.dateIdealized naive date,假設 Gregorian calendar 一直適用year, month, day
datetime.timeIdealized time,獨立於任何一日,假設每日剛好 24×60×60 = 86400 秒(冇 leap seconds)hour, minute, second, microsecond, tzinfo
datetime.datetimedate + time 嘅組合year, month, day, hour, minute, second, microsecond, tzinfo
datetime.timedeltaDuration:兩個 date/time/datetime 之差,精確到 microseconddays, seconds, microseconds
datetime.tzinfoTime zone 資訊嘅 abstract base class(time zone / daylight saving 調整)—
datetime.timezone實作 tzinfo,表示同 UTC 嘅固定 offset—
import datetime
datetime.datetime.today()          # datetime.datetime(2026, 10, 12, 12, 12, 22, 852513)
type(datetime.datetime.today())    # datetime.datetime
datetime.MAXYEAR                   # 9999
datetime.MINYEAR                   # 1

# datetime − datetime → timedelta
datetime.datetime.today() - datetime.datetime(2026, 1, 1)
# datetime.timedelta(days=284, seconds=43980, microseconds=789669)

# 轉做「日數」:除以 timedelta(days=1)
(datetime.datetime.today() - datetime.datetime(2026, 11, 22)) / datetime.timedelta(days=1)
# -40.48948315456018   ← 負數:仲有約 40.5 日先到 11 月 22 日

4.2 Extracting date and time:轉成 datetime dtype

Dataset 入面嘅日期時間通常被 pandas 當普通 string(object)讀入,要轉成 datetime dtype,兩個方法:

  1. 讀檔時 parse:pd.read_csv(..., parse_dates=['col'])
  2. 先當 string 讀,再用 pd.to_datetime() 轉
# 方法 1:讀入時 parse
air_quality = pd.read_csv(path + 'air_quality.csv', parse_dates=['date_time'])
air_quality.info()
# 95685 entries,14 columns;date_time → datetime64[ns]
# PM2.5, PM10, SO2, NO2, CO, O3, TEMP, PRES, DEWP, RAIN, WSPM → float64;wd, station → object

# 方法 2:讀入後轉
air_quality = pd.read_csv(path + 'air_quality.csv')     # date_time 係 object
air_quality['date_time'] = pd.to_datetime(air_quality['date_time'])   # → datetime64[ns]

air_quality['date_time'].loc[0]      # Timestamp('2013-03-01 00:00:00')  ← 每個 element 係 Timestamp

4.3 .dt accessor

.dt = accessor object for datetime-like properties of the Series values。Series dtype 係 datetime 時,經 .dt 可以逐個 element 用 datetime functions / attributes(同 Week 5 .str 一樣嘅邏輯)。

air_quality['date_time'].dt.month          # 3, 3, 3, ..., 2   dtype: int32
air_quality['year']             = air_quality['date_time'].dt.year
air_quality['month']            = air_quality['date_time'].dt.month
air_quality['day']              = air_quality['date_time'].dt.day
air_quality['hour']             = air_quality['date_time'].dt.hour
air_quality['quarter']          = air_quality['date_time'].dt.quarter       # 1–4
air_quality['day_of_week_num']  = air_quality['date_time'].dt.dayofweek     # Monday=0 … Sunday=6
air_quality['day_of_week_name'] = air_quality['date_time'].dt.day_name()    # 'Friday' 等(係 method,要括號)
air_quality.head(3)    # 3 rows × 21 columns(14 + 7 個新 columns)
⚠️ 易錯

.dt.year、.dt.month、.dt.dayofweek 係 attributes(冇括號);.dt.day_name() 係 method(要括號)。另外如果 column 仲係 object(未轉 datetime),用 .dt 會出 AttributeError: Can only use .dt accessor with datetimelike values。

4.4 Checking the time range 與 Timedelta 運算

air_quality['date_time'].max() - air_quality['date_time'].min()
# Timedelta('1460 days 23:00:00')     ← 2013-03-01 00:00 至 2017-02-28 23:00
date_time_range = air_quality['date_time'].max() - air_quality['date_time'].min()

date_time_range / pd.Timedelta(weeks=1)   # 208.70833333333334  ← Timedelta / Timedelta = float(幾多個星期)
date_time_range / 7                       # Timedelta('208 days 17:00:00')  ← Timedelta / number = Timedelta
date_time_range.days                      # 1460
⭐ 考試重點:Timedelta 運算結果 type
運算結果例子
Timestamp − TimestampTimedeltamax() - min() → 1460 days 23:00:00
Timedelta ÷ Timedeltafloat(單位換算)/ pd.Timedelta(weeks=1) → 208.708
Timedelta ÷ numberTimedelta/ 7 → 208 days 17:00:00
.daysint(只取日數部分)1460

驗算:1460 日 23 小時 = 1460.9583 日;÷ 7 = 208.7083 星期 = 208 日 + 0.7083×24 = 17 小時 ✔。

4.5 Time until / time prior to a specific date

  • date_time column − 特定日期 → 「time until」嗰個日期(或之後);特定日期 − column → 「time prior to」嗰個日期。
  • 結果係 Timedelta(dtype timedelta64[ns])——適合需要「時間跨度」而唔係實際日期嘅 features。
  • 因為 column 每個 element 係 Timestamp,所以要構造 Timestamp 或 datetime object 嚟減。
datetime.datetime.now() - air_quality['date_time'].max()   # Timedelta('3512 days 13:35:58.220781')
datetime.datetime(2022, 1, 1) - air_quality['date_time']   # 0: 3228 days 00:00:00 ... 95684: 1767 days 01:00:00

# pd.Timestamp 接受多種格式,更方便
pd.Timestamp('01-01-2022'); pd.Timestamp('2022/1/1'); pd.Timestamp('2022-01-01')
pd.Timestamp('01-01-2022') - air_quality['date_time']      # 同上結果

air_quality['time_until_2022'] = pd.Timestamp('01-01-2022') - air_quality['date_time']
air_quality['time_until_2022_days'] = air_quality['time_until_2022'] / pd.Timedelta(days=1)
# 3228.000000, 3227.958333, 3227.791667, ...   ← 1 小時 = 1/24 = 0.041667 日

4.6 Data prior to / after a specific date(Boolean filtering)

air_quality['date_time'] < pd.Timestamp('2016-01-01')     # Boolean Series:True ... False
air_quality['prior_2016_ind'] = (air_quality['date_time'] < pd.Timestamp('2016/01/01'))
air_quality[['date_time', 'prior_2016_ind']]
# 0      2013-03-01 00:00:00  True
# 95684  2017-02-28 23:00:00  False

4.7 Q5(homework)

import pandas as pd
air_quality = pd.read_csv(path + 'air_quality.csv')                                   # reload
air_quality['date_time'] = pd.to_datetime(air_quality['date_time'])                   # 轉 datetime
air_quality['year'] = air_quality['date_time'].dt.year                                # 抽 year
air_quality['time_since_2022_weeks'] = (air_quality['date_time'] - pd.Timestamp('2022-01-01')) / pd.Timedelta(weeks=1)
# (數據喺 2022 之前,所以會係負數)
air_quality_after_2016 = air_quality[air_quality['date_time'] >= pd.Timestamp('2016-01-01')]   # in or after 2016
air_quality_after_2016.shape[0], air_quality.shape[0]     # (29340, 95685)

5. Feature transformation:categorical → numerical(Encoding)

好多分析同 machine learning 需要 numeric input,所以要將 non-numeric features 轉成數字。通常用喺 distinct values 有限嘅 categorical features,呢個過程叫 encoding。

⭐ 考試重點:兩種 categorical features 同對應 encoding
類型定義例子Encoding
NominalValues in unranked categoriesRed, green, yellow, blue;True, FalseBinary(True/False → 1/0)、One-hot encoding
OrdinalValues in ranked categoriesHigh, Mid, Low;Hot, warm, cold, very coldOrdinal encoding:{High, Mid, Low} → {0, 1, 2}
  • Binary:For True or False variables,assign 0 or 1。
  • One-hot encoding(for nominal):將有多個值嘅 variable 轉成多個 binary (0/1) variables,每個 category 一個——即係會新增幾個 columns。
  • Ordinal encoding(for ordinal):用一個 integer variable,整數對應有次序嘅 categories。
💡 點解 nominal 唔用 0,1,2?

如果將 red=0、green=1、yellow=2,model 會以為 yellow > green > red,而且 yellow − green = green − red——但顏色根本冇次序。One-hot 令每個顏色係獨立嘅 0/1 column,冇虛假次序。相反,Size S/M/L 本身有次序,用 0/1/2 保留咗呢個資訊,又只用一個 column。

5.1 One-hot encoding:pd.get_dummies()

pandas.get_dummies(data, prefix=None, prefix_sep='_', dummy_na=False, ...)
df = pd.DataFrame([['a', 'green', 1], ['b', 'yellow', 2], ['c', 'red', 3]],
                  columns=['class', 'color', 'quantity'])
pd.get_dummies(df)   # default:所有 non-numeric columns 都 encode;numeric(quantity)保持
#    quantity  class_a  class_b  class_c  color_green  color_red  color_yellow
# 0         1     True    False    False         True      False         False
# 1         2    False     True    False        False      False          True
# 2         3    False    False     True        False       True         False

pd.get_dummies(df, prefix=['col1', 'col2'])   # 用 prefix 取代原 column 名
# quantity, col1_a, col1_b, col1_c, col2_green, col2_red, col2_yellow

新 column 名 = 原名 + prefix_sep('_') + category;新 pandas 版本輸出 bool(True/False),可以 dtype=int 改成 0/1。

🖼️ 圖片內容

PDF 第 26 頁 one hot.png 圖解 one-hot encoding,文字擷取不到;上面 code 輸出已示範同一概念。

5.2 Ordinal encoding:sklearn OrdinalEncoder

將 categorical features encode 成 integer array。兩個關鍵步驟:

  • OrdinalEncoder.fit(sample_data):學習所有值、決定次序(sample data 必須包含每個 column 所有 unique values)。
  • OrdinalEncoder.transform(data):將 encoding scheme 套用落 data。

Steps:① import 同 construct encoder → ② fit(sample_data)(default 按字母次序編號)→ ③(optional)睇 categories_ attribute → ④ transform(full_data),assign 返 DataFrame。

import pandas as pd
from sklearn.preprocessing import OrdinalEncoder

df = pd.DataFrame([['a','green'], ['b','yellow'], ['c','red'],
                   ['a','red'],   ['b','green'],  ['a','yellow']])
df.columns = ['class', 'color']

# sample data 要涵蓋所有 unique values
sample_data = pd.DataFrame({'class': df['class'].unique(), 'color': df['color'].unique()})  # method 1:programmatic
sample_data = df.head(3)                                                                    # method 2:自己檢查過頭 3 行已齊

enc = OrdinalEncoder()
enc.fit(sample_data.head(3))
enc.categories_
# [array(['a', 'b', 'c']), array(['green', 'red', 'yellow'])]   ← 字母次序
enc.transform(df)
# [[0., 0.],    a, green
#  [1., 2.],    b, yellow
#  [2., 1.],    c, red
#  [0., 1.],    a, red
#  [1., 0.],    b, green
#  [0., 2.]]    a, yellow
⚠️ Notebook 中 df.copy() 嘅用法

Notebook 寫 df_enc1 = df.copy() 然後 df_enc1 = enc.transform(df)——第二句將 df_enc1 重新指向一個 NumPy array,前面嘅 copy 其實冇用到。原 df 冇變係因為 transform() 本身唔會改 input。如果想得到 encode 咗嘅 DataFrame,應該寫 df_enc1 = df.copy(); df_enc1[['class','color']] = enc.transform(df)。

5.3 自訂次序:categories=

my_cat = [['b', 'a', 'c'], ['red', 'green', 'yellow']]   # 每個 column 一個 list,次序 = 編號
enc1 = OrdinalEncoder(categories=my_cat)
enc1.fit(sample_data.head(1))   # 提供咗 categories,sample data 內容唔重要(但仍要 call fit)
enc1.categories_                # [array(['b','a','c']), array(['red','green','yellow'])]
enc1.transform(df)
# b→0, a→1, c→2;red→0, green→1, yellow→2
# [[1., 1.], [0., 2.], [2., 0.], [1., 0.], [0., 1.], [1., 2.]]

enc2 = OrdinalEncoder()
enc2.fit_transform(df)          # fit + transform 一步完成(default 字母次序)
⭐ 考試重點:真正 ordinal 數據一定要自訂 categories

Default 字母次序對 S/M/L 會變成 L=0、M=1、S=2——次序錯晒。所以 ordinal features 必須用 OrdinalEncoder(categories=[['S','M','L']]) 明確指定次序。categories 係 list of lists(每個 column 一個 list),就算只 encode 一個 column 都要兩層括號。

5.4 Q6:one-hot + ordinal 綜合

df = pd.DataFrame([['a','S'], ['b','L'], ['c','M'], ['a','L'], ['b','S'], ['a','M']])
df.columns = ['class', 'size']

# Part 1:class 用 one-hot,concat 返原 df,drop 原 'class'
df = pd.concat([df, pd.get_dummies(df['class'], prefix='class')], axis=1).drop(columns=['class'])
#   size  class_a  class_b  class_c
# 0    S     True    False    False
# 1    L    False     True    False ...

# Part 2:size 用 ordinal,S=0, M=1, L=2
enc = OrdinalEncoder(categories=[['S', 'M', 'L']])
enc.fit(df[['size']])                  # 注意雙括號:encoder 要 2-D input
enc.categories_                        # [array(['S', 'M', 'L'])]
df['size'] = enc.transform(df[['size']])
#   size  class_a  class_b  class_c
# 0  0.0     True    False    False
# 1  2.0    False     True    False
# 2  1.0    False    False     True
# 3  2.0     True    False    False
# 4  0.0    False     True    False
# 5  1.0     True    False    False

5.5 Summary on encoding

Data類型建議 encoding
Apple, banana, pear?NominalOne-hot
Fat, Medium, Slim?OrdinalOrdinal encoding(自訂次序)
East, South, West, NorthCyclic用 sine 同 cosine transformations 保留循環性質
Geolocations (latitude, longitude)座標Haversine distance 或轉成 Cartesian coordinates
Text data文字TF-IDF、word embeddings,或 categorical text 用 one-hot

Cyclic encoding 例子:方向有 K=4K=4 個,第 kk 個(East=0, South=1, West=2, North=3):

xsin⁡=sin⁡ ⁣(2πkK),xcos⁡=cos⁡ ⁣(2πkK)x_{\sin} = \sin\!\left(\frac{2\pi k}{K}\right), \quad x_{\cos} = \cos\!\left(\frac{2\pi k}{K}\right)

咁 North(k=3k=3)同 East(k=0k=0)喺 (sin, cos) 平面上係相鄰,而唔係「相差 3」。同樣適用於 month(K=12K=12)、hour(K=24K=24)。

6. Feature transformation:numerical → categorical(Binning)

Binning:將一個 numerical column 分成多組,每組俾一個 label,變成 categorical data。例如 Height (cm) → tall, medium, short。

⭐ 考試重點:cut() vs qcut()
pd.cut()pd.qcut()
基礎Value-based:你提供想要嘅值範圍Quantile-based:你提供想要嘅組大小
整數參數bins=N:N 個 equal-width intervalsq=M:M 個 equal-sized 組(每組數據量相近)
自訂bins=[edge0, edge1, ...]q=[0, .25, .5, .75, 1.](quantiles)
每組數目可以極唔平均(skewed data)大致相同(ties 會令佢略為唔同)

6.1 Salary 例子:cut vs qcut

# 偏斜嘅真實薪金:大部分人薪金低,少數極高
salary = pd.Series([32000, 35000, 37000, 39000, 41000, 43000, 45000, 48000, 50000, 52000,
                    55000, 58000, 62000, 75000, 85000, 100000, 200000, 220000, 350000, 500000],
                   name='annual_salary')
salary.hist(bins=20)

salary_cut  = pd.cut(salary, bins=4, labels=["Low", "Medium-Low", "Medium-High", "High"])
salary_qcut = pd.qcut(salary, q=4, labels=["1st quartile", "2nd quartile", "3rd quartile", "4th quartile"])

print(salary_cut.value_counts().sort_index())
# Low 16, Medium-Low 2, Medium-High 1, High 1
print(salary_qcut.value_counts().sort_index())
# 1st quartile 5, 2nd quartile 5, 3rd quartile 5, 4th quartile 5
⭐ Worked calculation:cut 嘅 edges

Range = 500000 − 32000 = 468000;每個 bin 寬 = 468000 / 4 = 117000。Edges:32000(實際向左延伸 0.1% of range 以包含 min), 149000, 266000, 383000, 500000。

  • Low (≤149000):32000 … 100000 → 16 人
  • Medium-Low (149000, 266000]:200000, 220000 → 2
  • Medium-High (266000, 383000]:350000 → 1
  • High (383000, 500000]:500000 → 1

qcut 就按人數切:20 人 ÷ 4 = 每組 5 人。結論:skewed data 用 cut 會令大部分人擠喺同一組;想要每組人數平均就用 qcut。

🖼️ 圖片內容

PDF 第 32 頁 salary histogram 係圖片,文字擷取不到。

⚠️ Notebook 標題小錯

Notebook 有個小標題寫「cut: divides data into equal-sized groups」,但下面內文同結果都清楚顯示:cut 係 equal-width(等寬值域),qcut 先係 equal-sized(等人數)。考試以後者為準。

6.2 pandas.cut() 參數

pandas.cut(x, bins, right=True, labels=None, retbins=False, precision=3,
           include_lowest=False, duplicates='raise', ordered=True)
  • x:要 bin 嘅 array-like,必須 1-dimensional。
  • bins:
    • int:x 範圍內 equal-width bins 數目;x 嘅 range 會每邊延伸 0.1% 以包含 min 同 max。
    • sequence of scalars:bin edges,可以唔等寬;唔會延伸範圍。
    • IntervalIndex:指定確切 bins(不可重疊)。
  • labels:bins 嘅 labels;False 就只回傳 integer bin 編號。
  • right=True:區間係 (a, b]——左開右閉。
  • Returns:out(Categorical / Series,長度同 x 一樣);bins(只有 retbins=True 先回傳,即分界值)。
pm2p5_labels = pd.cut(air_quality['PM2.5'], bins=10)
pm2p5_labels, pm2p5_bins = pd.cut(air_quality['PM2.5'], bins=10, retbins=True)   # 要兩個變數接收
pm2p5_labels.sample(100)     # (1.181, 83.9], (83.9, 165.8], ...   dtype: category
pm2p5_bins
# [1.181, 83.9, 165.8, 247.7, 329.6, 411.5, 493.4, 575.3, 657.2, 739.1, 821.]   ← 每格相差 81.9

pd.cut(air_quality['PM2.5'], bins=10).value_counts(dropna=False)
# (1.181, 83.9] 59923;(83.9, 165.8] 22741;(165.8, 247.7] 8132;... (739.1, 821.0] 6   ← 極唔平均

驗算:PM2.5 min = 2、max = 821,寬度 = (821 − 2)/10 = 81.9;第一個 edge = 2 − 0.1%×819 = 2 − 0.819 = 1.181 ✔。

6.3 cut() with customized (unequal) edges

有時想用有實際意義、唔等寬嘅範圍,例如美國官方 PM2.5 分 6 級。提供所有 edges:pd.cut(series, bins=[edge0, edge1, ..., edge_max])。如果 edges 冇覆蓋晒所有數據,範圍外嘅值會變成 NaN category。

pd.cut(air_quality['PM2.5'], bins=[0, 12, 35.4, 55.4, 150.4, 250.4, 500.4])   # 6 個不等寬區間
pd.cut(air_quality['PM2.5'], bins=[0, 12, 35.4, 55.4, 150.4, 250.4, 500.4]).value_counts(dropna=False)
# (55.4, 150.4] 34257;(12.0, 35.4] 19853;(0.0, 12.0] 13576;(35.4, 55.4] 12457;
# (150.4, 250.4] 10821;(250.4, 500.4] 4474;NaN 247   ← 247 個值 > 500.4
pd.cut(air_quality['PM2.5'], bins=[0, 12, 35.4, 55.4, 150.4, 250.4, 500.4])[86709]   # nan(最大值嗰行)

# Q7:換 edges 避免 NaN —— 最後一個 edge 用 data 嘅 max
pd.cut(air_quality['PM2.5'],
       bins=[0, 12, 35.4, 55.4, 150.4, 250.4, air_quality['PM2.5'].max()]).value_counts(dropna=False)
# ... (250.4, 821.0] 4721   ← 4474 + 247 = 4721 ✔,冇 NaN
🖼️ 圖片內容

PDF 第 37 頁 PM2.5_category.png(官方 PM2.5 等級表)係圖片;上面 edges 同 labels 係由 code 得知。

6.4 cut() with customized bin labels

Default labels 只係數字區間(例如 (0.0, 12.0])。用 labels=[...] 自訂,令 column 變成有意義嘅 categorical column。Labels 數目 = edges 數目 − 1(slide:「6 commas in bins → 6 labels」,7 個 edges 有 6 個逗號)。

air_quality['PM2.5_cat'] = pd.cut(air_quality['PM2.5'],
    bins=[0, 12, 35.4, 55.4, 150.4, 250.4, air_quality['PM2.5'].max()],
    labels=['Good', 'Moderate', 'Unhealthy for sensitive groups',
            'Unhealthy', 'Very unhealthy', 'Hazardous'])
air_quality[['PM2.5_cat', 'PM2.5']]     # 9.0 → Good;18.0 → Moderate
air_quality['PM2.5_cat'].value_counts(sort=False)    # 按 category 次序,唔按數目排
# Good 13576, Moderate 19853, Unhealthy for sensitive groups 12457,
# Unhealthy 34257, Very unhealthy 10821, Hazardous 4721
⚠️ 易錯:右閉區間同 0

因為 right=True,區間係 (0, 12]——如果 data 有剛好等於 0 嘅值,會落唔到任何 bin 變 NaN。可以設 include_lowest=True 令第一個區間包括左端。

6.5 qcut() for equal-size cutting

qcut(x, q, labels=None, retbins=False, precision=3, duplicates='raise')
  • q:int = quantiles 數目(10 = deciles、4 = quartiles);或者 quantiles array,例如 [0, .25, .5, .75, 1.]。
  • Returns:Categorical / Series(labels=False 時係整數 array);retbins=True 時另外回傳 bins。
  • Note:qcut 之後每組數目仍可能唔相等,因為有重複值(ties),qcut 要遷就。
pd.qcut(air_quality['PM2.5'], q=4)
# Categories: [(1.999, 23.0] < (23.0, 59.0] < (59.0, 116.0] < (116.0, 821.0]]
pd.qcut(air_quality['PM2.5'], q=4).value_counts()
# (1.999, 23.0] 24708;(59.0, 116.0] 23908;(116.0, 821.0] 23645;(23.0, 59.0] 23424   ← 接近但唔完全相等

cut_data, my_bins = pd.qcut(air_quality['PM2.5'], q=4, retbins=True, labels=False)
my_bins                              # array([  2.,  23.,  59., 116., 821.])
air_quality['PM2.5'].describe()      # min 2, 25% 23, 50% 59, 75% 116, max 821   ← 同 qcut edges 一模一樣
💡 qcut 同 five-number summary 嘅關係

qcut(q=4) 嘅 edges 就係 min, Q1, median, Q3, max——即 describe() 嘅 five-number summary。所以 qcut(q=4) 就係「按 quartiles 分組」。因為好多讀數係整數(例如好多個 23.0),落喺邊界嘅重複值只能全部放喺同一組,所以 24708 ≠ 23424。

6.6 Q8:TEMP 分 5 個 equal-sized intervals

air_quality['TEMP'].describe()     # min -16.8, 25% 3.5, 50% 14.6, 75% 23.4, max 41.6
air_quality['TEMP_category'] = pd.qcut(air_quality['TEMP'], q=5,
                                       labels=['very cold', 'cold', 'moderate', 'warm', 'hot'])
# 最冷 20% → very cold … 最熱 20% → hot(labels 由低到高對應)
air_quality.head()                 # 5 rows × 24 columns
air_quality.to_pickle(path + 'air_quality.pkl')

(題目一處寫「coldest」、一處寫「very cold」,以 labels list 為準。)

7. Summary of the lesson

  • Outliers:Five-number summary;Upper/lower bound by IQR;Get the number of outliers;Drop or cap the value of outliers
  • Datetime:pd.to_datetime();.dt accessor 抽 year、month、day 等;Time until / time after(Timedelta data)
  • Categorical → numerical:One-hot encoding for nominal data(syntax);Ordinal encoder for ordinal data(syntax)
  • Binning:cut() vs qcut() 將 continuous data 分成 intervals

自我測驗 Self-test

Q1. 某 column:Q1=20Q_1 = 20,Q3=50Q_3 = 50。計 IQR 同 outlier bounds。值 −30、100、120 邊啲係 outliers?

IQR=50−20=30IQR = 50 - 20 = 30;1.5×IQR=451.5 \times IQR = 45。Upper = 50 + 45 = 95;Lower = 20 − 45 = −25。−30 < −25 → outlier;100 > 95 → outlier;120 → outlier。三個都係。

Q2. 寫 code:將 df['income'] 高過 95th percentile 嘅值 cap 喺 95th percentile,並確認之後冇值高過佢。
p95 = df['income'].quantile(0.95)
df.loc[df['income'] > p95, 'income'] = p95
(df['income'] > p95).sum()    # 0
Q3. Capping 同 dropping outliers 有咩分別?幾時應該 keep?

Dropping 會刪走成行(失去其他 columns 嘅資料);capping 保留該 data point,只係將極端值壓到 threshold,減低對分析嘅影響。如果 outlier 係 rare case 但真實(例如 China 人口),應該 keep;如果係輸入錯誤,可以 drop 或當 missing 再 impute;如果係不同類別混埋(regions vs countries),應該分開處理。

Q4. 一個 date_time column 係 object dtype。寫 code 轉 datetime,加一個 column 表示星期幾(名稱),同計每行距離 2020-01-01 幾多日(float)。
df['date_time'] = pd.to_datetime(df['date_time'])
df['weekday'] = df['date_time'].dt.day_name()
df['days_since_2020'] = (df['date_time'] - pd.Timestamp('2020-01-01')) / pd.Timedelta(days=1)
Q5. td = pd.Timedelta('10 days 12:00:00')。td / pd.Timedelta(days=1)、td / 2、td.days 各係咩?

td / pd.Timedelta(days=1) = 10.5(float);td / 2 = Timedelta('5 days 06:00:00');td.days = 10(int,只取日數部分)。

Q6. 以下 features 應該用咩 encoding?(a) Blood type A/B/AB/O (b) Education: Primary/Secondary/Bachelor/Master (c) Month 1–12 (d) Is_member True/False

(a) Nominal → one-hot;(b) Ordinal → ordinal encoding(自訂 categories 次序);(c) Cyclic → sine/cosine(12 月同 1 月相鄰);(d) Binary → 0/1。

Q7. OrdinalEncoder()(無參數)對 column ['Low','High','Mid'] fit 之後,categories_ 係咩?有咩問題?點修正?

Default 字母排序 → ['High', 'Low', 'Mid'],即 High=0、Low=1、Mid=2——次序同實際大小唔符。修正:OrdinalEncoder(categories=[['Low', 'Mid', 'High']]) → Low=0、Mid=1、High=2。

Q8. 數據 [1, 2, 3, 4, 5, 6, 7, 100]。pd.cut(x, bins=3) 同 pd.qcut(x, q=4) 各組數目大概點?

cut:range 1–100,寬 33 → (0.901, 34], (34, 67], (67, 100] → 7, 0, 1。qcut q=4:每組約 2 個 → 2, 2, 2, 2([1,2], [3,4], [5,6], [7,100])。示範 skewed data 下 cut 會極唔平均,qcut 按人數平均分。

Q9. pd.cut(s, bins=[0, 10, 20]),s 有值 0、5、20、25。結果係咩?

區間係 (0, 10] 同 (10, 20](右閉)。0 → NaN(唔包左端);5 → (0, 10];20 → (10, 20];25 → NaN(超出範圍)。可以用 include_lowest=True 包括 0,最後 edge 改成 s.max() 包括 25。

Q10. (des.loc['max'] - des.loc['75%']) / des.loc['75%'] 量度咩?點解 is_region 會出 inf?

量度 max 比 Q3 遠幾多倍(normalized distance),數值越大越可能有極端大值。is_region 嘅 75% percentile 係 0(大部分係國家),除以 0 → inf;呢個 column 係 binary,根本唔適合用呢個方法判斷 outliers。

一頁總結 Cheat sheet

⭐ 考前速記
  • Outlier:distinct from most others;成因:rare cases(keep)、measurement/input errors、mix-up of sources/categories。
  • 偵測:describe()(range、max vs 75%)、quantile(p)(p∈[0,1]p \in [0,1])、IQR、hist(bins=N, figsize=...)、boxplot(column=...)。
  • IQR=Q3−Q1IQR = Q_3 - Q_1;bounds = Q3+1.5⋅IQRQ_3 + 1.5 \cdot IQR、Q1−1.5⋅IQRQ_1 - 1.5 \cdot IQR;數:(s > upper).sum()。
  • Capping:df.loc[df[c] > p99, c] = p99(p99 = quantile(0.99))。
  • Advice:plot → keep/drop/cap → normal 用 mean+SD;skewed/outliers 用 five-number summary。
  • datetime classes:date、time、datetime、timedelta、tzinfo、timezone;MAXYEAR 9999、MINYEAR 1。
  • 轉 datetime:read_csv(parse_dates=['col']) 或 pd.to_datetime(s);element = Timestamp。
  • .dt.year/.month/.day/.hour/.quarter/.dayofweek(Mon=0)、.dt.day_name()。
  • Timestamp − Timestamp = Timedelta;÷ Timedelta = float;÷ number = Timedelta;.days。
  • 日期 filtering:df[df['date_time'] >= pd.Timestamp('2016-01-01')]。
  • Nominal(無序)→ one-hot pd.get_dummies(df, prefix=...);Ordinal(有序)→ OrdinalEncoder(categories=[[...]]);Binary → 0/1。
  • OrdinalEncoder:fit(sample 要齊所有值)→ categories_ → transform;或 fit_transform;default 字母次序;輸出 NumPy array。
  • Cyclic → sin/cos;geolocation → Haversine / Cartesian;text → TF-IDF / embeddings。
  • pd.cut(bins=N) equal-width(range 延伸 0.1%);bins=[edges] 自訂,範圍外 → NaN;labels 數 = edges − 1;retbins=True 回傳 edges;區間 (a, b]。
  • pd.qcut(q=M) equal-sized(quantile-based);q=4 edges = five-number summary;ties 令數目略唔等。