返回首页

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

第 5.2 节 编程练习#

预计学习时长: 45 分钟

练习方式: 新建一个 Jupyter Notebook,把下面的数据抄进去,然后每题独立完成。第 3 题内容最多,建议留够时间。

准备:先把数据建好#

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

练习 1:提取二维数组每一行的最大值、最小值及对应索引#

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

  1. 算出每行的最大值,存到 row_max;
  2. 算出每行的最小值,存到 row_min;
  3. 算出每行最大值所在的列号,存到 row_argmax;
  4. 算出每行最小值所在的列号,存到 row_argmin;
  5. 用 quarter_names[row_argmax] 把列号翻译成季度名,打印出来;
  6. 找出最好卖的城市和最难卖的城市(比较每行的合计),打印名字。

思考题: 第 6 题要比较「每行的合计」,为什么要先 sum(axis=1) 再 argmax(),不能直接对 sales 用 argmax()?

点击查看参考答案
# 1. 每行最大值
row_max = sales.max(axis=1)
print(row_max)
[165. 105. 230.  85. 175.]
# 2. 每行最小值
row_min = sales.min(axis=1)
print(row_min)
[120.  80. 195.  60. 145.]
# 3. 每行最大值的列号
row_argmax = sales.argmax(axis=1)
print(row_argmax)
[3 3 3 2 3]
# 4. 每行最小值的列号
row_argmin = sales.argmin(axis=1)
print(row_argmin)
[0 0 2 0 1]
# 5. 翻译成季度名
print(quarter_names[row_argmax])
print(quarter_names[row_argmin])
['Q4' 'Q4' 'Q4' 'Q3' 'Q4']
['Q1' 'Q1' 'Q3' 'Q1' 'Q2']
# 6. 最好卖 / 最难卖的城市
totals = sales.sum(axis=1)
print(city_names[totals.argmax()], totals.max())
print(city_names[totals.argmin()], totals.min())
广州 835.0
深圳 295.0

要点:

  • 「值」用 max / min,「位置」用 argmax / argmin,两者是一对;
  • 第 5 步的 quarter_names[row_argmax] 就是**「用位置查名字」**;
  • 第 6 题必须先 sum(axis=1) 把每行压成一个数,才能比较。

第 6 题为什么不能直接 sales.argmax()?

print(sales.argmax())
11

不写 axis 时,NumPy 会把整个数组当成一个整体,返回「第 11 个元素」这个下标。 那是一维下标,不是「第几个城市」——用它去索引 city_names 会得到完全不相干的结果。 聚合函数只要处理二维数组,就一定要写 axis。


练习 2:对数组进行缩尾处理,限制极端值的影响#

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

  1. 把 sales 摊平成一维(用 ravel()),存到 flat;
  2. 用 np.percentile(flat, [10, 90]) 取 10% 和 90% 分位数当上下界;
  3. 用 np.clip() 把超出上下界的值压住,存到 winsorized;
  4. 打印 winsorized;
  5. 比较缩尾前后的均值和标准差;
  6. 用 np.clip(sales, 80, 200) 给二维表设一个「80~200」的合理区间,打印结果。

思考题: 第 3 题里被改动的值有几个?为什么是「改成边界值」而不是「删掉」?

点击查看参考答案
# 1. 摊平
flat = sales.ravel()
print(flat.shape)    # (20,)
# 2. 上下界
lower, upper = np.percentile(flat, [10, 90])
print(lower, upper)
77.4 201.0
# 3. 缩尾
winsorized = np.clip(flat, lower, upper)
print(winsorized)
[120.  135.  150.  165.   80.   92.   88.  105.  200.  201.  195.  201.
  77.4  77.4  85.   78.  150.  145.  160.  175. ]
# 4. 对比统计量
print("缩尾前:均值", flat.mean(), "标准差", flat.std())
print("缩尾后:均值", winsorized.mean(), "标准差", winsorized.std())
缩尾前:均值 134.75 标准差 49.84964894560442
缩尾后:均值 133.99 标准差 45.15756747213029
# 5. 直接给二维表设区间
print(np.clip(sales, 80, 200))
[[120. 135. 150. 165.]
 [ 80.  92.  88. 105.]
 [200. 200. 195. 200.]
 [ 80.  80.  85.  80.]
 [150. 145. 160. 175.]]

要点:

  • 被改动的只有 4 个数:210 → 201、230 → 201、60 → 77.4、72 → 77.4;
  • 其余 16 个数一个都没动;
  • 标准差从 49.85 降到了 45.16,说明极端值的影响被压小了。

「压到边界」而不是「删掉」,这才是缩尾的意义:

print(len(flat), len(winsorized))     # 20 20 —— 数量不变

