6.3 常见坑与排错 本节摘要:把全书高频报错整理成一份可检索的排错手册。每个问题按"现象 → 成因 → 排查 → 解决"四步展开:维度与广播错误、复数与实数类型混用、不收敛警告、奇异矩阵、稀疏矩阵意外变稠密、stats 分布参数写反、p 值误解。开头一张索引表告诉你出错时先查哪一条,后附通用排查流程。 先说结论 阅读完本节,你应当能够: 看到报错先归类到七大类中的某一类,不再对着英文消息发懵; 对每类错误说出成因与排查顺序; 会读警告信息里的关键线索,区分"警告"与"致命错误"; 识别"不报错但结果错"的问题(参数写反、p 值误读)。 一、问题与直觉 跑科学计算的人,百分之八十的时间花在排错上。好消息是:SciPy 的报错远没有想象中花样繁多,翻来覆去就那么几类。
本节摘要:把全书高频报错整理成一份可检索的排错手册。每个问题按"现象 → 成因 → 排查 → 解决"四步展开:维度与广播错误、复数与实数类型混用、不收敛警告、奇异矩阵、稀疏矩阵意外变稠密、stats 分布参数写反、p 值误解。开头一张索引表告诉你出错时先查哪一条,后附通用排查流程。
阅读完本节,你应当能够:
跑科学计算的人,百分之八十的时间花在排错上。好消息是:SciPy 的报错远没有想象中花样繁多,翻来覆去就那么几类。我们把这几年见过的高频报错收进一张图鉴,每一条都按"现象 → 成因 → 排查 → 解决"四步写。你不需要背下来,只需要知道"报错长什么样 → 该查哪里"。
一个总原则先说在前面:报错消息的前三行比后三行重要。Python 的异常链很长,真正的病根通常在最下面那一行;而警告(Warning)不是错误,它告诉你"程序还能跑,但结果可能不对"——这类问题最危险,因为它不炸,等你发现时已经晚了。
| 类别 | 典型消息片段 | 排查入口 | 对应小节 |
|---|---|---|---|
| 维度与广播 | operands could not be broadcast together | 打印 shape 对齐尾部 | 3.1 |
| 复数与实数 | ComplexWarning 或复数结果 | 检查 dtype 与虚部 | 3.2 |
| 不收敛警告 | Optimal parameters not found | 核对初值与数据范围 | 3.3 |
| 奇异矩阵 | Singular matrix | 查秩与条件数 | 3.4 |
| 稀疏变稠密 | 内存暴涨或返回对象数组 | 检查返回类型 | 3.5 |
| 分布参数写反 | 拟合出负数均值或怪异结果 | 对照文档参数顺序 | 3.6 |
| p 值误解 | 结果看似自相矛盾 | 复习假设检验语义 | 3.7 |
这张表就是你的第一站:先看报错或现象属于哪一类,再跳去对应小节找四步解法。
现象:ValueError: operands could not be broadcast together with shapes (3,4) (4,)。
成因:NumPy 广播规则是"从尾部维度开始对齐,维度为 1 或相等才能广播"。(3,4) 和 (4,) 尾部都是 4,本来可以广播;真正常见的错误是把 (4,) 和 (3,1) 混在一起,或者忘了转置。
排查:在报错前一行打印参与运算的每个变量的 shape,逐个对齐尾部的维度。
解决:用 reshape、转置 或补维度把形状对齐。向量要对齐到矩阵的行,用 [:, None] 或 reshape(-1, 1);要对齐到列,直接保持一维。写完之后再打印一次 shape 验证。另一个思路是用 np.broadcast_shapes 先验证两组形状能不能广播,能则直接算,不能则提前发现。
a = np.ones((3, 4)) b = np.ones(4) # 能广播,结果是 (3, 4) c = np.ones((3, 1)) # 也能广播 # d = np.ones(3) + a # 报错:尾部 3 与 4 不匹配
现象:ComplexWarning: Casting complex values to real discards the imaginary part,或者算出来的结果带着不该有的虚部。
成因:最典型的源头是 FFT 和特征值分解——fft 的输出是复数,eig 对非对称矩阵可能给出复特征值。当你把这些结果直接塞进只接受实数的函数(比如某些统计函数、interp1d 的插值)时,程序会悄悄丢掉虚部,有时还会报警告。
排查:打印 dtype,或者用 np.iscomplexobj 检查变量是不是复数数组。
解决:分情况处理。如果虚部是数值误差(应该为 0),用 np.real 显式取实部,并写注释说明为什么安全;如果虚部不可忽略,说明问题本身需要复数(比如振荡系统的特征值),别硬取实部。显式转换比让程序静默丢弃好得多——至少你知道发生了什么。
import numpy as np from scipy import fft x = np.random.default_rng(3).normal(size=64) spec = fft.fft(x) print(np.iscomplexobj(spec)) # True mag = np.abs(spec) # 取模,保留完整信息
现象:OptimizeWarning: Covariance of the parameters could not be estimated,或者 RuntimeWarning: invalid value encountered,优化结果明显不对。
成因:curve_fit 等优化器是迭代算法,从初值出发找局部最优。初值给得离谱、模型本身不适配数据、数据里有 NaN 或极端值,都会让迭代走到死胡同。
排查:三步走。一,打印数据里有没有 NaN 和 inf,先清干净;二,把数据和初值曲线画出来,肉眼确认初值方向对不对;三,看迭代后的残差数量级,判断是没收敛还是收敛到了错误的地方。
解决:给 p0 合理初值是最有效的招;次选是给 bounds 约束参数范围,防止跑到无意义区域(比如负的方差);再不行就换优化方法,比如 lm 换 trf,或者简化模型——模型参数比数据能支撑的还多时,协方差必然算不出来。拟合类问题的系统解法在第 2 章有完整展开,这里记住排查顺序即可。
现象:LinAlgError: Singular matrix,或解方程组时警告"矩阵接近奇异,结果可能不准"。
成因:矩阵的某些行线性相关(比如两个特征完全共线的变量)、有整行零、或者数值上接近奇异。气象数据里两个站点的温度序列几乎一样,就是典型。
排查:用 np.linalg.matrix_rank 看秩是否等于矩阵阶数;用 np.linalg.cond 看条件数,条件数超过 10 的 12 次方量级就要警惕。
解决:先查数据——删除共线变量、检查是不是重复列。数据没问题就换算法:lstsq 最小二乘解对近奇异矩阵更稳,pinv 伪逆可以在严格奇异时给出解。这两个函数都在 numpy.linalg 里,SciPy 的 scipy.linalg.lstsq 同样适用。别硬解,奇异是数据告诉你的信息:你的模型里有冗余。
现象:程序跑得好好的,突然内存暴涨、卡死;或者某步运算后数据类型变成了 object、结果是一个稠密数组。
成因:scipy.sparse 的大部分运算保持稀疏,但有一批操作会返回稠密结果:布尔索引取出的子矩阵、某些逐元素函数(比如取指数)、以及把稀疏矩阵直接传给普通 NumPy 函数时的隐式转换。
排查:在可疑运算前后打印 type() 和 dtype,看稀疏类型(如 csr_matrix)是不是变成了 ndarray。
解决:检查返回类型,需要保持稀疏就用 tocsr、tocsc 显式转回;解稀疏方程组用 scipy.sparse.linalg.spsolve 而不是先求逆——inv 会把稀疏矩阵变成稠密,是内存爆炸的头号元凶。第 3 章的实战小节对稀疏格式的适用性有完整对比。
from scipy import sparse s = sparse.csr_matrix(np.eye(1000)) print(type(s @ s)) # csr 乘法保持稀疏 # print(type(s.toarray())) # 显式转稠密才需要
现象:用 norm 拟合出的"均值"是负数、方差是零,或者 t 检验的结果和常识相反。
成因:SciPy 的分布统一用 (shape 参数, loc, scale) 顺序,norm 里 loc 是均值、scale 是标准差,名字恰好对得上;但 t 分布的第一个参数是自由度 df,gamma 的第一个参数是形状,weibull_min 的第一个参数是形状参数——很多人在这些分布上把参数顺序记错。
排查:拟合结果异常时,打印 fit 返回值的顺序:先是所有形状参数,最后才是 loc, scale。对照文档确认每个位置的含义。
解决:别靠记忆,用 stats.norm.fit、stats.t.fit 的返回顺序对照文档;传参时用关键字参数(loc=, scale=)而不是位置参数,等于给自己上了一道保险。检验函数同样如此,ttest_ind 的两个参数是两组样本,不是均值和标准差——参数写反时程序不报错,结果却完全没意义,这类错误最难抓。
现象:p 值很小,但效应量看起来微不足道;或者 p 值大于 0.05,于是下结论"没有差异"。
成因:p 值被普遍误读为"原假设为真的概率"或"效应存在的概率",两者都不对。p 值回答的是:假设原假设为真,观察到当前或更极端数据的概率。样本量一大,微小到没意义的差异也能得到极小的 p 值;样本量一小,真实存在的差异也可能不显著。
排查:报告统计结果时,问自己三个问题——效应量是多少(比如均值差几个标准差)、置信区间多宽、样本量多大。
解决:p 值之外必须报效应量和置信区间;"不显著"不等于"没有差异",只说明数据不足以排除偶然。双侧与单侧检验也要想清楚:检验方向在收集数据前定,不能看了结果再挑对自己有利的那侧。统计检验的语义细节在第 5 章有完整讨论,这里先把误读的雷排掉。

