SciPy实战指南:工业级数值计算与数据分析核心引擎

发布时间:2026/10/27 14:49:47
SciPy实战指南:工业级数值计算与数据分析核心引擎

1. 这不是“又一个Python库”,而是你处理真实数据时绕不开的底层引擎

很多人第一次听说 SciPy,是在学完 NumPy 后被教程里一句“它和 NumPy 配合使用”轻轻带过;也有人在用 Matplotlib 画完图后,看到文档里写着“拟合曲线推荐用scipy.optimize.curve_fit”,才点开官网瞄了一眼——结果页面上密密麻麻的模块名:integrate、linalg、stats、signal、sparse……直接劝退。我当年也是这样。直到在某次实际项目中,需要从一段含噪传感器时序数据里精准提取周期成分,用纯 NumPy 手写 FFT 加窗+频谱校正,调了三天结果还是漂移;换上scipy.signal.find_peaks配合scipy.fft,三行代码跑通,误差从 ±8% 压到 ±0.3%。那一刻我才真正明白:SciPy 不是“增强版 NumPy”,它是把数学教科书里的算法,变成你.py文件里可调试、可复现、可嵌入生产流程的工业级函数集。

它解决的从来不是“能不能算”的问题,而是“能不能在真实约束下稳定、准确、高效地算出来”的问题。比如你手头有一组非线性回归数据,x 是温度(单位℃),y 是反应速率(单位 mol/s),模型是y = a * exp(-b/x)——这在化学动力学里叫阿伦尼乌斯方程。NumPy 只能帮你存数组、做四则运算;而 SciPy 的optimize.curve_fit会自动处理初值敏感性、雅可比矩阵数值近似、收敛判据(如梯度模长 < 1e-8)、甚至异常点鲁棒拟合(通过sigma参数加权)。这不是“锦上添花”,是当你面对客户给的 200MB 工业传感器原始 CSV、要求 2 小时内输出故障特征报告时,唯一能让你不熬夜重写 Fortran 子程序的救命稻草。

适合谁来读?如果你已经能熟练用 Pandas 读 CSV、用 Seaborn 画箱线图,但遇到“这个峰到底是不是真实信号”“这两组数据差异到底有没有统计意义”“模型残差明显不服从正态分布该怎么修正”这类问题时,习惯性去搜 Stack Overflow 而不是翻《概率论与数理统计》教材——那这篇就是为你写的。它不讲“什么是积分”,只讲“为什么scipy.integrate.quad默认用的是全局自适应高斯-克朗罗德算法,而不是辛普森法”;不解释“什么是特征值”,只告诉你“scipy.linalg.eigvalsh比numpy.linalg.eigvals快 3.7 倍,且对称矩阵下精度高 4 个数量级,因为底层调用的是 LAPACK 的DSYEVR”。

关键词“Python数据分析库 Scipy”背后,藏着三个被严重低估的事实:第一,它90%以上的函数都经过 LAPACK/BLAS/FFTW 等 C/Fortran 数值计算库深度优化,不是 Python 层面的“胶水代码”;第二,它的 API 设计严格遵循“最小惊讶原则”——比如所有统计分布对象(scipy.stats.norm)都统一提供.pdf、.cdf、.ppf、.rvs四个方法,学一个就通全部;第三,它和生态链的咬合度远超想象:Pandas 的.interpolate(method='spline')底层调scipy.interpolate.splrep,Scikit-learn 的PCA在svd_solver='arpack'时调scipy.sparse.linalg.eigsh。你不用 SciPy,但你的工具链已经在用它。

2. 核心模块拆解:不是功能罗列,而是按“问题域”重新组织认知

SciPy 官网把模块分成十几个目录,但实际工作中,没人按字母顺序查文档。我们按真实数据分析流中的高频问题域,把核心能力重构成四个主干模块。每个模块都对应一类明确的输入-输出契约,而非抽象概念。

2.1 信号处理:从“看不清的波形”到“可测量的特征”

提示:别再用np.fft手动拼接频谱了。scipy.signal是为工业现场设计的——它内置抗混叠滤波器设计、实时流式处理接口、以及针对非均匀采样的专用算法。

最常踩坑的是滤波器设计。比如你要滤除 50Hz 工频干扰,用scipy.signal.butter(4, [45, 55], 'bandstop', fs=1000)生成系数后,直接scipy.signal.filtfilt(b, a, data)——注意是filtfilt,不是lfilter。前者是零相位滤波,不会造成波形时间轴偏移,这对振动分析、ECG 特征定位至关重要。我曾见某团队用lfilter处理轴承加速度信号,导致冲击脉冲位置偏移 12ms,最终误判故障类型。

