3.2.4 数组运算

先读这张图:左边是“循环逐个算”,右边是“整块数组一起算”。看到不同形状相加时,先检查每一维能不能对齐,或者其中一维是不是可以被广播的 1。
- 理解向量化运算的概念和优势
- 掌握元素级运算和通用函数(ufunc)
- 理解广播机制(Broadcasting)的规则
- 熟练使用聚合函数进行统计计算
向量化运算:告别循环
Section titled “向量化运算:告别循环”向量化运算是 NumPy 的核心思想——对整个数组进行操作,不用写循环。
纯 Python vs NumPy
Section titled “纯 Python vs NumPy”import numpy as np
# 纯 Python:逐个计算prices = [100, 200, 300, 400, 500]discounted = []for p in prices: discounted.append(p * 0.8)print(discounted) # [80.0, 160.0, 240.0, 320.0, 400.0]
# NumPy:一行搞定prices = np.array([100, 200, 300, 400, 500])discounted = prices * 0.8print(discounted) # [ 80. 160. 240. 320. 400.]NumPy 数组的算术运算是逐元素进行的:
a = np.array([1, 2, 3, 4])b = np.array([10, 20, 30, 40])
print(a + b) # [11 22 33 44] 对应位置相加print(a - b) # [ -9 -18 -27 -36]print(a * b) # [ 10 40 90 160] 对应位置相乘(不是矩阵乘法!)print(a / b) # [0.1 0.1 0.1 0.1]print(a ** 2) # [ 1 4 9 16] 平方print(b % 3) # [1 2 0 1] 取余print(b // 3) # [ 3 6 10 13] 整除数组和单个数字(标量)运算时,会自动把标量应用到每个元素:
arr = np.array([10, 20, 30, 40])
print(arr + 5) # [15 25 35 45]print(arr * 2) # [20 40 60 80]print(arr / 10) # [1. 2. 3. 4.]print(1 / arr) # [0.1 0.05 0.033 0.025]arr = np.array([15, 23, 8, 42, 31])
print(arr > 20) # [False True False True True]print(arr == 23) # [False True False False False]print(arr != 8) # [ True True False True True]通用函数(ufunc)
Section titled “通用函数(ufunc)”NumPy 提供了大量的通用函数,可以对数组中每个元素应用数学运算:
常用数学函数
Section titled “常用数学函数”arr = np.array([1, 4, 9, 16, 25])
# 平方根print(np.sqrt(arr)) # [1. 2. 3. 4. 5.]
# 绝对值neg = np.array([-3, -1, 0, 2, 5])print(np.abs(neg)) # [3 1 0 2 5]
# 幂运算print(np.power(arr, 0.5)) # 和 sqrt 一样
# 指数和对数print(np.exp([0, 1, 2])) # [1. 2.718 7.389] e 的幂print(np.log([1, np.e, 10])) # [0. 1. 2.303] 自然对数print(np.log10([1, 10, 100])) # [0. 1. 2.] 以 10 为底print(np.log2([1, 2, 8, 64])) # [0. 1. 3. 6.] 以 2 为底# 创建 0 到 2π 的角度angles = np.linspace(0, 2 * np.pi, 5) # [0, π/2, π, 3π/2, 2π]
print(np.sin(angles)) # [ 0. 1. 0. -1. 0.] ← 正弦print(np.cos(angles)) # [ 1. 0. -1. 0. 1.] ← 余弦arr = np.array([1.2, 2.5, 3.7, -1.3, -2.8])
print(np.floor(arr)) # [ 1. 2. 3. -2. -3.] 向下取整print(np.ceil(arr)) # [ 2. 3. 4. -1. -2.] 向上取整print(np.round(arr)) # [ 1. 2. 4. -1. -3.] 四舍五入print(np.trunc(arr)) # [ 1. 2. 3. -1. -2.] 截断小数两个数组间的运算
Section titled “两个数组间的运算”a = np.array([3, 5, 7, 9])b = np.array([1, 4, 2, 8])
print(np.maximum(a, b)) # [3 5 7 9] 对应位置取较大值print(np.minimum(a, b)) # [1 4 2 8] 对应位置取较小值print(np.where(a > b, a, b)) # 同 maximum,但更灵活广播机制(Broadcasting)
Section titled “广播机制(Broadcasting)”当两个形状不同的数组运算时,NumPy 会自动”广播”较小的数组,使它们形状兼容。
最简单的例子
Section titled “最简单的例子”arr = np.array([1, 2, 3])
# 标量 + 数组 → 标量被广播成 [10, 10, 10]print(arr + 10) # [11 12 13]这其实就是广播——NumPy 把 10 扩展成了 [10, 10, 10],然后逐元素相加。
二维数组 + 一维数组
Section titled “二维数组 + 一维数组”matrix = np.array([ [1, 2, 3], [4, 5, 6], [7, 8, 9]])
row = np.array([10, 20, 30])
# row 被广播到每一行result = matrix + rowprint(result)# [[11 22 33]# [14 25 36]# [17 28 39]]广播的过程可以这样理解:
matrix: row (广播前): row (广播后):[[1, 2, 3], [10, 20, 30] → [[10, 20, 30], [4, 5, 6], [10, 20, 30], [7, 8, 9]] [10, 20, 30]]列向量 + 行向量
Section titled “列向量 + 行向量”col = np.array([[1], [2], [3]]) # shape: (3, 1) 列向量row = np.array([10, 20, 30]) # shape: (3,) 行向量
# 两者都被广播result = col + rowprint(result)# [[11 21 31]# [12 22 32]# [13 23 33]]flowchart TD A["两个数组形状不同?"] -->|是| B["从末尾对齐维度"] B --> C{"每个维度是否满足?<br/>1. 相等<br/>2. 其中一个为 1"} C -->|全部满足| D["广播成功 ✅<br/>维度为 1 的被复制扩展"] C -->|有不满足| E["广播失败 ❌<br/>报 ValueError"] A -->|否| F["形状相同,直接运算"]简单记忆:维度从后往前比,要么相等,要么其中一个是 1。
# ✅ 可以广播# (3, 4) + (4,) → (3, 4) 最后一维都是 4# (3, 4) + (1, 4) → (3, 4) 第一维 3 和 1 → 广播成 3# (3, 1) + (1, 4) → (3, 4) 两个维度都广播
# ❌ 不能广播# (3, 4) + (3,) → 报错!最后一维 4 ≠ 3,且都不是 1广播的实际应用
Section titled “广播的实际应用”# 标准化数据:每列减去该列的均值data = np.array([ [85, 170, 60], [92, 180, 75], [78, 165, 55], [90, 175, 70]]) # 4 个学生:成绩、身高、体重
# 计算每列均值col_mean = data.mean(axis=0) # [86.25 172.5 65. ] shape: (3,)
# 广播:(4, 3) - (3,) → (4, 3)centered = data - col_meanprint(centered)# [[-1.25 -2.5 -5. ]# [ 5.75 7.5 10. ]# [-8.25 -7.5 -10. ]# [ 3.75 2.5 5. ]]聚合函数把一组数据”汇总”成一个或一组值:
常用聚合函数
Section titled “常用聚合函数”arr = np.array([4, 7, 2, 9, 1, 5, 8, 3, 6])
print(np.sum(arr)) # 45 总和print(np.mean(arr)) # 5.0 均值print(np.median(arr)) # 5.0 中位数print(np.std(arr)) # 2.58 标准差print(np.var(arr)) # 6.67 方差print(np.min(arr)) # 1 最小值print(np.max(arr)) # 9 最大值print(np.argmin(arr)) # 4 最小值的索引print(np.argmax(arr)) # 3 最大值的索引print(np.cumsum(arr)) # [ 4 11 13 22 23 28 36 39 45] 累积和print(np.cumprod(arr[:5])) # [ 4 28 56 504 504] 累积积按轴(axis)聚合
Section titled “按轴(axis)聚合”对于多维数组,axis 参数控制沿哪个方向聚合:
matrix = np.array([ [1, 2, 3], [4, 5, 6], [7, 8, 9]])
# 不指定 axis:对所有元素聚合print(np.sum(matrix)) # 45
# axis=0:沿行方向(按列聚合)—— 上下压缩print(np.sum(matrix, axis=0)) # [12 15 18]
# axis=1:沿列方向(按行聚合)—— 左右压缩print(np.sum(matrix, axis=1)) # [ 6 15 24]axis 的理解方式——axis=0 消灭行,axis=1 消灭列:
- 原始矩阵的 shape 是
(3, 3),有 3 行、3 列。 axis=0:上下压缩,消灭行、保留列,结果 shape 是(3,),值是[12, 15, 18]。axis=1:左右压缩,消灭列、保留行,结果 shape 是(3,),值是[6, 15, 24]。
如果你不确定方向,就先打印 matrix.shape,再问自己“我要把哪一维压掉”。这比硬背 axis=0 或 axis=1 更可靠。
实战:成绩分析
Section titled “实战:成绩分析”# 5 个学生的 3 科成绩scores = np.array([ [85, 92, 78], # 学生 1:语文、数学、英语 [90, 88, 95], # 学生 2 [72, 65, 80], # 学生 3 [95, 98, 92], # 学生 4 [60, 55, 70] # 学生 5])
subjects = ["语文", "数学", "英语"]
# 每个学生的总分total = np.sum(scores, axis=1)print("每个学生的总分:", total) # [255 273 217 285 185]
# 每个学生的平均分avg_per_student = np.mean(scores, axis=1)print("每个学生的平均分:", avg_per_student)
# 每科的平均分avg_per_subject = np.mean(scores, axis=0)for sub, avg in zip(subjects, avg_per_subject): print(f" {sub}平均分: {avg:.1f}")
# 全班最高分是谁的哪科max_idx = np.unravel_index(np.argmax(scores), scores.shape)print(f"最高分: {scores[max_idx]} (学生{max_idx[0]+1}的{subjects[max_idx[1]]})")
# 哪个学生的总分最高best_student = np.argmax(total)print(f"总分最高: 学生{best_student + 1}, 总分 {total[best_student]}")np.where:条件选择
Section titled “np.where:条件选择”np.where 是 NumPy 版的三元表达式:
arr = np.array([85, 42, 91, 67, 55, 78])
# 及格的标记为 "PASS",不及格标记为 "FAIL"result = np.where(arr >= 60, "PASS", "FAIL")print(result) # ['PASS' 'FAIL' 'PASS' 'PASS' 'FAIL' 'PASS']
# 不及格的补到 60adjusted = np.where(arr >= 60, arr, 60)print(adjusted) # [85 60 91 67 60 78]学完这一页,建议自己改一遍上面的代码,并确认这 4 件事:
- 能把
prices * 0.8改成“涨价 15%”和“满减 20 元”的版本。 - 能解释
(4, 3) - (3,)为什么可以广播,以及(3, 4) + (3,)为什么会失败。 - 能分别用
axis=0和axis=1求出每科平均分、每个学生平均分。 - 能用
np.where把原始分数转换成“优秀 / 及格 / 需要补练”的标签。
真正掌握数组运算,不是记住函数名,而是看到输入 shape 后能预判输出 shape。
学完这一页,至少保留这张证据卡:
- 数组状态
- 操作前的形状、dtype、轴和样本值
- 操作
- 索引、切片、广播、reshape、线性代数,或随机/统计函数
- 输出
- 结果数组形状、值,或统计量
- 失败检查
- 轴混淆、视图/副本陷阱、广播不匹配或形状错误
- 期望产出
- 打印的形状和值,便于检查数组运算
| 类别 | 内容 | 示例 |
|---|---|---|
| 向量化运算 | 整个数组一起运算,不用循环 | arr * 2, a + b |
| 通用函数 | 逐元素的数学函数 | np.sqrt(), np.exp(), np.log() |
| 广播 | 不同形状的数组自动扩展 | (3,4) + (4,) → (3,4) |
| 聚合函数 | 汇总统计 | np.sum(), np.mean(), np.std() |
| axis 参数 | 控制聚合方向 | axis=0 按列,axis=1 按行 |
| np.where | 条件选择 | np.where(arr > 0, arr, 0) |
练习 1:向量化计算
Section titled “练习 1:向量化计算”# 计算华氏温度转摄氏温度# 公式:C = (F - 32) × 5/9import numpy as np
fahrenheit = np.array([32, 68, 100, 212, 72, 98.6])
# 用向量化运算一行完成转换celsius = (fahrenheit - 32) * 5 / 9练习 2:广播练习
Section titled “练习 2:广播练习”# 3 个商品的原价import numpy as np
prices = np.array([100, 200, 300])
# 3 种折扣率(列向量)discounts = np.array([[0.9], [0.8], [0.7]])
# 用广播计算每个商品在每种折扣下的价格(3×3 矩阵)final_prices = discounts * prices# 预期结果:# [[ 90. 180. 270.]# [ 80. 160. 240.]# [ 70. 140. 210.]]练习 3:成绩统计
Section titled “练习 3:成绩统计”# 生成 50 个学生的随机成绩(40~100 之间)rng = np.random.default_rng(seed=42)scores = rng.integers(40, 101, size=50)
# 1. 计算均值、中位数、标准差# 2. 找出最高分、最低分和它们的位置# 3. 统计各分数段人数:不及格(<60)、及格(60-69)、中等(70-79)、良好(80-89)、优秀(90+)# 4. 计算及格率参考实现与讲解
- 华氏转摄氏使用
(fahrenheit - 32) * 5 / 9;常见示例大约得到[0, 20, 37.78, 100, 22.22, 37]。 - Broadcasting 的规则是从右侧维度开始对齐。常见的“行向量加列向量”练习会得到 3x3 矩阵,因为每个行值都会和每个列值组合。
- 成绩统计要报告均值、中位数、标准差、最高和最低的索引或姓名、及格率和分箱计数。讲解重点是用数组运算完成,而不是手写循环。