返回首页

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

第 4.2 节 编程练习#

预计学习时长: 45 分钟

练习方式: 新建一个 Jupyter Notebook,把下面的数据抄进去,然后每题独立完成。第 5 题最难,多花点时间。

准备:先把数据建好#

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([120.0, 80.0, 200.0, 60.0])
b = np.array([100.0, 90.0, 150.0, 55.0])

练习 1:计算两个数组之间的归一化差值#

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

  1. 算出 a 和 b 的归一化差值:(a - b) / (|a| + |b|),存到 norm_diff;
  2. 打印 norm_diff;
  3. 用安全版(分母加 1e-9)再算一次,存到 safe_diff;
  4. 找出差距最大的那一对下标(用 np.argmax),打印出来。

思考题: 第 3 题加 1e-9 之后,结果和第 2 题有区别吗?为什么还要加?

点击查看参考答案
# 1. 归一化差值
norm_diff = (a - b) / (np.abs(a) + np.abs(b))
print(norm_diff)
[ 0.09090909 -0.05882353  0.14285714  0.04347826]
# 2. 安全版(分母加极小值)
safe_diff = (a - b) / (np.abs(a) + np.abs(b) + 1e-9)
print(safe_diff)
[ 0.09090909 -0.05882353  0.14285714  0.04347826]
# 3. 差距最大的是哪一对
idx = np.abs(norm_diff).argmax()
print(idx, a[idx], b[idx])
2 200.0 150.0

要点:

  • 归一化差值的范围是 -1 ~ 1:全为正是「a 比 b 大」,负数是「a 比 b 小」;
  • 0.1429 的意思是:a 比 b 大了约 14%;
  • 绝对值最大的是第 2 对(200 vs 150),差了约 14%。

第 3 题的 1e-9 为什么加了也没变化? 因为 a 和 b 里没有 0,分母本来就不可能是 0。 1e-9 是「保险」——只有在出现 0/0 时才起作用,把 nan 变成 0。

zero = np.array([0.0, 5.0])
print((zero - zero) / (np.abs(zero) + np.abs(zero)))      # [nan nan]
print((zero - zero) / (np.abs(zero) + np.abs(zero) + 1e-9))  # [0. 0.]

练习 2:根据二维向量的分量构造角度特征#

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

  1. 造一个 5 个点的数组 points,每行是一个点的 (x, y) 坐标: (3, 4)、(1, 0)、(0, 2)、(-1, -1)、(5, 5);
  2. 用 np.arctan2() 算出每个点的弧度角,存到 rad;
  3. 用 np.degrees() 转成角度,存到 deg;
  4. 打印 deg;
  5. 找出角度最大的点,打印它的坐标。

思考题: 第 2 题如果写成 np.arctan2(points[:, 0], points[:, 1])(参数反了),结果会变成什么样?

点击查看参考答案
# 1. 造点
points = np.array([
    [3.0, 4.0],
    [1.0, 0.0],
    [0.0, 2.0],
    [-1.0, -1.0],
    [5.0, 5.0],
])
# 2. 弧度角
rad = np.arctan2(points[:, 1], points[:, 0])
print(rad)
[ 0.92729522  0.          1.57079633 -2.35619449  0.78539816]
# 3. 转成角度
deg = np.degrees(rad)
print(deg)
[  53.13010235    0.           90.         -135.           45.        ]
# 4. 角度最大的点
i = deg.argmax()
print(i, points[i])
2 [0. 2.]

要点:

  • points[:, 1] 是 y 坐标,points[:, 0] 是 x 坐标;
  • arctan2(y, x) 的参数顺序是 (y, x),跟数学写法一致;
  • 第 3 个点 (0, 2) 得到 90°,因为它在正上方——这正是 arctan2 比 arctan(y/x) 强的地方(x = 0 不会除零)。

参数写反了不会报错,但结果全错!

print(np.degrees(np.arctan2(points[:, 0], points[:, 1])))
[  36.86989765   90.            0.         -135.           45.        ]

