在處理 Pandas DataFrame 時,
我們經常需要對資料進行「轉換」或「清理」。
這時候,新手最常遇到的問題就是:
「我到底該用 map() 還是 apply()?」
簡單來說,這兩者的核心差異在於**「作用的範圍」**:
df.map():微觀視角(Element-wise)。
它把表格看成一個個獨立的「儲存格」,
逐一對每一個格子做事。df.apply():宏觀視角(Axis-wise)。
它把表格看成一條條的
「直行(Column)」或「橫列(Row)」,
每次抓出一整行或一整列來做事。
(註:在 Pandas 2.1.0 之前的版本,df.map() 被稱為 df.applymap(),
新版已統一更名為 map()。)
接下來,我們直接透過兩段實用程式碼來深度解析!
實戰案例一:清理表格中的髒資料 (使用 df.map)
當我們從網頁或 PDF 萃取表格時,
格子裡常常會混入多餘的空白(例如 " Apple ")。
我們需要一個函數來清理這些空白,
並把全空的列與欄刪除。
來看看 normalize_table_df 函數:
import pandas as pd
def normalize_table_df(table_df: pd.DataFrame) -> pd.DataFrame:
normalized_df = table_df.copy()
# 關鍵點 1:使用 .map() 針對「每一個儲存格」進行字串清理
normalized_df = normalized_df.map(
lambda value: value.strip() if isinstance(value, str) else value
)
# 關鍵點 2:將空字串轉為標準缺失值,以便後續 dropna 辨識
normalized_df = normalized_df.replace("", pd.NA)
# 關鍵點 3:刪除全為 NA 的橫列與直行
normalized_df = normalized_df.dropna(axis=0, how="all")
normalized_df = normalized_df.dropna(axis=1, how="all")
return normalized_df.reset_index(drop=True)💡 為什麼這裡必須用 df.map() 而不是 df.apply()?
在這段程式碼中,
我們寫了一個 lambda 條件判斷:isinstance(value, str)。
我們希望程式去檢查:「這個格子裡裝的是不是字串?」
- ✅ 使用
df.map()時:value真的就是每一個格子的內容(例如" Apple "、123、NaN)。
所以isinstance(" Apple ", str)會回傳True,
接著順利執行.strip()清除空白。 - ❌ 如果錯用
df.apply()時:apply預設是一次抓一整欄(Column)。
這時傳進去的value會是一個
Pandas Series(一整排資料),而不是單一字串。
如果你問 Python:「這『一整排資料』是不是一個字串?」
(isinstance(Series, str)),答案永遠是False!
結果就是你的.strip()完全不會被執行,
資料根本沒有被清理到。
結論:當你需要對「每一個獨立的儲存格」
做型別檢查、字串操作或數學運算時,df.map() 是你的唯一首選。
實戰案例二:判斷表格是否具有意義 (理解 Axis-wise 的概念)
清理完表格後,
有時候我們會得到一些只有 1、2 個字的「破碎表格」。
我們需要一個機制來過濾掉這些垃圾資料。
來看看 is_meaningful_table 函數:
def is_meaningful_table(table_df: pd.DataFrame) -> bool:
if table_df.empty:
return False
# pandas DataFrame 的 .sum() 行為說明:
# 1. table_df.notna():把整個表格所有的格子變成 True/False (有值為 True, 沒值或 NA 為 False)
# 2. 第一次 .sum():會「逐個直行(column)」把 True 當成 1 算總和,回傳的是一個 Series,例如:colA 有 2 個值, colB 有 1 個值
# 3. 第二次 .sum():把剛剛算出來的 Series (每一行的總數) 再全部加總起來,得到非空值的總數。
# (註:Pandas 加總後回傳的型別其實是 numpy.int64,但在這做 >= 3 的比較,可直接當一般整數使用)
non_empty_cells = table_df.notna().sum().sum()
# 這裡的邏輯是:如果整張表格有值(非空)的格子加起來不到 3 個,我們就當它是誤判的垃圾碎片
return non_empty_cells >= 3💡 這裡的 .sum() 其實就體現了 df.apply() 的精神!
雖然這段程式碼沒有直接寫出 apply(),
但 Pandas 內建的 .sum()、.mean() 等聚合函數,
它們的運作邏輯與 df.apply() 完全一致,
都是 Axis-wise(以軸為單位) 的操作。
讓我們拆解 table_df.notna().sum().sum() 的過程:
table_df.notna():這是一個類似map的動作,
把每個格子變成 True/False。- 第一次
.sum():這就像是df.apply(sum)。
它預設沿著axis=0(直行)往下加總。
它不會一次把整張表加完,而是一欄一欄算,
最後吐出一個 Series(例如:A欄有2個值,B欄有1個值)。 - 第二次
.sum():這時候是對剛剛產生的 Series 做加總,
把 2 + 1 變成 3,得到最終的總數。
如果你硬要用 apply 來重寫第一次的 sum(),它會長這樣:table_df.notna().apply(lambda col: col.sum())
(當然,直接寫 .sum() 效能更好且更簡潔,這裡僅為觀念對照)。
📝 總結:一秒決定該用誰?
下次寫程式時,只要問自己一個問題:
「我的操作對象是誰?」
透過這兩個函數的完美搭配,
你不僅能將髒亂的表格洗得乾乾淨淨(map 的功勞),
還能精準剔除沒有價值的破碎表格(Axis-wise 聚合的功勞),
這就是 Pandas 資料處理的藝術!
推薦hahow線上學習python: https://igrape.net/30afN
在 Pandas 中,用 0 和 1 來代表方向,
對人類的直覺來說真的很容易搞混。
為了解決這個問題,
Pandas 官方非常貼心地提供了
字串別名 (String Aliases),
這也是目前非常推崇的寫法,
因為它能大幅提升程式碼的「可讀性」。
它們的對應關係如下:
axis=0等同於axis="index"(在某些函數中也可以寫axis="rows")- 好記法:Index(索引)是直直往下排的。
所以axis="index"代表動作是
**「由上往下,跨越一列一列」**去執行的。
- 好記法:Index(索引)是直直往下排的。
axis=1等同於axis="columns"- 好記法:Columns(欄位)是橫向往右排的。
所以axis="columns"代表動作是
**「由左往右,跨越一欄一欄」**去執行的。
- 好記法:Columns(欄位)是橫向往右排的。
把這個好習慣套用到你的程式碼中!
回到一開始的 normalize_table_df 函數,
我們把 dropna 的部分換成這個更好懂的寫法:
# ❌ 原本的寫法(過幾個月回來看可能會忘記 0 和 1 是什麼)
normalized_df = normalized_df.dropna(axis=0, how="all")
normalized_df = normalized_df.dropna(axis=1, how="all")
# ✅ 升級後的寫法(語意超清晰,一看就懂!)
normalized_df = normalized_df.dropna(axis="index", how="all")
# 刪除全部都是 NA 的「橫列」
normalized_df = normalized_df.dropna(axis="columns", how="all")
# 刪除全部都是 NA 的「直行」強烈建議:
以後寫 Pandas 時,
只要遇到需要設定 axis 的地方
(像是 .sum(), .mean(), .drop(), .dropna() 等),
都直接改用 "index" 和 "columns"。
這不僅能少死很多腦細胞,
如果以後有其他同事接手你的程式碼,
他們一定會非常感謝你!
推薦hahow線上學習python: https://igrape.net/30afN