All Revision Notes

COM6001 Computing with Big Data · Week 9

Feature Transformation (3): map / apply & EDA (1): Aggregating Statistics, Sampling, Correlation

COM6001 · Week 9 · Source: Lecture09_EDA_1.pdf / Lecture09_EDA_1.ipynb(Jupyter notebook export, 35 pages)· Lecturer: Dr. Aileen Hou

Contents · 12 sections

🎯 本講重點 (Learning outcomes)

  • 用 Series.map() 逐個 element 轉換數值:傳入 dictionary、function name、lambda 或 custom function。
  • 用 df.apply(func, axis=…) 逐個 column(axis=0)或逐個 row(axis=1)套用 function,例如用多個 columns 生成新 feature;知道 apply 慢,係 last resort。
  • 知道 Exploratory Data Analysis (EDA) 係乜(What / Why / How)同主要類型。
  • Aggregating statistics:mean()、median()、std()、quantile()、sum()、nunique()、mode()、count()、describe();用 include= / exclude=、numeric_only=True、select_dtypes() 揀 dtype。
  • 用 df.agg() 一次過做多個 aggregation,或者對唔同 columns 用唔同 functions(dictionary mapping)。
  • Sampling:df.sample() 嘅用途同參數(n、frac、replace、random_state)。
  • Correlation:df.corr()(Pearson / Kendall / Spearman)、點樣解讀 correlation matrix、用 unstack() 搵最強 correlations。
  • Semi-structured data(XML / JSON)概念同 XML vs JSON vs CSV 幾時用(programming 部分唔考)。

⭐ 考試必溫 Top-5

  1. map vs apply vs agg:map 逐個 element(Series);apply 逐個 row / column 套一個 function;agg 一次過套多個 aggregation functions,而且可以每個 column 用唔同 function。
  2. axis=0 = 逐個 column 計(default,結果每個 column 一個值);axis=1 = 逐個 row 計(結果每個 row 一個值)。
  3. Built-in function 名要加引號('mean'),自己 define 嘅 function 唔加引號(my_func)。
  4. 揀 dtype:describe(include=/exclude=);sum/mean/mode(numeric_only=True);非數值 columns 先 select_dtypes(exclude='number') 再 aggregate。
  5. Correlation coefficient 喺 −1 至 +1:越近 ±1 越強,越近 0 越弱;Pearson 量 linear,Spearman / Kendall 用 ranks(量 monotonic,對 outliers 較 robust);識讀 correlation matrix(diagonal = 1、symmetric)。
📄 講義範圍

開頭 outline 重溫 Feature Transformation(one-hot / ordinal encoding、cleaning texts、scaling),今講接住講 outline 最後兩項 Mapping new values 同 Applying functions,然後開始 EDA。Section 3 Semi-Structured Data 標明「programming not required by exam」,但最尾 Summary 仍然列咗識認 XML / JSON、同 CSV 嘅概念分別、幾時用。

1. Review: Feature Transformation

Outline 重溫之前學過嘅 feature transformation 工具:

  • Categorical → numerical:nominal data 用 one-hot encoding(pd.get_dummies());ordinal data 用 ordinal encoder(OrdinalEncoder)。詳見 Week 7。
  • Cleaning texts(.str accessor、regex)同 Scaling(Standard / MinMax / Robust)。詳見 Week 8。
  • 今講新內容:Mapping new values(map)同 Applying functions(apply)。

2. Mapping new values:Series.map()

map 同 apply 令我哋可以對 DataFrame 嘅每個 element,或者每個 row / column,有效率咁做 custom transformation。

⭐ map() 定義
  • Substitute each value in a Series / DataFrame with another value or a function’s output,即係逐個 element 換值。
  • Series 同 DataFrame 都有 map(DataFrame.map 要 pandas 2.1.0+);課堂用 Series.map()。
  • Mapping dictionary 或 function 經參數 arg= 傳入。
s.map(
    arg: 'Callable | Mapping | Series',
    na_action: "Literal['ignore'] | None" = None,
) -> 'Series'

arg 可以係 Callable(function)、Mapping(dictionary)或 Series。na_action='ignore' 會令 NaN 直接保留做 NaN,唔會傳入 function。

Dataset:air_quality.pkl(95,685 rows × 24 columns 嘅空氣質素 data,有 PM2.5、PM10、TEMP、RAIN、wd(wind direction)、WSPM(wind speed)、station,仲有上堂整好嘅 PM2.5_cat、TEMP_category、day_of_week_name 等)。

import pandas as pd
dir_path = "./Datasets/week_9/"
air_quality = pd.read_pickle(dir_path + "air_quality.pkl")

2.1 傳入 dictionary 做 arg

# change Mon - Fri to Weekday
myMap = {'Monday':'Weekday', 'Tuesday':'Weekday', 'Wednesday':'Weekday',
         'Thursday':'Weekday', 'Friday':'Weekday',
         'Saturday':'Weekend', 'Sunday':'Weekend', }

air_quality['day_of_week_name'].value_counts()
# Sunday 13931, Saturday 13812, Friday 13782, Thursday 13633,
# Monday 13536, Tuesday 13523, Wednesday 13468

air_quality['day_of_week_name'].map(myMap).value_counts()
# Weekday 67942
# Weekend 27743

檢查:Weekend = 13931 + 13812 = 27743 ✓;Weekday = 其餘 5 日加埋 = 67942 ✓。

⚠️ 易錯:dictionary 冇嘅 key → NaN

用 dictionary 做 map 時,如果某個值唔喺 dictionary 入面(例如串錯 'monday'),結果會變 NaN,唔會保留原值。所以 map 完記得 .isna().sum() 檢查。(比較:Series.replace(dict) 就會保留冇對應嘅原值。)

2.2 傳入 function name 做 arg

air_quality['day_of_week_name'].map(arg=len)      # 'Friday' -> 6, 'Tuesday' -> 7
air_quality['day_of_week_name'].map(str.upper)    # 'Friday' -> 'FRIDAY'
air_quality['day_of_week_name'].str.upper()       # 結果一樣(Week 8 嘅 .str accessor)

注意傳入嘅係 function 本身(len、str.upper),唔好加括號:map(len()) 係錯,因為咁會即刻 call 咗 function。

2.3 傳入 lambda function

