返回首页

Roadmap
更新于 2026年10月25日 全文 1607 字未阅读

第 3.2 节 编程练习#

预计学习时长: 45 分钟

练习方式: 新建一个 Jupyter Notebook,把下面的数据抄进去,然后每题独立完成。做完先自己核对输出,再展开参考答案。

准备:先把数据建好#

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],
])

city_names    = np.array(["北京", "上海", "广州", "深圳", "成都"])
quarter_names = np.array(["Q1", "Q2", "Q3", "Q4"])

a = np.array([1.0, 2.0, 3.0])
b = np.array([10.0, 20.0, 30.0])

练习 1:使用广播机制计算两个一维数组的外和矩阵#

题目: 完成下面 4 个任务:

  1. 用广播算出 a 和 b 的外和矩阵(每一对元素相加),存到 outer_sum;
  2. 打印 outer_sum 和它的 shape;
  3. 打印 outer_sum[0],验证它就是「a 的第 0 个元素分别加上 b 的三个元素」;
  4. 再算一个外积(每一对元素相乘),存到 outer_prod。

思考题: 为什么结果矩阵的形状是 (3, 3) 而不是 (3, 1) 或 (1, 3)?

点击查看参考答案
# 1. 外和
outer_sum = a[:, None] + b[None, :]
print(outer_sum)
[[11. 21. 31.]
 [12. 22. 32.]
 [13. 23. 33.]]
# 2. 形状
print(outer_sum.shape)    # (3, 3)
# 3. 第一行验证
print(outer_sum[0])
[11. 21. 31.]

对上了:1 加上 10、20、30 → 11、21、31。

# 4. 外积
outer_prod = a[:, None] * b[None, :]
print(outer_prod)
[[10. 20. 30.]
 [20. 40. 60.]
 [30. 60. 90.]]

要点: a[:, None] 形状是 (3, 1)(竖着),b[None, :] 形状是 (1, 3)(横着), 相加后 NumPy 把两者拉伸成 (3, 3) 再逐元素加。


练习 2:对二维数组的每一行乘以对应的缩放系数#

题目: 完成下面 5 个任务:

  1. 造一个长度 5 的系数数组 city_factor,内容是 [1.2, 0.9, 1.5, 0.8, 1.0];
  2. 用广播让 sales 的每一行乘上对应的系数,结果存到 scaled;
  3. 打印 scaled;
  4. 打印每个城市的合计,看缩放后谁最高;
  5. 故意用错方向试一次(写成 city_factor[None, :]),看看报什么错。

思考题: 第 5 题的报错信息里,两个形状分别是什么?它想告诉你什么?

点击查看参考答案
# 1. 系数
city_factor = np.array([1.2, 0.9, 1.5, 0.8, 1.0])
# 2. 按行缩放
scaled = sales * city_factor[:, None]
print(scaled)
[[144.  162.  180.  198. ]
 [ 72.   82.8  79.2  94.5]
 [300.  315.  292.5 345. ]
 [ 48.   57.6  68.   62.4]
 [150.  145.  160.  175. ]]
# 3. 每个城市的合计
totals = scaled.sum(axis=1)
print(totals)
[ 684.   328.5 1252.5  236.   630. ]
# 4. 谁最高
print(city_names[totals.argmax()])     # 广州
print(totals.max())                    # 1252.5
# 5. 故意用错方向
scaled = sales * city_factor[None, :]
ValueError: operands could not be broadcast together with shapes (5,4) (1,5)

要点: 报错的形状是 (5,4) 和 (1,5)。 从右往左比:最后一个维度 4 vs 5 → 两个都不是 1 → 广播不了。

这说明:[:, None] 不是可有可无的装饰。 长度等于行数的系数必须加 [:, None],变成 (5, 1),才能跟 (5, 4) 对上。


练习 3:计算两个数组中所有元素之间的两两差值#

