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
datetimemodule 各 classes;parse_dates/pd.to_datetime();.dtaccessor 抽 year/month/day/hour…;Timedelta運算;用日期做 Boolean filtering。 - Categorical → numerical:nominal vs ordinal;binary、one-hot encoding(
pd.get_dummies())、ordinal encoding(sklearnOrdinalEncoder)。 - Numerical → categorical:binning,
pd.cut()(equal-width / custom edges)vspd.qcut()(equal-sized / quantile-based)。
⭐ 考試必溫 Top-5
- ;;;超出即 potential outlier。識由
quantile(0.25)、quantile(0.75)計同數 outliers。 - Capping:
df.loc[df[col] > p99, col] = p99——保留 data points 但限制極端值。 - Nominal(無次序:red/green)→ one-hot(
get_dummies);Ordinal(有次序:High/Mid/Low)→ ordinal encoding(OrdinalEncoder(categories=[...]),default 跟字母排序)。 pd.cut(bins=N)將值域切成等寬區間(每組人數可以好唔平均);pd.qcut(q=M)按 quantiles 切成人數相近嘅組。Salary 例子:cut → 16/2/1/1;qcut → 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')
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 methods | Data 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
,位置 = (由 0 開始數):
- 25%: → 第 4 個 (44) 同第 5 個 (45) 之間 → ✔
- 50%: → 52 同 58 之間 → ✔
- 75%: → 82 同 83 之間 → ✔
; → 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
數值越大,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。
大過 upper bound 或細過 lower bound 嘅值 = potential outliers。Slide 補充:如果冇 data 高過 ,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()
驗算:;;Upper = ✔;Lower = ✔。Lower bound 係負數而 ATM 數冇可能負,所以只有 large outliers。
- 寫成
quantile(25)——參數要喺 [0,1],應為quantile(0.25)。 - 將 1.5 乘錯對象:係 加落 ,唔係 。
- 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
- Plot your data to identify if you have outliers.
- Handle outliers accordingly:keep them、drop them 或 capping them。
- 如果冇 outliers 而且 data 係 normal distribution → 用 mean 同 standard deviation 描述,並報告 data are normally distributed。
- 如果 data skewed 或有 outliers → 用 five number summary(min, Q1, median, Q3, max)描述,並報告 outliers。
- 唔肯定係咪 normal → 可以用 normal quantile plots 或者 Kolmogorov-Smirnov test(超出本科範圍,fun fact)。
COM6005 Statistical Modeling 嘅 descriptive statistics(mean/SD vs median/IQR、skewness、Normal distribution)同呢度直接相關:skewed data 用 robust statistics(median、IQR)。COM6004 Chapter 2(Data)講 data quality 嘅 outliers 同 noise,Chapter 3 Data Visualization 有 boxplot / histogram。
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.date | Idealized naive date,假設 Gregorian calendar 一直適用 | year, month, day |
datetime.time | Idealized time,獨立於任何一日,假設每日剛好 24×60×60 = 86400 秒(冇 leap seconds) | hour, minute, second, microsecond, tzinfo |
datetime.datetime | date + time 嘅組合 | year, month, day, hour, minute, second, microsecond, tzinfo |
datetime.timedelta | Duration:兩個 date/time/datetime 之差,精確到 microsecond | days, seconds, microseconds |
datetime.tzinfo | Time 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,兩個方法:
- 讀檔時 parse:
pd.read_csv(..., parse_dates=['col']) - 先當 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
| 運算 | 結果 | 例子 |
|---|---|---|
| Timestamp − Timestamp | Timedelta | max() - min() → 1460 days 23:00:00 |
| Timedelta ÷ Timedelta | float(單位換算) | / pd.Timedelta(weeks=1) → 208.708 |
| Timedelta ÷ number | Timedelta | / 7 → 208 days 17:00:00 |
.days | int(只取日數部分) | 1460 |
驗算:1460 日 23 小時 = 1460.9583 日;÷ 7 = 208.7083 星期 = 208 日 + 0.7083×24 = 17 小時 ✔。
4.5 Time until / time prior to a specific date
date_timecolumn − 特定日期 → 「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。
| 類型 | 定義 | 例子 | Encoding |
|---|---|---|---|
| Nominal | Values in unranked categories | Red, green, yellow, blue;True, False | Binary(True/False → 1/0)、One-hot encoding |
| Ordinal | Values in ranked categories | High, Mid, Low;Hot, warm, cold, very cold | Ordinal 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。
如果將 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
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 字母次序)
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? | Nominal | One-hot |
| Fat, Medium, Slim? | Ordinal | Ordinal encoding(自訂次序) |
| East, South, West, North | Cyclic | 用 sine 同 cosine transformations 保留循環性質 |
| Geolocations (latitude, longitude) | 座標 | Haversine distance 或轉成 Cartesian coordinates |
| Text data | 文字 | TF-IDF、word embeddings,或 categorical text 用 one-hot |
Cyclic encoding 例子:方向有 個,第 個(East=0, South=1, West=2, North=3):
咁 North()同 East()喺 (sin, cos) 平面上係相鄰,而唔係「相差 3」。同樣適用於 month()、hour()。
COM6004 Chapter 2 嘅 attribute types(nominal / ordinal / interval / ratio)就係決定 encoding 嘅依據;COM6004 Classification 嘅大部分 algorithms(同 COM6005 logistic regression)都要求 numeric input,所以 one-hot / ordinal encoding 係 modelling 前必經步驟。
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 intervals | q=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
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 有個小標題寫「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
因為 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(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();.dtaccessor 抽 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()vsqcut()將 continuous data 分成 intervals
自我測驗 Self-test
Q1. 某 column:,。計 IQR 同 outlier bounds。值 −30、100、120 邊啲係 outliers?
;。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() # 0Q3. 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)()、IQR、hist(bins=N, figsize=...)、boxplot(column=...)。 - ;bounds = 、;數:
(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 令數目略唔等。