All Revision Notes

COM6001 Computing with Big Data · Week 4

pandas Data Manipulation: Series, DataFrame, I/O, Combining & Renaming

COM6001 · Week 4 · Source: Lecture04_pandas_data_manipulation.pdf (exported Jupyter notebook, 41 pages) · Lab 4 Q1–Q5 · Datasets: superhero_powers.csv, superhero_info.xlsx, titanic.csv (不在此資料夾)

Contents · 8 sections

🎯 本講重點 (Learning outcomes)

  • 重溫 lambda functions 同 packages (install vs import) — Week 3 內容喺本講再講一次。
  • 理解 pandas 兩大 data structures:Series (1D labeled array) 同 DataFrame (2D table);Series 同 list 嘅分別。
  • 用 constructor pd.Series() / pd.DataFrame() 建立 objects;理解 index alignment 陷阱。
  • 分辨 attributes (shape, columns, index, dtypes) 同 methods (head(), tail(), info(), describe())。
  • 用 read_csv() / read_excel() 載入數據、處理 file paths、指定 dtype;用 to_csv() / to_pickle() 匯出。
  • 分辨 concat vs merge;merge 嘅 on / left_on / right_on 同 how (inner, outer, left, right, cross)。
  • 用 .str accessor 同 DataFrame.rename() (dict / function / lambda mapper) 改 column labels。

⭐ 考試必溫 Top-5

  1. concat (按 axis 疊 / 按 index 對齊) vs merge (按 key column 值配對,SQL join);how 五種結果。
  2. df['col'] → Series vs df[['col']] → DataFrame。
  3. CSV vs Pickle:CSV 通用可讀但唔保留 dtypes;Pickle 100% 還原 dtypes 但只可開 trusted source。
  4. Index alignment:用有自己 index 嘅 Series 建 DataFrame 再指定另一個 index → 全部 NaN。
  5. Rename:.str.upper() 要 assign 返;rename(mapper=str.upper) ✓ vs str.upper() ✗;lambda x: x.upper() ✓ vs lambda x: x.upper ✗。

1. Recap: Lambda functions & Packages (Week 3)

Exam 本講開頭重複咗 Week 3 嘅 lambda (Q6/Q7) 同 packages 部分 (今次叫 Q1) — 老師講兩次嘅內容,考試機會較高。詳細解釋見 Week 3,以下係精簡版:

# lambda arguments: expression  —— anonymous,一個 expression
(lambda x, y: x - y)(3, 5)            # -2
id_string = ['a3', 'b1', 'c5', 'd2']
id_string.sort(key=lambda x: x[1])    # ['b1', 'd2', 'a3', 'c5']

def my_func(adder):
    return lambda x: x + adder        # lambda 作為 return value
my_func(5)(3)                         # 8

# Q1:檢查 / 安裝 packages (用 %pip,保證裝喺 notebook 當前 environment)
%pip show pandas
%pip install openpyxl                 # read_excel 需要
import pandas as pd                   # Install 一次 / 每個 session 都要 import
⭐ 速記

Install = add the package to your computer (one-time per environment);Import = load the package into your current Python program (every time)。Install before import. %pip 優於 !pip。

2. The pandas package: Series & DataFrame

pandas 係 fast, powerful, flexible, easy to use 嘅 open-source data analysis and manipulation tool,建基於 Python。用之前要 import pandas as pd。

兩個主要 data structures (亦即 pandas library 嘅兩個 classes):

SeriesDataFrame
維度1D indexed array2D table,有 index (row labels) 同 column labels
類比Spreadsheet 嘅一個 columnExcel table / SQL database table
關係DataFrame 嘅每一個 column 都係一個 Series

Class 係建立 objects 嘅 blueprint,定義 objects 會有嘅 attributes 同 methods。

🖼️ 圖片內容

SeriesandDataFrame.png (Series 與 DataFrame 結構示意圖,PDF 第 6 頁) 係圖片,文字擷取不到;請對照原 PDF。

2.1 Series

1D labeled array,可以裝任何 data type (integers, strings, floats, Python objects)。由 index (axis labels 嘅集合) 同 data 組成。

⭐ Series vs Lists
  • Series 有靈活嘅 index labels (唔止整數位置)。
  • Series 支援更多 data analysis operations 同 methods,例如 s**2 (element-wise);list 做唔到。
  • 大數據量時 Series 一般比 Python list 快。

Constructor

pd.Series() 係 Series class 嘅 constructor (建立新 object 嘅 method);Python 中 constructor 同 class 同名。可以用 pd.Series.__init__ 睇 constructor 參數,或者睇官方文檔:

