第 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 个任务:
- 算出
a和b的归一化差值:(a - b) / (|a| + |b|),存到norm_diff; - 打印
norm_diff; - 用安全版(分母加
1e-9)再算一次,存到safe_diff; - 找出差距最大的那一对下标(用
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 对(
200vs150),差了约 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 个任务:
- 造一个 5 个点的数组
points,每行是一个点的(x, y)坐标:(3, 4)、(1, 0)、(0, 2)、(-1, -1)、(5, 5); - 用
np.arctan2()算出每个点的弧度角,存到rad; - 用
np.degrees()转成角度,存到deg; - 打印
deg; - 找出角度最大的点,打印它的坐标。
思考题: 第 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 个任务:
- 算
sales的总和(一个数); - 算每列的合计(
axis=0)和每行的合计(axis=1); - 算每列的平均值和每行的平均值;
- 算每列的最大值、最小值;
- 找出每个城市销售额最高的是哪个季度(用
argmax(axis=1)),并打印城市名和季度名; - 找出总和最高的城市,用
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 个任务:
- 造一个成绩数组
scores = np.array([80, 90, 75, 100, 60, 95]); - 用
np.sort()排出从小到大的一行,打印; - 用
np.argsort()拿到每个数排第几位,打印; - 用
argsort把成绩和名字一起按从高到低排(用argsort()[::-1]); - 找出第二高的分数;
- 找出分数最高的那个人的名字。
思考题: 第 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 个任务:
- 造一个 4 行的向量数组
vectors:[3, 4]、[1, 0]、[6, 8]、[0.5, 0.5]; - 算出每个向量的范数(长度),用
np.linalg.norm(vectors, axis=1); - 用
keepdims=True再算一次,打印它的形状; - 造一个开关
gate:范数大于 3 的位置为真; - 用
np.where()实现门控:范数 > 3 的行做y = 2x - 1,其余保持原样; - 打印结果。
思考题: 第 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): 最后一维2vs4→ 两个都不是 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 |
| 练习 2 | points[:, 1]、np.arctan2(y, x)、np.degrees() |
| 练习 3 | sum / mean / max / min + axis、argmax(axis=1)、用下标查名字 |
| 练习 4 | np.sort()、np.argsort()、[::-1]、argmax() |
| 练习 5 | np.linalg.norm(axis=1)、keepdims=True、np.where(条件, A, B) |
做完这 5 题,下一步 你已经会写「一整块数据一起算」的代码了。但还有一类操作专门处理**「把数组拆开、挑出来、拼回去」——比如按分位数把极端值压一压(缩尾)、把连续数值映射成离散区间、把一维序列切成一个个数据帧。第 5 章讲这些数组实用工具**,都是真实数据清洗里天天要用的。