Day 16 NumPy 運算與索引
引言
昨天認識了 ndarray 與基本運算,今天要把 NumPy 真正威力強大的部分學起來:索引、切片、廣播與統計函式。這幾個機制讓我們能用極短的程式碼完成大量資料的篩選、轉換與彙整,是資料科學家每天都在用的核心技能。學會之後,從此處理幾萬甚至幾百萬筆資料都不再痛苦,可以說是從「寫 Python」升級到「用 Python 做資料工作」的轉捩點。在真實的資料科學工作裡,NumPy 的角色雖然會逐漸被 Pandas 與 PyTorch 取代,但底層運算邏輯完全相同,所以這幾天打好的基礎會一直派上用場。
這幾個觀念也會一路用到後續的 Pandas 與 PyTorch:Pandas 的 DataFrame 篩選語法借鏡自 NumPy 的布林索引,PyTorch 的張量運算更直接延伸 NumPy 的廣播規則。先把基礎打穩,後面就會順很多。建議在進入今天的內容前,確認昨天安裝的 NumPy 還能正常匯入,並熟悉昨天的 ndarray 建立方式。今天的範例都不大,可以放心全部跑過一遍,確認輸出與預期一致。
索引與切片:取出你想要的部分
NumPy 的索引寫法和 Python 串列很像:中括號內放索引值,從 0 開始。多維陣列則用逗號分隔,例如 x[row, col]。切片用冒號寫成 x[start:stop:step],可以省略任何一段。掌握這些基本寫法之後,就能用一致的方式處理向量、矩陣甚至更高維度的張量。例如影像資料常用 (高度, 寬度, 通道) 三維陣列,這時用 x[:, :, 0] 就能取出第一個通道的所有像素。
import numpy as np
x = np.arange(1, 13).reshape(3, 4)
print(x)
# 輸出:
# [[ 1 2 3 4]
# [ 5 6 7 8]
# [ 9 10 11 12]]
print(x[0]) # 第一列:[1 2 3 4]
print(x[0, 0]) # 第一列第一個:1
print(x[-1, -1]) # 最後一列最後一個:12
print(x[1, :]) # 第二列整列
print(x[:, 1]) # 第二欄整欄
print(x[:2, :2]) # 左上 2x2 子矩陣
切片取出的是「檢視(view)」,修改切片會連動到原陣列。如果想拿到獨立副本,要用 x[:, 1].copy()。這個觀念在處理大型資料時很重要,能避免不必要的記憶體複製,同時也提醒我們修改資料時要格外小心。Python 串列的切片會自動產生新串列,但 NumPy 為了效能預設走 view 路線,這是新手常踩的雷,寫大型程式時要特別注意。當你不確定手上的陣列是 view 還是 copy,可以呼叫 x.base,如果回傳不是 None 就代表它是 view。
布林索引:用條件篩選資料
布林索引是 NumPy 最常用的篩選方式:用一個布林陣列當作索引,只有 True 的位置會被保留。這比寫一堆 if 條件或迴圈簡潔太多,特別適合做資料清洗。配合 NumPy 提供的逐元素比較,整個篩選邏輯可以一行寫完。例如把 NaN 改成 0、把大於某門檻的值過濾掉、或把符合條件的位置標記起來,都能用布林索引一行搞定。
import numpy as np
scores = np.array([85, 42, 73, 90, 58, 67])
mask = scores >= 60
print(mask) # 輸出:[ True False True True False True]
print(scores[mask]) # 輸出:[85 73 90 67]
print(scores[scores >= 60]) # 等同上一行
# 多條件:用 & (且)、| (或)
good = scores[(scores >= 60) & (scores < 90)]
print(good) # 輸出:[85 73 67]
# 直接修改符合條件的元素
scores[scores < 60] = 60
print(scores) # 輸出:[85 60 73 90 60 67]
注意多條件的寫法:每個條件要加括號,並用 & 與 | 而不是 and、or,因為後者是純 Python 的邏輯運算,沒辦法用在陣列上。這個寫法也是 Pandas 篩選 DataFrame 的基礎,會一路用到後面。實務上做資料清理時,最常見的動作就是把離群值或缺失值「抓出來」,布林索引正是這件事的核心工具。np.isnan() 也是常用的布林函式,搭配 ~ 可以反向取出「非缺失值」的元素。
廣播:不同形狀的陣列一起運算
廣播(broadcasting)讓形狀不完全相同的陣列也能做逐元素運算。例如把一個 (3, 4) 的矩陣加上 (4,) 的向量,NumPy 會自動把向量「廣播」成 (3, 4),整個計算一行就完成,不必手動做 tile 或 reshape。這個機制是 NumPy 寫起來又短又快的關鍵之一,理解之後許多看似複雜的運算都能化為直覺的陣列寫法。
import numpy as np
matrix = np.arange(12).reshape(3, 4)
print(matrix)
# 輸出:
# [[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
bias = np.array([10, 20, 30, 40])
print(matrix + bias)
# 輸出:
# [[10 21 32 43]
# [14 25 36 47]
# [18 29 40 48]]
# 欄方向的廣播
col_bias = np.array([[100], [200], [300]])
print(matrix + col_bias)
廣播的規則有兩條:維度數比較少的那個陣列,會在最左邊補 1;接著比較每個維度的大小,如果某一維不相等,必須其中一個是 1,否則會報錯。把規則記起來,未來除錯時可以省下不少時間。常見的應用包括「把每筆資料減去平均值做標準化」、「把特徵乘上縮放係數」、「加上偏置項」等,這些都是機器學習前處理天天會做的事。如果遇到形狀不相容的錯誤,先 print 兩邊的 shape,再試著手動 reshape 看看哪裡可以對齊。
統計與數學函式:沿著軸做彙總
NumPy 提供豐富的統計函式:sum()、mean()、min()、max()、std() 等。它們都能指定 axis,沿著某一維做彙總。例如對二維矩陣,axis=0 是「對每一欄做彙總」,axis=1 是「對每一列做彙總」。掌握 axis 的方向之後,從平均值到標準差都能用一行解決。keepdims 參數能讓結果保留原來的維度,方便後續做廣播運算,這在做神經網路時特別好用。
import numpy as np
x = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(x.sum()) # 全部加總:45
print(x.mean(axis=0)) # 每欄平均:[4 5 6]
print(x.mean(axis=1)) # 每列平均:[2 5 8]
# 累積、找最大、找位置
print(np.cumsum(x)) # 累積和
print(x.argmax()) # 最大值索引(攤平後)
print(x.argmax(axis=0)) # 每欄最大值索引
# 線性代數基礎
A = np.array([[2, 1], [1, 3]])
print(np.linalg.inv(A)) # 反矩陣
print(np.linalg.eigvals(A)) # 特徵值
axis 的方向很容易搞混,可以記一個小口訣:axis=0 是「沿著第一個維度(列)走訪,最後壓縮掉」,所以結果是「每欄一個數」。在後面做機器學習時,會大量用 axis 來區分「對樣本平均」或「對特徵平均」,務必熟悉。np.linalg 模組還提供了矩陣乘法、分解、求特徵值等線性代數運算,這些在後續的線性代數章節會再次出現。除此之外,np.median、np.percentile 也是常用工具,做資料分佈分析時幾乎一定會用到。
結語
今天把 NumPy 的核心機制走完一輪:索引與切片讓我們精準取出想要的元素,布林索引用條件快速篩選,廣播讓不同形狀的陣列能一起運算,統計函式則把資料快速彙總成結論。這幾個觀念的組合,能讓原本需要幾十行程式的工作縮短成兩三行,是資料處理的重要利器。建議自己挑一份小資料(例如成績單、銷售紀錄)練習今天的工具,從中體會「NumPy 寫起來跟 Excel 函式差不多,但能處理更大資料」這件事。如果你對廣播還不太熟練,可以試著把一個一維向量同時加到二維矩陣的每一列、每一欄,這對理解軸的方向非常有幫助。學會這些之後,接下來的 Pandas 章節會非常順手,因為它的核心觀念幾乎是 NumPy 的延伸。
明天,我們會進入 Pandas,看看它如何把 NumPy 的陣列包裝成有欄位、有索引的 DataFrame,並用更直覺的方式處理 CSV 與表格資料。Pandas 在資料科學的日常工作中使用頻率非常高,學會之後處理各種試算表資料都會更得心應手,等於補齊了從 NumPy 純陣列到真實世界表格之間的最後一塊拼圖。
留言
張貼留言