class pandas.Series(data=None, index=None, dtype=None, name=None, copy=None)
  • pd.Series(list) → index 係由 0 開始嘅自然數。
  • pd.Series(data_list, index=index_list) → index 用你提供嘅 list。
import pandas as pd
index_day = ['Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat', 'Sun', 'Mon']
data = [1, 2, 3, 4, 5, 6, 7, 8]
s = pd.Series(data)                    # index 0–7, dtype: int64
s = pd.Series(data, index=index_day)   # index = Mon … Sun, Mon (可以重複!)

Basic operations

s['new'] = 100     # insertion:用新 index label 直接 assign
s['Mon']           # retrieval by label → 返回兩行 (Mon 1 同 Mon 8),因為 label 重複
s[2]               # FutureWarning:用整數當 position 已 deprecated → 應用 s.iloc[2]  (= 3)

s.mean()           # 15.11  = (1+2+…+8+100)/9 = 136/9
s.max()            # 100
s + s              # element-wise addition
s * 2              # element-wise multiplication
s ** 3             # 每個元素立方 (new → 1000000)

import numpy as np
s['Tue'] = np.nan  # dtype 由 int64 變 float64 (NaN 係 float)
s.mean()           # 16.75 = (136 − 2)/8  ← NaN 自動被跳過
💡 直覺理解:點解 mean 由 15.11 變 16.75?

原本 9 個值總和 136,平均 136/9≈15.11136/9 \approx 15.11。將 Tue (=2) 改成 NaN 後,pandas 計 mean 時預設 skip NaN,剩 8 個值總和 134,平均 134/8=16.75134/8 = 16.75。分母都變咗!呢個係 Week 5–7 missing values 處理嘅伏筆。

⚠️ Series data type considerations

s['Tue'] = "NA" → FutureWarning:將 string 放入 float64 Series 係 “incompatible dtype”,將來會 raise error。"NA" 只係普通 string,唔係 missing value。Series 無法高效存 mixed types,numeric Series 嘅 missing values 應該用 np.nan。

2.2 DataFrame construction

Constructor pd.DataFrame() 可以接 dictionaries、lists、其他 DataFrames 等;可以指定 index、columns、dtype。所有 args 都有 default None,所以 pd.DataFrame() 會得到空 DataFrame。

class pandas.DataFrame(data=None, index=None, columns=None, dtype=None, copy=None)
pd.DataFrame([[1, 2], [3, 4]])                    # list of lists:每個內層 list 係一個 ROW
#    0  1
# 0  1  2
# 1  3  4
pd.DataFrame([[1, 2], [3, 4]], columns=["A", "B"])
#    A  B
# 0  1  2
# 1  3  4
pd.DataFrame({'A': [1, 2], 'B': [3, 4]})          # dict of lists:每個 key 係一個 COLUMN
#    A  B
# 0  1  3
# 1  2  4
pd.DataFrame(data={'A': [1, 2], 'B': [3, 4]}, index=['one', 'two'])
#      A  B
# one  1  3
# two  2  4
pd.DataFrame([[1, 2], ["Robin", "Aileen"]])       # mixed data types 都得 (column dtype 變 object)
⭐ 考試重點:list of lists vs dict of lists

List of lists → 每個 inner list 係一 row。Dict of lists → 每個 key 係一 column (key = column label)。 所以 [[1,2],[3,4]] 嘅第一行係 1 2,但 {'A':[1,2],'B':[3,4]} 嘅第一行係 1 3。

2.3 Using Series as columns — index alignment

Exam Key point: Series are objects that have their own index! 用 Series 建 DataFrame 時,pandas 會按 index label 對齊,唔係按位置。

s = pd.Series(range(1, 8))                  # index 係 0–6
index_day = ['Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat', 'Sun']
df = pd.DataFrame({'first': s, 'second': s**2}, index=index_day)
df     # 全部 NaN!因為 s 嘅 index (0–6) 同 'Mon'…'Sun' 完全對唔上
#       first  second
# Mon     NaN     NaN
# ...     ...     ...

# 正確做法:一開始就畀 Series 相同 index
s = pd.Series(range(1, 8), index=index_day)
df = pd.DataFrame({'first': s, 'second': s**2, 'third': s**3})
#       first  second  third
# Mon       1       1      1
# Tue       2       4      8
# ...
# Sun       7      49    343
💡 直覺理解

想像每個 Series 係一疊有「標籤」嘅卡。DataFrame 問:「Mon 嗰行嘅 first 係咩?」pandas 就去 Series 搵標籤 Mon 嘅卡 — 搵唔到就填 NaN (Not a Number)。佢唔會「按次序」塞入去。