air_quality['day_of_week_name'].map(
    lambda x: 'Weekend' if x in ['Saturday', 'Sunday'] else 'Weekday')

lambda 係一行嘅 anonymous function:lambda 輸入: 輸出。x 每次代表 Series 入面一個 value。結果同 dictionary 版本一樣,但寫法短好多(唔使列晒 7 日)。

2.4 傳入 custom function(function name)

def mapfunc(x):
    if x in ['Saturday', 'Sunday']:
        return 'weekend_mapfunc'
    else:
        return 'weekday_mapfunc'

air_quality['day_of_week_name'] = air_quality['day_of_week_name'].map(mapfunc)

呢度將結果 assign 返入原 column,所以原本嘅星期名會被覆蓋。Notebook 之後用 pd.read_pickle(...) reset 返個 DataFrame。map 本身唔會 in-place 改 data,一定要 assign 先會保存。

2.5 Q1:wind direction 例子

air_quality['wd'] 有 16 種方向(NE、ENE、E、NW、N、SW、ESE、SSW、NNE、S、WSW、SE、NNW、WNW、SSE、W)。取第一個字母就可以歸納做 4 個主方向:

def wd_to_letter(wd_full):
    return wd_full[0]
air_quality['wd'].map(wd_to_letter).value_counts()
# N 34068, S 26054, E 22699, W 12864

練習答案參考(notebook 留空畀學生做):

# Q1.1 lambda function -> 'E', 'W', 'S', 'N'
air_quality['wd'].map(lambda x: x[0]).value_counts()

# Q1.2 full function -> 'East', 'West', 'South', 'North'
def wd_to_name(wd_full):
    names = {'E': 'East', 'W': 'West', 'S': 'South', 'N': 'North'}
    return names[wd_full[0]]
air_quality['wd'].map(wd_to_name).value_counts()
💡 直覺

注意 wd_full[0] 只睇第一個字母,所以 "NE"、"NNE"、"NW"、"NNW" 全部歸做 N;"ENE"、"ESE" 歸 E。呢個係一種將 high-cardinality categorical feature 減少類別嘅方法(同 Week 7 嘅 binning 目的相似:減少 categories)。

3. Applying functions:df.apply()

⭐ apply() 重點
  • 將同一個 function 套用喺每個 sub-dimension(每個 row 或者每個 column)。
  • Highly flexible,但通常有更快嘅 alternatives:DataFrame / Series 本身有 built-in methods(例如 df.mean()),盡量用佢哋。apply 比較慢,use it as the last resort。
  • 沿一個 axis 套用:axis=0(default)= column by column;axis=1 = row by row。
  • args=() 傳額外 input arguments。
  • Built-in functions 用引號:df.apply('mean');自己 define 嘅 function 唔用引號:df.apply(my_func)。

3.1 Column-wise vs row-wise

# column-wise(axis=0, default):每個 column 一個值
air_quality[['PM2.5', 'PM10']].apply('mean')        # PM2.5 83.477884, PM10 111.899959
air_quality[['PM2.5', 'PM10']].mean()               # 同樣結果,而且更快

# row-wise(axis=1):每個 row 一個值
air_quality[['PM2.5', 'PM10']].apply('mean', axis=1)  # 9.0, 4.0, 4.0, 5.0, 4.5, ...
air_quality[['PM2.5', 'PM10']].mean(axis=1)           # 同樣結果
⭐ 記 axis 嘅方法
axis=0(default)axis=1
Function 每次收到一整個 column一整個 row
結果長度= column 數(例子:2 個值)= row 數(例子:95,685 個值)
例子每個污染物嘅平均值每個時間點 PM2.5 同 PM10 嘅平均

口訣:axis 係「沿住邊個方向收埋」:axis=0 將 rows 收埋(剩返 columns),axis=1 將 columns 收埋(剩返 rows)。

3.2 點解 apply(axis=1) 咁慢?(速度比較)

def pm_ratio(row):
    return row['PM2.5']/row['PM10']

%timeit pm_ratio(air_quality)          # 成個 df 傳入,call 一次:44.4 μs
pm_ratio(air_quality.iloc[0])          # 只傳一個 row:np.float64(1.0)

%%timeit
air_quality[['PM2.5', 'PM10']].apply(pm_ratio, axis=1)   # call 95,685 次:300 ms

%%timeit
air_quality['PM2.5']/air_quality['PM10']                 # vectorized,不 call function:44.8 μs
⭐ 結論:vectorized operation 遠快過 apply

pm_ratio(row) 個參數 row 定義得好鬆:只要 row['PM2.5'] 攞到數字,佢可以係一個 row,亦可以係成個 DataFrame。直接傳成個 DataFrame 只 call 一次,pandas 用 vectorized 運算(44 μs);apply(axis=1) 要逐 row call 95,685 次 Python function(300 ms),大約慢 6,700 倍(300 ms ÷ 44.4 μs ≈ 6,757)。所以有 built-in / vectorized 寫法就唔好用 apply。

ℹ️ 單位

μs = microsecond = 10−610^{-6} s;ms = millisecond = 10−310^{-3} s。%timeit 測一行,%%timeit 測成個 cell(Jupyter magic commands)。

3.3 用 apply() 生成新 features

apply 最常見用途:用 axis=1,根據多個現有 features 建立新 feature,尤其係 built-in functions 做唔到嘅邏輯。

# prepare the function to generate a new feature about activity decision
def activity_decision(row):
    if (row['PM2.5_cat'] in ['Good', 'Moderate']) & (row['TEMP_category'] in ['Warm', 'Hot']) & (row['RAIN'] == 0):
        return 'Go outside'
    else:
        return 'Stay inside'

# call the function using apply along axis=1
air_quality.apply(activity_decision, axis=1)
# 0 Stay inside, 1 Stay inside, ...   Length: 95685

每次 row 係一個 row(Series),所以可以同時睇 PM2.5_cat、TEMP_category、RAIN 三個 columns。要保存就 air_quality['activity'] = air_quality.apply(activity_decision, axis=1)。

⚠️ 留意

Function 入面 row['PM2.5_cat'] in [...] 得出嘅係普通 Python True / False,所以用 and 都得;Notebook 用 & 都 work,因為兩邊都係 bool。但喺 Boolean mask(成個 Series)嗰陣一定要用 & 加括號,唔可以用 and。