第一个点 (3, 4) 本该是 53.13°,写反了变成 36.87°—— 这正是 (4/3) 和 (3/4) 的区别。 这种 bug 最难查,因为代码跑得好好的。写完记得拿一个已知角度的点(比如 (1,0) 应该是 0°)验一下。


练习 3:使用聚合函数计算数组的总和、均值、最大值和最小值#

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

  1. 算 sales 的总和(一个数);
  2. 算每列的合计(axis=0)和每行的合计(axis=1);
  3. 算每列的平均值和每行的平均值;
  4. 算每列的最大值、最小值;
  5. 找出每个城市销售额最高的是哪个季度(用 argmax(axis=1)),并打印城市名和季度名;
  6. 找出总和最高的城市,用 argmax + 索引名字数组的方式打印出来。

思考题: 第 5 题和第 6 题都用 argmax,但 axis 写法不同,能说说区别吗?

点击查看参考答案
# 1. 总和
print(sales.sum())
2695.0
# 2. 每列 / 每行的合计
print(sales.sum(axis=0))
print(sales.sum(axis=1))
[610. 654. 678. 753.]
[570. 365. 835. 295. 630.]
# 3. 每列 / 每行的平均
print(sales.mean(axis=0))
print(sales.mean(axis=1))
[122.  130.8 135.6 150.6]
[142.5   91.25 208.75  73.75 157.5 ]
# 4. 每列的最大 / 最小
print(sales.max(axis=0))
print(sales.min(axis=0))
[200. 210. 195. 230.]
[60. 72. 85. 78.]
# 5. 每个城市最高的是哪个季度
best_q = sales.argmax(axis=1)
for i, q in enumerate(best_q):
    print(city_names[i], "->", quarter_names[q], sales[i, q])
北京 -> Q4 165.0
上海 -> Q4 105.0
广州 -> Q4 230.0
深圳 -> Q3 85.0
成都 -> Q4 175.0
# 6. 总和最高的城市
totals = sales.sum(axis=1)
print(city_names[totals.argmax()], totals.max())
广州 835.0

要点:

  • axis=0 是「对每一列」,结果长度 = 列数(4);
  • axis=1 是「对每一行」,结果长度 = 行数(5);
  • 「值」用 max,「位置」用 argmax——两者配套使用是最常见的套路。

第 5 题的 sales[i, q] 是什么意思? i 是行号、q 是列号,合起来就是「第 i 行第 q 列」的那个数。 「先用 argmax 拿到位置,再用位置去取值」——这个模式要练成条件反射。


练习 4:使用 sort() 和 argsort() 对数组进行排序并获取排序索引#

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

  1. 造一个成绩数组 scores = np.array([80, 90, 75, 100, 60, 95]);
  2. 用 np.sort() 排出从小到大的一行,打印;
  3. 用 np.argsort() 拿到每个数排第几位,打印;
  4. 用 argsort 把成绩和名字一起按从高到低排(用 argsort()[::-1]);
  5. 找出第二高的分数;
  6. 找出分数最高的那个人的名字。

思考题: 第 4 题为什么要 [::-1]?能不能不倒序、而是用别的方式从高到低排?

点击查看参考答案
# 1. 造数据
scores = np.array([80, 90, 75, 100, 60, 95])
names  = np.array(["小明", "小红", "小刚", "小美", "小李", "小张"])
# 2. 排序
print(np.sort(scores))
[ 60  75  80  90  95 100]
# 3. 排序索引
print(np.argsort(scores))
[4 2 0 1 5 3]

读法: 排好序的第 0 个数(60)原来是第 4 个;第 1 个(75)原来是第 2 个……

# 4. 名字跟着分数一起从高到低排
order = np.argsort(scores)[::-1]
print(names[order])
print(scores[order])
['小美' '小张' '小红' '小明' '小刚' '小李']
[100  95  90  80  75  60]
# 5. 第二高
print(np.sort(scores)[-2])     # 95
# 6. 最高分的人
print(names[scores.argmax()])   # 小美

要点:

  • sort() 给「排好序的数」,argsort() 给「每个数的名次」;
  • argsort() 的结果可以拿去索引任何同长度的数组——这就是「名字跟着分数一起排」的原理;
  • [::-1] 把名次倒过来,就是「从大到小」。

