返回首页

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

第 5.3 节 常见问题#

这一节收集了初学者在 NumPy 数组实用工具上最常卡的 10 个问题。每个问题都给出直接答案,能跑代码的都附上完整示例。

怎么用这一节 不用通读。遇到问题用 Ctrl+F 搜关键词,直接跳到对应那条。 这是整套课程的最后一节,看完这一节,五章内容就齐了。

准备一下数据,下面每题都会用到:

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

Q1:如何使用 np.max()、np.min() 和 np.argmax() 提取数组极值及其位置?#

答: max / min 给「值」,argmax / argmin 给「位置」。

import numpy as np

scores = np.array([80, 90, 75, 100, 60])

print(scores.max())        # 最大值
print(scores.min())        # 最小值
print(scores.argmax())     # 最大值在第几个(下标)
print(scores.argmin())     # 最小值在第几个(下标)
100
60
3
4

argmax 返回的是「下标」,不是值。

把位置翻译成名字,是这套工具最常见的用法:

names = np.array(["小明", "小红", "小刚", "小美", "小李"])
print(names[scores.argmax()])    # 小美

二维数组上要配 axis:

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(sales.max(axis=1))       # 每行最大值
print(sales.argmax(axis=1))    # 每行最大值在第几列
[165. 105. 230.  85. 175.]
[3 3 3 2 3]

四个函数的对照表:

函数给出什么返回类型
max()最大值是多少数字
argmax()最大值在第几个整数下标
min()最小值是多少数字
argmin()最小值在第几个整数下标

规律:arg 前缀 = 返回位置。

Q2:如何沿指定轴获取二维数组每行或每列的最大值和最小值?#

答: 用 axis 参数,axis=0 对列、axis=1 对行。

print(sales.max(axis=0))      # 每列的最大值
print(sales.min(axis=0))      # 每列的最小值
[200. 210. 195. 230.]
[60. 72. 85. 78.]
print(sales.max(axis=1))      # 每行的最大值
print(sales.min(axis=1))      # 每行的最小值
[165. 105. 230.  85. 175.]
[120.  80. 195.  60. 145.]

结果长度帮你确认有没有写对:

调用结果长度等于
sales.max(axis=0)4列数
sales.max(axis=1)5行数

不确定 axis 的时候,用这个办法: 先问自己「我要几个结果」。

  • 要 4 个(每个季度一个)→ axis=0
  • 要 5 个(每个城市一个)→ axis=1

别去背「0 是列」——数一下结果个数就知道了。

别忘了 arg 版本也能加 axis:

print(sales.argmax(axis=0))    # 每列最大值在第几行
print(sales.argmin(axis=1))    # 每行最小值在第几列
[2 2 2 2]
[0 0 2 0 1]

Q3:什么是 Winsorize(缩尾处理)?它与直接删除异常值有什么区别?#

答: 缩尾 = 把超出上下界的值「压」到边界上;删除 = 把它们从数据里拿掉。

import numpy as np

flat = sales.ravel()

lower, upper = np.percentile(flat, [10, 90])
winsorized = np.clip(flat, lower, upper)

对比一下:

缩尾(clip)直接删除
样本量不变(20 → 20)变少(20 → 18)
极端值留在边界上彻底消失
均值受影响小仍然可能被带偏
适合场景样本量小、极端值可能是真实值极端值确定是录入错误

缩尾之后,那些极端值不再「称霸」统计量:

print(flat.std())            # 缩尾前
print(winsorized.std())      # 缩尾后
49.84964894560442
45.15756747213029

标准差明显变小了。

什么时候该缩尾,什么时候该删?

  • 手滑多打了个 0(230 → 2300)——这是录入错误,应该修正或删除;
  • 收入分布里的高收入群体——这是真实存在的,删掉会扭曲结论,应该缩尾。

关键问题是:「这个极端值是真的,还是假的?」

Q4:如何使用分位数确定缩尾处理的上下界?#

答: 用 np.percentile()(或 np.quantile())取你想要的百分位。

flat = sales.ravel()

# 5% 和 95% 分位数
lower, upper = np.percentile(flat, [5, 95])
print(lower, upper)
71.4 211.0

一次取多个分位数,传个列表就行:

print(np.percentile(flat, [0, 25, 50, 75, 100]))
[ 60.    87.25 140.   167.5  230.  ]

np.quantile() 是同一件事,只是用 0~1 表示:

print(np.quantile(flat, [0.05, 0.95]))     # 等价于上面的 5% / 95%
[ 71.4 211. ]

选分位数的小经验:

分位数压掉的比例适合
1% / 99%最极端的 2%数据很干净,只想去掉录入错误
5% / 95%5%最常用
10% / 90%10%数据长尾明显、极端值很多

压得越多,数据越「整齐」,但丢失的信息也越多。

最后接上 np.clip():

winsorized = np.clip(flat, lower, upper)

这一套(percentile 定界 → clip 压住)就是缩尾处理的标准写法。

Q5:什么是数值量化?如何将连续数值映射到指定的离散区间?#

答: 把连续数字按边界切成几档,每一档打个标签。

import numpy as np

flat = sales.ravel()

# 按 100 / 150 / 200 分成 4 档
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]

档位编号的对应关系:

编号范围
0x < 100
1100 ≤ x < 150
2150 ≤ x < 200
3x ≥ 200

换成人话(标签):

labels = np.array(["低", "中", "高", "超高"])
print(labels[bins])
['中' '中' '高' '高' '低' '低' '低' '中' '超高' '超高' '高' '超高' '低' '低' '低' '低' '高'
 '中' '高' '高']

labels[bins] 就是「用数组当下标」——一行完成「编号 → 文字」的映射。

统计每档有几个:

print(np.bincount(bins))
[7 4 6 3]

digitize 是左闭右开!

  • 150 → 落在档位 2(高),不是 1(中);
  • 200 → 落在档位 3(超高)。

想改成右闭左开,加 right=True:

print(np.digitize(flat, [100.0, 150.0, 200.0], right=True))

Q6:如何将一维数组划分为固定长度的数据帧或窗口?#

答: 用 np.lib.stride_tricks.sliding_window_view()。

import numpy as np

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.]]

帧数 = 序列长度 − 窗口长度 + 1 = 7 − 3 + 1 = 5。

这个「滑动」的效果一眼能看出来——每一帧都往后挪一位。

有了帧就能算移动平均:

print(frames.mean(axis=1))
[12. 14. 16. 18. 20.]

sliding_window_view 是 NumPy 1.20+ 才有的函数。 老版本可以用土办法:

frames = np.array([series[i:i+3] for i in range(len(series) - 2)])

但新版本强烈推荐用 sliding_window_view——它不复制数据,速度快很多。

另外注意:sliding_window_view 返回的是视图,不是副本。 想改它,先 .copy()。

Q7:np.tile() 和 np.repeat() 有什么区别?#

答: tile 整体复制,repeat 元素级复制。

import numpy as np

a = np.array([1, 2, 3])

print(np.tile(a, 2))         # 整体重复 2 次
print(np.repeat(a, 2))       # 每个元素重复 2 次
[1 2 3 1 2 3]
[1 1 2 2 3 3]

tile 还能指定重复成几行几列:

print(np.tile(a, (2, 2)))
[[1 2 3 1 2 3]
 [1 2 3 1 2 3]]

repeat 也可以按元素重复:

print(np.repeat(a, [1, 2, 3]))    # 每个元素重复不同的次数
[1 2 2 3 3 3]

一张表记住区别:

np.tile(a, 2)np.repeat(a, 2)
结果[1 2 3 1 2 3][1 1 2 2 3 3]
规律整块重复每个元素重复
常用复制一份数据做对比把每个值展开成多个

一句话:tile 是复制粘贴整张表,repeat 是每个格子填两遍。

Q8:np.diff() 如何计算相邻元素之间的差值?如何指定差分阶数?#

答: np.diff() 默认算「后一个减前一个」,用 n= 指定做几阶差分。

import numpy as np

print(np.diff([1, 3, 6, 10]))
[2 3 4]

[1, 3, 6, 10] 的差分是 [3-1, 6-3, 10-6] = [2, 3, 4] ✓

二阶差分(差分的差分):

print(np.diff([1, 3, 6, 10], n=2))
[1 1]

读法: 一阶差分 [2, 3, 4],再差分一次 → [3-2, 4-3] = [1, 1]。

物理意义: 一阶差分是「速度」,二阶差分是「加速度」。

二维数组用 axis 决定方向:

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.]]
print(np.diff(sales, axis=0))    # 纵向差分(城市之间)
[[ -40.  -43.  -62.  -60.]
 [ 120.  118.  107.  125.]
 [-140. -138. -110. -152.]
 [  90.   73.   75.   97.]]