⭐ Conclusion

.apply(func) 每次攞 DataFrame 嘅一個 row 或 column(一個 Series),再對嗰個 Series 逐個套用 func。

4. Exploratory Data Analysis (EDA)

⭐ What / Why / How
  • What:EDA is a process of analyzing datasets to summarize their main characteristics. Any method that does not include formal modeling or hypothesis testing to look at the data is EDA, which often involves summary statistics and data visualizations.
  • Why:在作任何 assumptions 之前先睇 data。幫手 identify obvious errors、了解 patterns、detect outliers / anomalous events、搵 variables 之間有趣嘅關係。
  • How:Check for mistakes, assumptions;Detect outliers (cross-feature);Discover relationships among features;Preliminary selection of appropriate models。

Main types of EDA

類型內容幾時教
Summary statisticsAggregating statistics今講
Sampling from DataFrames今講
Correlations今講
Grouping by下一講
Pivoting tables下一講
VisualizationCharts / plots之後幾講

5. Basic Aggregating Statistic Methods

Aggregating statistics 將一堆數值總結成一個值,可以用喺:一個 Series / column、DataFrame by columns、DataFrame by rows。

⭐ 常用 functions 同用途
Function用途
mean()、median()Centre of the data
std()Spread of the data
min()、max()Range / span of the data
quantile(0.25)、quantile(0.75)Check the IQR(Q1Q_1、Q3Q_3)
sum()加總;對 Boolean 就係數 True 嘅數目
nunique()Number of unique values
describe()Summary of data
mode()Most popular values
nlargest()、nsmallest()Extreme values
count()Count non-NA cells for each column or row

5.1 Series 例子:TEMP 同 PM2.5

air_quality['TEMP'].count()       # 95685(non-NA 數目)
air_quality['PM2.5_cat'].count()  # 95685(category 都數得)
air_quality['TEMP'].mean()        # 13.729
air_quality['TEMP'].median()      # 14.6
[air_quality['TEMP'].quantile(0.25), air_quality['TEMP'].quantile(0.75)]    # [3.5, 23.4]
air_quality['PM2.5'].mean()       # 83.478
air_quality['PM2.5'].median()     # 59.0
[air_quality['PM2.5'].quantile(0.25), air_quality['PM2.5'].quantile(0.75)]  # [23.0, 116.0]
air_quality['RAIN'].sum()         # 6171.3
💡 點樣解讀(EDA 思維)
  • TEMP:mean 13.73 ≈ median 14.6,IQR = 23.4 − 3.5 = 19.9,分佈大致對稱(mean 略低過 median,有少少 left skew)。
  • PM2.5:mean 83.48 明顯 > median 59.0 → right-skewed(少數極高污染值拉高 mean)。IQR = 116 − 23 = 93。用 Week 6 嘅 outlier rule:upper fence = Q3+1.5×IQR=116+139.5=255.5Q_3 + 1.5 \times IQR = 116 + 139.5 = 255.5,max 821 遠高過佢 → 有 outliers。
  • 所以 skewed data 用 median / IQR 描述中心同 spread 比 mean / std 更 robust。

5.2 成個 DataFrame 做 aggregation:numeric_only vs select_dtypes

air_quality.info():dtypes 有 category(2)、datetime64[ns](1)、float64(12)、int32(6)、object(3)。直接 sum() 成個 DataFrame 會撞到非數值 columns,所以要揀 columns:

air_quality.sum(numeric_only=True)                  # numerical columns only(bool columns included)
air_quality.select_dtypes(include='number').sum()   # 數值 columns(bool columns not included)
air_quality['wd'].sum()                             # object column:會將所有 strings 駁埋一齊(無意義)

兩個結果喺呢個 dataset 一樣(18 個數值 columns),因為 dataset 冇 bool column。分別只係 bool:numeric_only=True 會計 bool,select_dtypes(include='number') 唔會。

air_quality['PM2.5_cat'].value_counts()
# Unhealthy 34257, Moderate 19853, Good 13576,
# Unhealthy for sensitive groups 12457, Very unhealthy 10821, Hazardous 4721
air_quality['PM2.5_cat'].mode()        # 0  Unhealthy  (ordered category)
type(air_quality['PM2.5_cat'].mode())  # pandas.core.series.Series
⚠️ mode() 回傳 Series,唔係單一值

因為可以有多個 modes(並列最多次)。要攞最常見嗰個值,用 position 0:air_quality['PM2.5_cat'].mode()[0]。

5.3 Separating numerical and categorical columns

⭐ 三種揀 dtype 嘅方法
方法適用例子 / 限制
describe(include=…, exclude=…)describe()air_quality.describe(include=['number','category']);可以一個或多個 types;listed dtype 最少要有一個存在,否則 error(例如 include=['bool'],因為 dataset 冇 bool column)
numeric_only=Truesum()、mean()、mode() 等 aggregative functionsair_quality.mean(numeric_only=True);唔可以指定 dtype;numeric_only=False 唔係「只揀非數值」,而係包晒所有 columns
select_dtypes(include=/exclude=) 然後 aggregate想對非數值 columns 做 aggregationair_quality.select_dtypes(exclude='number').mode()
air_quality.mean(numeric_only=True)     # PM2.5 83.48, PM10 111.90, TEMP 13.73, PRES 1011.40, ...
air_quality.mode(numeric_only=False)    # 所有 columns:27603 rows × 24 columns(!)

5.4 Q2–Q4 練習

# Q2: five-number statistics for all float64 columns
air_quality.describe(include='float64')

# Q3: describe() for String columns(String 喺 pandas 係 object dtype)
air_quality.describe(include='object')    # 輸出 count / unique / top / freq

# Q4
air_quality.select_dtypes(exclude='number').mode()                # 27603 rows × 6 columns
air_quality.select_dtypes(exclude=['number','datetime']).mode()   # 1 row:
#    wd   station  PM2.5_cat  TEMP_category  day_of_week_name
# 0  NE   Tiantan  Unhealthy  coldest        Sunday
⭐ Q4:點解第一個 mode() 有 27603 rows?