第 4 题不用 [::-1] 的写法:

order = np.argsort(-scores)     # 先取负,负得最少的就是最大的
print(names[order])
['小美' '小张' '小红' '小明' '小刚' '小李']

两种写法结果一样。[::-1] 更直观,-scores 更紧凑—— 但要小心:-scores 对整数数组没问题,遇到最小值取负可能溢出,所以实际项目里 [::-1] 更安全。


练习 5:根据向量范数判断是否执行指定的线性变换#

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

  1. 造一个 4 行的向量数组 vectors:[3, 4]、[1, 0]、[6, 8]、[0.5, 0.5];
  2. 算出每个向量的范数(长度),用 np.linalg.norm(vectors, axis=1);
  3. 用 keepdims=True 再算一次,打印它的形状;
  4. 造一个开关 gate:范数大于 3 的位置为真;
  5. 用 np.where() 实现门控:范数 > 3 的行做 y = 2x - 1,其余保持原样;
  6. 打印结果。

思考题: 第 3 题的 keepdims=True 如果不加,第 5 步的广播还能成立吗?为什么?

点击查看参考答案
# 1. 造向量
vectors = np.array([
    [3.0, 4.0],
    [1.0, 0.0],
    [6.0, 8.0],
    [0.5, 0.5],
])
# 2. 每个向量的长度
norms = np.linalg.norm(vectors, axis=1)
print(norms)
[ 5.          1.         10.          0.70710678]
# 3. 保留维度
norms_2d = np.linalg.norm(vectors, axis=1, keepdims=True)
print(norms_2d.shape)
(4, 1)
# 4. 开关
gate = norms_2d > 3
print(gate)
[[ True]
 [False]
 [ True]
 [False]]
# 5. 门控变换
result = np.where(gate, 2 * vectors - 1, vectors)
print(result)
[[ 5.   7. ]
 [ 1.   0. ]
 [11.  15. ]
 [ 0.5  0.5]]

要点:

  • 第 0 行长度 5 > 3 → 做了变换:2*[3,4] - 1 = [5, 7];
  • 第 1 行长度 1 < 3 → 原样不动;
  • 第 2 行长度 10 > 3 → 做了变换:2*[6,8] - 1 = [11, 15];
  • 第 3 行长度约 0.71 < 3 → 原样不动。

第 3 题的 keepdims=True 是第 5 步能广播成功的前提。

不加它,norms 的形状是 (4,),而 vectors 是 (4, 2): 最后一维 2 vs 4 → 两个都不是 1 → 直接报错。

norms = np.linalg.norm(vectors, axis=1)          # (4,)
gate = norms > 3                                  # (4,)
print(np.where(gate, 2 * vectors - 1, vectors))    # ValueError!
ValueError: operands could not be broadcast together with shapes (4,) (4,2)

加了 keepdims=True 之后,范数变成 (4, 1)——「4 行 1 列」, 正好跟 (4, 2) 的「4 行 2 列」行数对齐,广播就成了。

记住这个万能钥匙:凡是要拿「按行算出来的结果」去跟原数组广播,就加 keepdims=True。


练习小结#

题目用到的核心语法
练习 1(a - b) / (np.abs(a) + np.abs(b))、分母防 0、argmax
练习 2points[:, 1]、np.arctan2(y, x)、np.degrees()
练习 3sum / mean / max / min + axis、argmax(axis=1)、用下标查名字
练习 4np.sort()、np.argsort()、[::-1]、argmax()
练习 5np.linalg.norm(axis=1)、keepdims=True、np.where(条件, A, B)

做完这 5 题,下一步 你已经会写「一整块数据一起算」的代码了。但还有一类操作专门处理**「把数组拆开、挑出来、拼回去」——比如按分位数把极端值压一压(缩尾)、把连续数值映射成离散区间、把一维序列切成一个个数据帧。第 5 章讲这些数组实用工具**,都是真实数据清洗里天天要用的。

阅读 --