Learning Objectives
- 用 np.array 建立陣列,讀懂 shape、ndim 與 dtype
- 用向量化運算取代 for 迴圈,並說出串列與陣列做 * 2 的差別
- 在二維陣列上用 axis 算每一欄或每一列的統計量
- 用廣播規則判斷兩個形狀能不能一起運算、結果是什麼形狀
- 用布林遮罩篩選元素、計數與算比例,用 np.where 依條件換值
為什麼不用迴圈?
Python 的串列(list)什麼都能放,但要對每個元素做計算,就得寫迴圈:一個一個取出、計算、放回去。 資料有幾千、幾百萬筆時,這樣寫又長又慢。
NumPy 提供的 ndarray(n 維陣列)換了一種做法:陣列裡的元素都是同一種型別、在記憶體裡排在一起,
寫 arr * 60 時,NumPy 用編譯過的程式一次處理整個陣列。這種「對整個陣列下一個指令」的寫法叫向量化(vectorization):
- 程式比較短,意思也比較清楚(「每個元素乘 60」就寫成
* 60) - 資料量大時比 Python 迴圈快很多
- 下一課的 pandas 就建在 NumPy 上:資料表的每一欄底下都是一個陣列
這一課的範例都是自己打的幾個數字,用來看清楚每一步在做什麼。
逐步看:串列用迴圈,陣列一行完成
同一件事(把讀書時數換算成分鐘)寫兩次。注意右邊記憶體區:陣列 arr 畫成一列表格。
shape、ndim 與 dtype
每個陣列都有三個常看的屬性:
| 屬性 | 意思 | np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) |
|---|---|---|
shape |
每一軸的長度 | (2, 3):2 列 3 欄 |
ndim |
軸的數目(維度) | 2 |
dtype |
元素的型別,整個陣列只有一種 | float64(64 位元的小數) |
dtype 是陣列和串列最大的不同:串列可以混放字串與數字,陣列裡全部是同一種型別。
建立陣列時,NumPy 依內容決定 dtype:有小數就是 float64,全是整數就是整數型別。
關於整數型別的一個提醒:np.array([1, 2, 3]).dtype 在一般的 64 位元電腦上通常是 int64;
但這個頁面的 Python 跑在瀏覽器裡(Pyodide,32 位元環境),會顯示 int32。
兩者能表示的範圍不同(int32 最大約 21 億),一般的成績、人數用不到這麼大;要處理很大的整數時,可以寫 np.array(..., dtype="int64") 指定型別。
pandas 讀 CSV 得到的整數欄,在兩個環境都是 int64。
二維陣列可以想成表格:scores[1] 取第 1 列(從 0 數起),scores[:, 0] 取第 0 欄,scores[1, 2] 取一格。
算統計量時用 axis 指定沿著哪一軸算:axis=0 是沿著列往下,每一欄得到一個數;axis=1 是沿著欄往右,每一列得到一個數。
逐步看:二維陣列與 axis
一個 4 位學生 × 3 次小考的成績表。每一步對照右邊的表格,看取出的是哪一列、哪一欄。
廣播:形狀不同也能一起算
scores + 5 把 5 加到每一格;scores + bonus(bonus 是三個數)把三個數分別加到三欄。
形狀不同的陣列能一起運算,靠的是廣播(broadcasting)規則:
- 兩個形狀從最右邊的軸開始對齊;維度比較少的,在左邊補長度 1。
- 每一軸:長度相同可以;其中一個是 1,就把它延伸成另一個的長度;其他情況出錯。
| A 的形狀 | B 的形狀 | 對齊 | 結果 |
|---|---|---|---|
| (4, 3) | 純量 | 純量延伸到每一格 | (4, 3) |
| (4, 3) | (3,) | 3 與 3 相同;B 補成 (1, 3) 再延伸 | (4, 3) |
| (4, 3) | (4,) | 最右軸 3 與 4 對不齊 | 錯誤 |
| (4, 3) | (4, 1) | 3 與 1 → 延伸;4 與 4 相同 | (4, 3) |
第三列是最常見的錯:想讓每一列減去該列的平均(形狀 (4,)),卻因為對齊是從右邊開始而出錯。
解法是用 reshape(4, 1) 把它變成 4 列 1 欄。
逐步看:每格減去平均,以及一個對不齊的例子
同一個成績表,依序加純量、加一個 (3,) 的陣列、減去每次小考的平均,再試著減去每位學生的平均。
布林遮罩:用條件挑元素
final >= 60 也是向量化運算:每個元素各比一次,得到一個同樣形狀、裝著 True/False 的布林陣列。常見的用法:
| 寫法 | 得到 |
|---|---|
final[final >= 60] |
只留下條件為 True 的元素(布林遮罩) |
(final >= 60).sum() |
True 當 1、False 當 0 加起來=符合條件的個數 |
(final >= 60).mean() |
符合條件的比例 |
np.where(條件, 甲, 乙) |
條件成立的位置放甲,否則放乙 |
兩個條件要同時成立用 &、任一成立用 |,每個條件都要加括號:final[(final >= 60) & (final < 80)]。
不能用 Python 的 and、or,因為它們只能判斷一個 True 或 False,不能逐個元素比較。
逐步看:及格人數、比例與加分
用固定種子產生 10 個期末分數(每次執行都一樣),再用布林陣列計數與篩選。
實作:每一科減去該科平均
起始碼有一個 3 位學生 × 4 科的成績表。請用向量化運算(不要寫迴圈)依序印出:
- 每一科(每一欄)的平均,四捨五入到小數 1 位
- 每一格減去該科平均的結果,四捨五入到小數 1 位
- 每一科有幾位學生高於該科平均
小測驗:陣列、axis、廣播與遮罩
下面程式會印出什麼?
import numpy as np
a = np.array([1, 2, 3])
print(a * 2, [1, 2, 3] * 2)
scores 的形狀是 (4, 3)。scores.mean(axis=0) 的形狀是什麼?
下面哪一組和形狀 (4, 3) 的陣列一起運算時會出錯?
下面程式會印出什麼?
import numpy as np
x = np.array([3, 8, 5, 10, 1])
print(x[x > 4].sum(), (x > 4).sum())
對一百萬個數字做同樣的計算,NumPy 的向量化寫法通常比 Python 的 for 迴圈快,主要原因是什麼?
重點整理
- NumPy 陣列的元素同一種型別;
shape是每一軸的長度、ndim是軸數、dtype是元素型別。整數陣列在這個頁面(Pyodide)是int32,一般電腦通常是int64。 - 向量化:
arr * 60、arr >= 60一次對整個陣列運算,不寫迴圈;串列的* 2是重複,陣列的* 2是每個元素乘 2。 - 二維陣列
scores[列, 欄];axis=0沿著列往下(每一欄一個數),axis=1沿著欄往右(每一列一個數)。 - 廣播:形狀從右邊對齊,每一軸相同或其中一個是 1 才行;(4, 3) 與 (4,) 對不齊,要
reshape(4, 1)。 - 布林遮罩:
x[x > 4]篩選、(x > 4).sum()計數、(x > 4).mean()比例、np.where依條件換值;多個條件用&、|並加括號。 - 下一課的 Series 是「加上索引的一維陣列」:每個值都有名字,運算時依名字對齊。