第 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 个任务:
- 用广播算出
a和b的外和矩阵(每一对元素相加),存到outer_sum; - 打印
outer_sum和它的shape; - 打印
outer_sum[0],验证它就是「a的第 0 个元素分别加上b的三个元素」; - 再算一个外积(每一对元素相乘),存到
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 个任务:
- 造一个长度 5 的系数数组
city_factor,内容是[1.2, 0.9, 1.5, 0.8, 1.0]; - 用广播让
sales的每一行乘上对应的系数,结果存到scaled; - 打印
scaled; - 打印每个城市的合计,看缩放后谁最高;
- 故意用错方向试一次(写成
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 个任务:
- 用广播算出
a和b的两两差值矩阵(a减b),存到diff; - 打印
diff; - 打印
diff的形状; - 算出每一行绝对值最小的差值(也就是「
a的这个元素跟b的哪个元素最接近」); - 找出整体上最接近的那一对,打印出来。
思考题: 第 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 个任务:
- 造一个长度 4 的系数数组
quarter_factor,内容是[1.0, 1.1, 1.2, 1.3]; - 用广播让
sales的每一列乘上对应的系数,结果存到scaled; - 打印
scaled; - 打印每一列的合计;
- 打印
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)。从右往左比:4vs4相等 ✓, 再往左sales是5、quarter_factor相当于1→ 拉伸成 5 份 ✓。所以直接相乘就对。- 如果加了
[:, None]变成(4, 1),反而对不上了——最后一维是 1,跟4不匹配。
本章最该记住的一条: 系数长度 = 列数 → 直接用(按列); 系数长度 = 行数 → 加
[:, None](按行)。 别管[:, None]看着神秘,先数一数系数有几个,答案就出来了。
练习 5:对二维数组的每一列进行归一化处理#
题目: 完成下面 6 个任务:
- 算出
sales每一列的最大值; - 用广播把
sales的每一列都除以这一列的最大值,得到normalized; - 打印
normalized; - 验证:
normalized每一列的最大值现在应该都是 1,打印出来看看; - 再做一次 Z-Score 标准化(减每列均值、除每列标准差),存到
zscore; - 打印
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→ 对每一行做运算(结果长度 = 行数)记忆:压缩掉哪一维,结果就剩几个数。
练习小结#
| 题目 | 用到的核心语法 |
|---|---|
| 练习 1 | a[:, None] + b[None, :]、a[:, None] * b[None, :] |
| 练习 2 | sales * f[:, None]、[:, None] vs [None, :] |
| 练习 3 | a[:, None] - b[None, :]、diff.min(axis=1) |
| 练习 4 | sales * f(不加 [:, None])、axis=0 |
| 练习 5 | sales / sales.max(axis=0)、Z-Score、axis=0 vs axis=1 |
做完这 5 题,下一步 你已经能用广播「一行代码算出一张表」了。但还有一个更根本的问题没解决:为什么 NumPy 就比 Python 循环快? 第 4 章会讲向量化运算——把循环彻底消灭,还会介绍排序、
argsort、范数这些数组级利器。