第 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 个任务:
- 算出每行的最大值,存到
row_max; - 算出每行的最小值,存到
row_min; - 算出每行最大值所在的列号,存到
row_argmax; - 算出每行最小值所在的列号,存到
row_argmin; - 用
quarter_names[row_argmax]把列号翻译成季度名,打印出来; - 找出最好卖的城市和最难卖的城市(比较每行的合计),打印名字。
思考题: 第 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 个任务:
- 把
sales摊平成一维(用ravel()),存到flat; - 用
np.percentile(flat, [10, 90])取 10% 和 90% 分位数当上下界; - 用
np.clip()把超出上下界的值压住,存到winsorized; - 打印
winsorized; - 比较缩尾前后的均值和标准差;
- 用
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:将连续数值映射到离散区间,并对一维序列进行分帧#
题目: 分成两小问。
第一小问:量化。
- 用
np.digitize()把flat按[100, 150, 200]分成 4 档,存到bins; - 打印
bins; - 用
np.bincount(bins)统计每档有几个; - 造一个标签数组
labels = np.array(["低", "中", "高", "超高"]), 用labels[bins]把编号换成文字。
第二小问:分帧。
- 造一个序列
series = np.array([10., 12, 14, 16, 18, 20, 22]); - 用
sliding_window_view(series, 3)切成长度为 3 的滑动窗口; - 打印帧的形状和内容;
- 算出每一帧的平均值(这就是「移动平均」)。
思考题: 第 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 个任务:
- 用
np.tile()把[1, 2, 3]复制成一个 2 行 4 列的数组; - 用
np.repeat()把[1, 2, 3]的每个元素重复 2 次,打印; - 对比第 1、2 步的结果,说说两者的区别;
- 用
np.diff()算[10, 12, 14, 16, 18, 20, 22]的相邻差分; - 用
np.diff(sales, axis=1)算出每个城市每个季度环比涨了多少; - 算出第 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 个任务:
- 造两个数组
x = np.array([1., 2, 3])和y = np.array([4., 5, 6]); - 用
np.concatenate()把它们首尾接起来,打印; - 用
np.vstack()把它们竖着摞起来,打印; - 用
np.hstack()把它们横着排起来,打印; - 算 Q1 和 Q3 两列销售额的相关系数(记得转置);
- 打印这张 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转置。
练习小结#
| 题目 | 用到的核心函数 |
|---|---|
| 练习 1 | max/min(axis=1)、argmax/argmin(axis=1)、sum(axis=1)、用下标查名字 |
| 练习 2 | ravel()、np.percentile、np.clip、std() |
| 练习 3 | np.digitize、np.bincount、labels[bins]、sliding_window_view |
| 练习 4 | np.tile、np.repeat、np.diff(axis=1)、sales[:, :-1] |
| 练习 5 | np.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 章的「缩尾 + 分档 + 相关系数」跑一遍—— 这就是数据分析的日常动作。