如果改成删除,就会变成 16 个数了。 样本量本来就小的时候,删数据代价太大,压边界是更稳妥的做法。


练习 3:将连续数值映射到离散区间,并对一维序列进行分帧#

题目: 分成两小问。

第一小问:量化。

  1. 用 np.digitize() 把 flat 按 [100, 150, 200] 分成 4 档,存到 bins;
  2. 打印 bins;
  3. 用 np.bincount(bins) 统计每档有几个;
  4. 造一个标签数组 labels = np.array(["低", "中", "高", "超高"]), 用 labels[bins] 把编号换成文字。

第二小问:分帧。

  1. 造一个序列 series = np.array([10., 12, 14, 16, 18, 20, 22]);
  2. 用 sliding_window_view(series, 3) 切成长度为 3 的滑动窗口;
  3. 打印帧的形状和内容;
  4. 算出每一帧的平均值(这就是「移动平均」)。

思考题: 第 2 小问的序列长度是 7、窗口长度是 3,为什么结果是 5 帧而不是 7 帧?

点击查看参考答案
# 1. 分档
bins = np.digitize(flat, [100.0, 150.0, 200.0])
print(bins)
[1 1 2 2 0 0 0 1 3 3 2 3 0 0 0 0 2 1 2 2]
# 2. 每档几个
print(np.bincount(bins))
[7 4 6 3]

加起来正好 20 个 ✓

# 3. 换成标签
labels = np.array(["低", "中", "高", "超高"])
print(labels[bins])
['中' '中' '高' '高' '低' '低' '低' '中' '超高' '超高' '高' '超高' '低' '低' '低' '低' '高'
 '中' '高' '高']
# 4. 造序列并分帧
series = np.array([10.0, 12.0, 14.0, 16.0, 18.0, 20.0, 22.0])

frames = np.lib.stride_tricks.sliding_window_view(series, 3)
print(frames.shape)
print(frames)
(5, 3)
[[10. 12. 14.]
 [12. 14. 16.]
 [14. 16. 18.]
 [16. 18. 20.]
 [18. 20. 22.]]
# 5. 每一帧的平均(移动平均)
print(frames.mean(axis=1))
[12. 14. 16. 18. 20.]

要点:

  • 档位编号从 0 开始:0 是「低于 100」,3 是「不低于 200」;
  • digitize 是左闭右开:150 落在档位 2(高),200 落在档位 3(超高);
  • labels[bins] 就是「用数组当下标」——一行完成「编号 → 文字」的映射。

第 8 题为什么是 5 帧? 帧数 = 序列长度 − 窗口长度 + 1 = 7 − 3 + 1 = 5。

想数清楚:第一个窗口是 [10, 12, 14],最后一个是 [18, 20, 22], 一共 5 个起点(10、12、14、16、18 各开一窗)。

窗口往后挪一格,右边就会「掉」掉一个数——所以帧数一定比元素数少。


练习 4:使用 tile() 重复数组,并使用 diff() 计算相邻元素的差值#

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

  1. 用 np.tile() 把 [1, 2, 3] 复制成一个 2 行 4 列的数组;
  2. 用 np.repeat() 把 [1, 2, 3] 的每个元素重复 2 次,打印;
  3. 对比第 1、2 步的结果,说说两者的区别;
  4. 用 np.diff() 算 [10, 12, 14, 16, 18, 20, 22] 的相邻差分;
  5. 用 np.diff(sales, axis=1) 算出每个城市每个季度环比涨了多少;
  6. 算出第 5 步结果的增长率(差分 ÷ 原来的值 × 100%,保留 2 位小数)。

思考题: 第 6 题的分母要用 sales[:, :-1] 还是 sales[:, 1:]?为什么?

点击查看参考答案
# 1. tile:2 行 4 列
print(np.tile([1, 2, 3], (2, 4)))
[[1 2 3 1 2 3 1 2 3 1 2 3]
 [1 2 3 1 2 3 1 2 3 1 2 3]]
# 2. repeat:每个元素重复 2 次
print(np.repeat([1, 2, 3], 2))
[1 1 2 2 3 3]

区别一眼就看出来了:tile 是「整张表复制」,repeat 是「每个格子填两遍」。

# 3. 差分
print(np.diff([10, 12, 14, 16, 18, 20, 22]))
[2 2 2 2 2 2]
# 4. 环比变化量
change = np.diff(sales, axis=1)
print(change)
[[ 15.  15.  15.]
 [ 12.  -4.  17.]
 [ 10. -15.  35.]
 [ 12.  13.  -7.]
 [ -5.  15.  15.]]