另一个关键能力是峰值检测。scipy.signal.find_peaks(data, height=0.5, distance=50, prominence=0.3)的参数不是随便设的:height是绝对阈值,distance强制相邻峰间隔(避免把噪声毛刺当峰),prominence(突出度)才是物理意义最强的——它定义为峰顶到其左右最近“谷底连线”的垂直距离,直接反映该峰在局部地形中的显著性。某次处理电机电流谐波时,用prominence=0.1漏掉了关键的 3 次谐波峰,调到0.15后才稳定检出,因为 3 次谐波幅值虽小,但在频谱基底上“站得够高”。

实测对比:对一段 10 万点的音频信号做 STFT(短时傅里叶变换),scipy.signal.stft(data, fs=44100, nperseg=2048, noverlap=1024)比手动切片+np.fft快 4.2 倍,且自动处理窗函数(默认汉宁窗)和归一化,输出直接是(f, t, Zxx)三维数组,Zxx[i,j]就是第 j 个时刻、第 i 个频率点的复数谱值。你不需要懂什么是“能量守恒窗函数”,但要知道:如果nperseg设成 1024,noverlap必须小于 1024,否则会报ValueError: noverlap must be less than nperseg——这种细节,只有真正在产线调过参数的人才会刻进DNA。

2.2 积分与微分方程:让“理论模型”真正跑起来

注意:scipy.integrate.solve_ivp是当前推荐接口,odeint已标记为 legacy。新项目务必用前者,因它支持事件检测(event detection)和稠密输出(dense_output)。

很多用户卡在“ODE 求解器怎么选”。其实就三条铁律:

  1. 刚性方程(stiff)必用method='Radau'或'BDF':比如电池等效电路模型(Thevenin 模型),其时间常数跨 6 个数量级(毫秒级 RC 网络 vs 小时级 SOC 变化),用RK45会步长崩坏、计算爆炸;
  2. 需要精确捕捉突变点(如开关动作)时,启用events参数:定义一个函数lambda t,y: y[0]-0.5,当状态 y[0] 穿过 0.5 时自动暂停并返回该时刻,比手动插值准 10 倍;
  3. 要获取任意时刻的解,必须开启dense_output=True:返回的对象有.sol(t)方法,内部用 Hermite 插值,比保存离散点后线性插值误差低 3 个数量级。

举个硬核例子:求解热传导方程∂T/∂t = α ∂²T/∂x²的显式格式。新手常写循环for i in range(1, N-1): T_new[i] = T_old[i] + alpha*dt/dx**2*(T_old[i+1]-2*T_old[i]+T_old[i-1]),但这是 O(N) 时间复杂度,且稳定性要求alpha*dt/dx**2 < 0.5。换成scipy.sparse.linalg.spsolve(A, b)构建隐式格式矩阵 A(三对角矩阵),一次求解,无条件稳定,10 万网格点耗时仅 12ms。这里A是scipy.sparse.diags构造的稀疏矩阵,内存占用比全矩阵小 99.9%,这才是工业级实现。

2.3 统计推断:告别“p<0.05 就万事大吉”的幻觉

scipy.stats的强大,在于它把统计学的“假设-检验-决策”链条封装成原子操作,且每个环节都暴露可控参数。比如ttest_ind默认执行 Welch’s t-test(方差不等假设),而非教科书里的 Student’s t-test。为什么?因为现实数据几乎从不满足“两总体方差相等”这一脆弱前提。equal_var=False是默认值,你甚至不用改参数。

更关键的是效应量(effect size)计算。scipy.stats.ttest_ind只返回statistic和pvalue,但真正的业务价值在Cohen’s d:

from scipy.stats import ttest_ind import numpy as np a, b = np.random.normal(0, 1, 100), np.random.normal(0.5, 1.2, 100) t_stat, p_val = ttest_ind(a, b, equal_var=False) # 手动计算 Cohen's d(需考虑样本量校正) n1, n2 = len(a), len(b) s_pooled = np.sqrt(((n1-1)*np.var(a, ddof=1) + (n2-1)*np.var(b, ddof=1)) / (n1+n2-2)) cohens_d = (np.mean(a) - np.mean(b)) / s_pooled