2.4 Attributes and methods of DataFrame

Object 係名詞 (「我架車」),attribute 係形容詞 (「我架車嘅顏色係藍色」),method (特殊 attribute) 係動詞 (「我架車可以 drive()」)。Attributes 冇括號,methods 要括號。

Attribute返回Method作用
df.shapetuple (rows, columns)df.head(n)頭 n rows (default 5)
df.columnscolumn labelsdf.tail(n)尾 n rows
df.indexindex (row labels)df.info()簡要 summary:index dtype、column dtypes、non-null values、memory usage
df.dtypes每個 column 嘅 data typedf.describe()numerical columns 嘅 descriptive statistics
print("DataFrame shape:", df.shape)            # (7, 3)
print("Column names:", df.columns.tolist())    # ['first', 'second', 'third']
print("Index:", df.index.tolist())             # ['Mon', 'Tue', ..., 'Sun']

df = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]})
print(df.info())       # 印 summary 之後再印 None —— info() 自己 print,return None!
print(df.describe())
#          A    B
# count  3.0  3.0
# mean   2.0  5.0
# std    1.0  1.0
# min    1.0  4.0
# 25%    1.5  4.5
# 50%    2.0  5.0
# 75%    2.5  5.5
# max    3.0  6.0
💡 點解 print(df.info()) 最後有個 None?

同 Week 2 Q7 一樣原理:info() 係直接 print summary,return value 係 None,外面再 print() 就印出 None。describe 嘅 25% / 50% / 75% 係 quartiles:A = [1,2,3],25% = 1.5 (linear interpolation)、50% = median 2、std = 1 (sample std, n−1n-1)。

Lab Q2: DataFrame construction

city_list = ['Tokyo', 'Delhi', 'Shanghai', 'Sao Paulo', 'Mexico City']
pop_list = [37.4, 28.5, 25.6, 21.7, 21.6]
# Step 1
data_dict = {'city': city_list, 'population': pop_list}   # 避免用 dict 做名
# Step 2
country_index = ['Japan', 'India', 'China', 'Brazil', 'Mexico']
# Step 3
df = pd.DataFrame(data_dict, index=country_index)
# Step 4
print(type(df['city']))     # <class 'pandas.core.series.Series'>
print(type(df[['city']]))   # <class 'pandas.core.frame.DataFrame'>
⭐ 考試重點:df['city'] vs df[['city']]

df['city'] 用單一 label → 返回一個 Series (1D,一條 column)。df[['city']] 入面係一個 list of labels → 返回一個 DataFrame (2D,只有一個 column 嘅 table);list 可以放多個 labels,例如 df[['city', 'population']]。

3. Importing data

常見外部檔案:csv (structured)、spreadsheets (structured)、json (semi-structured)、xml (semi-structured)…

3.1 pd.read_csv()

Signature 好長 (sep, header, names, index_col, usecols, dtype, skiprows, nrows, na_values, parse_dates, encoding…)。要識讀嘅參數:

Parameter意思
filepath_or_bufferCSV 檔路徑或 file-like object — 冇 default value → required argument
sepDelimiter (default comma)
header用邊行做 column names
index_col用邊個 column 做 index
usecols只讀部分 columns
dtype指定 columns 嘅 data type (可以用 dict:{'col': 'string'})
parse_dates嘗試 parse 成 datetime
na_values額外當作 NA/NaN 嘅 strings
⭐ 考試重點:讀 function signature

Signature 入面冇 =default 嘅參數係 required (例如 filepath_or_buffer),有 default 嘅都係 optional。同 Week 3 default values 概念一樣。

File paths

  • Current path:"./"
  • 當前路徑下 sub-folder:"./sub_folder/" (slide 亦列 "/sub_folder/")
  • 另一個 drive:"D:/another_folder/" (absolute path)
df = pd.read_csv('your_file_with_its_path.csv')                 # 同一 folder
df = pd.read_csv('sub_folder/your_file_with_its_path.csv')      # relative path
df = pd.read_csv('D:/absolute_path_to_your_file/your_file.csv') # absolute path

dir_path = './Datasets/week_4/'          # 只放 folder,唔包括檔名
hero_power = pd.read_csv(dir_path + 'superhero_powers.csv')     # string 相加砌完整路徑
hero_power          # 667 rows × 168 columns (hero_names + 167 個 True/False power columns)
hero_power.head()   # 頭 5 rows
⚠️ 常見錯誤

dir_path 尾記得有 /。Notebook Q3 寫 dir_path = './Datasets/week_4' (冇尾 /),dir_path + 'titanic.csv' 就會變成 './Datasets/week_4titanic.csv' → FileNotFoundError。

