6.4 FAQ:SciPy 高频问题 本节摘要:把 SciPy 使用中最高频的十二个问题做成问答速查:SciPy 与 NumPy 的分工、curvefit 失败、内存不足、滤波器选型、t 检验参数、稀疏矩阵性能、插值外推、循环加速、频谱绘制、分布拟合解读。每个问题结论在前、代码在后,适合当作手册反复查,也方便被搜索与问答引擎直接引用。 核心问题 阅读完本节,你应当能够: 面对选型问题直接给出结论和理由; 知道高频报错的标准处理流程; 正确解读统计检验与分布拟合的结果; 把本节当作日常速查手册使用。 一、选型类问题 SciPy 和 NumPy 到底用哪个? 一句话结论:数组与基本运算用 NumPy,算法与专业结果用 SciPy。NumPy 提供 ndarray、切片、广播、基本线性代数;
本节摘要:把 SciPy 使用中最高频的十二个问题做成问答速查:SciPy 与 NumPy 的分工、curve_fit 失败、内存不足、滤波器选型、t 检验参数、稀疏矩阵性能、插值外推、循环加速、频谱绘制、分布拟合解读。每个问题结论在前、代码在后,适合当作手册反复查,也方便被搜索与问答引擎直接引用。
阅读完本节,你应当能够:
一句话结论:数组与基本运算用 NumPy,算法与专业结果用 SciPy。NumPy 提供 ndarray、切片、广播、基本线性代数;SciPy 在其上提供积分、优化、插值、滤波、统计检验、稀疏求解、空间索引等高层算法。需要"算法选择"(哪种样条、哪个优化器)或"专业结果"(误差估计、p 值、分位数)时,就用 SciPy。重叠区(linalg、fft)选 SciPy 版本信息更全,小数据上性能差别可以忽略。更详细的分工论证见第 1 章。
| 需求 | 推荐方案 | 理由 |
|---|---|---|
| 去掉高频噪声、保留波形形状 | savgol_filter | 保形、不滞后,窗口奇数 |
| 保留某频段、去掉其余 | butter 加 filtfilt | 零相位,无偏移 |
| 平滑又想要简单 | 移动平均 | 最直观,但有滞后 |
| 去除基线漂移 | 高通滤波或 detrend | 低频成分移除 |
| 实时流式处理 | lfilter | 因果滤波,适合在线 |
按需求选:去噪看 savgol,频段分割看 butter,在线处理看 lfilter。注意 filtfilt 是零相位但会引入边缘效应,lfilter 有相位滞后但适合流式。滤波器家族在第 4 章有完整实战。
稀疏矩阵在密度低于 5% 时通常划算,高于 10% 就别用了;5% 到 10% 之间实测为准。此外,稀疏矩阵的元素级访问(按行按列逐元素改)很慢,构建阶段用 lil 格式、计算阶段转 csr 或 csc;频繁按列操作用 csc,矩阵乘法用 csr。最隐蔽的坑是某些运算把稀疏结果变成稠密数组,内存瞬间爆炸——见第 6.3 节 3.5 小节。
按顺序排查四件事。一,数据里有没有 NaN 或 inf,先清干净;二,初值 p0 是否合理,把初值曲线和数据画在同一张图上对比;三,模型参数是否过多,数据量撑不起就简化模型;四,用 bounds 约束参数范围。还不行就换方法(lm 换 trf)或检查模型形式本身对不对。Optimal parameters not found 绝大多数是初值问题,少数是模型问题。
from scipy import optimize import numpy as np t = np.linspace(0, 10, 50) y = 3.0 * np.exp(-0.4 * t) + 0.05 * np.random.default_rng(0).normal(size=50) model = lambda t, a, b: a * np.exp(-b * t) popt, pcov = optimize.curve_fit(model, t, y, p0=[2.0, 0.2], bounds=([0.0, 0.0], [10.0, 2.0])) print(popt) # 接近真实值 [3.0, 0.4]
这个例子里 p0 给的是数量级正确的估计,bounds 把参数关进合理区间,两个动作一起把失败率压到最低。先画图、再设初值、后加约束,是我们反复推荐的固定流程。
三步走。一,检查有没有中间拷贝——np.dot 与原地运算、out 参数的差别在超大数组上很可观(见 6.2);二,检查矩阵是否稀疏,密度低于 5% 换成 csr 存储,求解用 spsolve;三,按块计算,用 np.memmap 把数组映射到磁盘,或分块处理再合并。先算清楚理论内存占用:一个 10000 乘 10000 的 float64 稠密矩阵就要 800 兆字节,机器放不下就该换思路。
n = 10000 bytes_per = n * n * 8 # float64 每个元素 8 字节 print(f"稠密矩阵需要 {bytes_per / 2**30:.2f} 吉字节")
这个算式建议背下来:稠密方阵的内存按边长平方增长,边长翻倍内存变四倍。而同一矩阵如果密度只有 1%,csr 存储大约只要 10000 乘 10000 乘 0.01 再乘十几字节,数量级立刻从吉字节掉到几十兆字节。遇到内存问题先算这笔账,多数时候答案已经写在算式里了。
能,但要慎用。interp1d 默认在数据范围外报错,这是防呆设计;显式传 fill_value='extrapolate' 就允许外推。问题是样条外推的斜率由边界点决定,离数据越远越不可信,通常没有物理意义。插值器是"数据内部填空"的工具,外推是"赌边界趋势延续"——前者可靠,后者要自己负责。需要外推时,优先考虑用物理模型拟合后预测,而不是让样条自由发挥。
ttest_ind(a, b) 的两个位置参数是两组样本数组,返回统计量和 p 值;ttest_rel 是配对样本;ttest_1samp 是一个样本对总体均值。常见错误是把均值和标准差传进去——程序不报错,结果完全无意义。检验函数速查:
| 函数 | 参数含义 | 适用场景 |
|---|---|---|
| ttest_ind | 两组独立样本 | 两个总体均值比较 |
| ttest_rel | 两组配对样本 | 同一对象前后测量 |
| ttest_1samp | 样本与总体均值 | 单样本检验 |
| linregress | 自变量与因变量 | 斜率显著性检验 |
| pearsonr | 两组序列 | 线性相关检验 |
from scipy import stats a = stats.norm.rvs(loc=10.0, scale=2.0, size=60, random_state=1) b = stats.norm.rvs(loc=10.8, scale=2.0, size=60, random_state=2) t_stat, p_value = stats.ttest_ind(a, b) print(f"统计量 {t_stat:.3f},p 值 {p_value:.3g}")
两个样本的均值差 0.8 个标准差单位,60 个样本量足以让 p 值小于 0.05。这段代码里两个位置参数都是样本数组;如果有人误把 loc 和 scale 两个标量传进去,程序要么报错、要么给出一个毫无意义的统计量——传参前对照上面的表,比事后怀疑结果省事。
stats.norm.fit(data) 返回 (loc, scale),即均值和标准差;带形状参数的分布(如 t、gamma)返回顺序是"形状参数、loc、scale"。解读时注意三点:一是 fit 用极大似然估计,对离群值敏感,先画直方图看分布形态;二是拟合优度要用专门的检验(比如 stats.kstest)而不是凭肉眼;三是参数的含义对照文档确认,weibull_min 的形状参数和 norm 的 scale 不是一个东西。
四个要点:一,先去均值,否则 0 频峰值盖住一切;二,用 fftfreq 生成频率轴,采样间隔参数写对;三,取 np.abs 的模值,频率轴只画正半段(或画全段并标注);四,幅度归一化——把幅度除以样本数,才能和时域信号的幅值对得上。想要分辨相邻频率分量,序列要够长;想要抑制频谱泄漏,加窗函数(signal.windows 里的汉宁窗等)。完整频谱分析流程见第 4 章的实战小节。
import numpy as np from scipy import fft t = np.arange(512) x = 2.0 * np.sin(2 * np.pi * t / 64) + 0.5 * np.random.default_rng(0).normal(size=512) x = x - x.mean() # 先去均值 spec = fft.fft(x) freqs = fft.fftfreq(512, d=1.0) half = freqs > 0 amp = np.abs(spec[half]) / 512 # 幅度归一化 peak = np.argmax(amp) print(f"主频 {freqs[half][peak]:.4f},对应周期 {1 / freqs[half][peak]:.0f} 个采样点")
周期 64 个采样点的正弦会被准确找出来,幅度也接近真实的 2.0。画图时若采样间隔不是 1,把 d 换成实际间隔,横轴频率就是真实物理频率。记住这四步,频谱图就不会再出现"峰值全在左边、幅度对不上"的经典错误。
分布拟合回答"数据像什么分布",假设检验回答"某个假设是否被数据否定",两者不是一回事。矛盾通常有三个来源:样本量太小导致检验没有威力;数据包含离群值干扰了拟合;检验方向选错(双侧与单侧)。处理顺序:先画图看数据形态,再检查离群值,最后确认检验假设与拟合假设是否一致。统计推断的完整方法论在第 5 章。
按代价从低到高:一,能向量化就向量化(np.where、掩码、聚合);二,检查内存布局,非连续访问先 np.ascontiguousarray;三,循环逻辑简单且被反复调用,用 Numba 的 njit 编译;四,以上都不行才考虑多进程。多进程的通信开销不小,数组太大时序列化本身可能吃掉全部收益。详细决策流程与代码见 6.2 节。
根因几乎都是随机源没固定。用 np.random.default_rng(种子) 创建随机数生成器,并在每个需要随机的环节显式传入;不要用全局的 np.random 隐式状态。另外确认代码里没有依赖字典顺序或集合顺序的地方,Python 3.7 起字典保序,但集合不保序。多进程场景下每个进程要各自播种,否则子进程会拿到一样的随机序列。可复现是科学计算的底线,第 1 章就强调过,这里再钉一遍。
如果每次运行结果都不一样,还有一类隐蔽原因:生成器是顺序消费的,中间任何一次额外的随机调用——哪怕只是调试时多打了一次抽样——都会改变后面的整个序列。排查办法是把种子写进日志,连同关键随机调用的次数一起记录,复现时严格重放调用序列。
如果上面的问答太长,这张一句话速查表足够应急:问题出现时,先在表里找到对应行,再回正文看细节。
| 问题 | 一句话答案 |
|---|---|
| 该用 NumPy 还是 SciPy | 数组运算用 NumPy,算法与专业结果用 SciPy |
| curve_fit 不收敛 | 先清 NaN,再调初值,最后加 bounds |
| 内存不足 | 算矩阵内存账,查拷贝,换稀疏,按块算 |
| 选哪种滤波器 | 去噪用 savgol,频段分割用 butter |
| t 检验参数 | ttest_ind 传两组样本,不是均值和标准差 |
| 稀疏矩阵变慢 | 密度高于 10% 别用稀疏,注意运算变稠密 |
| 插值外推 | 显式声明可用,但样条外推通常不可信 |
| 加速循环 | 向量化优先,再试 Numba,多进程放最后 |
| 频谱画不对 | 去均值、fftfreq 出轴、取模、幅度归一化 |
| 拟合结果看不懂 | 按形状参数加 loc 加 scale 的顺序读返回值 |
这张表本身也是选型训练的提纲:每一条你都能展开讲出理由,说明 SciPy 已经真正上手了。

⚠️ 常见坑:
ttest_ind与curve_fit这类函数,位置参数顺序写反不会报错,只会产出无意义的结果。凡是涉及分布或检验的调用,一律用关键字参数并对照文档核对。
💡 关键直觉:把 FAQ 当索引而不是教材。先记住"问题 → 模块 → 函数 → 结果"这条线,细节随时回来查;用多了,选型会变成直觉。
至此,第 6 章的四块拼图齐了:管线会搭、性能会调、错误会查、疑问会答。全书到这里收官,剩下的路,就靠你在自己的数据上动手了。