第 4.3 节 常见问题#
这一节收集了初学者在 NumPy 向量化运算上最常卡的 10 个问题。每个问题都给出直接答案,能跑代码的都附上完整示例。
怎么用这一节 不用通读。遇到问题用 Ctrl+F 搜关键词,直接跳到对应那条。
准备一下数据,下面每题都会用到:
import numpy as np
sales = np.array([
[120.0, 135.0, 150.0, 165.0],
[ 80.0, 92.0, 88.0, 105.0],
[200.0, 210.0, 195.0, 230.0],
[ 60.0, 72.0, 85.0, 78.0],
[150.0, 145.0, 160.0, 175.0],
])
scores = np.array([80, 90, 75, 100, 60])Q1:什么是向量化运算?与 Python for 循环相比有什么优势?#
答: 向量化 = 用一行数组表达式替代整个 for 循环。
循环写法:
prices = np.array([100.0, 200.0, 300.0])
for i in range(len(prices)):
prices[i] = prices[i] * 0.8向量化写法:
prices = prices * 0.8三个优势:
| for 循环 | 向量化 | |
|---|---|---|
| 速度 | 逐个解释执行 | 快 30~100 倍 |
| 代码行数 | 3 行 | 1 行 |
| 出错机会 | 下标越界、空数组、类型不匹配…… | NumPy 帮你兜住 |
为什么快这么多?
- NumPy 把数组打包成一块连续内存,CPU 缓存能高效读取;
- 运算在 C 层批量完成,能用 SIMD 指令一次算好几个;
- Python 每循环一次都要「查名字 → 取值 → 调函数 → 存回」,开销全花在调度上。
但别无脑向量化。 如果你只是想对 3 个数做点什么,写循环完全没问题。 向量化的收益随数据量增长——100 个数的时候差别不大,100 万的时候是天壤之别。
Q2:NumPy 的算术运算默认是逐元素执行的吗?#
答: 是的,默认全部逐元素(element-wise)。
a = np.array([1.0, 2.0, 3.0])
b = np.array([10.0, 20.0, 30.0])
print(a + b) # [11. 22. 33.] 逐个加
print(a * b) # [10. 40. 90.] 逐个乘但 * 有个例外,必须记住:
print(a * 2) # [2. 4. 6.] ← 逐元素乘 2
print(a.shape) # (3,)而如果 a 是二维的,* 就变成矩阵乘法了!
m1 = np.array([[1., 2.], [3., 4.]])
m2 = np.array([[5., 6.], [7., 8.]])
print(m1 * m2)[[ 5. 12.]
[21. 32.]]这是逐元素相乘(1*5, 2*6, 3*7, 4*8),不是矩阵乘法。
真正的矩阵乘法要用 @:
print(m1 @ m2)[[19. 22.]
[43. 50.]]一句话记住:
*和+一样,永远是逐元素;- 矩阵乘法用
@(或者np.matmul)。
Q3:如何计算两个数组的差值、乘积和逐元素比值?#
答: 直接用运算符,全都是逐元素的。
a = np.array([120.0, 80.0, 200.0])
b = np.array([100.0, 90.0, 150.0])
print(a - b) # 差值
print(a * b) # 乘积
print(a / b) # 逐元素比值[ 20. -10. 50.]
[12000. 7200. 30000.]
[1.2 0.88888889 1.33333333]两个数组形状不一样也能算——这就回到广播了:
print(a - np.array([10.0, 20.0])) # (3,) 和 (2,) → 报错ValueError: operands could not be broadcast together with shapes (3,) (2,)但长度相同就行:
print(a - np.array([10.0, 20.0, 30.0]))[110. 60. 170.]想「每个数除以它的总和」这种比例?
print(a / a.sum())[0.3 0.2 0.5]结果自动加起来等于 1——这就是「占比」。
Q4:如何计算归一化差值?如何避免分母为零?#
答: 公式:(a - b) / (|a| + |b|)。
a = np.array([120.0, 80.0, 200.0])
b = np.array([100.0, 90.0, 150.0])
print((a - b) / (np.abs(a) + np.abs(b)))[ 0.09090909 -0.05882353 0.14285714]分母为零的两种情况:
- 两个数都是 0 →
0 / 0→nan; - 还有一个更隐蔽的:两个数接近 0(比如
1e-300)→ 结果变得巨大。
zero = np.array([0.0])
print((zero - zero) / (np.abs(zero) + np.abs(zero)))[nan]三种解决办法:
办法一:给分母加一个极小值(最常用)。
denom = np.abs(a) + np.abs(b) + 1e-9
print((a - b) / denom)办法二:用 np.where 把分母为 0 的位置单独挑出来处理。
denom = np.abs(a) + np.abs(b)
denom = np.where(denom == 0, 1.0, denom) # 把 0 换成 1
print((a - b) / denom)办法三:用 np.divide 的 where 参数(最规范)。
result = np.zeros_like(a)
np.divide(a - b, np.abs(a) + np.abs(b),
out=result, where=(np.abs(a) + np.abs(b)) != 0)
print(result)初学阶段用办法一就够了。
1e-9足够小,对正常数据没有任何影响,但能挡住 0 除 0。
Q5:如何使用 np.arctan2() 根据二维向量的分量计算角度?#
答: np.arctan2(y, x),返回弧度。
import numpy as np
points = np.array([
[3.0, 4.0],
[1.0, 0.0],
[0.0, 2.0],
[-1.0, -1.0],
])
angles = np.degrees(np.arctan2(points[:, 1], points[:, 0]))
print(angles)[ 53.13010235 0. 90. -135. ]三个要点:
- 参数顺序是
(y, x),跟数学写法一致; - 结果要套
np.degrees()才变成角度(弧度 → 度); points[:, 1]是 y、points[:, 0]是 x,别搞反。
为什么不用 np.arctan(y / x)?
print(np.arctan2(2.0, 0.0)) # 1.5707963267948966 ✓ 正好 90°
print(np.arctan(np.array([2.0]) / np.array([0.0]))) # [nan] ✗ 除以 0而且 arctan 分不出象限:arctan(-1/1) 和 arctan(1/-1) 都会得到 -45°,
但它们明明是不同方向。arctan2 用两个参数就能判断象限。
参数写反不会报错,但结果全错。
print(np.degrees(np.arctan2(points[:, 0], points[:, 1])))[ 36.86989765 90. 0. -135. ]写完拿一个已知答案的点验一下(比如
(1, 0)必须是0°),能省掉几小时的调试。
Q6:sum()、mean()、min() 和 max() 如何通过 axis 参数指定计算方向?#
答: axis 就是「要消掉哪一维」。
import numpy as np
sales = np.array([
[120.0, 135.0, 150.0, 165.0],
[ 80.0, 92.0, 88.0, 105.0],
[200.0, 210.0, 195.0, 230.0],
[ 60.0, 72.0, 85.0, 78.0],
[150.0, 145.0, 160.0, 175.0],
])
print(sales.sum(axis=0)) # 每列
print(sales.sum(axis=1)) # 每行
print(sales.sum()) # 全部[610. 654. 678. 753.]
[570. 365. 835. 295. 630.]
2695.0四个函数全都支持 axis:
print(sales.mean(axis=0))
print(sales.min(axis=0))
print(sales.max(axis=0))[122. 130.8 135.6 150.6]
[60. 72. 85. 78.]
[200. 210. 195. 230.]还有一些进阶的聚合函数也支持 axis:
print(np.median(sales, axis=0)) # 中位数
print(sales.std(axis=0)) # 标准差
print(sales.cumsum(axis=0)) # 累计和[120. 135. 150. 165.]
[49.95998399 47.86397393 42.79532685 53.77211173]
[[120. 135. 150. 165.]
[200. 227. 238. 270.]
[400. 437. 433. 500.]
[460. 509. 518. 578.]
[610. 654. 678. 753.]]要「位置」而不是「值」,用
arg前缀的版本:print(sales.argmax(axis=0)) # 每列最大值是第几行 print(sales.argmin(axis=1)) # 每行最小值是第几列[2 2 2 2] [0 0 2 0 1]
Q7:axis=0 和 axis=1 分别表示什么?#
答: axis=0 对每一列,axis=1 对每一行。
这个说法容易记反,因为「0」让人想到「第 0 行」。
正确的记法是:axis = 「要消掉(压缩掉)的那一维」。
sales = np.array([
[120.0, 135.0, 150.0, 165.0],
[ 80.0, 92.0, 88.0, 105.0],
[200.0, 210.0, 195.0, 230.0],
[ 60.0, 72.0, 85.0, 78.0],
[150.0, 145.0, 160.0, 175.0],
])axis=0: 消掉「第 0 维」(行),每一行都被压成一个数 → 每列一个数。
print(sales.sum(axis=0))[610. 654. 678. 753.]结果长度是 4 = 列数。
axis=1: 消掉「第 1 维」(列),每一列都被压成一个数 → 每行一个数。
print(sales.sum(axis=1))[570. 365. 835. 295. 630.]结果长度是 5 = 行数。
一个万能判断法: 先算结果该有几个数。
- 我要「每个城市(5 个)一个数」→ 结果长度 5 → 行数 → 用
axis=1;- 我要「每个季度(4 个)一个数」→ 结果长度 4 → 列数 → 用
axis=0。结果长度 = 被压缩掉的维度之外的维度数量。
Q8:sort() 和 argsort() 有什么区别?#
答: sort() 给你「排好序的数」,argsort() 给你「每个数排第几位」。
scores = np.array([80, 90, 75, 100, 60])
print(np.sort(scores))[ 60 75 80 90 100]print(np.argsort(scores))[4 2 0 1 3]argsort() 的结果怎么读?
- 排好序的第 0 个数(60)原来是第 4 个 →
scores[4] = 60✓ - 排好序的第 1 个数(75)原来是第 2 个 →
scores[2] = 75✓ - 排好序的第 2 个数(80)原来是第 0 个 →
scores[0] = 80✓ - 排好序的第 3 个数(90)原来是第 1 个 →
scores[1] = 90✓ - 排好序的第 4 个数(100)原来是第 3 个 →
scores[3] = 100✓
argsort 最大的用处:让「名字」跟着「数字」一起排。
names = np.array(["小明", "小红", "小刚", "小美", "小李"])
order = np.argsort(scores)[::-1] # 从大到小的名次
print(names[order])['小美' '小红' '小明' '小刚' '小李']两个补充:
np.sort()不改原数组,它返回一个新数组;想原地排序用arr.sort();- 二维数组排序也要写
axis,不写会默认按最后一个轴排。
matrix = np.array([[3., 1.], [2., 4.]])
print(np.sort(matrix, axis=0)) # 每一列各自排
print(np.sort(matrix, axis=1)) # 每一行各自排[[2. 1.]
[3. 4.]]
[[1. 3.]
[2. 4.]]Q9:什么是向量的范数?如何使用 np.linalg.norm() 计算向量长度?#
答: 范数就是「向量的长度」。
最常见的是欧几里得范数(L2 范数),也就是两点之间的直线距离:
‖v‖ = √(v₁² + v₂² + ... + vₙ²)import numpy as np
v = np.array([3.0, 4.0])
print(np.linalg.norm(v)) # 5.0对二维数组算「每一行的长度」:
vectors = np.array([
[3.0, 4.0],
[1.0, 0.0],
[6.0, 8.0],
])
print(np.linalg.norm(vectors, axis=1))[ 5. 1. 10.]axis=1 的意思还是那句老话:消掉列维,每一行压成一个数(它的长度)。
keepdims=True 保留结构,方便后面广播:
norms = np.linalg.norm(vectors, axis=1, keepdims=True)
print(norms.shape) # (3, 1)(3, 1)其他常见的范数:
print(np.linalg.norm(v, ord=1)) # L1:绝对值之和 = 7.0
print(np.linalg.norm(v, ord=2)) # L2:默认,= 5.0
print(np.linalg.norm(v, ord=np.inf)) # 最大绝对值 = 4.0实战里最常见的三种范数用途:
- L2 —— 算欧氏距离(聚类、KNN、推荐系统里天天用);
- L1 —— 「曼哈顿距离」,网格地图上的路径长度;
np.inf—— 「最大分量」,做归一化时常用(把所有数除以最大的那个)。
Q10:如何结合条件判断、广播机制和矩阵运算,实现基于范数的门控变换?#
答: 三步走:算范数 → 造开关 → 用 np.where 做分支。
import numpy as np
vectors = np.array([
[3.0, 4.0],
[1.0, 0.0],
[6.0, 8.0],
])
# 第 1 步:算范数(keepdims=True 让形状保持 (3, 1),方便广播)
norms = np.linalg.norm(vectors, axis=1, keepdims=True)
# 第 2 步:造开关(这就是「条件判断」的向量化版)
gate = norms > 3
# 第 3 步:np.where 做分支(两个分支都整块算好,再按开关挑)
result = np.where(gate, 2 * vectors - 1, vectors)
print(result)[[ 5. 7.]
[ 1. 0.]
[11. 15.]]三个部分各自对应什么:
| 步骤 | 用到的 NumPy 能力 |
|---|---|
| 算范数 | np.linalg.norm(axis=1, keepdims=True) |
| 造开关 | 广播 + 比较运算(norms > 3) |
| 做分支 | np.where(条件, A, B) |
为什么这里三个都离不开?
- 没有
keepdims=True,norms是(3,),跟(3, 2)的vectors广播不上; - 没有广播,你没法一次算出 3 个范数;
- 没有
np.where,你就得写 for 循环逐个判断。
换一种门控规则试试(更贴近业务):
totals = np.array([570.0, 365.0, 835.0, 295.0, 630.0])
# 超过 500 万的重点城市给 1.5 倍系数,其余 1.0 倍
factors = np.where(totals > 500, 1.5, 1.0)
print(totals * factors)[ 855. 365. 1252.5 295. 945. ]这就是机器学习里常说的「门控(gating)」:用一个条件把数据分成两拨,分别处理。
这一节最该记住的一句话: 「条件判断」在 NumPy 里不是
if,而是np.where(条件, A, B)。 一旦你接受这个转换思路,99% 的循环都能变成一行。
小结#
这一节的 10 个问题,其实都围绕**「怎么把循环翻译成一行」**:
- 向量化 = 一行数组表达式,快 30~100 倍,还不容易写错;
- 算术运算默认逐元素,矩阵乘法要用
@; - 差值 / 乘积 / 比值 直接用
-*/,形状不同就靠广播; - 归一化差值:
(a-b)/(abs(a)+abs(b)),分母加1e-9防 0; np.arctan2(y, x)算角度,记得np.degrees()转角度、参数别写反;- 聚合函数 全都支持
axis; axis=0对列、axis=1对行,判断法:先想结果该有几个数;sort()给值、argsort()给名次,后者能让名字跟着数字排;np.linalg.norm(arr, axis=1)算每行长度,keepdims=True是广播的钥匙;- 范数门控三步:算范数 → 造开关 →
np.where做分支。
下一章预告 最后一章我们把注意力放在**「数组还能怎么折腾」上:怎么提取每行的极值和它对应的名字、怎么用分位数做缩尾处理**(压住极端值)、怎么把连续数值量化成离散区间、怎么把一维序列切成一个个数据帧,还有
tile/repeat/diff/concatenate/corrcoef这些数组实用工具——全是真实数据清洗里天天要用的手艺。