date_time column(datetime 唔係 number,所以冇被 exclude)入面好多 timestamps 出現次數一樣多,並列最多次。mode() 會回傳所有並列嘅 modes,所以 date_time 有 27603 個 modes;其他 columns 只有 1 個 mode,餘下 rows 就用 NaN 補齊。Exclude 埋 'datetime' 之後就只剩 1 row。(Q3 亦說明 describe() 對 object column 唔會出 five-number summary,而係 count / unique / top / freq。)

6. agg() for Aggregating Statistics

單一 aggregation 直接用 df.mean()、df.sum() 就得。如果想一次過睇幾個(例如 mean 同 sum 喺同一個 output),就用 df.agg()。

DataFrame.agg(func=None, axis=0, *args, **kwargs)
  • func:built-in function 名用引號(.agg('mean'));user-defined function 唔用引號(.agg(my_func))。
  • axis:default 0。axis=0 apply to columns;axis=1 apply to rows。
🖼️ agg.png(PDF 第 18 頁)

air_quality[['TEMP','PM2.5']].agg([my_sum,'min','max','mean']) 嘅結果:一個 4 行 × 2 欄嘅表——my_sum:TEMP 1.313702e+06、PM2.5 7.987581e+06;min:−16.8、2.0;max:41.6、821.0;mean:13.72945、83.47788。即係一次過將自訂 function(my_sum,冇引號)同 built-in functions(有引號)套落每個 column,每個 function 變一行。

6.1 單一 function(同 apply 相似)

air_quality[['TEMP','PM2.5']].agg('mean', axis=1)   # 每 row:4.25, 1.65, 0.80, ...
air_quality[['TEMP','PM2.5']].agg('mean', axis=0)   # 每 column:TEMP 13.729446, PM2.5 83.477884

例如 row 0:TEMP = −0.5,PM2.5 = 9.0 → (−0.5 + 9.0) / 2 = 4.25 ✓。

6.2 多個 functions(list)

def my_sum(df):
    return df.sum(axis=0)

air_quality[['TEMP','PM2.5']].agg([my_sum, 'min', 'max', 'mean']).T
#        my_sum        min    max    mean
# TEMP   1.313702e+06  -16.8  41.6   13.729446
# PM2.5  7.987581e+06    2.0  821.0  83.477884

.T(transpose)將 rows / columns 對調,令每個 feature 一行,易讀啲。留意 list 入面 my_sum 冇引號,'min' 有引號。

6.3 用 **kwargs 傳額外參數

air_quality[['TEMP','PM2.5']].agg('quantile', q=0.3)   # TEMP 5.6, PM2.5 29.0(30th percentile)

def count_between(col, low, high):
    return ((col < high) & (col > low)).sum()

air_quality['PM2.5'].agg(count_between, low=20, high=200, axis=0)   # 64692

q=0.3、low=20、high=200 都係 keyword arguments,會原封不動傳入 function。count_between 用 Boolean mask 再 .sum() 數 True(PM2.5 喺 20–200 之間嘅 rows = 64,692)。

6.4 唔同 features 用唔同 functions(dictionary mapping)

# this is correct usage of agg() with different functions on different columns
air_quality[['PM2.5', 'PM2.5_cat']].agg({'PM2.5': ['min','max','mean'],
                                         'PM2.5_cat':'nunique'})
#          PM2.5       PM2.5_cat
# min      2.000000    NaN
# max      821.000000  NaN
# mean     83.477884   NaN
# nunique  NaN         6.0
⭐ The charm of .agg()

可以對唔同 features 用唔同 functions:傳入 {column: function 或 [functions]} dictionary。結果係一個 table,某 column 冇用到嘅 function 位置就係 NaN。呢個係 apply 做唔到(或者好麻煩)嘅。

6.5 Q5:自訂 function + agg

要求:WSPM 顯示 min / max / mean;wd 用自訂 function most_two_freq() 回傳最常見兩個值嘅 tuple。

def most_two_freq(col):
    vc = col.value_counts()            # 已經由大至小排序
    return (vc.index[0], vc.index[1])

most_two_freq(air_quality['wd'])       # ('NE', 'ENE')

air_quality[['WSPM', 'wd']].agg({'WSPM': ['min', 'max', 'mean'],
                                 'wd': most_two_freq})
#                WSPM       wd
# min            0.000000   NaN
# max            12.000000  NaN
# mean           1.692044   NaN
# most_two_freq  NaN        (NE, ENE)

(Notebook 只顯示 expected output,function 留空;以上係參考答案。)

6.6 Summary on aggregative statistics

  • 可以用一個值總結一個 column 嘅 functions:count()、sum()、mean()、std()、max()、min()、mode()、nunique()…
  • Exam 好多都可以加 axis=1 改成 row 方向:df.count(axis=1)、df.nunique(axis=1)、df.sum(axis=1)、df.mean(axis=1)、df.min(axis=1)、df.max(axis=1)、df.mode(axis=1)。
  • describe() 用 include= / exclude=;sum()、mean()、mode() 用 numeric_only=True;非數值 columns 先 select_dtypes()。
  • .agg() 嘅好處:唔同 features 用唔同 functions。Built-in function 用 ' ' 包住,user-defined 唔使。

6.7 map vs apply vs agg 比較

⭐ 考試重點:揀邊個 method?
map()apply()agg()
作用單位每個 element每個 row / column(一個 Series)每個 column(或 row),可以多個 functions
常見對象SeriesDataFrame(亦有 Series.apply)DataFrame / Series
輸入 arg / funcdict、function、lambda、Series一個 functionfunction、list of functions、dict {column: function(s)}
典型用途換值 / recode(Monday → Weekday)用多個 columns 生成新 feature(axis=1)Summary statistics 一次過出晒
輸出長度同原 Series 一樣視乎 function(aggregate → 縮短;逐 row 轉換 → 一 row 一值)通常縮短成 summary

Q6 答案參考

  • A:student ID p123456 → 123456(逐個 cell 轉換)→ map(),例如 df['id'].map(lambda x: x[-6:]),或者 Week 8 嘅 df['id'].str[-6:]。
  • B:所有 columns 一行平均值 → 直接用 built-in df.mean()(或 df.agg('mean') / df.apply('mean'))。
  • C:salary 用 mean、occupation 用 mode(唔同 columns 唔同 functions)→ agg() with a mapping:df.agg({'salary': 'mean', 'occupation': lambda s: s.mode()[0]})。

7. Sampling