3.2 pd.read_excel()

同 read_csv 差唔多,但要指定 sheet:

hero_dc = pd.read_excel(dir_path + 'superhero_info.xlsx', sheet_name='DC Comics')
hero_marvel = pd.read_excel(dir_path + 'superhero_info.xlsx', sheet_name='Marvel Comics')
# 如果出現 missing "openpyxl" error → %pip install openpyxl

3.3 Previewing DataFrames

hero_power.info()     # RangeIndex: 667 entries;Columns: 168;dtypes: bool(167), object(1)
hero_dc.head()        # 頭 5 rows
hero_marvel.tail(10)  # 尾 10 rows
hero_dc.columns       # Index(['name', 'Gender', 'Eye color', 'Race', 'Hair color', 'Height',
                      #        'Publisher', 'Alignment', 'Weight'], dtype='object')
hero_marvel.index     # RangeIndex(start=0, stop=388, step=1)

留意:dtype 係 pandas 自動推斷 (inferred) 嘅,有時會出乎意料。例如 Marvel data 入面 Height / Weight 有 -99 呢類「假」數值、Race / Hair color 有 -,其實代表 missing — 呢啲係 Week 5–7 data cleaning 要處理嘅問題。

3.4 pandas dtypes

用 df.dtypes 睇。pandas dtypes 唔同 Python types:整數 column 係 int64 而唔係 int,浮點係 float64 而唔係 float。

pandas dtype意思對應 Week 1 attribute type
int6464-bit integerNumerical (discrete)
float6464-bit floating-pointNumerical (continuous)
objectstring 或 mixed typesCategorical / text
boolBoolean valuesBinary
datetime64[ns]date and timeNumerical (dates)
categorycategorical dataCategorical (nominal / ordinal)
🖼️ 圖片內容

dtypes.png (dtype 對照表圖片,PDF 第 18 頁) 文字擷取不到;請對照原 PDF。上表係根據 notebook 文字列表整理。

3.5 指定 dtype 讀入

hero_dtype = {'name': 'string', 'Gender': 'category', 'Eye color': 'string',
              'Race': 'string', 'Hair color': 'string', 'Publisher': 'string',
              'Alignment': 'category'}
hero_dc = pd.read_excel(dir_path + 'superhero_info.xlsx', sheet_name='DC Comics', dtype=hero_dtype)
hero_dc.info()
#  8   Weight      144 non-null     float64     ← DC: 215 rows 只有 144 個 non-null → float64
hero_marvel.info()
#  8   Weight      388 non-null     int64       ← Marvel: 冇 missing → int64
hero_dc.shape, hero_marvel.shape          # (215, 9), (388, 9)
hero_dc.columns == hero_marvel.columns    # array([True, True, ..., True])  兩邊 columns 一樣
💡 點解 DC 嘅 Weight 係 float64?

DC 嘅 Weight 有 71 個 missing (215 − 144)。NaN 本身係 float,所以 pandas 要把成個 column 變 float64 先放得落 — 同上面 s['Tue'] = np.nan 令 Series 由 int64 變 float64 同一道理。

3.6 Exporting data: CSV vs Pickle

用 DataFrame.to_csv() 同 DataFrame.to_pickle() 儲存:

⭐ 考試重點:CSV vs Pickle
CSV (to_csv / read_csv)Pickle (to_pickle / read_pickle)
格式Standard text formatBinary file
Readability✓ 其他程式 / 工具都讀到、可預覽✗ 唔可以預覽
保留 dtypes?✗ column dtypes (Python / pandas 特有) 唔會保存,讀返嚟可能變✓ identical DataFrame,dtypes 100% 還原
安全—⚠️ 只 load trusted source 嘅 pickle files
hero_dc.to_pickle(dir_path + 'hero_dc.pkl')
hero_marvel.to_pickle(dir_path + 'hero_marvel.pkl')

Lab Q3: Data import and export (Titanic)

dir_path = './Datasets/week_4/'
Titanic_dtype = {'PassengerId': 'string', 'Pclass': 'category', 'Name': 'string',
                 'Sex': 'string', 'Age': 'string', 'Fare': 'category'}
Titanic = pd.read_csv(dir_path + 'titanic.csv', dtype=Titanic_dtype)   # Step 1
print(Titanic.dtypes)                                                  # Step 2
Titanic.to_pickle(dir_path + 'Titanic.pkl')                            # Step 3
print(pd.read_pickle(dir_path + 'Titanic.pkl').dtypes)                 # Step 4:同 Step 2 一樣
Titanic.to_csv(dir_path + 'myTitanic.csv', index=False)                # Step 5
print(pd.read_csv(dir_path + 'myTitanic.csv').dtypes)                  # Step 6:dtypes 變返推斷值

