All Revision Notes

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

  1. .loc[m:n] 係 label-based,頭尾都包括;.iloc[m:n] 係 position-based,包頭唔包尾(同 Python slicing 一樣)。
  2. df.loc[x](單一 label)→ Series;df.loc[[x]](list)或 df.loc[m:n](slice)→ DataFrame。
  3. 多個條件要用 element-wise operators &、|、~,每個條件加括號;用 and/or 會出 error。== 係比較,= 係 assignment。
  4. df_ref = df 只係多一個名指向同一個 object;要獨立副本必須 df.copy()。
  5. 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。

⚠️ 常見錯誤:冇 assign 返

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 小 bug

原 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
⭐ 考試重點:function object vs function call
  • rename() 需要一個 callable(可以被 call 嘅 function),佢會自己對每個 label call 一次。
  • str.upper(冇括號)= function object 本身 → rename 會做 str.upper('name') → 'NAME' ✔。
  • lambda x: x.upper() → lambda 本身係 callable,入面有 call upper(),回傳 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
⭐ 考試重點:reference vs copy
df_ref = dfdf_copy = df.copy()
係咪新 object?唔係,同一個 object(id 相同)係,新 object(id 唔同)
改 df_ref / df_copy 會唔會影響 df?會唔會
is 結果df is df_ref → Truedf 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 comment 與輸出不符

原 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)已足夠理解。

💡 直覺:chain .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!
⚠️ Notebook 問題:「Why we see Humans in the result?」

因為 '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:

⭐ 考試重點:2-D indexing 語法
  • 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 vs Option 2

Option 1 係「chained indexing」:先篩 rows 得到一個中間 DataFrame,再揀 columns。讀資料冇問題,但如果用嚟改值(assignment),可能只係改咗中間副本(pandas 會出 SettingWithCopyWarning)。改值時一定用 Option 2 嘅單一 .loc[rows, cols] = ...。

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
⭐ 考試重點:「setting with enlargement」

對 .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   NaN
hero = 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):

BMI=weight (kg)height (m)2\text{BMI} = \frac{\text{weight (kg)}}{\text{height (m)}^2}
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 → 90/1.852=90/3.4225≈26.390 / 1.85^2 = 90/3.4225 \approx 26.3。

⚠️ 數據陷阱

輸出可見有啲 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。
⭐ 考試重點:Data cleaning 大綱(Week 5–7 都會用)
  • 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 == NaN 係 False。如果兩個 column 喺同一位置都係 NaN,== 會當佢哋唔同。更穩陣可以用 col1.equals(col2)(將同位置 NaN 視為相等)或者 col1.compare(col2).shape[0] == 0。

10. Take away points

  • Copying(reference vs .copy())
  • Selecting by .loc or .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

⭐ 考前速記
  • .str accessor 對 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。
  • .loc label-based、slice 包尾;.iloc position-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())。