⭐ Reasons for sampling
  • 用 sampling「take a peek」睇下 data。
  • Large data:random sample 令 computation 容易啲。
  • Machine learning:用一組 random samples train model,另外 hold 一組 samples 做 testing。
  • Outcomes 唔平均(imbalanced)時做 over- 或 under-sampling。
DataFrame.sample(n=None, frac=None, replace=False, weights=None,
                 random_state=None, axis=None, ignore_index=False)
參數意思
n抽幾多 rows(例如 200)
frac抽幾大比例(例如 0.1 = 10%);同 n 二揀一
replaceFalse(default)= without replacement,同一 row 唔會抽兩次;True = with replacement,可能重複
weights每個 row 被抽中嘅權重(可用嚟 over-sample 少數類)
random_stateRandom seed,固定之後每次抽到一樣嘅 sample(reproducible)
ignore_indexTrue 就將結果 index 重設為 0…n−1
🖼️ Sampling.png 同兩幅 histogram(PDF 第 23–24 頁)

Sampling.png:上面係全部 data 嘅 air_quality['TEMP'].hist(),兩條箭咀指去下面兩幅唔同嘅 sample(200) histogram:兩幅大致形狀同上面似(−10 至 35、兩個峰),但每條 bar 高度都唔同,即係每次 sample 結果有少少差異。

全部 95,685 rows 嘅 TEMP histogram:範圍約 −17 至 42;0–5 有一個峰(約 14,700),10 附近有凹位(約 12,300),20–25 係最高峰(約 17,800),35 以上同 −10 以下都好少。sample(200):同樣係雙峰(約 5–10 同 20 左右),範圍只係約 −8 至 35,冇咗兩邊最極端嘅值,同下面 describe() 比較嘅結論一樣。

air_quality['TEMP'].hist()                # 全部 95,685 rows 嘅 histogram
air_quality.sample(200)["TEMP"].hist()    # 只用 200 rows,形狀大致相似但較粗糙

air_quality.sample(2000, replace = True).duplicated().sum()   # 16(有重複 rows)

replace=True 唔會避免同一 row 被抽多次,所以 output 可能有 duplicated rows,可以用 duplicated().sum() 檢查(呢次 2000 個入面有 16 個重複)。

Sample vs population:比較 describe()

sample(200) 第 1 次sample(200) 第 2 次全部 95,685 rows
mean14.0214.5013.73
std11.8011.4611.32
min−8.0−8.4−16.8
25%2.984.653.5
50%13.7516.214.6
75%24.6524.1523.4
max35.736.241.6
💡 觀察
  • 每次 sample 結果都唔同(冇設 random_state)→ sampling variability。
  • Mean、median、std 同全體好接近:200 rows 已經可以大致描述 TEMP 分佈。
  • 但 min / max(extreme values)差好遠:極端值好少,細 sample 好難抽中。所以用 sample 搵 outliers 唔可靠。

8. Correlation

假設每個 record 有兩個 random features(兩個 columns),我哋想量度佢哋點樣一齊變動(move together)。例如身高 xx 同體重 yy:身高增加時,體重係咪都傾向增加?Covariance 同 Correlation 就係用嚟量度呢種 co-movement。

🖼️ corr.png(PDF 第 25 頁)
Covariance(x,y)=1n∑i=1n(xi−xˉ)(yi−yˉ)ρxy=Correlation(x,y)=cov(x,y)var(x)var(y)\text{Covariance}(x, y) = \frac{1}{n}\sum_{i=1}^{n}(x_i - \bar{x})(y_i - \bar{y}) \qquad \rho_{xy} = \text{Correlation}(x, y) = \frac{cov(x, y)}{\sqrt{var(x)}\sqrt{var(y)}}

旁邊註明 correlation 係 covariance "Normalized w.r.t. the std":除以兩個 standard deviations 之後,數值就固定喺 −1 至 +1 之間,唔受單位影響。(留意呢度 covariance 用 1/n1/n;pandas cov() 預設用 1/(n−1)1/(n-1),但係計 correlation 時分子分母會抵銷,所以 corr() 結果一樣。)

⭐ Correlation coefficient 解讀
  • Correlation coefficient 喺 −1 至 +1 之間。
  • 越接近 1(或 −1),relationship 越強;越接近 0,relationship 越弱。
  • 正數 = 一齊升(positive);負數 = 一個升另一個跌(negative)。
DataFrame.corr(method='pearson', min_periods=1, numeric_only=False)
  • method:default 用 Pearson coefficient;亦可以揀 Kendall 或 Spearman。
  • min_periods:計 correlation 最少要幾多 observations;唔夠就回傳 NaN。
  • numeric_only:只用數值 columns。

8.1 三種 methods(細 DataFrame 例子)

data = {'A': [45, 37, 42],
        'B': [38, 31, 26],
        'C': [10, 15, 17]}
df = pd.DataFrame(data)

df.corr(method='pearson', min_periods=3)
#           A         B         C
# A  1.000000  0.458388 -0.583324
# B  0.458388  1.000000 -0.989268
# C -0.583324 -0.989268  1.000000

df.corr(method='kendall', min_periods=3)    # A-B 0.333, A-C -0.333, B-C -1.0
df.corr(method='spearman', min_periods=3)   # A-B 0.5,   A-C -0.5,   B-C -1.0
df.corr(min_periods=4)                      # 全部 NaN:只有 3 rows,唔夠 4 個 observations
⭐ Pearson vs Kendall vs Spearman
PearsonSpearmanKendall
量度Linear relationshipMonotonic relationship(strength and direction)Strength of dependence(基於 rank 嘅 concordance)
用乜計Actual valuesRanksRanks
類型ParametricNon-parametricNon-parametric
Outliers / non-linear敏感More robustMore robust
Range−1 至 +1(−1 perfect negative,0 none,+1 perfect positive)

8.2 手計驗證(補充,slides 冇列公式)

r=∑i(xi−xˉ)(yi−yˉ)∑i(xi−xˉ)2∑i(yi−yˉ)2r = \frac{\sum_{i}(x_i - \bar{x})(y_i - \bar{y})}{\sqrt{\sum_{i}(x_i - \bar{x})^2 \sum_{i}(y_i - \bar{y})^2}}

