COM6001 Computing with Big Data · Week 5
Pandas Data Manipulation (II)
COM6001 · Week 5 · Source: Lecture05_notebook.pdf(Jupyter notebook export, 51 pages)· Lecturer: Dr. Aileen Hou
Contents · 13 sections
🎯 本講重點 (Learning outcomes)
- 用 string accessor
.str及DataFrame.rename()改 column labels,明白點解mapper要傳「function 本身」而唔係「function 嘅結果」。 - 分清 reference(
df_ref = df)同 copy(df.copy()),識用is/id()驗證。 - Selecting:
select_dtypes()揀 columns;.loc(label-based)vs.iloc(integer position-based)揀 rows;Boolean indexing、& | ~、isin()。 - Subsetting:
df.loc[row_indexer, column_indexer]同時揀 rows + columns。 - Modifying data:用 selection + assignment 更新 cell / row / column / block,加新 row / column。
- Sorting:
sort_values()、nlargest()、nsmallest()同keep參數。 - Data cleaning 入門:點解要 clean(Garbage in, Garbage out)、unnecessary data(constant values、duplicated columns)用
nunique()、value_counts()、compare()檢查。
⭐ 考試必溫 Top-5
.loc[m:n]係 label-based,頭尾都包括;.iloc[m:n]係 position-based,包頭唔包尾(同 Python slicing 一樣)。df.loc[x](單一 label)→ Series;df.loc[[x]](list)或df.loc[m:n](slice)→ DataFrame。- 多個條件要用 element-wise operators
&、|、~,每個條件加括號;用and/or會出 error。==係比較,=係 assignment。 df_ref = df只係多一個名指向同一個 object;要獨立副本必須df.copy()。rename(mapper=str.upper)✔、lambda x: x.upper()✔;lambda x: x.upper✘(回傳 method object)、str.upper()✘(即刻 call 冇 argument)。
0. 背景:hero dataset 同 notebook 結構
今堂 notebook 用一個 superhero dataset hero.csv(537 rows × 178 columns):頭幾個 columns 係 name、gender、eye color、race、hair color、height、publisher、alignment、weight、hero_names,之後 167 個 bool columns 代表 superpowers(例如 agility、flight、super strength)。hero.info() 顯示 dtypes: bool(167), float64(1), int64(2), object(8)。另外第一個 column Unnamed: 0 係之前 save CSV 時連 index 一齊寫出嚟嘅產物,之後會 drop 走。
Notebook 先複習 Week 4 嘅 Renaming(Section 5),然後係 Week 5 新內容:1. Copying → 2. Selecting → 3. Subsetting → 4. Modifying → 5. Sorting → 6. Data Cleaning (1)。
import pandas as pd
dir_path = "./Datasets/week_5/" # dataset 唔喺 study folder 入面
hero = pd.read_csv(dir_path + "hero.csv")
PDF 係由 Jupyter HTML 印出嚟,好多 DataFrame 輸出被截斷或者欄位錯位,部分 code 行尾亦被切走(例如 ascending=[Tru…)。以下解釋以可讀到嘅 code 同輸出(rows × columns 數目、Series 值)為準,被截斷嘅部分會根據輸出推斷並註明。
1. Renaming (Week 4 複習)
1.1 String accessor .str
hero.columns 係一個 Index object(唔係 string),所以唔可以直接 call .upper()。要經 string accessor .str 先可以對每個 element 逐個套用 string methods(elementwise)。
hero.columns = hero.columns.str.upper() # 全部大楷
hero.columns = hero.columns.str.lower() # 全部細楷
hero.columns = hero.columns.str.title() # 每個字第一個字母大楷:'Eye Color'
hero.columns.str.capitalize() # 只有第一個字嘅第一個字母大楷:'Eye color'
ExamSlide 特別強調:It is INCORRECT to call upper() or lower() directly on Series or DataFrame objects,因為佢哋係 str 嘅 methods,必須經 .str。同樣邏輯之後會見到 datetime accessor .dt(Week 7)同 categorical accessor .cat。
Notebook 示範咗:執行 hero.columns.str.upper() 之後再睇 hero.columns,label 冇變。因為 .str.upper() 回傳一個新嘅 Index,原本嘅 object 唔會被改。要 hero.columns = hero.columns.str.lower() assign 返先生效。
1.2 DataFrame.rename()
DataFrame.rename(mapper=None, *, index=None, columns=None, axis=None, copy=True, ...)
columns=可以係 dict(舊名 → 新名,只改部分 columns),或者係一個 function(套用落每一個 label)。mapper=+axis='columns'(或axis='index')效果等同columns=/index=。
hero = hero.rename(columns={'hero_names': 'hero names'}) # dict:只改一個
hero = hero.rename(columns=str.upper) # function:全部改大楷
hero = hero.rename(mapper=str.lower, axis='columns') # 等同 columns=str.lower
hero.columns = hero.columns.str.lower()
hero.to_pickle(dir_path + "hero.pkl") # 存成 pickle,保留 dtype,下次 read_pickle 讀返
原 notebook 寫咗 hero.to_pickle("dir_path+hero.pkl")——成個係 string literal,會真係 save 成一個叫 dir_path+hero.pkl 嘅檔案。正確寫法係 hero.to_pickle(dir_path + "hero.pkl")(冇引號包住 dir_path)。
1.3 Lambda functions 作為 mapper
hero.rename(mapper=lambda x: x.lower(), axis='columns') # 全部細楷
hero.rename(mapper=lambda x: x[:-1], axis='columns') # 每個 label 刪最後一個字元:'name' → 'nam'
hero.rename(mapper=lambda x: x.upper(), axis='columns') # 全部大楷
輸出可見 x[:-1] 令 columns 變成 nam、gende、heigh、publishe……(冇 assign 返,所以只係預覽)。
1.4 Q5 (Week 4):點解 lambda x: x.upper 唔得?
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
rename()需要一個 callable(可以被 call 嘅 function),佢會自己對每個 label call 一次。str.upper(冇括號)= function object 本身 → rename 會做str.upper('name')→'NAME'✔。lambda x: x.upper()→ lambda 本身係 callable,入面有 callupper(),回傳 string ✔。lambda x: x.upper→ lambda 回傳嘅係 bound method object(未 call),所以新 column 名變成<built-in method upper of str object at 0x...>(notebook 輸出正正係咁)✘。str.upper()→ 喺傳入 rename 之前已經即刻 call,而且冇俾 string argument →TypeError✘。
2. Copying DataFrames
Copying = 建立一個內容一樣、但獨立嘅新 DataFrame,令修改新 DataFrame 唔會影響原本嗰個。要用 DataFrame.copy(),佢會 copy indices 同 data。可以用 is operator 或 id() 檢查兩個名係咪指向同一個 object。
df = pd.DataFrame([[1, 2, 3],
[4, 5, 6],
[7, 2, 9]],
columns=['A', 'B', 'C'],
index=['Row 1', 'Row 2', 'Row 3'])
df_ref = df # 只係另一個名(reference)
df_copy = df.copy() # 真正嘅副本
print(id(df), id(df_ref), id(df_copy))
# 6074892656 6074892656 5889568256 ← 頭兩個一樣
df['org_ind'] = True # 改 df
df_ref['ref_ind'] = True # 改 df_ref = 改 df
df_copy['copy_ind'] = False # 只改 df_copy
df.columns # A B C org_ind ref_ind
df_ref.columns # A B C org_ind ref_ind (同 df 一模一樣)
df_copy.columns # A B C copy_ind (只有自己嘅改動)
df is df_ref # True
df is df_copy # False
df_ref = df | df_copy = df.copy() | |
|---|---|---|
| 係咪新 object? | 唔係,同一個 object(id 相同) | 係,新 object(id 唔同) |
| 改 df_ref / df_copy 會唔會影響 df? | 會 | 唔會 |
is 結果 | df is df_ref → True | df is df_copy → False |
同樣道理適用於 Python list:a_ref = a; a_ref[2] = 4 會令 a 都變;a.copy() 先係獨立。
a = [1, 2, 3]
a_copy = a.copy()
a_ref = a
a_ref[2] = 4 # a 都變成 [1, 2, 4]
print(a_ref is a) # True
print(a_copy is a) # False
Variable 名就好似「標籤貼紙」。df_ref = df 只係喺同一個箱度多貼一張貼紙;df.copy() 先係搵個新箱,將啲嘢複製入去。所以喺 function 入面改 DataFrame 之前,如果唔想影響原本數據,記得先 .copy()(Week 7 OrdinalEncoder 例子都有用 df.copy())。
Copying integers(optional,self-study)
Integer 冇 copy() method,因為 int 係 immutable,冇需要 copy。就算用 copy.deepcopy(),Python 都唔會整新 object:
- 細而常用嘅 integers(slide 寫 [-5, 255]/[-5, 256];CPython 實際係 −5 至 256)會被 intern:每個值只有一個 object,所以
i1 = 1; i2 = 1嘅id完全一樣。 - 範圍外(例如 −6):
i1 = -6同i2 = -6分開寫會係兩個唔同 object(id 唔同);但i1_ref = i1同copy.deepcopy(i1)都只係指返同一個 object。
3. Selecting columns
3.1 select_dtypes():按 data type 揀 columns
DataFrame.select_dtypes(include=None, exclude=None) # → 回傳 DataFrame
include:要保留嘅 dtypes;exclude:要排除嘅 dtypes。- 可以用 pandas dtypes:
int64、float64、object、bool、datetime64、category;亦可以用 Python types:int、float、str、bool。 - 廣義類別:
'number'包晒所有 numeric(int64、float64);object包 string-like。
hero = pd.read_pickle(dir_path + "hero.pkl")
hero.select_dtypes(include=['int64']) # 537 rows × 1 column(height)
hero.select_dtypes(include=['int']) # 用 int 都得,同樣結果
hero.select_dtypes(include=['number','string']) # 537 rows × 8 columns:name, eye color, race, ..., height, weight
Exam呢招最常用喺 data cleaning:numeric_cols = df.select_dtypes(include='number').columns、cat_cols = df.select_dtypes(exclude='number').columns,之後分開處理 numerical 同 categorical columns(Week 6 imputation 會再用)。
3.2 用 labels 揀 columns
hero[['gender', 'race']] # 傳入一個 list of labels → 537 rows × 2 columns(DataFrame)
hero['race'] # 單一 label → Series
df['a'](單括號、單一 label)回傳 Series;df[['a']](雙括號,即 list)回傳 DataFrame。呢個規則同下面 .loc[4] vs .loc[[4]] 一樣。
4. Selecting rows:.loc 與 .iloc
揀 rows 有兩大類方法:
- Index based:
df.loc[]= label based selection;df.iloc[]= integer (position) based selection。 - Condition based:Boolean indexing,例如
df[df['age'] > 30]或df.loc[df['age'] > 30]。
4.1 .loc indexer
# 如果由 hero.csv 讀,先 drop 走多餘 column
hero.drop(columns=['Unnamed: 0'], inplace=True) # inplace=True:直接改 hero 本身
hero.loc[1:4] # labels 1 至 4(包括 4)→ 4 rows × 177 columns
type(hero.loc[4]) # pandas.core.series.Series (一個 label)
type(hero.loc[[4]]) # pandas.core.frame.DataFrame (list of labels)
type(hero.loc[1:4]) # pandas.core.frame.DataFrame (slice)
.loc[m:n] 包含兩端
.loc[m:n] is a label-based slicing from label m to label n (inclusive both m & n)。呢點同 Python slicing(最後一個 index excluded)唔同,因為呢個唔係用數字位置 slice,而係用 labels。所以 hero.loc[1:4] 有 4 rows(1,2,3,4),而 hero.iloc[1:4] 只有 3 rows(position 1,2,3)。
4.2 用 set_index() 令 index 變成有意義嘅 labels
hero = hero.set_index('name') # 'name' column 變成 row index
hero.to_pickle(dir_path + 'hero_name_as_index.pkl')
hero.index # Index(['Abin Sur', 'Adam Strange', ..., 'Ymir'], dtype='string', name='name', length=537)
hero.loc['Superman'] # 一個 label → Series(Name: Superman, Length: 178)
hero.loc['Superman':'Wonder Woman'] # label range(包括 Wonder Woman)→ 13 rows × 178 columns
原 notebook 喺 hero.loc['Superman'] 旁邊寫「select one row --> a DataFrame」,但輸出明明係 Name: Superman, Length: 178, dtype: object,即係 Series。記住規則:單一 label → Series。
4.3 .iloc indexer
.loc 用 label,好難「移動」:想攞 'Superman' 嘅下一行,hero.loc['Superman'+1] 係唔得嘅(string 加 int)。要攞連續 rows,用 .iloc 做 slicing 更好:
.iloc嘅 integer positions:start inclusive,stop exclusive,同標準 Python slicing 一樣。
hero.iloc[0] # 第一行:Name: Abin Sur(Series)
hero.iloc[1] # 第二行:Name: Adam Strange
hero.iloc[1:4] # position 1,2,3 → Adam Strange, Alan Scott, Amazo(3 rows)
# Superman 嘅下一行:先 .loc 由 Superman 開始 slice,再 .iloc[1]
hero.loc['Superman':] # 356 rows(由 Superman 到最尾)
hero.loc['Superman':].iloc[1] # Name: Swamp Thing
PDF 第 20 頁有一張 iloc_slicing.png 圖解 iloc slicing,文字擷取不到;請對照原 PDF。上面嘅文字規則(start inclusive、stop exclusive)已足夠理解。
.loc + .iloc將 label 同 position 兩種世界接駁:先用 label 搵「起點」(.loc['Superman':] 產生一個新 DataFrame,Superman 喺 position 0),再喺新 DataFrame 用 position 行一步(.iloc[1])。
5. Selecting rows with Boolean criteria
.loc 可以接收一個 Boolean Series,揀出值為 True 嘅 rows——好似俾 indexer 一串「逐行 True/False」。df[condition] 同 df.loc[condition] 可以互換。條件表達式必須逐行輸出 True/False,例如一個 column 同某值比較。
df = pd.DataFrame([[1, 2, 3],
[4, 5, 6],
[7, 2, 9]],
columns=['A', 'B', 'C'],
index=['Row1', 'Row2', 'Row3'])
df['A'] == 4 # 比較(condition)→ Row1 False, Row2 True, Row3 False
df['A'] = 4 # assignment!成個 column A 變晒 4
df[df['B'] == 2] # 唔用 .loc 都得 → Row1, Row3
df.loc[df['B'] == 2] # 同上
# df.iloc[df['B'] == 2] ✘ 錯!iloc 唔收帶 index 嘅 Boolean Series
== vs =df['A'] == 4 係 condition expression(回傳 Boolean Series);df['A'] = 4 唔係 condition,係 assignment statement,會將成個 column 覆蓋。Q1.1 就係考呢點:hero['alignment'] = 'good' 會將所有 hero 嘅 alignment 改成 'good',唔係揀出 good 嘅 heroes。
5.1 用 .iloc 做 Boolean selection(optional)
條件嘅輸出有兩部分:indexable(index:Row1, Row2…)同 values(True/False)。.iloc 只識 values,所以要用 .values 剝走 index:
mask = df['B'] == 2
mask.values # array([ True, False, True])
mask.index # Index(['Row1', 'Row2', 'Row3'], dtype='object')
df.iloc[mask.values] # → Row1, Row3
5.2 Cascaded Boolean indexing:&、|、~
多個條件要用 element-wise Boolean operators:&(AND)、|(OR)、~(NOT)。用 Python 嘅 and/or 唔會 element-wise 運作,會 raise error。
# 此時 df 嘅 A 已經全部係 4
df.loc[(df["B"] == 2) & (df["C"] < 6)] # 只有 Row1(B=2 且 C=3)
df.loc[(df["B"] == 2) | (df["C"] < 6)] # Row1, Row3
Python 中 & 嘅 precedence 高過 ==、<。如果寫 df["B"] == 2 & df["C"] < 6,會先計 2 & df["C"],結果完全錯/出 error。每個條件一定要用括號包住。
5.3 Series.isin()
isin(values):Series 每個 element 如果喺提供嘅 set 入面就 True,否則 False。之後可以 .sum() 數 True(True = 1),即係符合條件嘅 rows 數目。
hero['race'].isin(["Human"]) # Boolean Series, Length 537
hero['race'].isin(["Human"]).sum() # np.int64(165) → 165 個 Human
(~hero['race'].isin(['Human', 'Mutant'])).sum() # np.int64(315) → 唔係 Human 亦唔係 Mutant
hero[~hero['race'].isin(['Human ', 'Mutant'])] # ⚠️ 480 rows,結果仲見到 Human!
因為 'Human ' 尾有一個空格,同 data 入面嘅 'Human' 唔相等,所以 Human 冇被排除,只排除咗 Mutant。結果係 480 rows(537 − Mutant 數目),而唔係 315。教訓:字串比較係完全精確嘅,多一個空格都唔同——呢個亦係 data cleaning 要處理 inconsistent values 嘅原因(可以用 .str.strip() 清走頭尾空格)。
5.4 Q1 練習(含答案)
# Q1.2 讀 data、set index、drop 'Unnamed: 0',揀 alignment == 'good'
import pandas as pd
path = './Datasets/week_5/'
hero = pd.read_csv(path + 'hero.csv')
hero = hero.set_index('name')
hero = hero.drop(columns=['Unnamed: 0'])
hero.loc[hero['alignment'] == 'good'] # with .loc → 347 rows × 176 columns
hero[hero['alignment'] == 'good'] # without .loc → 347 rows × 176 columns
# Q1.3 Female 而且 height > 200
hero[(hero['gender'] == 'Female') & (hero['height'] > 200)]
# → 6 rows:Bloodaxe(218), Frenzy(211), Hela(213), She-Hulk(201), Thundra(218), Wolfsbane(366)
# Q1.4 eye color 既唔係 green 亦唔係 red
hero[~hero['eye color'].isin(['green', 'red'])] # 428 rows
(~hero['eye color'].isin(['green', 'red'])).sum() # np.int64(428)
6. Subsetting:同時揀 rows 同 columns
Subsetting = 同時選 rows 同 columns,用逗號分隔 row indexer 同 column indexer:
df.loc[row_indexer, column_indexer]:label-based,start 同 stop inclusive。df.iloc[row_indexer, column_indexer]:position-based,start inclusive,stop exclusive。- row_indexer / column_indexer 可以係 slices、lists、arrays 或 Boolean masks。
:代表「全部」。
hero.loc[:, 'race'] # 所有 rows,'race' column → Series(dtype string)
hero.loc['Adam Strange':'Amazo', 'race':'hair color'] # rows Adam Strange..Amazo;columns race..hair color(全包)
# race hair color
# Adam Strange Human Blond
# Alan Scott - Blond
# Amazo Android -
hero.iloc[1:4, 2:10:2] # rows position 1–3;columns position 2,4,6,8(step 2)
# → race, height, alignment, hero_names
PDF 第 31 頁有 subsetting.png 圖解 row/column indexer,文字擷取不到;請對照原 PDF。
6.1 Two-dimensional conditional selection
Boolean 條件可以放喺 row indexer 或 column indexer。注意:我哋通常係對 columns 嘅值落條件,但產生嘅 Boolean Series 係逐行嘅,所以佢係放喺第一個參數(row indexer)。
# row-index:(hero['flight'] == True) & (hero['gender'] == 'Male')
# column-index:":" 全部 columns
hero.loc[(hero['flight'] == True) & (hero['gender'] == 'Male'), :] # 136 rows × 176 columns
# Q2:只顯示 'hero_names' 同 'race'
hero[(hero['flight'] == True) & (hero['gender'] == 'Male')][['hero_names', 'race']] # option 1:無 .loc
hero.loc[(hero['flight'] == True) & (hero['gender'] == 'Male'), ['hero_names', 'race']] # option 2:.loc
# → 136 rows × 2 columns
Option 1 係「chained indexing」:先篩 rows 得到一個中間 DataFrame,再揀 columns。讀資料冇問題,但如果用嚟改值(assignment),可能只係改咗中間副本(pandas 會出 SettingWithCopyWarning)。改值時一定用 Option 2 嘅單一 .loc[rows, cols] = ...。
df.loc[condition, ['col1','col2']] 對應 COM6002 SQL 嘅 SELECT col1, col2 FROM table WHERE condition:column indexer ≈ SELECT(projection),row condition ≈ WHERE(selection);&/|/~ ≈ AND/OR/NOT;isin([...]) ≈ IN (...)。
7. Modifying data
7.1 Updating values
用「selection + assignment statement」改指定 cells:
- 單一 cell:
df.loc[x_label, y_label] = new_value或df.iloc[x, y] = new_value - 成個 row / column:
df.iloc[x, :] = new_value或df.iloc[:, y] = new_value - 一個 block:
df.iloc[x1:x2, y1:y2] = new_value
df = pd.DataFrame([[1, 2, 3], [4, 5, 6], [7, 2, 9]], columns=['A', 'B', 'C'])
df.iloc[2, 1] = -100 # 第 3 行第 2 列
# A B C
# 0 1 100 3 ← (notebook 之前已改 B[0]=100)
# 1 4 5 6
# 2 7 -100 9
df.iloc[0:2, 0:2] = 999 # rows 0–1、columns 0–1(stop exclusive)
# A B C
# 0 999 999 3
# 1 999 999 6
# 2 7 -100 9
df.iloc[0, :] = 1000 # 成個 row 0
# A B C
# 0 1000 1000 1000
7.2 Adding entries(加 column / row)
df[3] = [3, 3, 3] # 新 column,label 係 integer 3
df.loc[3] = [3, 3, 3, 4] # 新 row,label 3(要俾齊 4 個 columns 嘅值)
df[10000] = True # 新 column '10000',全部 True
對 .loc 用一個唔存在嘅 label 做 assignment,pandas 唔會報錯,而係新增嗰個 row / column。Notebook 有個好例子:當 df 嘅 index 係 'Row1','Row2','Row3' 時執行 df.loc[0, 'B'] = 100,因為冇 label 0,結果係多咗一個新 row 0,只有 B = 100,其他 columns 係 NaN;而因為出現 NaN,int columns 被轉成 float64(1 → 1.0)。
A B C 10000
Row1 1.0 2.0 3.0 True
Row2 4.0 5.0 6.0 True
Row3 7.0 2.0 9.0 True
0 NaN 100.0 NaN NaNhero = pd.read_pickle(path + 'hero_name_as_index.pkl')
hero = hero.reset_index() # 'name' 變返普通 column,index 變返 0..536
# 一定要 assign 返!reset_index() 預設回傳新 DataFrame
hero.loc[10000] = hero.loc[533] # 複製 X-Man 嗰行,新增一行 label 10000(加喺最尾)
hero.tail() # 最後一行 index 係 10000
Notebook 部分 cells 係亂序執行(In[] 號碼跳來跳去),所以某些輸出嘅 column 數(177/178/179/180)同 df 內容未必同上一格一致,例如 df 一時有 column 3、一時有 10000。理解每句 code 嘅效果就夠。
7.3 Updating a selection of types
將 column labels 收集成一個變數,再用嚟做 selection 同 assignment——好適合一次過改多個 columns:
numeric_cols = hero.select_dtypes(include='number').columns # height, weight
hero[numeric_cols] = hero[numeric_cols] * 100 # 全部 numeric columns ×100
hero[numeric_cols].iloc[:5]
# height weight
# 0 18500 9000.0
# 1 18500 8800.0
hero[numeric_cols] = hero[numeric_cols] / 100 # revert
7.4 Q3 Heroes' BMI
BMI 公式(height 要用 meter,而 dataset 係 cm):
hero['BMI'] = hero['weight'] / (hero['height'] / 100) ** 2
hero['BMI'].mean() # notebook 預期約 26(輸出 np.float64(26.23272651733959))
例:Abin Sur height 185 cm、weight 90 kg → 。
輸出可見有啲 hero 嘅 height / weight 係 -99(例如 Swamp Thing height -99、Ymir weight -99.0),即係用特殊值代表 missing。直接計 BMI 會出現無意義嘅數。Week 6 會學點樣用 na_values / describe() 搵出呢類 placeholder。
8. Sorting
df.sort_values():按一個或多個 columns,升序或降序。df.nlargest(n, columns):最大嘅 n 行(降序)。df.nsmallest(n, columns):最細嘅 n 行(升序)。
# 先按 race 升序,同一 race 內按 height 降序(code 行尾被截斷,由輸出推斷 ascending=[True, False])
hero[['race', 'height']].sort_values(by=['race', 'height'], ascending=[True, False])
# race height
# 211 Symbiote 229.0
# 514 Symbiote 229.0
# 515 Symbiote 226.0
# ... (Symbiote 內 height 由大到細)
8.1 nlargest() 與 keep 參數
根據 help(hero.nlargest):等同 df.sort_values(columns, ascending=False).head(n),但 more performant。唔可以用喺 object / category dtype columns(會 TypeError)。
keep 處理 ties(並列)
keep= | 意思 |
|---|---|
'first'(default) | 並列時優先保留最早出現嘅 |
'last' | 並列時優先保留最後出現嘅 |
'all' | 保留所有同「最細入圍值」並列嘅 rows,即使超過 n 行 |
Help 文件例子(population):nlargest(3, 'population') → France, Italy, Malta;keep='last' → France, Italy, Brunei(434000 並列中最後一個);keep='all' → France, Italy, Malta, Maldives, Brunei(5 行)。注意 nlargest 唔係揀「n 個不同值」。
hero.nlargest(6, 'height', keep="last")
# Fin Fang Foom 975, Galactus 876, Groot 701, Wolfsbane 366, MODOK 366, Ymir 305
# → 305 有三個並列(Onslaught, Sasquatch, Ymir),keep='last' 揀最後嘅 Ymir;6 rows
hero.nlargest(6, 'height', keep="all")
# → 8 rows:多咗 Onslaught 305、Sasquatch 305(所有並列 305 都保留)
8.2 Q4:最細嘅 6 個「正數」height,保留並列
hero[hero['height'] > 0].nsmallest(6, 'height', keep='all')
# 先排除 -99 等非正數,再 nsmallest;notebook 輸出 6 rows × 177 columns
# (名單包括 Utgard-Loki、Krypto、Rocket Raccoon、Bloodwraith、Anti-Monitor、Giganta)
> 0?因為 -99 代表 missing。如果唔先過濾,最細嘅幾個全部都會係 -99,冇意義。呢個正正示範 sorting 之前要先了解數據。
9. Data Cleaning (1):Handling unnecessary data
9.1 What is data cleaning?
ExamDefinition: Data cleansing or data cleaning is the process of detecting and correcting (or removing) corrupt or inaccurate records from a record set, table, or dataset and refers to identifying incomplete, incorrect, inaccurate or irrelevant parts of the data and then replacing, modifying, or deleting the dirty or coarse data.
- Why? Garbage in, Garbage out——輸入數據有問題,分析/模型結果都唔會可靠。
- Data analysis procedures:Data collection → Data cleaning → Data analysis → Insights
- Data Audit:每個 dataset 都唔同,可能有唔同問題:Inconsistent values、Unnecessary data、Missing data、Incorrect encodings。
- Removing unnecessary data (dropping):Repeated columns、Duplicated rows、Constant values
- Handling the missing data:Dropping、Imputing using constants、Imputing using statistics、Imputing using models
- Handling the outliers
檢查工具:Constant values → value_counts()、nunique();One-to-one mapped data(冇額外資訊)→ value_counts()、nunique();Duplicated data → compare()、duplicated()、drop_duplicates()、drop()。
9.2 World Development Indicators (wdi) dataset
wdi = pd.read_csv(dir_path + 'world_development_indicators.csv')
wdi.info(verbose=True)
# RangeIndex: 446 entries;22 columns
# 例如 atms_per_100000 417 non-null、alcohol_consumption_per_capita 220 non-null、country_category 289 non-null
wdi[['country_name', 'planet', 'Country Name', 'Country Code']]
# country_name planet Country Name Country Code
# 0 Afghanistan Earth Afghanistan AFG
# 1 Albania Earth Albania ALB
一眼睇落已經見到可疑嘅地方:planet 好似全部都係 Earth;country_name 同 Country Name 好似一模一樣;Country Code 似乎同國家名一一對應。
9.3 Constant values:nunique() 與 value_counts()
DataFrame.nunique():回傳每個 column 嘅 unique values 數目。dropna=True(default)只數 non-NaN;dropna=False將 NaN 都當成一個 unique value。Series.value_counts():每個值出現幾多次。normalize=True輸出比例(%);normalize=False(default)輸出絕對次數。
wdi.nunique(dropna=True)
# country_name 217, year 2, Country Name 217, Country Code 217,
# country_category 2, is_region 2, planet 1 ← planet 只有 1 個值!
wdi.nunique(dropna=False) - wdi.nunique(dropna=True)
# 結果 = 1 嘅 columns 代表有 NaN:atms_per_100000, compulsory_education_years,
# health_expenditure_pct_of_gdp, population_density, alcohol_consumption_per_capita, country_category
wdi['planet'].value_counts(normalize=True) # Earth 1.0(100%)
wdi['planet'].value_counts() # Earth 446
Conclusion:用 nunique() 或 value_counts() 檢查有冇 column 只有一個值。Constant column(例如 planet 全部 Earth)對分析冇任何資訊量,應該 drop。
小技巧:nunique(dropna=False) - nunique(dropna=True) 等於 1 嘅 column 就係含有 missing values 嘅 column。
9.4 Duplicated columns
wdi['country_name'] == wdi['Country Name'] # 446 個 True/False,太長難睇
wdi['country_name'].compare(wdi['Country Name']) # 空 DataFrame(self / other 冇 row)→ 冇差異
(wdi['country_name'] == wdi['Country Name']).value_counts() # True 446 → 全部相同
Series.compare(other) 只會列出有差異嘅位置(self vs other 兩欄);輸出係空嘅 = 兩個 column 完全一樣(亦可以用 .shape 確認 0 rows)。
9.5 Q5:寫 function 回傳單一值判斷兩 column 是否相同
def assert_single_value_result(col1, col2, return_type):
"""return_type='bool':全部相同回傳 True;
return_type='int':全部相同回傳 0,否則回傳不相同的數目 (>0)"""
if return_type == 'bool':
return (col1 == col2).all() # 全部 True 先係 True
else:
return (col1 != col2).sum() # 數不相同嘅個數
assert_single_value_result(wdi['country_name'], wdi['Country Name'], return_type='bool') # np.True_
assert_single_value_result(wdi['country_name'], wdi['Country Name'], return_type='int') # np.int64(0)
NaN == NaN 係 False。如果兩個 column 喺同一位置都係 NaN,== 會當佢哋唔同。更穩陣可以用 col1.equals(col2)(將同位置 NaN 視為相等)或者 col1.compare(col2).shape[0] == 0。
COM6004 Chapter 2(Data)講 data quality(noise、outliers、missing values、duplicate data)同 data preprocessing;今堂嘅 constant / duplicated columns 就係 feature selection 入面最基本嘅「刪走 redundant / irrelevant features」。COM6002 嘅 Normalization 概念亦係避免重複儲存同一資訊(例如 Country Name 同 Country Code 一一對應)。
10. Take away points
- Copying(reference vs
.copy()) - Selecting by
.locor.iloc,1-D 或 2-D - Updating data and sorting data
- Why data cleaning;Unnecessary data
- Duplicated columns and rows:
compare()、duplicated() - Constant or one-to-one mapped values:
value_counts()、nunique()
(功課提交:將問題 copy 去新 notebook 作答,print 成 PDF 交 Moodle,確保 code 同 output cells 清楚可見。)
自我測驗 Self-test
Q1. 一個 DataFrame 嘅 index 係 0–9(default RangeIndex)。df.loc[2:5] 同 df.iloc[2:5] 各有幾多 rows?
df.loc[2:5] 係 label-based,包含 2,3,4,5 → 4 rows。df.iloc[2:5] 係 position-based,stop exclusive,包含 position 2,3,4 → 3 rows。
Q2. 執行以下 code 之後,df 有冇 X column?df2 = df; df3 = df.copy(); df2['X'] = 1; df3['Y'] = 2
有 X,冇 Y。df2 只係 df 嘅 reference(df2 is df 為 True),改 df2 即係改 df;df3 係獨立副本,改佢唔影響 df。
Q3. 解釋點解 hero.rename(mapper=lambda x: x.upper, axis='columns') 出咗奇怪 column 名,而 mapper=str.upper 冇括號反而啱?
rename 需要一個 callable,佢會對每個 label 自己 call。str.upper 就係 function object,rename 會做 str.upper(label)。lambda x: x.upper 雖然 lambda 係 callable,但佢回傳 x.upper 呢個未被 call 嘅 bound method,所以新名係 <built-in method upper of str object at ...>。正確應為 lambda x: x.upper()。至於 str.upper() 係即刻 call 而冇 argument,會 TypeError。
Q4. 寫一句 code:揀出 gender 為 Male 或 height 大於 200 嘅 heroes,只顯示 race 同 publisher。
hero.loc[(hero['gender'] == 'Male') | (hero['height'] > 200), ['race', 'publisher']]要點:用 | 唔係 or;每個條件加括號;條件放 row indexer,column list 放第二個位置。
Q5. hero['race'].isin(['Human ', 'Mutant']) 有咩問題?點修正?
'Human ' 尾有空格,同 data 中嘅 'Human' 唔相等,所以 Human 唔會被 match。修正:寫 'Human';或者先清理 data:hero['race'] = hero['race'].str.strip()。
Q6. 有 df(index 為 'r1','r2',columns A,B 皆 int)。執行 df.loc[0, 'A'] = 5 後會點?
因為冇 label 0,pandas 會 enlargement:新增一個 index 為 0 嘅 row,A = 5,B = NaN。由於出現 NaN,B column(甚至 A)會變成 float64。唔會報錯——呢個係常見 bug 來源;如果想改「第一行」應該用 df.iloc[0, 0] = 5。
Q7. heights = [305, 701, 366, 305, 975, 366, 305](index 0–6)。nlargest(4, keep='first')、keep='last'、keep='all' 各回傳邊啲 index?
排序:975(4), 701(1), 366(2), 366(5), 305(0), 305(3), 305(6)。頭 4 個係 975, 701, 366, 366 —— 第 4 名 366 有兩個並列,兩個都入到,所以三種 keep 都係 index [4, 1, 2, 5]。如果改成 nlargest(5):first → 加 index 0;last → 加 index 6;all → 加 0、3、6(共 7 rows)。
Q8. 點樣用一句 code 搵出 DataFrame 入面所有 constant columns 嘅名?
wdi.columns[wdi.nunique(dropna=False) == 1] # → Index(['planet'])nunique() 回傳每 column unique 數,等於 1 即 constant。用 dropna=False 可以避免「一個值 + 一堆 NaN」被誤判為 constant。
Q9. wdi.nunique(dropna=False) - wdi.nunique(dropna=True) 結果為 1 代表咩?
代表嗰個 column 含有至少一個 NaN(因為 dropna=False 將 NaN 多數一個 unique value)。結果 0 = 冇 missing values。
一頁總結 Cheat sheet
.straccessor 對 Index/Series 逐個做 string method;要 assign 返df.columns = ...先生效。rename(columns={'old':'new'});rename(columns=str.upper);rename(mapper=lambda x: x.lower(), axis='columns')。傳 function 本身,唔好加()。df_ref = df→ 同一 object;df.copy()→ 獨立;用is/id()驗證。select_dtypes(include='number')/exclude='number'→ 分 numeric / categorical columns。df['a']→ Series;df[['a']]→ DataFrame;.loc[4]→ Series;.loc[[4]]、.loc[1:4]→ DataFrame。.loclabel-based、slice 包尾;.ilocposition-based、slice 唔包尾。set_index('name')令 labels 有意義;reset_index()還原(記得 assign)。- Boolean indexing:
df[cond]=df.loc[cond];.iloc要cond.values。 &|~+ 括號;==比較 vs=assignment。isin([...])+.sum()數符合條件 rows;小心字串空格。- Subsetting:
df.loc[row_cond, ['c1','c2']];df.iloc[1:4, 2:10:2]。 - 改值:
df.loc[r, c] = v、df.iloc[i, j] = v、df.iloc[0:2, 0:2] = v;唔存在嘅 label → 新增 row/column(enlargement)。 sort_values(by=[...], ascending=[True, False]);nlargest/nsmallest(n, col, keep='first'|'last'|'all')。- Data cleaning:Garbage in, Garbage out;collection → cleaning → analysis → insights。
- Unnecessary data:constant(
nunique()==1、value_counts(normalize=True))、duplicated columns(compare()空 /(a==b).value_counts())。