题目: 完成下面 4 个任务:

  1. 用广播算出 a 和 b 的两两差值矩阵(a 减 b),存到 diff;
  2. 打印 diff;
  3. 打印 diff 的形状;
  4. 算出每一行绝对值最小的差值(也就是「a 的这个元素跟 b 的哪个元素最接近」);
  5. 找出整体上最接近的那一对,打印出来。

思考题: 第 4 题为什么要先套一层 np.abs()?如果直接写 diff.min(axis=1),得到的是「差得最负」的那一对,还是「差得最近」的那一对?

点击查看参考答案
# 1. 两两差值
diff = a[:, None] - b[None, :]
print(diff)
[[ -9. -19. -29.]
 [ -8. -18. -28.]
 [ -7. -17. -27.]]
# 2. 形状
print(diff.shape)    # (3, 3)
# 4. 每行最接近的一对(先取绝对值)
closest = np.abs(diff).min(axis=1)
print(closest)
[9. 8. 7.]

读法: a 的第 0 个元素是 1.0,它跟 b 的 10.0 差 9,跟 20.0 差 19,跟 30.0 差 29—— 所以最接近的是 10.0,差值 9。

# 5. 整体最接近的那一对
row, col = np.unravel_index(np.abs(diff).argmin(), diff.shape)
print(a[row], b[col])    # 3.0 10.0

读法: 全部 9 对里,绝对值最小的是 7(即 3.0 − 10.0),所以最接近的一对是 a 的第 2 个元素 3.0 和 b 的第 0 个元素 10.0。

要点: diff 的形状是 (3, 3),第 0 行对应 a 的第 0 个元素,第 0 列对应 b 的第 0 个元素。

  • axis=1 → 在每行里找最小 → 得到 3 个数(每个 a 元素的最优匹配);
  • axis=0 → 在每列里找最小 → 得到 3 个数(每个 b 元素的最优匹配)。

为什么第 4 题要套 np.abs()? 因为 diff 里的数有正有负。diff.min(axis=1) 找的是最小的那个数(最负的那个), 而不是离 0 最近的数。

print(diff.min(axis=1))          # [-29. -28. -27.]   ← 最负的那对
print(np.abs(diff).min(axis=1))  # [ 9.   8.   7.]   ← 真正的「最接近」

一句话:想找「最接近」,先取绝对值。


练习 4:对二维数组的每一列应用不同的缩放系数#

题目: 完成下面 5 个任务:

  1. 造一个长度 4 的系数数组 quarter_factor,内容是 [1.0, 1.1, 1.2, 1.3];
  2. 用广播让 sales 的每一列乘上对应的系数,结果存到 scaled;
  3. 打印 scaled;
  4. 打印每一列的合计;
  5. 打印 quarter_factor 和 quarter_factor[:, None] 的形状,对比一下。

思考题: 第 2 题为什么不需要写 [:, None]?第 5 题的对比说明了什么?

点击查看参考答案
# 1. 系数
quarter_factor = np.array([1.0, 1.1, 1.2, 1.3])
# 2. 按列缩放(不用加 [:, None])
scaled = sales * quarter_factor
print(scaled)
[[120.  148.5 180.  214.5]
 [ 80.  101.2 105.6 136.5]
 [200.  231.  234.  299. ]
 [ 60.   79.2 102.  101.4]
 [150.  159.5 192.  227.5]]
# 3. 每一列的合计
print(scaled.sum(axis=0))
[610.  719.4 813.6 978.9]
# 4. 形状对比
print(quarter_factor.shape)            # (4,)
print(quarter_factor[:, None].shape)   # (4, 1)

要点:

  • quarter_factor 形状是 (4,),而 sales 是 (5, 4)。从右往左比:4 vs 4 相等 ✓, 再往左 sales 是 5、quarter_factor 相当于 1 → 拉伸成 5 份 ✓。所以直接相乘就对。
  • 如果加了 [:, None] 变成 (4, 1),反而对不上了——最后一维是 1,跟 4 不匹配。