最后补三条通用经验。一是缩小数据:报错时把数组截成前一百行,往往立刻复现且好排查,真实项目里"数据太大看不出问题"的困境大半能靠这招解决。二是固定随机种子:排错时随机性是最讨厌的干扰项,先固定种子让问题稳定复现。三是把警告当错误:开发阶段把 warnings 过滤器的警告提升为异常,强制自己处理每一个可疑信号,别等到生产环境才发现。
这第三条在数值计算里尤其值得,因为它能拦住一类"静默错误":比如插值结果悄悄带上了复数、稀疏运算悄悄返回了稠密数组,程序不报错,但后续所有结果都建立在错误的数据上。开发期宁可让程序炸得难看,也别让它错得安静。一个小技巧是写一个检查函数,在每步关键运算后断言 dtype 与有限性,相当于给数据流装传感器:
def check_finite(a, name): """断言数组有限,出错时给出环节名。""" if not np.all(np.isfinite(a)): raise ValueError(f"环节 {name} 出现 NaN 或无穷值")
把它插在管线的每两步之间,问题在哪一段发生,错误信息会直接告诉你。这套"断言检查"的思路和 6.1 节的入口校验是一对:入口防脏数据,中间防坏运算。
⚠️ 常见坑:
ttest_ind的两个位置参数是两组样本数组,不是均值与标准差;t分布的参数是自由度不是位置。参数顺序写反不报错,结果却完全作废——这类错误比报错更阴险。
💡 关键直觉:报错消息的底部才是病根;先分类再动手。把"报错 → 分类 → 查索引表 → 四步解决"走顺,排错就从痛苦变成肌肉记忆。
这一节是按错误类型组织的,适合"出事了来查"。下一节换个角度,按问题场景组织十二个高频问答,适合平时翻着看。