
1. 从列表到ndarrayNumPy到底解决了什么问题我在带新人的时候有个很典型的现象大家学Python学了个基础就会处理列表然后拿列表去做数据分析跑几个循环一算发现数据量一大就卡得不行。这时候我一般会反问一句“你知道Python列表里装的是什么吗”Python的列表本质上是对象的引用数组每个元素都是一个完整的Python对象包括类型信息、引用计数这些额外的包袱。如果你写一个for循环把一万个数加起来解释器要反复做类型检查、对象创建和销毁速度自然上不去。而NumPy的做法完全不同它用C语言实现了多维数组容器ndarray数组中的所有元素在内存中是连续排列的、类型一致的底层直接调用了经过高度优化的BLAS、LAPACK等数值计算库。所以同样的求和处理NumPy的向量化写法可以比Python原生for循环快几十倍甚至上百倍。这篇文章适合谁看刚学完Python基础语法、想进入数据分析和科学计算领域的人以及已经接触过NumPy但一直停留在“抄函数、背API”阶段想知道背后原理的人。我会从环境配置讲到核心数据结构再从广播机制讲到实际场景最后把我踩过的坑一并交代清楚。全程保姆级照着做就行。2. 环境准备装Python、装NumPy、配编辑器一次说清热词里有一堆“python安装”“numpy安装”“numpy 2.2.5”“numpy 1.19.5 安装包下载”之类的搜索词说明很多新人卡在了第一步。这一章把环境问题一次性讲透。2.1 Python环境官网装还是Anaconda装如果你只是单纯想学Python和NumPy不想折腾环境我建议装Anaconda。它自带Python解释器和一堆科学计算包包括NumPy、pandas、matplotlib、scikit-learn装完直接能用。缺点是包体积大启动稍慢。适合“我就要快点跑起来”的人。如果你更倾向干净的环境就去Python官网下载对应系统的安装包。Windows下安装时务必勾选“Add Python to PATH”不然后面命令行里输python会提示找不到命令。macOS和Linux一般自带Python 3但版本可能偏老建议还是装新的官方版本。提示Python 2早就不维护了。现在所有的教程、库、工具都以Python 3为准别看到网上的老教程用print hello就照抄那是Python 2的写法。2.2 安装NumPypip、conda和Linux源码编译装好Python之后在命令行里执行这条命令pip install numpy这是最常规的做法。如果你在中国大陆下载速度不理想的话可以临时指定使用清华的PyPI镜像pip install numpy -i https://pypi.tuna.tsinghua.edu.cn/simple要指定版本号就加pip install numpy2.2.5 pip install numpy1.19.5这里多说一句NumPy 2.x已经发布好几年了numpy 2.2.5属于较新的版本。很多老项目用的还是1.x比如1.19.5。如果项目里没特别要求建议装最新的稳定版性能更好新特性也更多。如果有老代码依赖旧版行为再来指定版本。Linux用户还有一种情况需要手动编译安装多见于服务器上无法直接用pip的情况或者想针对当前CPU优化指令集。过程大概是wget https://github.com/numpy/numpy/releases/download/v2.2.5/numpy-2.2.5.tar.gz tar xzf numpy-2.2.5.tar.gz cd numpy-2.2.5 pip install .但说实话日常使用没必要走源码编译pip install numpy就足够了。源码编译很容易遇到缺依赖、编译器版本不匹配的问题除非你是做二次开发或者对性能有极端要求否则别自找麻烦。2.3 编辑器与解释器配置PyCharm和VS Code谁更省心环境变量配好之后还要让编辑器知道你用的是哪个Python解释器。PyCharm的做法打开Settings - Project - Python Interpreter点齿轮图标选Add Interpreter选择System Python或已有的conda环境找到python.exe就完成了。建一个Python文件输入import numpy as np如果能正常跑说明解释器配对了。VS Code的做法装Python扩展微软官方那个按快捷键CtrlShiftP输入“Python: Select Interpreter”选择对应的Python版本。很多报ModuleNotFoundError: No module named numpy的朋友其实代码文件用的解释器和装包的解释器不是同一个——终端目前用的是conda的pythonVS Code右下角选的却是系统自带python这种错位是新手最容易遇到的事。验证安装成功的标准代码就这一行import numpy as np print(np.__version__)能打印出版本号说明一切正常。看到类似UserWarning: failed to initialize numpy: no module named numpy这种报错第一反应就是解释器选错了或者包根本没装进当前环境。3. 玩转ndarray创建、索引、切片、形状这些操作是地基你后面学pandas、学scikit-learn绕来绕去都是在跟ndarray打交道。pandas的DataFrame底层存储靠ndarrayscikit-learn接受的特征输入本质也是ndarray。这块掌握好了后面学什么都快。3.1 五种创建ndarray的方式日常开发中创建数组基本就靠这五种import numpy as np # 方式1从列表或元组创建 a np.array([1, 2, 3, 4, 5]) b np.array([[1, 2, 3], [4, 5, 6]]) # 方式2指定形状的全0、全1数组 zeros np.zeros((3, 4)) ones np.ones((2, 3)) # 方式3等差数列注意arange右端点是开区间 c np.arange(0, 10, 2) # [0 2 4 6 8] d np.arange(10) # [0 1 2 ... 9] # 方式4等间隔取数linspace闭区间常用于画图 x np.linspace(0, 2*np.pi, 100) # 方式5随机数数组 random_arr np.random.rand(3, 3)np.arange和np.linspace是我用得最频繁的两个。arange(0, 10, 2)是从0开始小于10结束步长2。linspace(0, 2*np.pi, 100)表示在0到2π之间均匀取100个数注意两端的值都包含。画函数曲线时linspace比arange更常用因为不需要手动算步长直接告诉它取几个点就行。np.random.rand、randn、randint系列在模拟数据和验证算法时非常好用。比如你要造一个测试用的矩阵直接data np.random.randn(100, 5)就拿到了一个100行5列、服从标准正态分布的数组比手动录入数据方便太多了。3.2 几个必须背下来的数组属性创建数组之后先要能看懂它长什么样arr np.array([[1, 2, 3], [4, 5, 6]]) print(arr.shape) # (2, 3)2行3列 print(arr.ndim) # 2二维 print(arr.size) # 6元素个数 print(arr.dtype) # int64元素类型shape是全世界数据分析师天天都在看的属性。它返回一个元组里面每一维的大小。写代码调试的时候第一件事就是print(arr.shape)确认数据形状是否符合预期。很多报错都和形状不匹配有关。dtype也很关键。默认情况下整数数组是int64浮点数数组是float64。当你的数据量很大时可以显式指定更省内存的类型arr np.array([1, 2, 3], dtypenp.float32)这在深度学习场景下尤其常见尤其当数据规模大的时候float64变成float32能省一半内存速度还有改善。3.3 索引和切片一维和二维的区别一维数组的索引和Python列表几乎一样arr np.array([10, 20, 30, 40, 50]) print(arr[0]) # 10 print(arr[-1]) # 50 print(arr[1:4]) # [20 30 40]二维数组就有意思了用[行, 列]的方式索引matrix np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) print(matrix[1, 2]) # 6第2行第3列 print(matrix[0, :]) # [1 2 3]第1行所有列 print(matrix[:, 1]) # [2 5 8]所有行的第2列 print(matrix[1:, :2]) # 第2行到最后前两列这里:表示取该方向上的全部。理解[:, 1]这种写法以后你会发现它比pandas里的loc和iloc更底层也更灵活。很多算法题中处理矩阵的操作本质都是先切片再操作。除了普通切片还有两种索引方式在数据筛选时特别有用。一个是布尔索引arr np.array([1, 2, 3, 4, 5]) mask arr 3 print(mask) # [False False False True True] print(arr[mask]) # [4 5]甚至可以直接一步到位print(arr[arr 3])另一类是花式索引用一个整数数组去取对应位置idx np.array([0, 2, 4]) print(arr[idx]) # [1 3 5]3.4 形状操作reshape、flatten、transposereshape是用的最多的形状调整操作注意它要求新形状的元素总数和原数组一致arr np.arange(12) matrix arr.reshape((3, 4)) print(matrix) # 输出 # [[ 0 1 2 3] # [ 4 5 6 7] # [ 8 9 10 11]]reshape(-1, 1)和reshape(1, -1)这种带-1的写法很实用-1表示“剩下的自动推断”。比如你有个长度100的一维数组要变成100行1列的二维数组直接arr.reshape(-1, 1)就行不用自己算列数。flatten和ravel都是把多维数组展开成一维区别是flatten返回一份拷贝ravel尽量返回视图flat matrix.flatten() raveled matrix.ravel()transpose和T属性都是转置矩阵transposed matrix.T # 3行4列变4行3列这里有个很重要的坑reshape返回的不一定总是拷贝它可能是原数组的一个视图。你修改reshape后的数组原数组有时也会跟着变。如果担心后续操作互相干扰用copy()显式复制一份再操作。matrix_copy matrix.reshape((3, 4)).copy()4. 广播机制和向量化不用for循环代码快上几十倍如果你刷过一些numpy教程一定见过“广播”这个词。我最早看官方文档时被绕晕过后来发现只要抓住三条规则广播就再也不会迷惑你了。4.1 为什么劝你别写for循环很多人刚开始写数值计算第一反应是result np.zeros(100) for i in range(100): result[i] np.sin(i) * 2这个写法在Python里能跑但完全浪费了NumPy的设计初衷。NumPy的每个通用函数ufunc都是向量化实现的底层用C语言循环替代了Python循环。正确的写法是i np.arange(100) result np.sin(i) * 2实测一个100万元素的数组运算for循环写法和向量化写法性能差距通常在20倍以上数据量越大差距越明显。向量化的潜台词是让循环发生在C语言层面Python只负责调度。4.2 广播机制三条规则记牢就行广播解决的问题是两个形状不同的数组做运算时如何自动对齐维度。规则一如果两个数组的维度不同在左侧补1补齐。规则二如果两个数组在某个维度上的长度不一致长度为1的那个会拉伸到和另一个一样长。规则三如果两个数组在某个维度上的长度既不相同、也不存在长度为1的情况就无法广播直接报错。看个最简单的例子arr np.array([[1, 2, 3], [4, 5, 6]]) result arr 100 print(result) # 输出 # [[101 102 103] # [104 105 106]]标量100的形状是()补1后变成(1, 1)两个方向上都能拉伸所以直接给每个元素加100。更有用的例子是行向量加到每一行上row np.array([10, 20, 30]) result arr row print(result) # 输出 # [[11 22 33] # [14 25 36]]row的形状是(3,)补1后是(1, 3)与arr的形状(2, 3)比较第0维上1能拉伸成2第1维上3和3相等所以广播成功。这个场景在实际中极其常见——给矩阵的每一行加一个均值或者做标准化时减去均值、除以标准差。如果换成列向量情况不一样col np.array([[10], [20]]) result arr col # 输出 # [[11 12 13] # [24 25 26]]col的形状是(2, 1)与(2, 3)比较第0维相等第1维1能拉伸成3广播成功。力的乱用会造成不少隐蔽bug。比如arr是(3, 4)你想加一个(4,)的向量没问题但如果你加一个(3,)的向量就会报错因为3和4既不相同也没有1。很多新手就在这里卡住后来学会先print(shape)心里就不慌了。广播机制在图片处理中也非常典型。一张RGB图片是(height, width, 3)的数组想调整亮度就加一个形状为(3,)的向量广播会自动在空间维度上逐像素完成加法代码里连一个循环都不用写。4.3 通用函数把一个函数应用到每个元素上NumPy提供了一整套数学函数直接作用在数组上默认逐元素计算x np.array([0, np.pi/2, np.pi]) y np.sin(x) # [0. 1. 1.2e-16] z np.exp(np.array([1, 2])) # [2.71828183 7.3890561]像np.add、np.subtract、np.multiply、np.divide、np.sqrt、np.abs、np.power都属于通用函数。和Python内置的math.sin不同numpy的sin接受的是ndarray返回的是ndarray速度还快得多。聚合函数是另一大类它们把整个数组或某一轴上的数据归约成一个值data np.array([[1, 2, 3], [4, 5, 6]]) print(np.sum(data)) # 21全部元素求和 print(np.mean(data, axis0)) # [2.5 3.5 4.5]按列求平均 print(np.max(data, axis1)) # [3 6]按行求最大axis0是沿着行方向移动也就是对每一列操作axis1是沿着列方向移动对每一行操作。这个“axis等于操作方向”的理解各人有各人看法但在我实践中最简单的方式是拿一个小矩阵跑一遍看结果形状就清楚了。例如data.shape是(2, 3)np.sum(data, axis0)结果形状是(3,)说明消掉的是第0维。4.4 一个真实性能对比我在教学中习惯现场跑一个对比直观感受向量化的威力。生成1000万个随机数求它们的正弦和然后取平均import numpy as np import time data np.random.rand(10_000_000) # 向量化 start time.time() result_vec np.mean(np.sin(data)) end time.time() print(end - start) # 我机器上约0.15秒 # 原生for循环 start time.time() total 0 for val in data: total np.sin(val) result_loop total / len(data) end time.time() print(end - start) # 我机器上约6秒以上同样的数学操作性能差距三四十倍。这种差距在真实项目中会直接影响用户体验。我见过有人用for循环处理几百万行数据跑一次要几分钟换成向量化写法后秒级完成优化思路其实就这么简单。5. numpy数据分析和可视化实用组合拳学NumPy不是为了玩概念最终还得落到具体场景。这一章讲几个最常见的用法组合都是热词里反复出现的词numpy和pandas库的使用、python画图横坐标太密集、numpy科学计算、层次聚类python。5.1 数据清洗中高频出现的几个numpy操作拿到原始数据第一步往往是清洗。pandas负责表格化的数据操作但底层很多逻辑还是靠numpy。下面几个场景你们迟早会遇到场景一把NaN替换成固定值或均值data np.array([[1.0, np.nan, 3.0], [4.0, 5.0, np.nan]]) clean_data np.nan_to_num(data, nan0.0) # 把NaN变0有时候用均值填充col_mean np.nanmean(data, axis0) # 按列求均值自动忽略NaN nan_mask np.isnan(data) # 把每列的NaN替换为该列均值 for i in range(data.shape[1]): data[nan_mask[:, i], i] col_mean[i]例子属于典型先找位置再赋值很多数据清洗的底层逻辑都长这样。场景二截断异常值clip函数scores np.array([10, 200, 35, -5, 88, 320]) scores_clipped np.clip(scores, 0, 100) print(scores_clipped) # [10 100 35 0 88 100]clip把超出上下界的值直接拉回边界在特征处理去极值时相当常用。场景三where做条件替换在NumPy里实现if-else风格的逐元素选择用np.where比循环高效得多scores np.array([55, 80, 42, 90]) label np.where(scores 60, pass, fail) print(label) # [fail pass fail pass]5.2 统计特征计算数据分析最基础的就是算均值和标准差numpy一条命令的事x np.random.randn(1000) mean_val np.mean(x) std_val np.std(x) var_val np.var(x) min_val np.min(x) max_val np.max(x) median_val np.median(x)特别提醒一下NumPy的np.std默认计算总体标准差叫population standard deviation分母是n。pandas的DataFrame.std()默认计算样本标准差分母是n-1。在使用时一定注意两者差异否则统计结果会有偏差尤其在样本量较小的场景。5.3 线性代数解方程、矩阵乘法、特征值NumPy的np.linalg模块解决线性代数问题非常方便。比如解线性方程组3x 2y 12 x y 5A np.array([[3, 2], [1, 1]]) b np.array([12, 5]) solution np.linalg.solve(A, b) print(solution) # [2. 3.]即x2, y3矩阵乘法用运算符或np.dotA np.array([[1, 2], [3, 4]]) B np.array([[5, 6], [7, 8]]) C A B print(C) # [[19 22] # [43 50]]特征值和特征向量在机器学习的PCA、层次聚类、谱聚类等算法中很常见eigenvalues, eigenvectors np.linalg.eig(A)5.4 画图时横坐标太密集配合matplotlib的解决方案热词里“python画图横坐标太密集”这个问题特别经典。用matplotlib画图坐标轴上有100个点的标签全部显示就会挤成一团import numpy as np import matplotlib.pyplot as plt x np.linspace(0, 10, 100) y np.sin(x) plt.plot(x, y) plt.xticks(np.arange(0, 11, 2)) # 每2个单位显示一个刻度 plt.show()用np.arange(0, 11, 2)生成稀疏的刻度位置比不设置清晰多了。更进一步还能旋转标签plt.xticks(rotation45)再配合plt.tight_layout()自动调整布局基本上能解决九成“坐标轴标签叠在一起”的问题。np.linspace在这里还有一个用途生成密集的x坐标让曲线更平滑。你画sin(x)如果只用10个点折线会很明显用np.linspace(0, 2*np.pi, 500)取500个点就很圆滑了。6. 踩坑实录视图、dtype、版本兼容这些老问题最后聊几个我用NumPy这些年踩过的坑。每一个都在实际项目中出现过解决了以后你会觉得NumPy瞬间亲切了很多。6.1 视图与副本改了切片为什么原数组也变了先看这段代码arr np.array([1, 2, 3, 4, 5]) sub arr[:3] sub[0] 999 print(arr) # [999 2 3 4 5]直接切片出来的sub是原数组的视图没有复制数据。修改sub会直接改到arr。这在Python列表里是不可想象的但NumPy为了性能就是这么设计的——避免不必要的大数组拷贝。如果你希望修改切片不影响原数组要显式复制sub arr[:3].copy()带布尔索引和花式索引的结果通常不是视图而是新数组。这一点规则比较微妙普遍经验是涉及布尔掩码和花式索引时放心修改直接用冒号和整数做切片时留心是不是视图。调试时如果发现原数组被“莫名”改了八成是这个原因。6.2 dtype的坑整数除法和溢出问题两个整数数组做除法结果是什么类型很多人以为是浮点型但NumPy的/运算符在整数数组上也返回浮点数。真正容易出错的是//地板除a np.array([5, 6, 7]) b np.array([2, 2, 2]) print(a / b) # [2.5 3. 3.5] print(a // b) # [2 3 3]另一个经典坑是整数溢出arr np.array([100], dtypenp.int8) print(arr 30) # -126而不是130int8能表示的范围是-128到12710030溢出了结果绕回负值。比如你读取某张老格式图像数据时像素值用uint8存储做加法就很容易溢出。解决方法是先转换成更大的类型或浮点类型arr np.array([100], dtypenp.int8).astype(np.int16)任何时候只要涉及运算结果可能会超出原类型范围就先astype转换。6.3 broadcast误伤减法操作把向量广播成了矩阵广播虽然强大但也容易制造隐蔽bug。比如有一个矩阵X想减掉每列的均值写成了X np.random.rand(100, 5) mean_col np.mean(X, axis0) # shape (5,) centered X - mean_col # 正确广播到每一行但如果你想按行去均值写成了X - mean_row其中mean_row的形状是(100,)那广播规则会把(100,)视为(1, 100)然后尝试与(100, 5)对齐在第二个维度上100和5不匹配直接报错。这种报错还算好的最怕某种情况下广播成功了但含义不对结果数据全错但程序不报错。我的习惯是做任何广播前先看一眼两个数组的shape再用一个小的样例数据跑一下确认每个位置的数值符合直觉最后再上全量数据。6.4 版本兼容NumPy 2.x带来的麻烦从NumPy 1.x升级到2.x后部分老代码会报错。比如np.float_、np.int_等别名在2.x中被移除要用np.float64、np.int64代替。np.row_stack、np.col_stack的某些行为也有调整。如果你的项目突然出现AttributeError: module numpy has no attribute float_多半是代码在2.x环境下跑但依赖的是1.x的写法。解决方案有两种代码适配新版本把移除的别名换成新写法。环境锁定旧版本pip install numpy2或者使用1.19.5这类老版本。具体选哪个要看你项目里的其他依赖。比如某些老版本pandas、scikit-learn和新版numpy配合会有兼容性问题这时候锁numpy版本反而是更稳妥的做法。我一般建议团队项目统一在requirements.txt里写清楚版本号避免不同机器环境不一致带来的诡异问题。6.5 性能相关的最后提醒避免隐式拷贝NumPy用起来顺手之后性能隐患也要留意。np.array(list)这种操作如果入参是列表会执行一次数据拷贝对于超大数组会消耗额外内存和时间。np.asarray(list)则不会做无谓拷贝能复用就复用。还有np.concatenate在循环里反复调用会反复分配内存性能很差。如果循环次数很多建议先分配好最终大小的空数组再按索引填充或者在循环用列表收集最后一次性拼接。这些都是数据量上来以后才明显感知到的问题早注意到能省下很多优化时间。我个人的经验是NumPy的入门不需要死记硬背每个函数抓住ndarray这个核心数据结构理解它在内存里的组织方式再掌握广播和索引两把利器后面遇到新函数只需要查文档就行。把每个操作的输出形状、返回的是视图还是拷贝、dtype发生了什么变化这三个问题养成下意识自查的习惯你会比很多人少踩很多坑。