Step 7 observation:Pickle 讀返嚟 dtypes 完全一樣 (string / category 保留);CSV 讀返嚟 pandas 重新推斷 — 例如 string 變返 object,category 嘅 Pclass 變 int64,Fare / Age 變 float64 (具體結果視乎數據)。原因:CSV 只係純文字,冇記錄 dtype 資訊。

⚠️ 小陷阱

to_csv() 預設會把 index 都寫入去,讀返時會多咗一個 Unnamed: 0 column;加 index=False 可避免 (slide 冇提,但實務常見)。

4. Combining DataFrames: concat vs merge

兩種方法,視乎 DataFrames 係咪有完全或部分相同嘅 columns / indices。

🖼️ 圖片內容

Combining.png 同 Concat.png (PDF 第 21–22 頁,concat / merge 示意圖) 係圖片,文字擷取不到;請對照原 PDF。

4.1 pd.concat()

pandas.concat(objs, axis=0, join='outer', ignore_index=False, keys=None,
              levels=None, names=None, verify_integrity=False, sort=False, copy=True)
df1 = pd.DataFrame({'account_id': [1, 2, 3, 4],
                    'gender': ['female', 'male', 'female', 'male'],
                    'age': [55, 25, 29, 39]})
df2 = pd.DataFrame({'account_id': [4, 5, 6, 7],
                    'gender': ['female', 'male', 'female', 'male'],
                    'age': [19, 28, 14, 15]})

pd.concat([df1, df2], axis=0)            # 上下疊 rows,保留原 index → 0,1,2,3,0,1,2,3
pd.concat([df1, df2], ignore_index=True) # 上下疊,重新編 index → 0…7
pd.concat([df1, df2], axis=1)            # 左右並排 columns (按 index 對齊) → 4 rows × 6 columns
#    account_id  gender  age  account_id  gender  age
# 0           1  female   55           4  female   19
# ...

join 參數 (default 'outer') 決定 另一條 axis 上 labels 唔一致時點處理:

a = pd.DataFrame({'A': [1, 2], 'B': [3, 4]})
b = pd.DataFrame({'B': [5, 6], 'C': [7, 8]})
pd.concat([a, b])                 # outer:columns A, B, C;冇嘅位填 NaN
pd.concat([a, b], join='inner')   # inner:只保留共同 column B
⚠️ 留意

account_id 4 喺 df1 同 df2 都出現,但 concat 唔理內容,只係機械式咁疊 — 結果有兩行 account_id = 4 (仲要 gender / age 唔同)。如果想按 account_id 配對,就要用 merge。

4.2 pd.merge()

pandas.merge(left, right, how='inner', on=None, left_on=None, right_on=None,
             left_index=False, right_index=False, sort=False, suffixes=('_x', '_y'),
             copy=True, indicator=False, validate=None)
Param意思
on兩個 DataFrames 都有、名字相同嘅 key column。若 on=None 又唔係 merge on indexes,default 用兩邊 columns 嘅交集。
left_on左 DataFrame 嘅 key column (當左右 key 名唔同時用)
right_on右 DataFrame 嘅 key column
howjoin type,default 'inner'
suffixes兩邊有同名但非 key 嘅 columns 時加後綴,default ('_x', '_y')
left = pd.DataFrame({'col1': [1, 2, 3, 4, 5], 'col2': ['a', 'b', 'c', 'd', 'e']})
right = pd.DataFrame({'col1': [3, 4, 5, 6, 7], 'col3': ['f', 'g', 'h', 'i', 'j']})
right2 = pd.DataFrame({'col4': [3, 4, 5, 6, 7], 'col3': ['f', 'g', 'h', 'i', 'j']})

pd.merge(left, right)        # 冇 on → 自動用共同 column col1;default inner
#    col1 col2 col3
# 0     3    c    f
# 1     4    d    g
# 2     5    e    h
pd.merge(left, right2, left_on='col1', right_on='col4')   # key 名唔同
#    col1 col2  col4 col3
# 0     3    c     3    f
# 1     4    d     4    g
# 2     5    e     5    h

4.3 how: inner / outer / left / right / cross

