1.1 SciPy 是什么:与 NumPy 的分工 本节摘要:SciPy 是构建在 NumPy 之上的科学计算库,二者分工可以概括为"NumPy 提供数组与基本运算,SciPy 提供面向科学计算的高层算法"。本节用对比与实例讲清这个边界,并解释 SciPy 为什么大量复用经过数十年验证的 FORTRAN 经典算法库。 1.1 SciPy 是什么:与 NumPy 的分工 学习目标 阅读完本节,你应当能够: 用一句话说清 SciPy 与 NumPy 的分工; 举出 SciPy 中三个典型的子模块及其适用问题; 解释"为什么 SciPy 的函数比手写实现更可靠"; 判断一个具体问题应该用 NumPy 还是 SciPy 解决。
本节摘要:SciPy 是构建在 NumPy 之上的科学计算库,二者分工可以概括为"NumPy 提供数组与基本运算,SciPy 提供面向科学计算的高层算法"。本节用对比与实例讲清这个边界,并解释 SciPy 为什么大量复用经过数十年验证的 FORTRAN 经典算法库。

阅读完本节,你应当能够:
刚接触 Python 科学计算的人,几乎都会被一个问题卡住:NumPy 和 SciPy 到底有什么区别?看文档,NumPy 有数组运算,SciPy 也有线性代数;NumPy 能算傅里叶变换,SciPy 也能。两边的函数名字还经常一样——numpy.fft 和 scipy.fft、numpy.linalg 和 scipy.linalg,连文档首页都在互相引用。
这不是设计混乱,而是历史形成的合理分工。理解这个边界,你才能在两边的"重合区"做出正确选择,也才能读懂大量网上教程里"建议用 SciPy"的劝告到底在说什么。
我们先做一个思想实验。你面前有两样东西:一盒积木,和一栋用积木搭好的桥。NumPy 是积木盒——它给你的是构建数值计算的基础构件(数组、基本运算、简单的分解);SciPy 是那座桥——它把积木按工程标准组装好,你直接走过去就行。桥的组装过程经历了数十年、无数人的验证,比自己现搭可靠得多。
SciPy 诞生于 2001 年,比 NumPy 还早几年(NumPy 的前身 Numeric 更早,但 SciPy 项目一度因基础数组库的分裂而受阻)。后来的故事是:SciPy 团队把自己的底层数组实现拆出去,独立成了 NumPy 项目,SciPy 则专注于更高层的算法库。所以两者的关系不是"竞争",而是"上游与下游"——SciPy 的所有模块都依赖 NumPy 数组作为数据结构。
这个历史解释了为什么两边会有重叠函数:scipy.linalg 里的函数大多是对 NumPy 对应功能的超集,增加了更多分解方法和更丰富的返回信息;scipy.fft 则提供了比 NumPy 更多的前后端选择(比如支持 fftconvolve 这类组合功能)。重叠不可怕,怕的是不知道哪个更合适。
NumPy 管"怎么存、怎么算得快",SciPy 管"算什么、怎么算得对"。
展开说:NumPy 的核心是 ndarray——多维数组的存储、切片、广播、基本逐元素运算,以及少量经过优化的线性代数操作。SciPy 的核心是面向具体科学问题的算法:积分的自适应求积、优化的多种全局与局部算法、插值的各类样条、信号滤波器的设计、统计分布与检验、稀疏矩阵的存储与求解、空间数据的 KDTree、特殊函数的数值计算。
换个角度理解:如果你在写机器学习框架的底层、做大规模数组的朴素运算,用 NumPy;如果你在做"科研计算"——算积分、拟合参数、滤波、检验假设,用 SciPy。
SciPy 的很多核心算法不是自己从零写的,而是封装了经典 FORTRAN 库:积分用 QUADPACK,优化用 MINPACK,线性代数用 LAPACK。这些库从七八十年代起就在大型机、超级计算机上运行,被无数论文引用,边界情况和数值稳定性经过了残酷的实战检验。
举个直观的例子:手写一个 Simpson 积分很简单,十几行代码的事。但真遇到被积函数在区间内有剧烈变化或奇异点,朴素实现会静默地给出错误结果。scipy.integrate.quad 用的是自适应算法——它会自动在函数变化剧烈的地方加密采样点,并给出误差估计。这种"带误差估计的积分"是手写实现难以企及的。
判断标准很简单:当你的需求只是"对数组做数学运算",不需要算法级的决策时,NumPy 就够。比如:逐元素计算、矩阵乘法、简单求均值方差、numpy.linalg 里已有的 SVD 和特征值分解、numpy.fft 的标准 FFT。
但有两个信号提示你应该转向 SciPy:一是你需要算法选择(哪种插值方法、哪种优化器、哪种滤波器),二是你需要专业结果(误差估计、显著性、分位数、稀疏矩阵的高效求解)。
光讲概念不够,我们用一个具体问题看差距。假设我们要对一组带噪声的离散点做平滑,再求它的峰值位置。
先用 NumPy 的朴素思路:对相邻点做滑动平均,然后找最大值所在下标。
import numpy as np x = np.linspace(0, 10, 200) y = np.sin(x) + 0.2 * np.random.default_rng(1).normal(size=len(x)) # NumPy 思路:朴素滑动平均 window = 5 smoothed = np.convolve(y, np.ones(window) / window, mode='same') peak = np.argmax(smoothed) print("峰值位置(NumPy 朴素法):", x[peak])
这个解法有两个隐患:一是滑动平均会削平峰形,峰值位置可能偏移;二是我们完全不知道平滑结果的可信度。换成 SciPy 的思路,用信号处理模块的 Savitzky-Golay 滤波(在保持峰形的前提下平滑),再用 find_peaks 返回峰值位置和高度:
from scipy.signal import savgol_filter, find_peaks smoothed_sg = savgol_filter(y, window_length=11, polyorder=3) peaks, props = find_peaks(smoothed_sg, height=0.5) print("峰值位置(SciPy 方法):", x[peaks[0]], "峰值高度:", props["peak_heights"][0].round(3))
同一个需求,NumPy 要自己拼装算法、处理边界效应;SciPy 给你经过验证的滤波器设计和峰值检测器,还附带了高度、宽度等结构信息。这就是"积木"与"桥"的区别——不是说 NumPy 做不到,而是 SciPy 让你直接站在桥上过河。
| 需求 | 用 NumPy | 用 SciPy | 理由 |
|---|---|---|---|
| 数组存储与切片 | 必选 | — | 这是 ndarray 的主场 |
| 矩阵乘法、SVD | 可以 | 更推荐 | scipy.linalg 更多算法与信息 |
| 定积分 | — | integrate.quad | 自适应算法带误差估计 |
| 最小二乘拟合 | 可以 | 推荐 optimize.curve_fit | 自带参数协方差与统计量 |
| 快速傅里叶变换 | 可以 | 看场景 | scipy.fft 前后端选择更灵活 |
| 统计检验 | — | stats | 分布函数与检验全覆盖 |
| 稀疏矩阵 | — | sparse | 专业存储格式与求解器 |
⚠️ 常见坑:把 SciPy 当作"更大的 NumPy"随便 import 全部子模块(
from scipy import *),既慢又容易与 NumPy 同名函数冲突。规范做法是按需导入:from scipy import integrate, optimize。
💡 关键直觉:看到陌生数值问题,先问自己"这是存储问题还是算法问题"——存储问题找 NumPy,算法问题找 SciPy,十有八九不会错。
知道该用哪个库是一回事,知道库里的哪个函数是另一回事。当你拿着一个具体问题面对 SciPy 时,我习惯用三步法定位:
第一步,按问题类型找模块。想一下问题属于哪一类:求面积还是求变化率(integrate)、找最优还是找根(optimize)、补点还是平滑(interpolate、signal)、分类还是检验(spatial、stats)。模块名本身就是答案的一半。
第二步,看模块首页的索引。每个模块的文档首页都有一份"函数按用途分类"的清单,比如信号模块把滤波、谱分析、窗函数分开列,找起来比全局搜索快得多。
第三步,用帮助函数确认签名。在环境里对目标函数调用帮助文档(如 help(optimize.curve_fit)),看参数顺序和返回值结构,再搜一两段官方示例。三步下来,绝大多数问题都能在十分钟内找到入口。
SciPy 每隔几年会调整一些函数的推荐用法,比如早年常用 interpolate.spline,现在官方推荐 CubicSpline;scipy.fftpack 逐步被 scipy.fft 取代。看到网上旧教程里的函数名,先确认你装的版本里它是否还在、有没有替代品——方法是在环境里试跑,报错或警告通常是迁移信号。这也是为什么本教程统一推荐当前稳定版本的用法,遇到旧代码时按此对照升级。
分工讲再多,不如拼一次真流水线。假设你手里有一份 90 天的日平均气温记录,中间有几天仪器故障导致数据缺失,序列本身带着测量噪声,你想回答的问题是:这段时间气温有没有明显的上升趋势。这个任务恰好需要 interpolate、signal、stats 三个模块各干一段:
import numpy as np from scipy import interpolate, signal, stats rng = np.random.default_rng(7) day = np.arange(90) temp = 15 + 8 * np.sin(2 * np.pi * day / 30) + 0.02 * day + rng.normal(0, 0.6, 90) temp[[20, 21, 22, 45, 46]] = np.nan # 模拟五天观测缺失 # 第一步:interpolate 补缺失,用三次样条保住局部形态 mask = ~np.isnan(temp) fill = interpolate.interp1d(day[mask], temp[mask], kind='cubic') temp_filled = np.where(np.isnan(temp), fill(day), temp) # 第二步:signal 平滑噪声,但不削平趋势的转折 smooth = signal.savgol_filter(temp_filled, window_length=9, polyorder=2) # 第三步:stats 检验趋势是否显著 slope, intercept, r_value, p_value, se = stats.linregress(day, smooth) print(f"升温斜率 {slope:.3f} 度每天,p 值 {p_value:.2e}")
每个模块各管一段,谁也不越界:interpolate 只负责把缺失的五个点补上(三次样条比填均值更能保留局部形态);signal 只负责滤掉测量噪声(选 Savitzky-Golay 而不是滑动平均,是因为它平滑的同时不钝化趋势的转折);stats 只负责给结论一个概率判断——p 值远小于 0.05,我们才敢说"升温不是随机波动造成的"。
| 流水线步骤 | 所属模块 | 用到的函数 | 这一步回答的问题 |
|---|---|---|---|
| 补缺失值 | interpolate | interp1d | 缺掉的几天大概是多少度 |
| 滤噪声 | signal | savgol_filter | 去掉随机抖动后趋势长什么样 |
| 检验趋势 | stats | linregress | 升温是真实信号还是噪声运气 |
想想如果只用 NumPy,这段路会是什么样:样条要手写,滤波窗要手写,回归统计量要手写,每一段都得自己证明正确性。而这里每个函数都经过数十年验证,你真正要做的判断只有一件事——这一步属于哪类问题,然后挑对模块、调对参数。这也是前面三步法说的"按问题类型找模块":定位对了,这条流水线十分钟就能搭起来。
这条流水线也有自己的边界。比如缺失段如果长达一个月,三次样条插出来的中间值基本就是猜——这时候与其插值,不如承认数据不足,把缺失段剔除或单独标注,别让插值结果混进后续拟合里冒充真实观测。再比如平滑窗口选多大,是个没有标准答案的取舍:窗口太小噪声没滤干净,窗口太大又把真实波动削平了。我们建议每个参数都亲自动手调一遍,感受结果随之变化的幅度——参数从哪来、变了会怎样,这才是"会调"和"会用"的分水岭,也是把模块分工用好的最后一公里。
下一节我们解决动手前最关键的一步:怎么把环境搭好、装对版本,让后面所有示例都能跑起来。