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()匯出。 - 分辨
concatvsmerge;merge嘅on/left_on/right_on同how(inner, outer, left, right, cross)。 - 用
.straccessor 同DataFrame.rename()(dict / function / lambda mapper) 改 column labels。
⭐ 考試必溫 Top-5
concat(按 axis 疊 / 按 index 對齊) vsmerge(按 key column 值配對,SQL join);how五種結果。df['col']→ Series vsdf[['col']]→ DataFrame。- CSV vs Pickle:CSV 通用可讀但唔保留 dtypes;Pickle 100% 還原 dtypes 但只可開 trusted source。
- Index alignment:用有自己 index 嘅 Series 建 DataFrame 再指定另一個 index → 全部
NaN。 - Rename:
.str.upper()要 assign 返;rename(mapper=str.upper)✓ vsstr.upper()✗;lambda x: x.upper()✓ vslambda 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):
| Series | DataFrame | |
|---|---|---|
| 維度 | 1D indexed array | 2D table,有 index (row labels) 同 column labels |
| 類比 | Spreadsheet 嘅一個 column | Excel 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 有靈活嘅 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 自動被跳過
原本 9 個值總和 136,平均 。將 Tue (=2) 改成 NaN 後,pandas 計 mean 時預設 skip NaN,剩 8 個值總和 134,平均 。分母都變咗!呢個係 Week 5–7 missing values 處理嘅伏筆。
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 → 每個 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.shape | tuple (rows, columns) | df.head(n) | 頭 n rows (default 5) |
df.columns | column labels | df.tail(n) | 尾 n rows |
df.index | index (row labels) | df.info() | 簡要 summary:index dtype、column dtypes、non-null values、memory usage |
df.dtypes | 每個 column 嘅 data type | df.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, )。
describe() 嘅 mean、std、quartiles 正係 COM6005 Module 1 descriptive statistics;pandas 嘅 std() 預設用 sample standard deviation (分母 ):
例:A = [1,2,3],,。
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)…
Structured vs semi-structured vs unstructured data 正係 COM6002 Big Data Management 嘅內容;亦對應 Week 1 嘅 Variety。
3.1 pd.read_csv()
Signature 好長 (sep, header, names, index_col, usecols, dtype, skiprows, nrows, na_values, parse_dates, encoding…)。要識讀嘅參數:
| Parameter | 意思 |
|---|---|
filepath_or_buffer | CSV 檔路徑或 file-like object — 冇 default value → required argument |
sep | Delimiter (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 |
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 |
|---|---|---|
int64 | 64-bit integer | Numerical (discrete) |
float64 | 64-bit floating-point | Numerical (continuous) |
object | string 或 mixed types | Categorical / text |
bool | Boolean values | Binary |
datetime64[ns] | date and time | Numerical (dates) |
category | categorical data | Categorical (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 有 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 (to_csv / read_csv) | Pickle (to_pickle / read_pickle) | |
|---|---|---|
| 格式 | Standard text format | Binary 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 |
how | join 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 | Keys | SQL 對應 | Key order | 上例結果 (col1) |
|---|---|---|---|---|
inner (default) | 兩邊 keys 嘅交集 | INNER JOIN | 保留 left keys 次序 | 3, 4, 5 (3 rows) |
outer | 兩邊 keys 嘅聯集 | FULL OUTER JOIN | keys 按 lexicographical 排序 | 1–7 (7 rows);1,2 嘅 col3 = NaN;6,7 嘅 col2 = NaN |
left | 只用 left 嘅 keys | LEFT OUTER JOIN | 保留 left 次序 | 1–5 (5 rows);1,2 嘅 col3 = NaN |
right | 只用 right 嘅 keys | RIGHT OUTER JOIN | 保留 right 次序 | 3–7 (5 rows);6,7 嘅 col2 = NaN |
cross | Cartesian product | CROSS 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 數 = 。所以 5 × 5 = 25。注意 cross 唔用 key,所以同名 column col1 兩份都保留,pandas 用 suffixes _x、_y 分辨。
pd.merge() 直接對應 COM6002 SQL 嘅 JOIN:pd.merge(left, right, on='col1', how='left') ≈ SELECT * FROM left LEFT JOIN right ON left.col1 = right.col1;。pd.concat(axis=0) 就似 SQL 嘅 UNION ALL。Inner / outer 亦對應 Week 2 set 嘅 intersection (&) / union。
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 用 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 寫 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 冇;要經.straccessor。 .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。
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 對每個名 callstr.upper(x)✓。str.upper()(有括號) = 即刻 call,但冇畀 string 佢 → TypeError (缺 argument),根本傳唔到 function 入去 ✗。lambda x: x.upper()= 一個 function,body 入面 callx.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 清楚印出。
Week 4 嘅 loading → combining → renaming 係 CRISP-DM「Prepare Data」嘅第一步,亦係 COM6004 Chapter 2 Data preprocessing (aggregation、integration) 嘅實作。下一講 Week 5 會開始處理本講見到嘅 missing values / 怪值 (例如 Weight = −99、NaN)。
自我測驗 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 笛卡兒積 ()。- 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 唔加括號。