返回首页

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

第 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 个230211被压到上界
第 12 个6071.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 档:

档位编号范围含义
0x < 100低
1100 ≤ x < 150中
2150 ≤ x < 200高
3x ≥ 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。

本节小结#

  1. 行极值:max(axis=1) 拿值、argmax(axis=1) 拿位置,再用位置查名字;
  2. 缩尾处理:np.percentile 定上下界 → np.clip 压住,改值不改数量;
  3. 数值量化:np.digitize(值, 边界) 分档,左闭右开,labels[bins] 换标签;
  4. 分帧:sliding_window_view(序列, 窗口长度),帧数 = 长度 - 窗口 + 1;
  5. np.tile 整体复制,np.repeat 元素级复制;
  6. np.diff 算相邻差分,结果会少一个元素,axis 决定方向,n=2 做二阶差分;
  7. 拼接:concatenate 首尾接、vstack 竖着摞、hstack 横着排;
  8. 相关性:np.corrcoef(数据.T),别忘了转置,结果在 -1 ~ 1 之间。

下一节预告 这一章的工具函数不少,第 5.2 节会用 5 道动手题把它们串起来练一遍——从「提取每行极值并查出名字」到「缩尾 + 分档 + 相关系数」的小组合。练完你就能把这一章的工具用到自己的数据上了。

阅读 --