⭐ 考試重點:五種 how
howKeysSQL 對應Key order上例結果 (col1)
inner (default)兩邊 keys 嘅交集INNER JOIN保留 left keys 次序3, 4, 5 (3 rows)
outer兩邊 keys 嘅聯集FULL OUTER JOINkeys 按 lexicographical 排序1–7 (7 rows);1,2 嘅 col3 = NaN;6,7 嘅 col2 = NaN
left只用 left 嘅 keysLEFT OUTER JOIN保留 left 次序1–5 (5 rows);1,2 嘅 col3 = NaN
right只用 right 嘅 keysRIGHT OUTER JOIN保留 right 次序3–7 (5 rows);6,7 嘅 col2 = NaN
crossCartesian productCROSS JOIN保留 left 次序5 × 5 = 25 rows;col1_x, col2, col1_y, col3
pd.merge(left, right, on='col1', how='inner')   # 同 default 一樣
pd.merge(left, right, on='col1', how='outer')
#    col1 col2 col3
# 0     1    a  NaN
# 1     2    b  NaN
# 2     3    c    f
# 3     4    d    g
# 4     5    e    h
# 5     6  NaN    i
# 6     7  NaN    j
pd.merge(left, right, on='col1', how='left')    # col1 = 1,2,3,4,5
pd.merge(left, right, on='col1', how='right')   # col1 = 3,4,5,6,7
pd.merge(left, right, how='cross')              # 25 rows;兩邊都有 col1 → 變 col1_x / col1_y
💡 Cross join row 數

Cross join row 數 = nleft×nrightn_{left} \times n_{right}。所以 5 × 5 = 25。注意 cross 唔用 key,所以同名 column col1 兩份都保留,pandas 用 suffixes _x、_y 分辨。

Lab Q4: Data combining (superheroes)

# Q4.1 載入 pickle,沿 rows concat 並重新編 index
hero_dc = pd.read_pickle(dir_path + 'hero_dc.pkl')
hero_marvel = pd.read_pickle(dir_path + 'hero_marvel.pkl')
hero_info = pd.concat([hero_dc, hero_marvel], ignore_index=True)
# Q4.2
hero_info.shape           # (603, 9)  = 215 + 388 rows
# Q4.3
hero_power = pd.read_csv(dir_path + 'superhero_powers.csv', dtype={'hero_names': 'string'})
print(hero_info.columns)  # name, Gender, ..., Weight
print(hero_power.columns) # hero_names, Agility, ..., Omniscient (168 個)
# Q4.4 名稱 column 唔同名 → left_on / right_on;inner 只保留兩邊都有嘅 heroes
hero_merged = pd.merge(hero_info, hero_power, left_on='name', right_on='hero_names', how='inner')
hero_merged.head(2)       # 2 rows × 177 columns (9 + 168)
# Q4.6
hero_concat = pd.concat([hero_info, hero_power], axis=1, join='inner')
print(f'there are {hero_merged.shape[0]} rows in hero_merged and {hero_concat.shape[0]} rows in hero_concat')
# there are 537 rows in hero_merged and 603 rows in hero_concat
⭐ 考試重點:Q4.1 / Q4.5 / Q4.6 解釋
  • Q4.1 用 concat:DC 同 Marvel 兩個 DataFrames 有相同 columns,只係唔同 heroes → 沿 rows 疊埋 (axis=0, ignore_index=True)。
  • Q4.5 (Q4.4 用 merge):hero_info 同 hero_power 係唔同 columns,要按「hero 名」呢個 key 將同一 hero 嘅資料放喺同一 row。concat 只按 index 位置對齊,唔會識配對名字,所以要用 merge (key 名唔同 → left_on='name', right_on='hero_names')。
  • Q4.6 點解 hero_merged 少啲 rows (537 vs 603)? concat(axis=1, join='inner') 係按 index (0, 1, 2…) 對齊:hero_info index 0–602,hero_power index 0–666,交集係 0–602 → 603 rows — 但第 k 行嘅 info 同 power 未必係同一個 hero!merge inner 係按名字值配對,只保留兩邊都有嘅 heroes (537 個),所以較少但正確。
⚠️ Notebook 入面嘅 bug

Notebook 寫 hero_merged.to_pickle("dir_path+hero.pkl") — 成串喺引號入面,變咗字面檔名 dir_path+hero.pkl 存喺當前 folder,而唔係 ./Datasets/week_4/hero.pkl。正確寫法:hero_merged.to_pickle(dir_path + 'hero.pkl')。(因為讀取時亦用同一錯字串,所以 notebook 仍然 work。)

5. Renaming columns

5.1 String accessor .str

hero.columns 返回一個 Index object;.str 係 string accessor,令你可以對 Series / Index 嘅每個元素 (element-wise) 用 string methods。

