1. 装numpy踩过的坑:pip卡死、ModuleNotFoundError和版本冲突
如果你最近刚上手Python数据分析,八成会在某个深夜对着终端里转圈的进度提示发呆。我就是这么过来的——照着教程敲下pip install numpy,结果卡在installing backend dependencies足足十分钟没动静,最后忍无可忍按了Ctrl+C,再运行import numpy,迎面一行红字:ModuleNotFoundError。
先别急着怀疑人生。这三个问题基本覆盖了新手装numpy绝大多数的翻车场景,我把排查思路和解决办法一次说清楚。
1.1 三种安装方式的取舍
装numpy有几种路径,按推荐程度排序应该是这样:
| 安装方式 | 命令 | 适合场景 | 踩坑概率 |
|---|---|---|---|
| pip安装(预编译wheel) | pip install numpy | 绝大多数情况,最快最省心 | 低 |
| conda安装 | conda install numpy | 已经用Anaconda管理环境 | 低 |
| 源码编译 | pip install numpy --no-binary :all: | 特殊平台或需要定制优化 | 极高,不推荐 |
日常使用直接pip install numpy就够。PyPI上对Windows、macOS和主流Linux发行版都提供了预编译的wheel包,安装过程就是把编译好的二进制文件拷到site-packages里,几秒钟搞定。源码编译是给特定平台或需要特定编译参数(比如配合某些BLAS库)的场景用的,新手完全没必要碰。
卡在installing backend dependencies这一步,恰恰说明pip没有找到现成的wheel,退回源码编译流程去装构建依赖了。为什么找不到wheel?大概率是Python版本太新或太老,或者操作系统架构比较特殊(比如ARM架构的Windows),PyPI上还没有对应的预编译包。
遇到这种情况,最省事的解法是换Python版本。我个人实测下来,Python 3.8到3.12之间的版本,numpy的wheel支持最完善。太新的Python版本刚发布时,主流库的wheel经常要等一两个月才能跟上,这是规律。如果不想换Python,也可以试试指定版本安装:
pip install numpy==1.26.4老一点的版本往往有更多平台的wheel,兼容性反而更好。
1.2 ModuleNotFoundError:装没装对地方的排查步骤
ModuleNotFoundError: No module named 'numpy'这个报错,90%的根因不是没装,而是装错了环境。Python的环境体系很容易让新手懵:你可能有系统自带的Python、有Anaconda自带的Python、有虚拟环境里的Python,每个环境都是独立的,包互不相通。
先记住一个判断口诀:**你在哪个环境里运行代码,包就必须装在那个环境里。**排查步骤很简单:
# 第一步:确认当前python的路径 which python # 第二步:确认当前pip的路径 which pip # 第三步:查看当前环境里有没有numpy pip list | grep numpy如果python和pip分别指向不同的环境,那装多少遍都没用。再补充一个高频错误:先用pip install numpy装好了,然后打开某些IDE(比如VSCode、PyCharm)运行代码,还是报ModuleNotFoundError。原因往往是IDE的Python解释器选错了——IDE默认用的Python环境,和你在终端里装包的环境不是同一个。
通用的解法,养成用虚拟环境的好习惯:
python -m venv myenv source myenv/bin/activate # Windows系统用 myenv\Scripts\activate pip install numpy虚拟环境会把Python和所有包隔离在一个独立目录里,环境混乱问题从根上解决。如果装了Anaconda,用conda创建环境同理:
conda create -n myenv python=3.10 conda activate myenv conda install numpy1.3 numpy版本不匹配,多半是混用包管理器惹的祸
装完numpy只是开始,真正让人抓狂的是版本不匹配。记忆里最典型的一次:拿conda装了一个科学计算包,它顺手把numpy从1.26降到了1.21,然后原本跑得好好的数据处理脚本,突然各种报错——某函数参数变了、某个语法不认识了。
这种连锁反应的根源,是conda和pip混用。conda有自己的依赖解析逻辑,pip也有自己的一套,两边互不知晓对方装了什么。conda升级某个包时,可能按照它的规则重新装一遍numpy,把pip装的那个覆盖掉。
这几年处理了不知道多少次版本问题之后,我的经验是:一个环境只用一个包管理器。要么全用conda,要么全用pip。如果实在需要混用,先pip装numpy,再用--no-deps参数让conda跳过依赖处理:
conda install somepackage --no-deps另外,项目跑起来一段时间后,建议顺手把依赖版本固定下来:
pip freeze > requirements.txt下次换机器、换环境,一行pip install -r requirements.txt就能复现环境。这个习惯能帮你省掉无数个排查依赖的深夜。
2. 为什么说numpy比list快:一次实测和背后的原理
网上一搜numpy相关的话题,必有"和list比快在哪"这个灵魂拷问。光说"快"太空洞了,我用自己机器上的数据说话。下面这段代码对100万个随机数做加法:
import time import numpy as np # 用list做 data_list = list(range(1_000_000)) start = time.time() result = [x + 1 for x in data_list] print(f"list耗时: {time.time() - start:.4f}秒") # 用numpy做 data_np = np.arange(1_000_000) start = time.time() result = data_np + 1 print(f"numpy耗时: {time.time() - start:.4f}秒")在我这台普通的办公机器上,list实现大概要0.05秒,numpy只需要0.001秒左右,差距差不多50倍。数据量越大,这个差距越离谱。
2.1 快的原因拆解:连续内存、C循环和CPU缓存
numpy为什么能这么快?三个层面:
第一层:数据存储方式。Python的list看似存了一堆数字,实际每个元素都是一个完整的Python对象,list内部存的是指向这些对象的指针。Python对象自带类型信息、引用计数等额外开销,100万个整数就对应100万个对象,内存又散又大。numpy的ndarray则是将数据连续地存在一块内存里,类型完全一致,没有对象包装,内存占用小得多。打个比方:list像一个放满便当盒的大抽屉,每个盒子里装一个数字;ndarray像一整块打包好的板子,所有数字按顺序码得整整齐齐。
第二层:运算方式。对list做x + 1这种操作,Python解释器要逐元素地解释执行,每个元素都走一遍完整的类型检查和方法查找流程。numpy则把加法包装成C语言级别写好的通用函数(ufunc),直接在编译好的二进制代码里循环处理整块数据,省掉了逐元素解释的开销。
第三层:CPU缓存友好性。现代CPU读写内存时,会把相邻的数据一次性加载到高速缓存里。ndarray的数据在内存里是连续的,CPU按顺序遍历时命中缓存的概率极高,几乎没有等待内存的时间。list的指针跳转则可能到处寻址,高速缓存命中率低,大量时间浪费在等内存上。
2.2 什么时候list反而更合适
说了这么多numpy的优势,但numpy不是银弹。实际情况里,list有它无可替代的场合:
- 异构数据。list可以同时存数字、字符串、对象;ndarray要求同类型。要存混合类型的数据,硬用numpy只会有无尽的对象类型转换烦恼。
- 频繁增删元素。ndarray创建后长度基本固定,往里插数据、删数据很别扭;list的append、pop随手就来。
- 小数据量。几百个元素的运算,两种方案的性能差距可以忽略。这时候选更顺手、代码更易读的那个,没必要为了一点点性能牺牲可读性。
- 与第三方库的接口。很多Python库的接口默认接受list,返回list,强转成numpy反而要来回倒腾。
对数据处理和科学计算场景,大块的数值运算交给numpy就好;平时写脚本处理小逻辑,list依然是好伙伴。两者各有定位,不是谁替代谁的关系。
3. ndarray基本功:创建、取值、shape和dtype
无论你是看教程还是读文档,绕不开的核心归根结底是一件事:把数据放进ndarray,然后高效地把数据取出来。这一节我把最常见的操作过一遍,这些是后续一切分析工作的地基。
3.1 创建数组的常用方式
import numpy as np # 从Python列表创建 a = np.array([1, 2, 3]) # 全零、全一、空数组 zeros = np.zeros((3, 4)) # 3行4列全0 ones = np.ones((2, 3)) # 2行3列全1 empty = np.empty((2, 2)) # 申请内存但不初始化,值不可控,一般别用 # 等差序列 seq = np.arange(0, 10, 2) # [0, 2, 4, 6, 8] lin = np.linspace(0, 1, 5) # [0., 0.25, 0.5, 0.75, 1.],5个等差点 # 随机数 rand = np.random.rand(3, 3) # 均匀分布[0,1) normal = np.random.randn(3) # 标准正态分布 random_int = np.random.randint(0, 10, size=5) # 整数随机数np.arange和np.linspace的区别容易混淆:arange按步长生成,终点可能不包含;linspace按个数生成,包含首尾。做坐标轴、分组区间时linspace更常用,因为它能保证端点准确落在你的目标区间。
3.2 几个必须清楚的数组属性
a = np.zeros((3, 4)) print(a.shape) # (3, 4),每个维度的长度 print(a.ndim) # 2,维度数量 print(a.dtype) # float64,元素类型 print(a.size) # 12,元素总数 print(a.itemsize) # 8,每个元素占的字节数这里新手最容易忽略的是dtype。numpy的整数类型有好几种:int8、int16、int32、int64,区别是能表示的数值范围。默认情况下列表转数组,64位系统上是int64;zeros和ones默认float64。如果你的数据只需要整数且范围不大,可以显式指定小类型来省内存:
a = np.array([1, 2, 3], dtype=np.int16) a.astype(np.float32) # 需要时用astype转换类型大数据的场景,dtype直接影响内存占用。1000万个浮点数的float64占80MB,float32占40MB,省一半内存有时候比省一点计算时间更关键。
3.3 索引与切片:从基础到花式取值
一维数组的切片逻辑和list基本一致,二维及以上就要小心了。
a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) a[0, 1] # 第一行第二列,结果是2 a[1] # 取第二行 a[:, 1] # 所有行的第二列,结果是[2, 5, 8] a[1:, :2] # 从第二行到最后,取前两列三者的区别值得展开说:a[1]只写了行索引,得到一整行;a[:, 1]用逗号分隔,冒号在行位置表示所有行,结果是第二列的一个一维数组;a[1:, :2]同时切了行和列,得到二维子块。
布尔索引和花式索引是取值操作里最常用的两个技巧。布尔索引就是用一个布尔数组作为掩码:
data = np.array([10, 25, 3, 47, 8]) mask = data > 10 data[mask] # 返回[25, 47] data[data % 2 == 0] # 返回[10, 8]花式索引则是用整数数组指定位置:
data[[0, 2, 4]] # 取出第1、3、5个元素 a[[0, 2], [0, 1]] # 取出(0,0)和(2,1)两个位置的值3.4 切片是视图:新手最容易掉进去的坑
二维数组的切片有个非常容易踩的坑:切片拿到的是原数组的视图,不是副本。修改切片,原数组也跟着变。
a = np.array([1, 2, 3, 4]) b = a[1:3] # b是a的视图,不是新数组 b[0] = 99 print(a) # [1, 99, 3, 4],原数组也被改了想要独立的数据副本,必须显式复制:
b = a[1:3].copy()这个坑在数据处理时会造成排查半天的"幽灵bug"——数据莫名变了,回头一看是哪个视图被改过。我的习惯是:需要进一步处理的切片一律先.copy(),只有纯读取操作才直接用视图,性能和安全性之间求个平衡。
好在这个机制也有实用的一面。对大数组做切片后得到视图,不复制数据,内存占用小,执行也快。如果只是读取,视图反而占据优势——前提是你心里清楚知道它是个视图。
4. 向量化思维:让numpy真正发挥威力的核心
装好numpy、学会创建数组和取值,这还只是工具层面。真正让numpy改变你代码质量的,是思维方式上的转变——从"逐元素处理"变成"整体处理"。这个过程有个专门的名字:向量化。
4.1 别再写for循环了
新手写numpy代码最容易犯的错,就是把numpy数组当成list,用for循环一个个处理元素:
# 新手写法:循环遍历每个元素 result = np.zeros_like(data) for i in range(len(data)): result[i] = data[i] * 2 + 1 # 正确写法:直接用向量化运算 result = data * 2 + 1两种写法结果一致,性能差了几十倍。for循环把numpy的优势全丢掉了——每次迭代都要走Python解释器,类型检查、方法查找一个不少,等于用C语言的高效存储配合了Python的龟速循环。向量化则直接在C层面处理整个数组。
我见过一个真实的例子:一段计算k线指标的函数,用for循环跑全市场数据要40秒,改成向量化之后只要0.5秒。这种性能提升不是靠改配置、调参数,纯粹是换了一套思维模式。
4.2 广播机制:不同shape的数组如何运算
广播(broadcasting)是numpy最强大的特性之一,也是新手比较容易懵的地方。它说的是:不同形状的数组做运算时,numpy会自动把维度补全,让小数组"拓展开"匹配大数组。
a = np.array([[1, 2, 3], [4, 5, 6]]) b = 10 a + b # 标量自动扩展到每个位置,结果是[[11,12,13],[14,15,16]] c = np.array([10, 20, 30]) a + c # 一维数组自动扩展成两行,结果: # [[11, 22, 33], # [14, 25, 36]]广播的规则简化来说:从最后一个维度往前比,如果两个数组的某个维度相等、或其中一个为1、或其中一个缺失,就能广播;否则会报错。实际操作里用个例子感受下shape为(3,1)和(3,)数组的运算;shape为(3,1)的数组视为3行1列,shape为(3,)视为一行三个元素,两者做加法时,(3,1)的列自动复制三份,(3,)的行自动复制三行,结果shape为(3,3)。用这个机制做"每行减去该行均值"之类的操作非常顺手:
data = np.random.randn(5, 3) mean_per_row = data.mean(axis=1, keepdims=True) # shape (5,1) centered = data - mean_per_row # 每行减去各自均值注意这里的keepdims=True,它保留住了维度信息,让shape从(5,)变成(5,1),广播才能正确按行对齐。如果不加这个参数,得到(5,)形状后去减(5,3)的数组,广播规则会尝试按列对齐,结果就乱了。这是我学广播时印象最深的一个细节。
4.3 聚合运算和axis参数的确定性理解
numpy的聚合函数很多:sum、mean、max、min、std、var、median、percentile等等,日常统计基本靠它们。这里最混乱的是axis参数——网上说法众说纷纭,"axis=0是行还是列"这个问题能吵一屏。
我的理解方式简单粗暴:axis=0表示按行方向压缩(跨行计算),结果保留列数;axis=1表示按列方向压缩(跨列计算),结果保留行数。
data = np.array([[1, 2, 3], [4, 5, 6]]) data.sum(axis=0) # 按行方向压缩,每一列求和,结果是[5, 7, 9] data.sum(axis=1) # 按列方向压缩,每一行求和,结果是[6, 15]不用背口诀,动手写个小例子跑一遍,比记任何口诀都直观。多试几次你会有肌肉记忆。
4.4 把循环改写成向量化的实战经验
学向量化最好的方式,就是看别人怎么把循环"翻译"成向量操作。举一个我常用来教学的场景:给数组每个元素按阈值归类。
# 循环写法 for i in range(len(data)): if data[i] > 0: data[i] = 1 else: data[i] = 0 # 向量化写法 result = np.where(data > 0, 1, 0)np.where就是向量化的if-else:第一个参数是条件,第二个是满足时的值,第三个是不满足时的值。这个函数在数据处理里出场率极高。
再举个例子,把一维数组每三个连续元素求和(滑动窗口汇总):
# 循环写法 result = [] for i in range(len(data) - 2): result.append(sum(data[i:i+3])) # 向量化写法 result = data[:-2] + data[1:-1] + data[2:]向量化写法初看可能不太习惯,但了解了切片的对齐机制后就通透了。核心思路是:把对每个元素的操作,改写成对整个数组的对齐操作。
写向量化代码的过程,本质上是在锻炼自己对数据形状的感知力。一旦掌握,写数据处理代码的思路会变得完全不一样,先想清楚要做什么,再选对应形状的数组运算,一气呵成。
5. 科学计算高频实战:矩阵求逆、三维数组运算和统计指标
前面聊的都是单兵作战的招式,现在把它们组合起来,看看在真实科学计算需求里怎么用。这里挑三个热搜词最集中的方向展开:矩阵运算、三维数组运算和统计。
5.1 矩阵运算:求逆、行列式和线性方程组
numpy中线性代数功能集中在np.linalg模块里,这个模块都是经过底层优化(通常是LAPACK库)的实现,直接可用的性能相当好,不需要自己另起炉灶写算法。
import numpy as np A = np.array([[2, 1], [1, 3]]) # 矩阵求逆 A_inv = np.linalg.inv(A) # 行列式 det = np.linalg.det(A) # 解线性方程组 A x = b b = np.array([1, 2]) x = np.linalg.solve(A, b)np.linalg.inv和np.linalg.solve的区别值得拎出来讲:很多新手需求其实只是解方程组Ax = b,却先求逆矩阵再用矩阵乘。不谈数值稳定性的话,solve直接用高斯消元法,计算复杂度更低、数值更稳,应该优先选它。一个快速判断法则是:单独需要"你看看A的逆长什么样"这种诊断需求才用inv,解方程组用solve就好。
记住行列式的民间解读:行列式为0意味着矩阵不可逆。在实战里这个性质经常用来做"共线性检测":当数据矩阵的行列式接近0,说明列之间存在线性相关,模型参数估计会不稳定。我处理回归类问题时会顺手打印一下相关系数矩阵的行列式,是个便宜好用的诊断手段。
5.2 三维数组相乘:*、np.dot和np.matmul的区别
三维数组相乘是很多人问的高频问题,网上搜索结果也常互相打架。关键的分歧在于"乘"字指代哪种操作。numpy里根本不只有一个"乘法":逐元素乘法用*,矩阵乘法用np.dot或np.matmul或@运算符。
a = np.random.rand(2, 3) b = np.random.rand(2, 3) c = a * b # 逐元素乘,结果形状(2,3),对应位置相乘 a_mat = np.random.rand(2, 3) b_mat = np.random.rand(3, 4) d = a_mat @ b_mat # 矩阵乘法,结果形状(2,4)对二维数组来说,np.dot和np.matmul基本等价。差别在三维及以上浮现:np.dot处理高维时行为比较老派,它把数组按"把后两维当作矩阵,其余维度展平"的规则来算;np.matmul则严格按"批量矩阵乘法"处理,把前两个维度当作batch维度,批量执行矩阵乘法。
# batch矩阵乘法:一次算多个矩阵的乘法 a = np.random.rand(4, 2, 3) # 4个2x3矩阵 b = np.random.rand(4, 3, 5) # 4个3x5矩阵 c = a @ b # 结果(4, 2, 5),4对矩阵分别相乘在深度学习的批量线性层计算里,这种batch matmul就是靠这个机制跑起来的。现代代码里@运算符用的最多,写起来最简洁、语义最清楚。我的建议是:日常用@或np.matmul,逐元素操作就用*,基本不会踩坑。
5.3 统计指标组合:以股价波动率计算为例
统计函数单拎出来都不复杂,组合起来才是数据分析的日常。我用一个经典的生产级例子说明:计算某只股票收益率的年化波动率。
import numpy as np # 假设closes是过去120个交易日的收盘价 closes = np.array([10.0, 10.5, 10.2, 10.8, 11.2, ...]) # 收益率 returns = np.diff(closes) / closes[:-1] # 年化波动率 = 日收益率标准差 * 年化因子(252个交易日) daily_std = np.std(returns) annual_vol = daily_std * np.sqrt(252) print(f"日波动率: {daily_std:.4f}") print(f"年化波动率: {annual_vol:.4f}")这段代码里出现的np.diff、np.std、np.sqrt都是高频操作。注意我用closes[:-1]做分母,而不是closes[1:]——日收益率要对应"今天的价格减去昨天的价格再除以昨天的价格",这里的对齐思维和4.4节讲的一脉相承。
处理真实数据时,数据里经常混着NaN(缺失值)。如果你直接对含NaN的数组调np.mean、np.std,结果会是NaN。此时用np.nanmean、np.nanstd这类忽略NaN的版本:
data_with_nan = np.array([1.0, np.nan, 3.0, 4.0]) print(np.nanmean(data_with_nan)) # 结果是2.6667np.diff同样有nan的坑,稳妥的替代方案是先手动去掉NaN再计算:clean = data_with_nan[~np.isnan(data_with_nan)]。这个~np.isnan()的布尔索引组合,在数据清洗里出场率极高,建议作为肌肉记忆记下。
5.4 排序、去重和条件筛选的常用组合
除了统计,np.sort、np.unique、np.argsort、np.where也是高频操作。做个简单的组合案例:给成绩数据按降序排序,并取出排名前3的分数和对应位置。
scores = np.array([78, 92, 85, 91, 66, 99, 72]) # 降序排序 sorted_desc = np.sort(scores)[::-1] # np.sort默认升序,[::-1]翻转 # 对应位置的排名(从小到大,序号对应原数组位置) order = np.argsort(scores) # [4, 6, 0, 2, 3, 1, 5] top3_scores = scores[order[-3:]] # 最大的3个分数np.argsort返回的是索引,妙用在于可以保持住"原始位置"信息。比如你有销售数据,用argsort能知道"销量最高的那几个门店是几号",而不仅仅是"最高销量是多少"。这类场景在数据分析和报表里到处都是。
6. 从numpy到生态联动:pandas、scipy、matplotlib和版本管理的教训
最后聊一个经常被忽略、却总能关键时刻卡你一下的话题:numpy不是孤岛。它身后是整个Python科学计算生态——pandas的DataFrame底层存的就是numpy数组,scipy的优化和信号处理函数以numpy数组为接口,matplotlib画图时传进去的数据本质上也是numpy数组。
6.1 numpy在生态中的"地基"地位
把numpy理解为地基,生态里其他库都是建在地基上的房子,这个比喻很贴切。pandas的Series和DataFrame创建时传入的numpy数组,内部存储共用同一块数据;df.values返回的底层数据就是ndarray。这意味着:
import pandas as pd import numpy as np df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]}) arr = df.values # 这是一个numpy数组,shape为(3, 2) arr[0, 0] = 100 # 修改会不会影响原df?要看情形,但很可能影响所以,如果你已经会操作numpy数组,相当于拿到了生态里的通用语言。pandas的窗口函数、分组统计接口可以帮你省掉大量手写循环的麻烦,但对数据做切片、筛选、按位判断等底层操作时,你脑子里跑的往往还是ndarray的那套规则。学过numpy再来学pandas,曲线会平缓很多。
6.2 numpy、scipy、pandas、matplotlib之间的版本关系
这些包彼此之间有版本依赖关系,典型的是numpy版本太老或太新,导致pandas或scipy报错。常见报错长这样:
ValueError: numpy.ndarray size changed, may indicate binary incompatibility这个报错翻译成人话:你当前环境的numpy和某个包(通常是pandas、scikit-learn等)的二进制编译版本对不上。比如某个包是用numpy 1.24的头文件编译的,现在环境里numpy升级到了1.26,两者内部结构定义不一致,于是报错。
处理版本冲突的三条经验:
- 尽量使用conda环境管理版本。conda的依赖解析器会默认安装兼容的numpy版本,自动帮你避雷。独立环境用
environment.yml锁定版本组合最靠谱。 - 尽量避免pip和conda混用。前面1.3节已经说过,包管理器打架是版本冲突的第一来源。我是吃过足够多的亏才彻底改成"一个环境只走一个包管理器"的。
- 升级某个包时,把numpy、scipy、pandas、matplotlib成套升级或成套不升。这几个库的版本兼容矩阵官方会测试,但要靠你自己跟进。稳妥的策略是全用conda,升级时
conda update numpy pandas scipy matplotlib一起升。
6.3 用项目环境锁版本,而不是靠运气
多年跟版本问题搏斗下来,我现在固定采用这套流程:
- 每个项目建独立环境(conda或venv都行,选一个就好)。
- 装包时顺手把版本写进配置文件,conda用environment.yml,pip用requirements.txt。
- 升依赖前先确认哪些脚本可能受影响——尤其是numpy这种地基级别的库,升级一次可能引起连锁反应。
- 出问题先看报错,优先找"版本"关键字,再检查环境。
这种流程化的处理方式,把"版本不匹配"这个不确定的坑,变成了可控的风险管理。踩过的坑越多越明白:很多bug不是代码逻辑的问题,而是环境的问题。环境管好了,代码的自然bug反而更好定位和修复。
6.4 一个具体的跨库联动示例
最后给一个把numpy、pandas和matplotlib串起来的示例,看一眼生态协作的日常画风:
import numpy as np import pandas as pd import matplotlib.pyplot as plt # 用numpy生成模拟数据 dates = np.arange('2024-01-01', '2024-01-31', dtype='datetime64[D]') values = np.random.randn(len(dates)).cumsum() # 用pandas承载数据并做滚动平均 s = pd.Series(values, index=dates) s_ma5 = s.rolling(5).mean() # 用matplotlib画出原始序列和均线 plt.figure(figsize=(10, 4)) plt.plot(s.index, s.values, label='原始数据') plt.plot(s_ma5.index, s_ma5.values, label='5日均线') plt.legend() plt.show()这段代码里有几个值得注意的点:np.arange配合datetime64[D]生成日期序列,这是numpy内置的日期类型,可直接被pandas当作索引;s.values从pandas又拿回numpy数组,整个过程数据没有复制,底层的连续内存一直是同一块。写出这样的代码,意味着你对整个科学计算生态的核心数据流转已经心里有数了。
我自己平时写代码的习惯是:数据一进来先转成numpy数组做快速探索,看shape、看dtype、看取值范围,心里有数之后再决定用pandas的哪些高级接口。探索性的小分析用numpy直接怼,结构化的表格式分析交给pandas,画图交给matplotlib。这个分工到现在用着都很顺手。如果你也在从零接触这个生态,从numpy入手是成本最低的起点——它足够底层,能让你看清数据是怎么被存储和搬运的;又足够出活,随手就能完成一批统计分析。把这套基础打扎实,后面学什么都快。