Pearson A vs B:Aˉ=41.33\bar{A} = 41.33,Bˉ=31.67\bar{B} = 31.67。Deviations:A = (3.67, −4.33, 0.67),B = (6.33, −0.67, −5.67)。

  • 分子:3.67×6.33+(−4.33)(−0.67)+0.67×(−5.67)=23.22+2.89−3.78=22.333.67 \times 6.33 + (-4.33)(-0.67) + 0.67 \times (-5.67) = 23.22 + 2.89 - 3.78 = 22.33
  • 分母:32.67×72.67=2374≈48.72\sqrt{32.67 \times 72.67} = \sqrt{2374} \approx 48.72
  • r=22.33/48.72≈0.458r = 22.33 / 48.72 \approx 0.458 ✓(同 output 0.458388 一致)

Spearman A vs B:先轉 ranks。A = (45, 37, 42) → ranks (3, 1, 2);B = (38, 31, 26) → ranks (3, 2, 1)。Rank 差 d=(0,−1,1)d = (0, -1, 1),∑d2=2\sum d^2 = 2,n=3n = 3:

ρ=1−6∑d2n(n2−1)=1−6×23×8=0.5\rho = 1 - \frac{6\sum d^2}{n(n^2 - 1)} = 1 - \frac{6 \times 2}{3 \times 8} = 0.5

結果 0.5,同 output 一致 ✓。

Kendall A vs B:3 對 pairs。(row0, row1):A 跌、B 跌 → concordant;(row0, row2):A 跌、B 跌 → concordant;(row1, row2):A 升、B 跌 → discordant。τ=C−Dn(n−1)/2=2−13=0.333\tau = \frac{C - D}{n(n-1)/2} = \frac{2 - 1}{3} = 0.333 ✓。

8.3 Linear vs monotonic:S、S1、S2、S3 例子

s  = pd.Series([1, 2, 3, 4, 5])
s1 = pd.Series([5, 6, 7, 8, 9])   # s + 4
s2 = 5 - s                        # 4, 3, 2, 1, 0
s3 = s**3                         # 1, 8, 27, 64, 125
df = pd.DataFrame({'S': s, 'S1': s1, 'S2': s2, 'S3': s3})
df.corr()
#            S        S1        S2        S3
# S   1.000000  1.000000 -1.000000  0.943118
# S1  1.000000  1.000000 -1.000000  0.943118
# S2 -1.000000 -1.000000  1.000000 -0.943118
# S3  0.943118  0.943118 -0.943118  1.000000
⭐ 重點解讀
  • S vs S1 = +1:S1 = S + 4,perfect linear(加常數唔影響 correlation)。
  • S vs S2 = −1:S2 = 5 − S,perfect negative linear。
  • S vs S3 = 0.943(唔係 1):S3 = S3S^3 係完全 monotonic(一路升),但唔係直線,所以 Pearson < 1。如果用 method='spearman',ranks 完全一致,結果會係 1。
  • Correlation 只講兩者一齊變動,唔代表 causation。

8.4 Air quality 例子

# if you have long columns, this is not recommended
air_quality[['PM2.5','PM10']].corr()             # 0.877604

# if you have long columns, this is more recommended to avoid long computation time
air_quality[['PM2.5','PM10']].sample(200).corr() # 0.665054

air_quality[['PM2.5','RAIN']].corr()             # -0.018063(幾乎冇 linear relationship)
⚠️ 留意 sample 嘅代價

Notebook 建議長 columns 先 sample() 再 corr() 去慳計算時間,但同一對 features,全體得 0.878,200 rows sample 只得 0.665:細 sample 嘅 correlation 估計可以偏差好大(尤其 PM2.5 呢類有極端值嘅 skewed data)。慳時間嘅同時要留意 sample size。

8.5 Q7:所有數值 columns 嘅 correlation matrix

air_quality.corr() 會出錯,因為有幾個 non-numeric columns。Notebook 嘅 corr_mat shape 係 (12, 12),columns 係 PM2.5、PM10、SO2、NO2、CO、O3、TEMP、PRES、DEWP、RAIN、WSPM、time_since_2022_weeks,啱啱好係 12 個 float64 columns,所以參考答案:

corr_mat = air_quality.select_dtypes(include='float64').corr()
corr_mat.shape                       # (12, 12)
corr_mat.loc['PM2.5','TEMP']         # -0.0903
corr_mat.loc['PM2.5',:].abs()        # abs() 睇強度,唔理正負
corr_mat['PM2.5'].nlargest(5)        # PM2.5 1.0, PM10 0.847, CO 0.767, NO2 0.671, SO2 0.464
corr_mat['PM2.5'].nsmallest(5)       # WSPM -0.311, O3 -0.158, TEMP -0.090, PRES -0.048, time_since_2022_weeks -0.023
ℹ️ 數字差異

Notebook 入面 corr_mat 嘅 PM2.5–PM10 係 0.846921,但 In[113] 直接計係 0.877604。Notebook 冇顯示 Q7 答案點樣計 corr_mat,所以講義冇交代差異原因;以上 select_dtypes 寫法係根據 shape 同 column 名推斷。

8.6 Q8:解讀 correlation matrix

⭐ Q8 參考答案
  1. 點解 diagonal 永遠係 1? 每個 feature 同自己比較,完全一齊變動(perfect positive linear relationship)。
  2. 總共幾多個 cross-correlation coefficients? 12 × 12 = 144 格,減 12 個 diagonal = 132 個 off-diagonal;matrix 係 symmetric(rxy=ryxr_{xy} = r_{yx}),所以 unique pairs = 12×112=66\frac{12 \times 11}{2} = 66 個。
  3. PM2.5 同其他 features 嘅關係:
    • Strong positive:PM10(0.85)、CO(0.77)、NO2(0.67)→ 呢啲污染物通常一齊升(同源,例如燃燒 / 交通)。
    • Moderate positive:SO2(0.46)。
    • Weak negative:WSPM(−0.31)→ 風速大,污染物較易被吹散;O3(−0.16)。
    • Almost none(接近 0):TEMP(−0.09)、PRES(−0.05)、RAIN(−0.02)、time_since_2022_weeks(−0.02)。

8.7 搵最強嘅 six correlations:unstack()

