第 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
4argmax 返回的是「下标」,不是值。
把位置翻译成名字,是这套工具最常见的用法:
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]档位编号的对应关系:
| 编号 | 范围 |
|---|---|
0 | x < 100 |
1 | 100 ≤ x < 150 |
2 | 150 ≤ x < 200 |
3 | x ≥ 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 与 Q30.975092374691495相关系数绝对不能说明因果关系。
原因有三个:
- 可能是「共同原因」。 冰淇淋销量和溺水事故数高度相关, 但谁也没导致谁——天气热才是那个「共同原因」。
- 可能是「反向因果」。 医院里「医生越多的地方,病人越多」, 不是医生导致病人多,而是病人多所以医生多。
- 可能只是「巧合」。 样本量小的时候,相关系数会偶然偏大。
另外还有两个技术上的坑:
- 相关系数只衡量「线性」关系,U 形关系算出来可能接近 0;
- 离群值会严重扭曲相关系数——这正是我们上一题讲缩尾处理的原因之一。
所以:相关系数是「探索线索」,不是「结论」。
小结#
这一节的 10 个问题,其实都围绕**「数组的实用工具」**:
max/min给值,argmax/argmin给位置;axis=0对列、axis=1对行,不确定就数结果个数;- 缩尾 = 把值压到边界,
clip+percentile的标准组合; - 分位数定上下界,5%/95% 最常用;
- 数值量化:
np.digitize分档,左闭右开,labels[bins]换标签; - 分帧:
sliding_window_view,帧数 = 长度 − 窗口 + 1; tile整体复制,repeat元素级复制;diff算相邻差分,n=指定阶数,结果少一个元素;- 拼接三兄弟:
concatenate/vstack/hstack; corrcoef(数据.T)算相关系数,但相关 ≠ 因果。
课程到这里就结束啦 五章、25 道编程练习、50 个常见问题,你都走完了。
接下来怎么练? 找一个真实的 CSV 文件,用
np.loadtxt()/np.genfromtxt()或 Pandas 读进来, 然后把本章的工具跑一遍:缩尾 → 分档 → 算相关矩阵。 把「教程里的例子」换成「自己的数据」,这一步才是真正学会的开始。