p<0.05 只说明“不太可能偶然发生”,而cohens_d=0.8才说明“差异大到肉眼可见”。某次 A/B 测试中,p=0.003 但cohens_d=0.12,产品团队果断放弃上线——因为提升太小,用户根本感知不到。

分布拟合方面,scipy.stats.fit(v1.9+)支持全自动分布选择。但老版本更常用scipy.stats.kstest(Kolmogorov-Smirnov)或anderson(Anderson-Darling)。后者对尾部敏感,更适合金融风控场景:anderson(data, dist='expon')返回(statistic, critical_values, significance_level),若statistic > critical_values[2](对应 5% 显著性),则拒绝指数分布假设。我用它诊断过某设备故障间隔时间,发现实际服从威布尔分布(Weibull),而非假设的指数分布,从而修正了 MTBF(平均故障间隔)预测模型,将备件库存误差从 35% 降至 7%。

2.4 稀疏计算与线性代数:当数据大到内存报警时的生存指南

当你的特征矩阵维度是 100 万 × 5000(如推荐系统用户-物品交互表),numpy.array会吃掉 37GB 内存。scipy.sparse.csr_matrix用三元组(row, col, data)存储,内存占用通常 < 1GB。但关键不在存储,而在计算:scipy.sparse.linalg.cg(共轭梯度法)求解Ax=b,对稀疏矩阵 A,迭代 50 次即可达到1e-8精度,而numpy.linalg.solve直接 OOM。

实战技巧:构建稀疏矩阵时,永远优先用coo_matrix((data, (row, col)), shape=(m,n)),再转csr。因为 COO 格式追加元素 O(1),CSR 追加是 O(nnz)。某次处理基因表达数据(10 万基因 × 2 万样本),用循环csr_matrix逐行赋值,耗时 47 分钟;改用 COO 预分配再转换,耗时 18 秒。

特征值问题更典型。scipy.linalg.eigsh(A, k=10, which='LM')求最大 10 个特征值,比numpy.linalg.eigvals(A)快两个数量级,且内存友好。原理是 Arnoldi 迭代,只存 Krylov 子空间基向量(k×n 矩阵),而非全矩阵。某次做图像哈希降维,用它替代 PCA,处理 1 万张 256×256 图像,内存从 12GB 降到 1.3GB,时间从 32 分钟缩至 4.5 分钟。

3. 实战全流程:从原始 CSV 到可交付报告的端到端拆解

我们以一个真实场景收束:某工厂产线每秒采集 8 通道振动传感器数据(CSV 格式,每行 9 列:timestamp, ch1..ch8),需每日自动生成《轴承健康度日报》,包含三项核心指标:① 主频能量占比(判断是否共振);② 冲击脉冲指标(判断是否点蚀);③ 趋势斜率(判断劣化速度)。整个流程必须在 3 分钟内完成,且支持人工复核。

3.1 数据加载与预处理:避开 Pandas 的“温柔陷阱”

Pandas 的read_csv对大文件很慢,且默认把数字当 float64,浪费内存。正确姿势:

import numpy as np # 直接用 numpy 加载,指定 dtype data = np.loadtxt('vib_20240501.csv', delimiter=',', skiprows=1, usecols=range(1,9), # 只读取 ch1-ch8,跳过 timestamp dtype=np.float32) # float32 节省 50% 内存 # 归一化:每通道独立减均值除标准差(防某通道量纲过大主导结果) data_norm = (data - np.mean(data, axis=0, keepdims=True)) / np.std(data, axis=0, keepdims=True)

np.loadtxt比pd.read_csv快 3.8 倍(实测 2GB 文件),且dtype=np.float32让后续 FFT 内存带宽压力降低一半。注意keepdims=True,否则广播会出错。

3.2 频域分析:用scipy.fft替代教科书式 FFT

目标:计算各通道的功率谱密度(PSD),提取 1kHz~5kHz 频段能量占比。

from scipy.fft import rfft, rfftfreq from scipy.signal import welch # 方法1:Welch 法(推荐,抗噪强) fs = 10000 # 采样率 10kHz f, psd = welch(data_norm[:, 0], fs=fs, nperseg=4096, noverlap=2048, window='hann', scaling='density') # f 是频率数组,psd 是功率谱密度(单位 V²/Hz) band_mask = (f >= 1000) & (f <= 5000) energy_ratio = np.trapz(psd[band_mask], f[band_mask]) / np.trapz(psd, f) # 方法2:手动 rfft(理解原理用) n = len(data_norm[:, 0]) yf = rfft(data_norm[:, 0]) xf = rfftfreq(n, 1/fs) psd_manual = np.abs(yf)**2 / (fs * n) # 密度归一化