corr_mat.unstack().sort_values(ascending=False).duplicated()   # Length: 144
strongest_corr = corr_mat.unstack().sort_values(ascending=False).drop_duplicates().head(6)
# PM2.5  PM2.5   1.000000
# PM10   PM2.5   0.846921
# DEWP   TEMP    0.826201
# PM2.5  CO      0.766816
# CO     NO2     0.714192
# PM2.5  NO2     0.670551
  1. unstack():將 12 × 12 matrix 變成 144 個值嘅 Series,index 係 (row, column) pair。
  2. sort_values(ascending=False):由大至小排。
  3. drop_duplicates():因為 matrix symmetric,(PM10, PM2.5) 同 (PM2.5, PM10) 數值一樣,排完會相鄰;drop 走重複值。12 個 diagonal 1.0 亦會縮剩一個。
  4. head(6):攞頭 6 個。
⚠️ 細節
  • 結果第一個係 PM2.5–PM2.5 嘅 1.0(self-correlation),所以真正嘅 feature pairs 只有 5 對。
  • drop_duplicates() 係按數值去重:如果兩對唔同 features 恰巧 correlation 一模一樣,都會被 drop 錯。
  • ascending=False 只搵最強 positive;想包括強 negative,要先 .abs() 再排。

9. Semi-Structured Data(programming 唔考)

ℹ️ 範圍

標題寫明「Introduction to Semi-Structured Data (programming not required by exam)」。但 Summary 要求:can recognize XML or JSON format;can tell conceptually their differences from CSV;can tell when to use them。所以概念要識,parsing code 唔使背。

9.1 Structured vs Semi-structured vs Unstructured

⭐ 三種 data 比較
Data TypeStructureExamplesCharacteristics
StructuredRigid schema, fixed formatSQL databases、CSV files、Excel spreadsheetsPre-defined schema、tabular format、consistent structure
Semi-StructuredFlexible schema, self-describingXML、JSON、YAML、NoSQL databasesSchema is embedded in data、hierarchical、flexible structure
UnstructuredNo predefined structureText documents、images、videos、audio filesNo organized schema、difficult to parse programmatically

9.2 點解 XML 同 JSON 係 semi-structured

<students>
  <student id="101">
    <name>Alice</name>
    <grades>
      <math>A</math>
      <science>B+</science>
    </grades>
  </student>
  <student id="102">
    <name>Bob</name>
    <!-- Different structure allowed -->
    <attendance>95%</attendance>
  </student>
</students>
{
  "students": [
    {"id": 101, "name": "Alice",
     "grades": {"math": "A", "science": "B+"}},
    {"id": 102, "name": "Bob", "attendance": "95%"}
  ]
}
  • Self-describing:每個值都帶住自己嘅名(tag <name> 或 key "name"),schema 嵌喺 data 入面。
  • Hierarchical:可以 nested(grades 入面有 math、science)。
  • Flexible:Alice 有 grades,Bob 冇 grades 但有 attendance,兩個 records 結構唔同都得;CSV 就要每 row 同一組 columns。
  • 認樣:XML 用 <tag>…</tag> 同 attributes(id="101");JSON 用 { } objects、[ ] arrays、"key": value。

9.3 XML vs JSON vs CSV:幾時用

⭐ Data format comparison
FormatUse when…
XML需要 strong schema validation 同 data integrity(用 XML schema definition .xsd file 檢查 conformance,睇 .xsd 就知所有 tags 同 tree structure,例如 data.gov.hk 嘅 special traffic news);document-oriented data;enterprise systems 要 standardized data exchange;需要 rich metadata 同 namespaces
JSONBuilding web APIs / web services;application configuration files;mobile app development;real-time data exchange
CSVSpreadsheet / simple tabular data;quick data analysis 同 prototyping;data migration between systems;non-technical users 要睇 / 改 data

9.4 Parsing 例子(只供參考,唔考 programming)

import xml.etree.ElementTree as ET
tree = ET.parse(dir_path+'CountryData.xml')
root = tree.getroot()
print("Root Tag:", root.tag)            # data
for child in root:
    print(child.tag, child.attrib)      # country {'name': 'Liechtenstein'} / Singapore / Panama

for country in root.findall('country'):
    name = country.get('name')                  # 讀 attribute
    gdppc = int(country.find('gdppc').text)     # 讀 child tag 嘅 text
# Highest 141100, Lowest 13600, Average 71533.33

XML 係一棵 tree:root → children(country)→ 再落一層(gdppc)。Attribute 用 .get(),tag 入面嘅文字用 .find(...).text。

import requests
url = "https://nominatim.openstreetmap.org/search"
params = {"q": "Shatin", "format": "jsonv2", "limit": 1}
headers = {"User-Agent": "my-notebook-geocoder/1.0 (...)"}   # Nominatim usage policy 要求
response = requests.get(url, params=params, headers=headers, timeout=15)
response.raise_for_status()
results = response.json()        # JSON → Python list / dict
top = results[0]
float(top["lat"]), float(top["lon"])   # 22.3815527, 114.1904697

JSON API 例子(OpenStreetMap geocoder):送 HTTP request,response.json() 將 JSON 轉成 Python list / dict,再用 key 攞值。

10. Summary

  • Semi-structured data:識認 XML / JSON format;概念上分得清同 CSV 嘅分別;知道幾時用。
  • EDA: Aggregating statistics:
    • Basic aggregating stat functions:sum()、mean()…
    • Aggregate 之前識揀唔同 dtypes。
    • Exam 知道 map / apply / agg 嘅分別。
    • Exam 知道點解要 sample。
    • Exam 識計 correlation matrix 同描述佢(邊啲 features strongly / weakly correlated)。

自我測驗 Self-test

Q1. s = pd.Series(['Mon', 'Sat', 'mon']),s.map({'Mon': 'Weekday', 'Sat': 'Weekend'}) 輸出係乜?

['Weekday', 'Weekend', NaN]。Dictionary mapping 冇對應 key('mon' 細楷)就變 NaN,唔會保留原值。

Q2. df[['PM2.5','PM10']].apply('mean') 同 df[['PM2.5','PM10']].apply('mean', axis=1) 各自輸出幾多個值?

第一個(axis=0):每個 column 一個值 → 2 個值。第二個(axis=1):每個 row 一個值 → 95,685 個值。

