第 5.1 节 核心知识点#
预计学习时长: 45 分钟
本节目标:
- 会提取每行的最大值、最小值以及它们对应的是哪一列;
- 会用分位数 +
np.clip做缩尾处理;- 会用
np.digitize把连续数值量化成分档标签;- 会用
sliding_window_view把一维序列切成滑动窗口;- 分得清
tile()和repeat();- 会用
diff()算相邻差分、concatenate()拼数组、corrcoef()算相关系数。
准备一下本章要用的数据:
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:提取每行的极值(Row Extremes)#
问题: 一张 5 行 4 列的销售表,我想知道每个城市最好的那个季度是哪个。
分两步:先用 max(axis=1) 拿值,再用 argmax(axis=1) 拿位置。
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],
])
row_max = sales.max(axis=1)
print(row_max)[165. 105. 230. 85. 175.]这是「每行的最大值」,5 个数对应 5 个城市。
位置在哪?用 argmax:
row_argmax = sales.argmax(axis=1)
print(row_argmax)[3 3 3 2 3]第 0 行最大值是第 3 个位置,第 1 行也是第 3 个……第 3 行是第 2 个。
把位置翻译成名字:
quarter_names = np.array(["Q1", "Q2", "Q3", "Q4"])
print(quarter_names[row_argmax])['Q4' 'Q4' 'Q4' 'Q3' 'Q4']最小值同理:
print(sales.min(axis=1)) # 每行最小值
print(sales.argmin(axis=1)) # 每行最小值的位置[120. 80. 195. 60. 145.]
[0 0 2 0 1]「值」和「位置」是一对,必须配对使用:
你要的 用哪个 最大值是多少 max(axis=1)最大值在第几列 argmax(axis=1)最小值是多少 min(axis=1)最小值在第几列 argmin(axis=1)然后用位置去查名字数组——这就是从数字里查出名字的标准流程。
如果想同时拿到「谁」和「多少」:
city_names = np.array(["北京", "上海", "广州", "深圳", "成都"])
best = sales.max(axis=1).argmax()
print(city_names[best], sales.max(axis=1)[best])广州 230.0知识点 2:缩尾处理(Winsorize)#
问题: 我的数据里可能混进了几个极端值(比如手滑多打了个 0,变成 23000)。 它们会把平均值、标准差全部带偏——但这些数据又不能直接删(样本量本来就小)。
解法:缩尾处理(Winsorize)——把超出上下界的值「压」到边界上,而不是删掉。
第一步:定上下界。用分位数。
import numpy as np
flat = sales.ravel() # 摊平成一维,20 个数
print(flat)[120. 135. 150. 165. 80. 92. 88. 105. 200. 210. 195. 230. 60. 72.
85. 78. 150. 145. 160. 175.]取 5% 和 95% 分位数当上下界:
lower, upper = np.percentile(flat, [5, 95])
print(lower, upper)71.4 211.0意思就是:比 71.4 小的,都当成 71.4;比 211 大的,都当成 211。
第二步:用 np.clip 压住。
winsorized = np.clip(flat, lower, upper)
print(winsorized)[120. 135. 150. 165. 80. 92. 88. 105. 200. 210. 195. 211.
71.4 72. 85. 78. 150. 145. 160. 175. ]对比一下原数据:
| 位置 | 原值 | 缩尾后 | 变化 |
|---|---|---|---|
| 第 11 个 | 230 | 211 | 被压到上界 |
| 第 12 个 | 60 | 71.4 | 被抬到下界 |
其他数据一个都没动。
缩尾 vs 直接删异常值,区别在哪?
缩尾(clip) 直接删 样本量 不变 变少 极端值的信息 保留在边界上 彻底丢失 均值受到的影响 小 仍然可能有影响 缩尾是「改值」,删除是「改数量」。 样本量小的时候,缩尾几乎总是更稳妥。
顺便看看均值的变化:
print(flat.mean()) # 缩尾前
print(winsorized.mean()) # 缩尾后134.75
134.37极端值被压住了,均值也跟着从 134.75 拉回到 134.37。
np.clip 还能直接用在二维数组上:
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.]]这个特别常用:给销售额设一个「上限 200、下限 80」的合理区间,超出的都拉回来。
知识点 3:数值量化与分帧(Quantize and Frame)#
3.1 数值量化:把连续值变成离散档位#
问题: 我有一列连续的销售数字,但分析的时候只想分「低 / 中 / 高 / 超高」四档。
解法:np.digitize()——按你给的边界,把数分到不同的桶里。
import numpy as np
flat = sales.ravel()
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]怎么读这个结果? 你给了 3 个边界 [100, 150, 200],就分成了 4 档:
| 档位编号 | 范围 | 含义 |
|---|---|---|
0 | x < 100 | 低 |
1 | 100 ≤ x < 150 | 中 |
2 | 150 ≤ x < 200 | 高 |
3 | x ≥ 200 | 超高 |
对照着看几个:
for value, bucket in zip(flat, bins):
print(value, "->", bucket)120.0 -> 1
135.0 -> 1
150.0 -> 2
165.0 -> 2
80.0 -> 0
92.0 -> 0
88.0 -> 0
105.0 -> 1
200.0 -> 3
210.0 -> 3
195.0 -> 2
230.0 -> 3
60.0 -> 0
72.0 -> 0
85.0 -> 0
78.0 -> 0
150.0 -> 2
145.0 -> 1
160.0 -> 2
175.0 -> 2注意:左闭右开——150 落在档位 2(高),不是 1(中)。
把档位翻译成标签:
labels = np.array(["低", "中", "高", "超高"])
print(labels[bins])['中' '中' '高' '高' '低' '低' '低' '中' '超高' '超高' '高' '超高' '低' '低' '低' '低' '高'
'中' '高' '高']这一步就是「用数组当下标」——labels[bins] 直接把编号映射成文字。
想统计每一档有几个?
print(np.bincount(bins))[7 4 6 3]正好 20 个。
np.bincount()是统计「每个值出现了几次」的利器。
3.2 分帧:把一维序列切成一个个数据帧#
问题: 我有一串按时间排好的数据,想把它切成长度为 3 的滑动窗口, 这样每个窗口就是「连续 3 个点」,可以用来算移动平均。
解法:np.lib.stride_tricks.sliding_window_view()。
import numpy as np
series = np.arange(12) # 0 到 11
print(series)[ 0 1 2 3 4 5 6 7 8 9 10 11]切成窗口长度为 3 的帧:
frames = np.lib.stride_tricks.sliding_window_view(series, 3)
print(frames.shape)
print(frames)(10, 3)
[[ 0 1 2]
[ 1 2 3]
[ 2 3 4]
[ 3 4 5]
[ 4 5 6]
[ 5 6 7]
[ 6 7 8]
[ 7 8 9]
[ 8 9 10]
[ 9 10 11]]规律:窗口长度 3,序列长度 12 → 一共 12 - 3 + 1 = 10 帧。
这个「滑动」的效果一眼就能看出来——每一帧都往后挪一位。
有了帧,就能算移动平均了:
print(frames.mean(axis=1))[ 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.]每一帧的均值,天然就是「移动平均」的结果。
sliding_window_view是 NumPy 1.20 才有的函数。 老版本没有的话,可以用下面的土办法替代(结果一样):frames = np.array([series[i:i+3] for i in range(len(series) - 2)])但新版本强烈推荐用
sliding_window_view——它不复制数据,速度快很多。
知识点 4:数组重复与差分(Tile and Diff)#
4.1 tile 和 repeat:两种「重复」#
它们很容易混,但重复的规律完全不同。
np.tile():整体重复。
import numpy as np
a = np.array([1, 2, 3])
print(np.tile(a, 2)) # 重复 2 次(接在后面)
print(np.tile(a, (2, 2))) # 变成 2 行 2 列[1 2 3 1 2 3]
[[1 2 3 1 2 3]
[1 2 3 1 2 3]]np.tile(a, 2) 的意思是「把整个数组复制 2 遍,接在后面」。
np.repeat():元素级重复。
print(np.repeat(a, 2))[1 1 2 2 3 3]「每个元素自己重复 2 遍」。
一张表记住区别:
np.tile(a, 2)np.repeat(a, 2)结果 [1 2 3 1 2 3][1 1 2 2 3 3]规律 整体重复 每个元素重复 常用场景 复制一份数据做对比 把每个值展开成多个
np.tile是「复制粘贴整张表」,np.repeat是「每个格子填两次」。
4.2 diff:相邻差分#
问题: 我想知道每个季度比上季度涨了多少。
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],
])
print(np.diff(sales, axis=1))[[ 15. 15. 15.]
[ 12. -4. 17.]
[ 10. -15. 35.]
[ 12. 13. -7.]
[ -5. 15. 15.]]读法: 第 0 行是 [120, 135, 150, 165] 的差分 →
135-120=15、150-135=15、165-150=15 ✓
注意结果的列数变少了! 4 列变成 3 列——差分必然少一个。
按行差分(axis=0):
print(np.diff(sales, axis=0))[[ -40. -43. -62. -60.]
[ 120. 118. 107. 125.]
[-140. -138. -110. -152.]
[ 90. 73. 75. 97.]]第 0 行是「上海 - 北京」(第 1 行减第 0 行)。
一维数组的差分:
print(np.diff([1, 3, 6, 10]))[2 3 4]差分常用在「求变化量」: 累计和的逆运算。
print(np.cumsum([1, 3, 6, 10])) # 累计和
print(np.diff([1, 3, 6, 10])) # 差分[ 1 4 10 20]
[2 3 4]差分还能用来算「增长率」:
change = np.diff(sales, axis=1) rate = change / sales[:, :-1] print(np.round(rate * 100, 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]]注意
sales[:, :-1]——分母要比分子少一列,因为差分本来就少一个。 这个「分子分母长度不一样」的细节,是初学者最容易出错的地方。
4.3 高阶差分#
np.diff() 还可以做「二阶差分」——差分的差分。
print(np.diff([1, 3, 6, 10], n=2))[1 1]读法: 一阶差分 [2, 3, 4],再差分一次 → [1, 1]。
物理意义: 一阶差分是「速度」,二阶差分是「加速度」。
知识点 5:数组拼接与相关性计算(Concat and Correlate)#
5.1 拼接:concatenate / vstack / hstack#
import numpy as np
a = np.array([1, 2])
b = np.array([3, 4])
print(np.concatenate([a, b]))[1 2 3 4]concatenate 就是「接龙」——把几个数组首尾接起来。
按行接和按列接,用 vstack / hstack:
print(np.vstack([a, b]))[[1 2]
[3 4]]print(np.hstack([a, b]))[1 2 3 4]三个函数的区别:
| 函数 | 干什么 | 例子 |
|---|---|---|
concatenate | 首尾接(最灵活) | [1,2] + [3,4] → [1,2,3,4] |
vstack | 竖着摞(按行接) | [1,2] + [3,4] → 2 行 |
hstack | 横着排(按列接) | [1,2] + [3,4] → 一行 |
concatenate要求被接的数组「除了拼接的那一维,其余维度都一样」。print(np.concatenate([np.zeros((2, 3)), np.zeros((4, 3))])) # OK print(np.concatenate([np.zeros((2, 3)), np.zeros((2, 4))])) # 报错ValueError: all the input array dimensions except for the concatenation axis must match exactly, but along dimension 1, the array at index 0 has size 3 and the array at index 1 has size 4接 2×3 和 2×4 是不行的——列数对不上。
5.2 相关系数:np.corrcoef#
问题: Q1 和 Q2 的销售额有关系吗?
解法:np.corrcoef()——算出两两之间的皮尔逊相关系数**。**
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],
])
corr = np.corrcoef(sales.T) # 注意转置 .T
print(np.round(corr, 4))[[1. 0.9904 0.9751 0.9875]
[0.9904 1. 0.9699 0.9895]
[0.9751 0.9699 1. 0.9876]
[0.9875 0.9895 0.9876 1. ]]这张表怎么读?
- 对角线全是 1——自己跟自己的相关系数当然是 1;
- 第 0 行第 1 列是 0.9904——Q1 和 Q2 的相关系数;
- 表是对称的(
corr[i, j] == corr[j, i])。
相关系数的取值范围是 -1 ~ 1:
| 值 | 含义 |
|---|---|
+1 | 完全正相关(一个涨另一个必涨) |
0 | 无线性关系 |
-1 | 完全负相关 |
这里四个季度之间都是 0.97 以上,说明它们高度正相关——很合理,旺季大家一起涨。
为什么要
.T转置?np.corrcoef()默认是按行算的:每一行算一个变量和所有行变量的相关性。 但我们想要的是「每一列是一个变量」——所以要先转置,让列变成行。np.corrcoef(sales.T) # ✅ 按列算:4 个季度之间的关系 np.corrcoef(sales) # ❌ 按行算:5 个城市之间的关系转置
sales.T是corrcoef最容易忘的一步,忘了结果形状就不对。
只算两个变量之间的相关系数,更简单:
q1 = sales[:, 0]
q2 = sales[:, 1]
print(np.corrcoef(q1, q2))[[1. 0.99043051]
[0.99043051 1. ]]或者手算(知道公式的话):
print(np.corrcoef(q1, q2)[0, 1])0.9904305064007455注意 [0, 1]——np.corrcoef() 返回的是一张 2×2 的相关系数矩阵,
[0, 1] 取的是「q1 和 q2 之间的那一个相关系数」。
想手算相关系数的话,协方差和标准差的「自由度」必须一致:
# ✅ 都用 ddof=0(总体) print(np.cov(q1, q2, ddof=0)[0, 1] / (q1.std() * q2.std())) # ✅ 都用 ddof=1(样本) print(np.cov(q1, q2)[0, 1] / (q1.std(ddof=1) * q2.std(ddof=1)))两边混用会算出 1.238 这种离谱的数——因为
np.cov默认是ddof=1, 而arr.std()默认是ddof=0。初学阶段直接用np.corrcoef()就好。
相关系数只说明「一起变」,不说明「谁导致谁」。 相关 ≠ 因果。 这是初学者最容易上当的地方,详见第 5.3 节 Q10。
本节小结#
- 行极值:
max(axis=1)拿值、argmax(axis=1)拿位置,再用位置查名字; - 缩尾处理:
np.percentile定上下界 →np.clip压住,改值不改数量; - 数值量化:
np.digitize(值, 边界)分档,左闭右开,labels[bins]换标签; - 分帧:
sliding_window_view(序列, 窗口长度),帧数 =长度 - 窗口 + 1; np.tile整体复制,np.repeat元素级复制;np.diff算相邻差分,结果会少一个元素,axis决定方向,n=2做二阶差分;- 拼接:
concatenate首尾接、vstack竖着摞、hstack横着排; - 相关性:
np.corrcoef(数据.T),别忘了转置,结果在 -1 ~ 1 之间。
下一节预告 这一章的工具函数不少,第 5.2 节会用 5 道动手题把它们串起来练一遍——从「提取每行极值并查出名字」到「缩尾 + 分档 + 相关系数」的小组合。练完你就能把这一章的工具用到自己的数据上了。