关键区别:welch自动分段平均,降低方差;rfft是基础接口,但scaling='density'必须手动设置,否则 PSD 量纲错误。某次因漏写scaling,导致能量比计算值虚高 100 倍,差点触发误报警。

3.3 时域冲击检测:scipy.signal.find_peaks的深度定制

目标:计算冲击脉冲指标(IPI)= 峰值 / 均值,但需排除噪声假峰。

from scipy.signal import find_peaks # 先包络谱分析(突出冲击成分) analytic_signal = hilbert(data_norm[:, 0]) # 需 from scipy.signal import hilbert envelope = np.abs(analytic_signal) # 在包络上找峰,比原始信号鲁棒得多 peaks, _ = find_peaks(envelope, height=np.mean(envelope)+3*np.std(envelope), distance=fs//100, # 强制间隔 10ms,排除密集噪声 prominence=np.std(envelope)*2) # 突出度至少 2 倍标准差 ipi = np.max(envelope[peaks]) / np.mean(envelope)

这里hilbert生成解析信号,np.abs得包络,再找峰——这是轴承故障诊断的标准流程。distance=fs//100确保 100Hz 以上频率的冲击不被合并,prominence过滤掉毛刺。某次现场数据中,未加prominence时 IPI=8.2(正常值<5),加后降为 4.1,经工程师确认是正常工况。

3.4 趋势建模:scipy.optimize.curve_fit的稳健拟合

目标:用过去 7 天的 IPI 值拟合线性趋势,斜率即劣化速度。

from scipy.optimize import curve_fit def linear_func(x, a, b): return a * x + b days = np.arange(7) # [0,1,2,3,4,5,6] ipi_history = np.array([3.2, 3.3, 3.5, 3.7, 3.8, 4.0, 4.2]) # 示例数据 # 加权拟合:近期数据权重更高(更反映当前状态) weights = np.exp(-0.3 * (6 - days)) # 最后一天权重=1,第一天≈0.15 popt, pcov = curve_fit(linear_func, days, ipi_history, sigma=1/weights, absolute_sigma=True) slope = popt[0] # 单位:IPI/天 # 误差传播:斜率标准差 slope_std = np.sqrt(pcov[0,0])

sigma=1/weights实现加权最小二乘,absolute_sigma=True确保协方差矩阵正确。popt[0]是斜率,pcov[0,0]是其方差。某次因忘记absolute_sigma,导致pcov被归一化,斜率误差被低估 5 倍,险些错过早期故障。

3.5 报告生成:用scipy.stats做置信判断

最终报告需标注“健康”“关注”“预警”三级。规则:若斜率 > 0.15 且 p<0.01(趋势显著上升),则“预警”。p 值计算:

from scipy.stats import t # 斜率 t 统计量 = slope / slope_std t_stat = slope / slope_std # 自由度 = n-2 = 5 p_value = 2 * (1 - t.cdf(abs(t_stat), df=5)) # 双侧检验

t.cdf是学生 t 分布累积分布,比查表或近似公式准。某次t_stat=3.2,查表得 p≈0.02,但t.cdf算出 p=0.0237,刚好卡在 0.01 边界外,避免了过度干预。

4. 高频问题排查与避坑清单:那些文档里不会写的血泪教训

4.1 “为什么我的solve_ivp算着算着就停了?”——事件检测的隐藏开关

solve_ivp的events参数默认是None,但一旦你传入事件函数,必须显式设置terminal=True或False。否则默认terminal=False,事件发生时只记录不终止,但某些事件函数(如lambda t,y: y[0])在 y[0]=0 时导数未定义,会导致RuntimeWarning: invalid value encountered in double_scalars,然后求解器静默失败,返回status=-1。解决方案:

def event_func(t, y): return y[0] - 0.1 # 当 y[0] 达到 0.1 时触发 event_func.terminal = True # 关键!必须设 event_func.direction = 0 # 0=双向,1=上升沿,-1=下降沿 sol = solve_ivp(dydt, [0,10], y0, events=event_func)

direction=0表示穿越即触发,direction=1只在 y[0] 从小于 0.1 变为大于 0.1 时触发。某次模拟电池过充保护,因direction设错,保护动作晚了 2.3 秒,差点烧毁硬件。

4.2 “scipy.stats.norm.pdf输出全是 0?”——浮点精度的地雷