本章最该记住的一条: 系数长度 = 列数 → 直接用(按列); 系数长度 = 行数 → 加 [:, None](按行)。 别管 [:, None] 看着神秘,先数一数系数有几个,答案就出来了。


练习 5:对二维数组的每一列进行归一化处理#

题目: 完成下面 6 个任务:

  1. 算出 sales 每一列的最大值;
  2. 用广播把 sales 的每一列都除以这一列的最大值,得到 normalized;
  3. 打印 normalized;
  4. 验证:normalized 每一列的最大值现在应该都是 1,打印出来看看;
  5. 再做一次 Z-Score 标准化(减每列均值、除每列标准差),存到 zscore;
  6. 打印 zscore 每列的均值,你猜会是什么?

思考题: 第 6 题为什么每一列的平均值都接近 0?这跟 Z-Score 的定义有什么关系?

点击查看参考答案
# 1. 每列最大值
col_max = sales.max(axis=0)
print(col_max)
[200. 210. 195. 230.]
# 2. 按列归一化
normalized = sales / col_max
print(normalized)
[[0.6        0.64285714 0.76923077 0.7173913 ]
 [0.4        0.43809524 0.45128205 0.45652174]
 [1.         1.         1.         1.        ]
 [0.3        0.34285714 0.43589744 0.33913043]
 [0.75       0.69047619 0.82051282 0.76086957]]
# 3. 验证:每列最大值都是 1
print(normalized.max(axis=0))
[1. 1. 1. 1.]
# 4. Z-Score 标准化
zscore = (sales - sales.mean(axis=0)) / sales.std(axis=0)
print(zscore)
[[-0.04003204  0.08774867  0.33648534  0.26779681]
 [-0.84067281 -0.81063056 -1.11227098 -0.84802323]
 [ 1.5612495   1.65468918  1.38800202  1.47660186]
 [-1.24099319 -1.22848136 -1.18237209 -1.35014225]
 [ 0.56044854  0.29667407  0.57015571  0.45376682]]
# 5. 每列均值
print(zscore.mean(axis=0))
[ 0.00000000e+00 -2.10942375e-16  1.77635684e-16  1.11022302e-16]

看出问题了吗? 这些数全都是 0——只不过带着一点点浮点误差。 1e-16 是小到可以忽略的程度(计算机没法精确表示 0.1 这种小数,只能无限接近)。

想看干净的 0,做一步四舍五入:

print(np.round(zscore.mean(axis=0), 10))
[ 0. -0.  0.  0.]

要点:

  • 第 4 题用 axis=0 求每列最大值,长度是 4(列数),直接跟 (5, 4) 的 sales 相除就行;
  • 第 5 题也是同理,sales.mean(axis=0) 和 sales.std(axis=0) 形状都是 (4,),两个都能广播;
  • 第 6 题每列均值都是 0,因为 Z-Score 减掉的就是这一列自己的均值——减完必然平均为 0(理论上,浮点误差会让它非常接近 0)。

区分两个 axis:

  • axis=0 → 对每一列做运算(结果长度 = 列数)
  • axis=1 → 对每一行做运算(结果长度 = 行数)

记忆:压缩掉哪一维,结果就剩几个数。


练习小结#

题目用到的核心语法
练习 1a[:, None] + b[None, :]、a[:, None] * b[None, :]
练习 2sales * f[:, None]、[:, None] vs [None, :]
练习 3a[:, None] - b[None, :]、diff.min(axis=1)
练习 4sales * f(不加 [:, None])、axis=0
练习 5sales / sales.max(axis=0)、Z-Score、axis=0 vs axis=1

做完这 5 题,下一步 你已经能用广播「一行代码算出一张表」了。但还有一个更根本的问题没解决:为什么 NumPy 就比 Python 循环快? 第 4 章会讲向量化运算——把循环彻底消灭,还会介绍排序、argsort、范数这些数组级利器。

阅读 --