hero = pd.read_pickle(dir_path + 'hero.pkl')
hero.columns.str.upper()        # 'NAME', 'GENDER', 'EYE COLOR', ... 全大寫
hero.columns.str.lower()        # 全小寫
hero.columns.str.title()        # 每個字首字母大寫:'Eye Color'
hero.columns.str.capitalize()   # 只係第一個字首字母大寫:'Eye color', 'Hero_names'
hero.columns                    # ← 仍然係原本!上面全部只係返回新 Index,冇改原本
hero.columns = hero.columns.str.lower()   # 要 assign 返先會更新
⭐ 考試重點
  • 直接 call hero.columns.upper() 或 series.upper() 係錯 — upper()/lower() 係 string 嘅 methods,Series / Index 冇;要經 .str accessor。
  • .str.upper() 返回新 object,要 assign 返 hero.columns = ... 先會改原 DataFrame。
  • 同類 accessors:.dt (datetime)、.cat (categorical) — 之後會用到。

5.2 DataFrame.rename()

DataFrame.rename(mapper=None, *, index=None, columns=None, axis=None, copy=True, inplace=False, ...)

mapper / index / columns 可以係 dict (舊名 → 新名) 或者一個 function (會 apply 落每個 label)。

hero = hero.rename(columns={'hero_names': 'hero names'})   # dict:只改指定 column
hero = hero.rename(columns=str.upper)                      # function:所有 column 名大寫
hero = hero.rename(mapper=str.lower, axis='columns')       # 等價:mapper + axis
hero.columns = hero.columns.str.lower()
hero.to_pickle(dir_path + 'hero.pkl')

5.2.1 Lambda as mapper

hero.rename(mapper=lambda x: x.lower(), axis='columns')   # 全小寫
hero.rename(mapper=lambda x: x[:-1], axis='columns')      # 刪每個 column 名最後一個字元 → 'nam', 'gende', ...
hero.rename(mapper=lambda x: x.upper(), axis='columns')   # 全大寫 ✓
hero.rename(mapper=lambda x: x.upper, axis='columns')     # ✗ column 名變成 "<built-in method upper of str object at 0x...>"

注意:以上冇 assign 返 hero = ...,所以只係顯示結果,冇改到 hero。

⭐ Lab Q5:點解有啲 work 有啲唔 work?
hero.rename(mapper=lambda x: x.upper,   axis='columns')  # ✗ wrong
hero.rename(mapper=lambda x: x.upper(), axis='columns')  # ✓ correct
hero.rename(mapper=str.upper,           axis='columns')  # ✓ correct
hero.rename(mapper=str.upper(),         axis='columns')  # ✗ wrong

mapper 要嘅係一個 function 本身 (pandas 會對每個 column 名 x 自己 call 佢)。

  • str.upper (冇括號) = 傳入 function 本身 → pandas 對每個名 call str.upper(x) ✓。
  • str.upper() (有括號) = 即刻 call,但冇畀 string 佢 → TypeError (缺 argument),根本傳唔到 function 入去 ✗。
  • lambda x: x.upper() = 一個 function,body 入面 call x.upper(),返回大寫 string ✓。
  • lambda x: x.upper = 一個 function,但 body 只係攞 method object 而冇 call,返回嘅係 “built-in method upper” 呢個 method 本身 → 新 column 名變成 method 嘅描述 ✗。

一句總結:傳 function 時唔加括號;喺 function body 入面要 call 時先加括號。 (同 Week 2 help(list.append) 唔加括號同一道理。)

6. Main take-aways

  • Lambda functions
  • pandas package
  • DataFrame and Series structures
  • Reading and writing data (CSV, Excel, Pickle)
  • Data combining: difference between concat and merge
  • Columns renaming

How to submit:將題目 copy 去新 notebook 作答,交 question-answer-only notebook 嘅 PDF 版本去 Moodle,確保 code 同 output cells 清楚印出。

自我測驗 Self-test

Q1. s = pd.Series([10, 20, 30], index=['a', 'b', 'a'])。s['a'] 返回咩?s.iloc[1] 呢?

s['a'] 返回一個 Series,包含兩個值 (a: 10, a: 30),因為 label 'a' 重複。s.iloc[1] 用 position → 20。

Q2. s = pd.Series([2, 4, 6, 8]);s[1] = np.nan 之後 s.mean() 同 s.dtype 係咩?

Mean = (2 + 6 + 8)/3 = 16/3 ≈ 5.33 (NaN 被 skip,分母變 3)。dtype 由 int64 變 float64,因為 NaN 係 float。

Q3. pd.DataFrame([[1, 2, 3], [4, 5, 6]]) 同 pd.DataFrame({'x': [1, 2, 3], 'y': [4, 5, 6]}) 嘅 shape 各係咩?