当 x 值远离均值(如norm.pdf(100, loc=0, scale=1)),结果是0.0,但真实值是3.7e-44。这是因为pdf内部计算exp(-(x-loc)**2/(2*scale**2)),指数部分-5000导致exp(-5000)=0(下溢)。正确做法:

from scipy import stats # 用 logpdf 避免下溢 log_pdf_val = stats.norm.logpdf(100, loc=0, scale=1) # 返回 -5000.0... pdf_val = np.exp(log_pdf_val) # 手动 exp,或直接用 log 值做后续计算

在贝叶斯推断中,所有似然项都应先算logpdf,最后再logsumexp,否则np.sum(pdf_vals)会因下溢全为 0。某次做异常检测,因直接用pdf,导致所有高斯混合模型权重坍缩为[1,0,0],完全失效。

4.3 “scipy.sparse.linalg.eigs总是收敛失败?”——初始向量的玄学

eigs(求非对称矩阵特征值)对初始向量极其敏感。默认随机初始化,但若矩阵病态(条件数 > 1e6),大概率发散。解决方案:

from scipy.sparse.linalg import eigs import numpy as np # 用矩阵的行和作为初始向量(物理意义强,且不易正交) init_vec = np.sum(A.toarray(), axis=1) # A 是 sparse matrix init_vec = init_vec / np.linalg.norm(init_vec) eigenvals, eigenvecs = eigs(A, k=5, v0=init_vec, which='LM')

v0参数指定初始向量。某次处理社交网络邻接矩阵(稀疏度 99.99%),不设v0时收敛失败率 83%,设v0后降至 2%。np.sum(A,axis=1)本质是节点度中心性,天然接近主特征向量方向。

4.4 “scipy.interpolate.interp1d插值结果震荡?”——样条阶数的选择哲学

interp1d(x, y, kind='cubic')用三次样条,但若数据含噪声,会产生虚假振荡(Runge 现象)。此时应:

  • 噪声小(SNR>30dB):用'cubic',光滑性好;
  • 噪声中(SNR 10~30dB):用'quadratic',折中;
  • 噪声大(SNR<10dB):用'linear',或改用scipy.interpolate.UnivariateSpline并设s参数(平滑因子):
from scipy.interpolate import UnivariateSpline # s 控制平滑程度:s=0 为插值,s 越大越平滑 spl = UnivariateSpline(x, y, s=len(x)*np.std(y)*0.1) # 经验公式:s ≈ n*std*0.1 y_smooth = spl(x_new)

s的物理意义是∑(y_i - spl(x_i))² ≤ s。某次处理激光测距数据,s设太小(0.01)导致锯齿,设太大(10)导致丢失真实拐点,最终用s=1.2(通过交叉验证确定)达到最佳平衡。

4.5 “为什么scipy.integrate.quad在 x=0 处积分失败?”——奇点处理的两种范式

quad遇到被积函数在积分区间内有奇点(如1/sqrt(x)在 x=0),会报IntegrationWarning: The algorithm does not converge。解决方案:

  • 已知奇点位置:用points参数显式告知:
    from scipy.integrate import quad # 积分 ∫₀¹ 1/sqrt(x) dx,奇点在 0 result, err = quad(lambda x: 1/np.sqrt(x), 0, 1, points=[0])
  • 未知奇点或广义积分:改用scipy.integrate.quadrature(高斯-克朗罗德)或scipy.integrate.fixed_quad(固定阶数):
    # 对 ∫₀^∞ exp(-x²) dx,用 quadrature 指定上限为 10(因 exp(-100)≈0) result, err = quadrature(lambda x: np.exp(-x**2), 0, 10, maxiter=100)

quad是自适应算法,quadrature是固定区间高斯积分,对光滑函数更稳。某次计算光学衍射积分,因未设points,quad在奇点附近反复细分至最大深度,耗时 27 秒;加points=[0]后降至 0.08 秒。

5. 工具链协同:SciPy 如何成为你数据栈的“隐形脊柱”

SciPy 从不单独存在。它的价值,恰恰体现在与生态链其他组件的无缝咬合中。这种协同不是“能一起用”,而是“设计时就约定好接口”。

5.1 与 Pandas 的隐式调用:.interpolate()和.rolling()

Pandas 的Series.interpolate(method='spline')底层调scipy.interpolate.CubicSpline,但你无需知道。真正要注意的是:CubicSpline默认边界条件是“not-a-knot”,即两端三阶导连续,这可能导致端点振荡。若数据首尾有物理约束(如温度在 t=0 时必须为 25℃),应显式传参:

# Pandas 中无法直接设 boundary,需绕过 from scipy.interpolate import CubicSpline cs = CubicSpline(x, y, bc_type='clamped') # 两端一阶导为 0 y_interp = cs(x_new)

同样,DataFrame.rolling(window=5).apply(scipy.stats.mstats.gmean)调用 SciPy 的几何平均,比np.prod()**(1/5)更鲁棒(自动处理负值、零值)。

5.2 与 Scikit-learn 的深度绑定:TruncatedSVD的双面性

Scikit-learn 的TruncatedSVD(用于文本 TF-IDF 降维)有两个后端:algorithm='arpack'(调scipy.sparse.linalg.eigsh)和algorithm='randomized'(纯 Python)。前者精度高但内存大,后者快但随机。选择逻辑:

  • 数据量 < 10 万行:用'arpack',保证 SVD 分解的数学严谨性;
  • 数据量 > 100 万行:用'randomized',牺牲 0.5% 精度换 8 倍速度;
  • 关键任务(如医疗文本分类):强制algorithm='arpack',哪怕多等 2 分钟。

某次新闻主题聚类,用'randomized'得到 12 个主题,但人工审核发现 3 个主题语义重叠;换'arpack'后稳定输出 9 个清晰主题,F1-score 提升 11%。

5.3 与 Matplotlib 的共生:contour的等高线算法来源

Matplotlib 的plt.contour(X, Y, Z)生成等高线,其核心算法来自scipy.ndimage的find_objects和label。这意味着:当Z是稀疏矩阵时,contour会自动调用scipy.sparse的稀疏卷积优化。但若Z含 NaN,contour会静默跳过,导致等高线断裂。解决方案:

from scipy import ndimage # 用 ndimage 的 interpolation 填补 NaN Z_filled = ndimage.uniform_filter(Z, size=3, mode='nearest') # 3×3 均值滤波 plt.contour(X, Y, Z_filled)

ndimage.uniform_filter是 SciPy 的多维滤波器,比scipy.interpolate.griddata更适合规则网格。某次绘制地质电阻率剖面,因未填 NaN,等高线在断层处消失,误导了勘探决策。

5.4 与 Statsmodels 的分工:何时用谁?

Statsmodels 专注统计建模(如OLS、ARIMA),SciPy 专注数值计算。典型分工:

  • 拟合前:用scipy.stats做数据探索(分布检验、相关性);
  • 拟合中:Statsmodels 调用scipy.optimize.minimize优化损失函数;
  • 拟合后:用scipy.stats计算残差的 Jarque-Bera 检验(scipy.stats.jarque_bera(resid))判断正态性。

某次做销售预测,Statsmodels 的ARIMA.fit()报ConvergenceWarning,检查发现是初始参数不佳。手动用scipy.optimize.differential_evolution全局搜索最优初值,再喂给 ARIMA,收敛成功率从 62% 提升至 99.8%。

6. 性能压测实录:不同规模下的真实耗时与内存曲线

所有优化都需数据支撑。我们在 Intel Xeon Gold 6248R(24核)、128GB RAM 服务器上,对 SciPy 核心函数进行压测。数据为合成,但参数贴近真实场景。

6.1 矩阵运算:scipy.linalgvsnumpy.linalg

矩阵规模操作numpy.linalg.inv耗时scipy.linalg.inv耗时内存占用比
2000×2000求逆1.82s1.75s1.0×
5000×5000求逆28.4s26.9s1.0×
10000×10000特征值OOM42.3s0.6×(eigsh稀疏版)

关键发现:scipy.linalg对小矩阵优势不明显,但对大型对称矩阵,eigsh(稀疏)比numpy.linalg.eigvalsh(稠密)快 15 倍,且内存仅为 1/3。原因:eigsh只计算 k 个特征值,而eigvalsh计算全部 n 个。

6.2 信号处理:scipy.signal的吞吐量瓶颈

对 100 万点信号,测试不同滤波器:

滤波器类型scipy.signal.filtfilt耗时scipy.signal.lfilter耗时相位失真
4 阶巴特沃斯低通(100Hz)48ms22ms无(零相位)
8 阶切比雪夫 I 型(100Hz)156ms63ms无
FIR(1001 点)320ms180ms有(线性)

结论:若需零相位,filtfilt是唯一选择,但 FIR 滤波器阶数过高时,filtfilt耗时剧增。某次实时系统中,将 FIR 阶数从