差分的结果必然少一个元素!

  • 4 列 → 差分后 3 列;
  • 5 行 → 差分后 4 行。

所以算增长率时,分母要「切掉最后一个」:

change = np.diff(sales, axis=1)          # 4 行 3 列
rate = change / sales[:, :-1] * 100      # 分母也是 4 行 3 列

写成 sales[:, 1:] 就错位了。

Q9:np.concatenate()、np.vstack() 和 np.hstack() 分别适用于什么场景?#

答: 三个都是拼接,只是方向不同。

import numpy as np

a = np.array([1, 2])
b = np.array([3, 4])

print(np.concatenate([a, b]))    # 首尾接
print(np.vstack([a, b]))         # 竖着摞
print(np.hstack([a, b]))         # 横着排
[1 2 3 4]
[[1 2]
 [3 4]]
[1 2 3 4]

怎么选?

场景用哪个
把两批数据接成一条长序列concatenate
把两个「样本」摞成一张表vstack
把两个「特征」并成更宽的表hstack
不确定concatenate(最灵活)

concatenate 还能指定沿哪一维接:

x = np.array([[1, 2], [3, 4]])   # 2 行 2 列
y = np.array([[5, 6], [7, 8]])

print(np.concatenate([x, y], axis=0))    # 上下接 → 4 行 2 列
print(np.concatenate([x, y], axis=1))    # 左右接 → 2 行 4 列
[[1 2]
 [3 4]
 [5 6]
 [7 8]]
[[1 2 5 6]
 [3 4 7 8]]

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 不行——列数对不上。

Q10:如何使用 np.corrcoef() 计算相关系数矩阵?相关系数能否说明因果关系?#

答: np.corrcoef(数据.T)——别忘了转置。

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)
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(自己跟自己);
  • 第 0 行第 1 列 = 0.9904,是 Q1 和 Q2 的相关系数;
  • 矩阵是对称的。

相关系数的取值范围是 -1 ~ 1:

值含义
+1完全正相关
0没有线性关系
-1完全负相关

只算两个变量:

print(np.corrcoef(sales[:, 0], sales[:, 2]))
[[1.         0.97509237]
 [0.97509237 1.        ]]

或者直接取矩阵里的那一个数:

print(corr[0, 2])     # Q1 与 Q3
0.975092374691495

相关系数绝对不能说明因果关系。

原因有三个:

  1. 可能是「共同原因」。 冰淇淋销量和溺水事故数高度相关, 但谁也没导致谁——天气热才是那个「共同原因」。
  2. 可能是「反向因果」。 医院里「医生越多的地方,病人越多」, 不是医生导致病人多,而是病人多所以医生多。
  3. 可能只是「巧合」。 样本量小的时候,相关系数会偶然偏大。

另外还有两个技术上的坑:

  • 相关系数只衡量「线性」关系,U 形关系算出来可能接近 0;
  • 离群值会严重扭曲相关系数——这正是我们上一题讲缩尾处理的原因之一。

所以:相关系数是「探索线索」,不是「结论」。

小结#

这一节的 10 个问题,其实都围绕**「数组的实用工具」**:

  1. max/min 给值,argmax/argmin 给位置;
  2. axis=0 对列、axis=1 对行,不确定就数结果个数;
  3. 缩尾 = 把值压到边界,clip + percentile 的标准组合;
  4. 分位数定上下界,5%/95% 最常用;
  5. 数值量化:np.digitize 分档,左闭右开,labels[bins] 换标签;
  6. 分帧:sliding_window_view,帧数 = 长度 − 窗口 + 1;
  7. tile 整体复制,repeat 元素级复制;
  8. diff 算相邻差分,n= 指定阶数,结果少一个元素;
  9. 拼接三兄弟:concatenate / vstack / hstack;
  10. corrcoef(数据.T) 算相关系数,但相关 ≠ 因果。

课程到这里就结束啦 五章、25 道编程练习、50 个常见问题,你都走完了。

接下来怎么练? 找一个真实的 CSV 文件,用 np.loadtxt() / np.genfromtxt() 或 Pandas 读进来, 然后把本章的工具跑一遍:缩尾 → 分档 → 算相关矩阵。 把「教程里的例子」换成「自己的数据」,这一步才是真正学会的开始。

阅读 --