第一個:list of lists,每個 inner list 一 row → (2, 3)。第二個:dict of lists,每個 key 一 column → (3, 2)。

Q4. 點解以下 DataFrame 全部係 NaN?s = pd.Series([1, 2, 3]); pd.DataFrame({'v': s}, index=['x', 'y', 'z'])

Series 有自己嘅 index 0, 1, 2;DataFrame 按 index label 對齊,'x','y','z' 喺 s 入面搵唔到,所以全填 NaN。應該 pd.Series([1,2,3], index=['x','y','z'])。

Q5. df['city'] 同 df[['city']] 有咩分別?

前者返回 Series (1D);後者入面係 list of column labels,返回只有一個 column 嘅 DataFrame (2D)。

Q6. 你將一個有 category 同 string dtype 嘅 DataFrame 存做 CSV 再讀返,會發生咩?想完整保留 dtypes 應該點?

CSV 唔儲存 dtype 資訊,讀返時 pandas 重新推斷:string → object,category → int64 / object 等。想 100% 保留就用 to_pickle() / read_pickle(),但 pickle 唔可預覽、只應開 trusted source。

Q7. A 有 key [1, 2, 3, 4],B 有 key [3, 4, 5]。pd.merge(A, B, on='key', how=?) 喺 inner / outer / left / right / cross 各有幾多 rows?

inner:{3, 4} → 2 rows;outer:{1, 2, 3, 4, 5} → 5 rows;left:4 rows;right:3 rows;cross:4 × 3 = 12 rows。(假設 keys 冇重複。)

Q8. DC (215 rows) 同 Marvel (388 rows) 有相同 columns,應該用 concat 定 merge 合併?寫出 code。

用 concat 沿 rows 疊:hero_info = pd.concat([hero_dc, hero_marvel], ignore_index=True) → (603, 9)。merge 係用嚟按 key 將唔同 columns 嘅資料配對。

Q9. 寫 code 將 df 所有 column 名轉細楷並把空格換成底線 (例如 'Eye color' → 'eye_color')。

df.columns = df.columns.str.lower().str.replace(' ', '_'),或者 df = df.rename(columns=lambda x: x.lower().replace(' ', '_'))。記得 assign 返。

Q10. 點解 rename(mapper=str.upper()) 錯,而 rename(mapper=str.upper) 啱?

mapper 需要一個 function 本身,由 pandas 對每個 label call。str.upper 係 function object ✓;str.upper() 會即刻執行 (而且冇 argument → TypeError),傳入嘅唔係 function ✗。同理 lambda x: x.upper() ✓ 但 lambda x: x.upper 返回 method object 而唔係 string ✗。

一頁總結 Cheat sheet

⭐ 考前速記
  • Series = 1D labeled array (index + data);DataFrame = 2D table,每個 column 係一個 Series。Constructor 同 class 同名:pd.Series(), pd.DataFrame()。
  • Series vs list:flexible index labels、element-wise ops (s**2)、較快。
  • Series:s['new'] = v 插入;s['label'] 取值 (重複 label 返回多個);position 用 s.iloc[i];mean() skip NaN;NaN 令 int → float;missing 用 np.nan 唔好用 "NA"。
  • DataFrame:list of lists → rows;dict of lists → columns;Series 會按 index 對齊 (對唔上 → NaN)。
  • Attributes (冇括號):shape, columns, index, dtypes;Methods:head(n), tail(n), info() (return None), describe()。
  • df['c'] → Series;df[['c']] → DataFrame。
  • read_csv(path, sep, header, index_col, usecols, dtype, parse_dates, na_values);path 唯一 required;read_excel(path, sheet_name=...) 要 openpyxl。
  • dtypes:int64, float64, object, bool, datetime64[ns], category (≠ Python int / float)。
  • CSV:可讀、通用、唔保留 dtypes;Pickle:binary、100% 保留 dtypes、只開 trusted source。
  • concat(objs, axis=0, join='outer', ignore_index=False):按 axis 疊、按 index 對齊、唔理內容。
  • merge(left, right, how='inner', on / left_on / right_on, suffixes=('_x','_y')):按 key 值配對 = SQL JOIN。inner 交集 · outer 聯集 · left / right 保留一邊 · cross 笛卡兒積 (n×mn \times m)。
  • Superhero:concat DC+Marvel → 603;merge inner on name → 537;concat axis=1 inner 按 index → 603 但配錯人。
  • Rename:df.columns = df.columns.str.lower() (要 assign);rename(columns={'old':'new'});rename(columns=str.upper);rename(mapper=lambda x: x.lower(), axis='columns')。傳 function 唔加括號。