# 5. 环比增长率
rate = change / sales[:, :-1] * 100
print(np.round(rate, 2))
[[12.5  11.11 10.  ]
 [15.   -4.35 19.32]
 [ 5.   -7.14 17.95]
 [20.   18.06 -8.24]
 [-3.33 10.34  9.38]]

要点:

  • np.diff(sales, axis=1) 的列数比原来少 1(4 列 → 3 列),因为差分必然少一个;
  • 第 0 行的 [15, 15, 15] 就是 135-120、150-135、165-150;
  • 第 4 行第 2 列是 -5,说明成都的 Q2 比 Q1 跌了 5 万。

第 6 题的分母必须是 sales[:, :-1]!

差分的结果 change 是「后一个减前一个」,所以它的第 j 列对应的是 原来的第 j+1 列。要拿它去除以「变化之前的那个数」, 分母就得取去掉最后一列的原数组:

change / sales[:, :-1]     # ✅ 分母:120, 135, 150
change / sales[:, 1:]      # ❌ 分母:135, 150, 165 —— 错位了

一句话:分子是「后减前」,分母就要用「前」。


练习 5:使用 concatenate() 拼接数组,并计算两个变量之间的相关系数#

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

  1. 造两个数组 x = np.array([1., 2, 3]) 和 y = np.array([4., 5, 6]);
  2. 用 np.concatenate() 把它们首尾接起来,打印;
  3. 用 np.vstack() 把它们竖着摞起来,打印;
  4. 用 np.hstack() 把它们横着排起来,打印;
  5. 算 Q1 和 Q3 两列销售额的相关系数(记得转置);
  6. 打印这张 2×2 的相关系数矩阵,并说出「Q1 和 Q3 的相关系数」是矩阵里的哪个数。

思考题: 第 5 题如果不写 .T,np.corrcoef(sales) 算出来的是什么?

点击查看参考答案
# 1. 造数据
x = np.array([1.0, 2.0, 3.0])
y = np.array([4.0, 5.0, 6.0])
# 2. 首尾接
print(np.concatenate([x, y]))
[1. 2. 3. 4. 5. 6.]
# 3. 竖着摞
print(np.vstack([x, y]))
[[1. 2. 3.]
 [4. 5. 6.]]
# 4. 横着排
print(np.hstack([x, y]))
[1. 2. 3. 4. 5. 6.]
# 5. Q1 和 Q3 的相关系数
corr = np.corrcoef(sales.T)
print(corr)
[[1.         0.99043051 0.97509237 0.98747276]
 [0.99043051 1.         0.96991028 0.98949899]
 [0.97509237 0.96991028 1.         0.98759119]
 [0.98747276 0.98949899 0.98759119 1.        ]]
# 6. Q1 与 Q3 的相关系数 = 矩阵的 [0, 2]
print(corr[0, 2])
0.975092374691495

要点:

  • 这张矩阵是 4×4,因为 sales.T 是 4 行(每个季度一行),算的是4 个变量两两之间的关系;
  • 对角线全是 1(自己跟自己);
  • 矩阵是对称的,corr[0, 2] 和 corr[2, 0] 相同;
  • Q1 和 Q3 的相关系数是 0.975,高度正相关。

第 5 题如果不转置会怎样?

print(np.corrcoef(sales).shape)
(5, 5)

算的是「5 个城市」之间的关系,而不是「4 个季度」之间的关系—— 因为 corrcoef 默认按行算,每一行是一个变量。 想按列算变量,就必须先 .T 转置。


练习小结#

题目用到的核心函数
练习 1max/min(axis=1)、argmax/argmin(axis=1)、sum(axis=1)、用下标查名字
练习 2ravel()、np.percentile、np.clip、std()
练习 3np.digitize、np.bincount、labels[bins]、sliding_window_view
练习 4np.tile、np.repeat、np.diff(axis=1)、sales[:, :-1]
练习 5np.concatenate、np.vstack、np.hstack、np.corrcoef(数据.T)

做完这 5 题,下一步 恭喜,五章的核心内容你都学完了!

  • 第 1 章:造数组(array / zeros / arange / reshape)
  • 第 2 章:取数据(索引、切片、Fancy Indexing、布尔掩码、视图 vs 副本)
  • 第 3 章:广播([:, None]、外和、按行按列缩放、归一化)
  • 第 4 章:向量化(np.where、聚合函数 + axis、sort / argsort、范数)
  • 第 5 章:实用工具(极值、缩尾、量化分帧、重复差分、拼接相关性)

接下来最值得做的一件事:把这些函数用到你自己的数据上。 找一个 CSV,导进来,用第 5 章的「缩尾 + 分档 + 相关系数」跑一遍—— 这就是数据分析的日常动作。

阅读 --