Q3. 點解 df['PM2.5']/df['PM10'](44.8 μs)比 df[['PM2.5','PM10']].apply(pm_ratio, axis=1)(300 ms)快咁多?

前者係 vectorized operation,pandas 一次過喺底層處理成個 column;後者要逐 row call Python function 95,685 次,每次都有 function call 同建立 row Series 嘅 overhead。所以 apply 係 last resort。

Q4. 以下邊句會出錯?(a) df.apply('mean') (b) df.apply(my_func) (c) df.apply('my_func')(my_func 係 notebook 自己 define)

(c)。自己 define 嘅 function 唔應該加引號;字串會被當成 pandas built-in function 名去搵,搵唔到就出錯。(a) built-in 加引號正確;(b) user-defined 唔加引號正確。

Q5. 寫一行 code:對 Price 計 min、max、mean,同時對 District 計 nunique。

df.agg({'Price': ['min', 'max', 'mean'], 'District': 'nunique'})。輸出係 4 rows × 2 columns 嘅 table,冇用到嘅組合係 NaN。

Q6. describe(include=['bool']) 喺 air_quality 會出錯,點解?想對 object columns 計 mode 應該點寫?

因為 include= 列出嘅 dtype 最少要有一個喺 DataFrame 存在,而 air_quality 冇 bool column。Object columns 計 mode:air_quality.select_dtypes(include='object').mode()(numeric_only 唔可以揀非數值 columns)。

Q7. 一個 column 嘅 mean = 83.5,median = 59.0。描述分佈形狀,並建議用乜統計量描述中心。

Mean 明顯大過 median → right-skewed(少數極大值拉高 mean)。應該用 median(同 IQR)描述中心同 spread,因為佢哋對 outliers robust。

Q8. 列出三個 sampling 嘅原因;sample(2000, replace=True) 同 sample(2000) 有乜分別?

原因:(1) large data 用 random sample 令計算容易;(2) ML 用 random samples train,hold 另一組做 testing;(3) outcomes 唔平均時做 over-/under-sampling。replace=True 係 with replacement,同一 row 可以被抽多次(可用 duplicated().sum() 檢查);default replace=False 每 row 最多抽一次。

Q9. x=(1,2,3,4)x = (1, 2, 3, 4),y=x2=(1,4,9,16)y = x^2 = (1, 4, 9, 16)。Pearson 同 Spearman correlation 邊個會等於 1?點解?

Spearman = 1:ranks 完全一樣(1, 2, 3, 4),monotonic increasing。Pearson < 1(約 0.98):關係唔係直線。同 notebook S vs S3(0.943)道理一樣。

Q10. 一個 10 × 10 correlation matrix 有幾多個 unique cross-correlation coefficients?點解 unstack() 之後要 drop_duplicates()?

10×92=45\frac{10 \times 9}{2} = 45 個。Matrix 係 symmetric,(A, B) 同 (B, A) 數值一樣,unstack() 後每對出現兩次(diagonal 1.0 亦重複 10 次),排序後 drop 重複先唔會重複計。

Q11. 以下 data 各適合 XML、JSON 定 CSV?(a) 手機 app 同 server 實時交換資料;(b) 銀行之間要嚴格 schema validation 嘅 data exchange;(c) 俾 non-technical 同事用 Excel 改嘅銷售表。

(a) JSON(mobile app / real-time / web API);(b) XML(strong schema validation,用 .xsd;enterprise standardized exchange);(c) CSV(spreadsheet / simple tabular,non-technical users 可睇可改)。

一頁總結 Cheat sheet

⭐ 考前速記
  • s.map(arg):逐 element;arg = dict(冇 key → NaN)、function name(len、str.upper,唔加括號)、lambda(lambda x: … if … else …)、custom function;要 assign 先保存。
  • df.apply(func, axis=0/1, args=()):每次一個 column(0, default)或 row(1);built-in 用 'mean',自訂用 my_func;慢,last resort;axis=1 用多個 columns 生成新 feature。
  • Vectorized(df['a']/df['b'],μs)≫ apply(axis=1)(ms,逐 row call)。
  • EDA = 唔包 formal modeling / hypothesis testing,用 summary statistics + visualization 總結 data;目的:搵 errors、patterns、outliers、relationships、初步揀 model。
  • Aggregation:mean/median centre;std spread;min/max range;quantile(0.25/0.75) IQR;sum(Boolean 數 True);nunique;mode(回傳 Series,用 [0]);count(non-NA);nlargest/nsmallest;describe。
  • 揀 dtype:describe(include=/exclude=)(dtype 要存在);sum/mean/mode(numeric_only=True)(唔可以指定 dtype,包 bool);非數值 → select_dtypes(exclude='number') 再 aggregate。
  • mode() 對 datetime 會回傳所有並列 modes → 好多 rows + NaN 補齊。
  • df.agg(func, axis, **kwargs):單一('mean')、list([my_sum,'min','max'])、kwargs('quantile', q=0.3)、dict({'PM2.5':['min','max'], 'PM2.5_cat':'nunique'})。.T transpose。
  • 選 method:逐 cell 換值 → map;成行 / 成列套 function → apply;多個 / 唔同 columns 唔同 summary → agg。
  • df.sample(n, frac, replace, weights, random_state);原因:peek、大 data 易計、train/test、over-/under-sampling;sample 估 mean/std 準,但 min/max 唔準。
  • df.corr(method='pearson'|'kendall'|'spearman', min_periods);−1 至 +1,近 ±1 強、近 0 弱;Pearson = linear(actual values),Spearman / Kendall = ranks(non-parametric、robust to outliers、monotonic);observations < min_periods → NaN。
  • Matrix:diagonal = 1,symmetric,unique pairs = k(k−1)2\frac{k(k-1)}{2};PM2.5 強正相關 PM10 / CO / NO2,弱負 WSPM,近 0 TEMP / RAIN。
  • 最強 pairs:corr_mat.unstack().sort_values(ascending=False).drop_duplicates().head(6)(第一個係 self 1.0)。
  • Structured(rigid schema:SQL、CSV)/ Semi-structured(self-describing、hierarchical、flexible:XML、JSON、YAML、NoSQL)/ Unstructured(text、image、video)。XML → schema validation(.xsd)、enterprise、metadata;JSON → web API、config、mobile、real-time;CSV → tabular、quick analysis、migration、non-technical users。