今天是《100天精通Python》系列的第 50 天,主题是 numpy 进阶:数组操作和常用方法。
前面 49 天我们从 Python 基础语法、流程控制、函数、文件读写、面向对象一路走过来,已经具备写 Python 脚本的能力。但从今天起要进入真正的数据处理阶段。无论是数据分析、机器学习、量化回测,还是图像处理,底层几乎都是 numpy 的多维数组。如果你打算继续学 pandas、scikit-learn、OpenCV,numpy 是绕不开的底子。
很多同学学 numpy 只停留在“用 np.array() 创建一个数组、打印 shape”这一步,真正要处理数据时还是用 for 循环逐个元素计算,代码长、速度慢,还容易在索引上出错。这篇文章把这些问题补齐:数组创建、形状操作、拼接拆分、统计排序、布尔索引、广播机制,以及最重要的“向量化批量任务”到底怎么用。
全文按“环境准备 -> 核心操作 -> 常用方法 -> 批量任务 -> 性能观察 -> 问题排查”的顺序展开,每个知识点都带可运行的代码。建议打开 Jupyter Notebook 或 VS Code 跟着敲一遍,遇到报错先看第 11 节的排查表。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目类型 | Python 科学计算第三方库 |
| 核心对象 | ndarray 多维数组 |
| 主要功能 | 数组创建、形状变换、拼接拆分、统计聚合、排序去重、条件筛选、广播运算 |
| 安装方式 | pip install numpy |
| 运行环境 | Windows / Linux / macOS 均可,纯 CPU 可运行,无独立显卡要求 |
| 推荐 Python 版本 | Python 3.9 及以上,具体以 numpy 当前版本官方支持范围为准 |
| 是否支持批量任务 | 支持,向量化运算可一次处理整批数据 |
| 是否提供 API | 提供完整 Python 函数式 API,支持 np.save / np.load 数据持久化 |
| 典型应用 | 数据预处理、科学计算、机器学习特征工程、图像像素运算 |
| 学习门槛 | 需要掌握 Python 基础语法,了解列表和循环即可开始 |
2. 适用场景与使用边界
numpy 适合三类人:第一类是刚入门数据处理,想用 Python 替代 Excel 做批量计算的开发者;第二类是准备学机器学习,需要处理特征矩阵和标签向量的同学;第三类是写自动化脚本时经常面对大量数字、日志、传感器数据的工程师。
先说能解决的问题。numpy 最核心的价值是“批量”:一个数组里几千万元素,求和、求均值、筛选、归一化,一行代码完成,不用写循环。配合 pandas 做表格处理、matplotlib 做可视化、scikit-learn 做建模,numpy 就是所有这些工具的数据底座。
再看边界。numpy 不是万能的,下面这几种情况不建议硬用:
- 处理带列名、带多种类型的表格数据,优先选 pandas,而不是 numpy 二维数组。
- 需要频繁从磁盘读写结构化数据,优先用数据库或 Parquet、CSV 等格式,不要只用 np.save。
- 数据量极大且需要 GPU 加速,numpy 默认跑 CPU,可以考虑 cupy,但那是另一个技术栈。
- 复杂的字符串处理、正则匹配,用 Python 标准库和 re 模块更顺手。
另外提醒一点:numpy 处理的数据来源要合规,尤其是爬虫抓来的数据、用户隐私数据、有版权的内容,在本地分析、二次加工和对外发布前,必须确认授权范围。这是工程习惯,不是额外负担。
3. 环境准备与 numpy 安装
numpy 是第三方库,先确认 Python 环境,再安装。
打开终端或命令行,检查 Python 和 pip:
python --version pip --version如果 Python 已经装好,直接安装 numpy:
pip install numpy国内网络环境慢的话,可以指定清华镜像源:
pip install numpy -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后,进入 Python 环境验证:
import numpy as np print(np.__version__)能打印出版本号就说明安装成功。比如输出 1.26.4 或 2.1.0,不同版本 API 略有差异,但下面代码在主流版本上都能跑。
如果你同时管理多个项目,建议把 numpy 装进虚拟环境,避免和系统环境互相干扰:
python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate pip install numpy还有一个常见问题:如果你用的是刚发布的新版本 Python,而 numpy 还没有对应的预编译包,pip install numpy可能会触发源码编译,报错里会出现 “error: failed to build 'numpy' when getting requirements to build wheel”。后面第 11 节会专门讲怎么处理。稳妥的做法是用当前稳定版 Python 配最新版 numpy。
4. 数组创建与基础属性
先从一个最简单的数组开始。
import numpy as np arr1 = np.array([1, 2, 3, 4]) print(type(arr1)) print(arr1.shape) print(arr1.dtype) print(arr1.ndim) print(arr1.size)输出含义:
type(arr1)是<class 'numpy.ndarray'>,也就是 numpy 的数组对象。shape是形状,一维数组返回(4,)。dtype是元素类型,这里是int64或int32,取决于平台。ndim是维度数,一维数组是 1。size是元素总个数,这里是 4。
创建二维数组也很直观:
arr2 = np.array([[1, 2, 3], [4, 5, 6]]) print(arr2.shape) # (2, 3) print(arr2.ndim) # 2除了从列表创建,numpy 提供了一批内置的创建函数。
# 全零、全一、单位矩阵 zeros = np.zeros((2, 3)) ones = np.ones((3, 2)) eye = np.eye(3) # 等差数组 arange_arr = np.arange(0, 10, 2) # [0 2 4 6 8] linspace_arr = np.linspace(0, 1, 5) # [0. 0.25 0.5 0.75 1. ] # 随机数组 rng = np.random.default_rng(42) rand_arr = rng.random((2, 3)) # 0-1 均匀分布 rand_int = rng.integers(0, 100, (3, 3)) # 0-99 整数这里注意np.arange和np.linspace的区别:arange指定步长,linspace指定个数。linspace(0, 1, 5)会在 0 到 1 之间均匀生成 5 个数,做坐标轴、时间序列时非常常用。
写代码时建议用np.random.default_rng(42)而不是老的np.random.rand(),前者可复现性更好,也更符合新版本推荐写法。
5. 数组形状操作:reshape、flatten、转置
维度调整是 numpy 使用频率最高的操作之一。
5.1 reshape 重塑形状
a = np.arange(12) print(a) b = a.reshape(3, 4) print(b) c = a.reshape(2, 2, 3) print(c)reshape不会改变数据本身,只是把数组从一种形状映射到另一种形状,前提是元素总数一致。12可以变成(3, 4)、(2, 2, 3)、(4, 3),但不能变成(5, 2),因为元素个数对不上。
不确定某个维度大小时,用-1让 numpy 自动推断:
d = a.reshape(-1, 4) print(d.shape) # (3, 4)这里-1表示“剩下的维度自己算”,对快速变形很有用。
5.2 ravel 与 flatten 展平
m = np.arange(6).reshape(2, 3) print(m) print(m.ravel()) # 展平为一维 print(m.flatten()) # 展平为一维两个方法都返回一维数组,区别在于ravel在可能的情况下返回原数组的视图,修改它可能影响原数组;flatten总是返回一份拷贝,改它不会影响原数组。对初学者,拿不准时就用flatten()。
5.3 转置
m = np.array([[1, 2, 3], [4, 5, 6]]) print(m.T) # 快速转置 print(m.transpose(1, 0)) # 等价写法二维数组转置就是行列互换。做矩阵运算、特征处理时经常用到。reshape和transpose的区别一定要搞清楚:reshape只改形状不换数据顺序,transpose会改变数据在维度的排列方向,两者结果有时看起来一样,语义完全不同。
6. 数组拼接、拆分与元素增删
实际处理数据时,经常要把多个数组合并,或者把一个数组按规则切开。
6.1 拼接
a = np.array([[1, 2], [3, 4]]) b = np.array([[5, 6], [7, 8]]) # 按行拼接,上下叠 print(np.concatenate([a, b], axis=0)) # 按列拼接,左右并 print(np.concatenate([a, b], axis=1)) # 上下堆叠 print(np.vstack([a, b])) # 左右堆叠 print(np.hstack([a, b])) # 新增维度堆叠 print(np.stack([a, b], axis=0))concatenate是通用拼接函数,vstack和hstack是垂直/水平堆叠的快捷方式。stack和前面几个不同,它会增加一个维度,把两个(2, 2)数组变成(2, 2, 2)或(2, 2, 2),常用于批量给模型准备输入数据。
6.2 拆分
arr = np.arange(10) part1, part2, part3 = np.split(arr, [2, 7]) print(part1) # [0 1] print(part2) # [2 3 4 5 6] print(part3) # [7 8 9]np.split(arr, [2, 7])表示在索引 2 和索引 7 处切开,得到三段。如果每个子数组长度不要求相等,用np.array_split:
parts = np.array_split(np.arange(10), 3) for p in parts: print(p)array_split适合把数据平均分成 N 份,比如 10 条数据分 3 份,结果是 4、3、3。
6.3 增删元素
arr = np.array([1, 2, 3]) print(np.append(arr, [4, 5])) # [1 2 3 4 5] print(np.insert(arr, 1, 99)) # [1 99 2 3] print(np.delete(arr, [0, 2])) # [2]注意:np.append、np.insert、np.delete都不会修改原数组,而是返回一个新数组。如果在一个大循环里反复append,性能和内存都很差,正确做法是先收集到列表,最后一次性转成 numpy 数组。
7. 统计、排序与去重等常用方法
这一节是数据处理最常用的工具箱。
7.1 聚合统计
arr = np.array([[1, 2, 3], [4, 5, 6]]) print(arr.sum()) # 21 print(arr.sum(axis=0)) # [5 7 9] print(arr.sum(axis=1)) # [6 15] print(arr.mean()) # 3.5 print(arr.std()) # 标准差 print(arr.var()) # 方差 print(arr.min()) # 1 print(arr.max()) # 6 print(arr.argmin()) # 0,最小值索引 print(arr.argmax()) # 5,最大值索引这里最容易搞混的是axis。记住一条规则:axis=0表示沿着第 0 维方向聚合,相当于把每一列“压扁”;axis=1表示沿着第 1 维方向聚合,相当于把每一行“压扁”。
对形状(2, 3)的数组:
sum(axis=0)结果形状是(3,),对应每列的和。sum(axis=1)结果形状是(2,),对应每行的和。
不确定的时候,先在小数组上打印结果对比一下。
7.2 排序
scores = np.array([88, 65, 92, 73, 59]) print(np.sort(scores)) # 升序 [59 65 73 88 92] print(np.argsort(scores)) # 排序后原索引 [4 1 3 0 2]np.sort返回排序后的数组,np.argsort返回排序后的索引位置。argsort在需要“排名”或“按另一列顺序重排”时很有用。
二维数组排序要指定 axis:
m = np.array([[3, 1, 2], [6, 5, 4]]) print(np.sort(m, axis=1)) # 每行排序7.3 去重
tags = np.array(["a", "b", "a", "c", "b", "a"]) values, counts = np.unique(tags, return_counts=True) print(values) # ['a' 'b' 'c'] print(counts) # [3 2 1]return_counts=True可以在去重的同时返回每个值的出现次数,做类别统计非常方便,比手动写循环快得多。
7.4 其他常用方法
# 累加 print(np.cumsum(np.array([1, 2, 3, 4]))) # [1 3 6 10] # 裁剪,把数据限制在 [low, high] 区间 print(np.clip(np.array([1, 5, 9]), 2, 7)) # [2 5 7] # 四舍五入 print(np.round(np.array([1.234, 5.678]), 2)) # 是否存在满足条件 / 是否全部满足 print(np.any(np.array([1, 2, 3]) > 2)) print(np.all(np.array([1, 2, 3]) > 0))np.clip在归一化、处理异常值时很好用,比如把用户评分限制在 1 到 5 之间,一行搞定。
8. 布尔索引、条件筛选与 np.where
Python 列表做筛选一般要写循环,numpy 可以直接用布尔数组做索引,这是 numpy 最有吸引力的特性之一。
arr = np.array([10, 25, 30, 45, 60]) mask = arr > 30 print(mask) # [False False False True True] print(arr[mask]) # [45 60] # 多条件组合 print(arr[(arr > 20) & (arr < 50)]) # [25 30 45] # 把偶数元素改成 -1 arr[arr % 2 == 0] = -1 print(arr) # [-1 25 -1 45 -1]几个要点:
- 布尔索引返回的是原数组的拷贝,不是视图,修改结果不会影响原数组。
- 多条件组合必须用
&、|、~,并且每个条件都要加括号,不能用 Python 的and、or。 - 布尔索引的速度非常快,因为它只在 C 层做了一次遍历。
条件判断后需要替换值时,用np.where:
scores = np.array([55, 78, 92, 43, 88]) result = np.where(scores >= 60, "pass", "fail") print(result) # ['fail' 'pass' 'pass' 'fail' 'pass']np.where(条件, 满足时取值, 不满足时取值)是批量处理里出现频率极高的函数。比如把缺失值填 0、把负值归零、给数据打分,都可以用 where 完成。
9. 广播机制与向量化批量任务
广播是 numpy 高阶操作的核心,也是新手最容易报错的点。
广播的规则可以简化成三条:
- 两个数组比较维度时,从最后一个维度开始往前比。
- 每个维度要么相等,要么其中一个是 1。
- 如果两个维度都不相等且都不是 1,就无法广播,报 ValueError。
最常见的场景是“每行减去该行的均值”,也就是按行或者按列做标准化。
data = np.random.default_rng(42).random((5, 3)) print("原始数据:") print(data) mean = data.mean(axis=0) std = data.std(axis=0) print("每列均值:", mean) print("每列标准差:", std) # 一行完成标准化:形状 (5,3) 与 (3,) 广播 normalized = (data - mean) / std print("标准化后:") print(normalized) # 验证:每列均值接近 0,标准差接近 1 print(normalized.mean(axis=0)) print(normalized.std(axis=0))这里data的形状是(5, 3),mean的形状是(3,)。numpy 会把(3,)自动补成(1, 3),再沿着第 0 维扩展到(5, 3),于是每一行都减去了同一列均值。这个过程就是广播。
再看一个批量归一化的例子。假设你有 10000 个样本,每个样本 20 个特征,需要对所有样本做标准化:
samples = np.random.default_rng(0).random((10000, 20)) mean = samples.mean(axis=0) std = samples.std(axis=0) # 一次计算所有样本,不需要 for 循环 samples_norm = (samples - mean) / std print(samples_norm.shape)这就是 numpy 处理批量任务的典型写法:先分析数据形状,再用广播去掉循环。无论是 100 条还是 10000 条数据,代码是一样的,计算都在 C 层完成。
10. 性能观察:向量化比循环快多少
很多同学知道 numpy 快,但不知道快多少。用 Python 自带的timeit可以直接对比。
import timeit import numpy as np n = 1_000_000 rng = np.random.default_rng(0) arr = rng.random(n) def loop_sum(): total = 0 for x in arr: total += x return total def numpy_sum(): return arr.sum() print("Python 循环耗时:", timeit.timeit(loop_sum, number=5)) print("numpy 耗时:", timeit.timeit(numpy_sum, number=5))在自己机器上跑一下,大概率会看到 numpy 方案比纯 Python 循环快一个数量级以上。数据量越大、运算越复杂,差距越明显。
原因很简单:Python 循环每处理一个元素都要做一次类型解释和边界检查,而 numpy 把数据存在连续内存里,底层调用的是编译好的 C/Fortran 代码,一次处理一整块数据。所以写 numpy 的正确思路是“从单元素操作切换到整数组操作”,尽量少写 for 循环。
性能优化时的几个观察点:
- 数据量大时,优先检查是否用了向量化写法,而不是急着上多线程。
reshape默认返回视图,不复制数据,内存开销小;flatten会复制,大数组慎用。- 反复在循环里拼接数组会造成大量内存拷贝,先收集再一次性转数组。
- 如果机器内存紧张,处理超大数组时尽量用
dtype更紧凑的类型,比如float32代替float64。
11. 常见问题与排查方法
下面是 numpy 学习和使用中最高频的一批问题。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| pip install numpy 报 error: failed to build 'numpy' when getting requirements to build wheel | 网络差、pip 版本旧、或当前 Python 版本没有对应预编译包,触发了源码编译 | 查看完整报错,确认是否在编译 | 先pip install --upgrade pip,再用镜像源安装;优先使用稳定版 Python 搭配新版 numpy |
| import numpy 报 ModuleNotFoundError | numpy 未安装,或安装到了另一个 Python 环境 | 运行pip list查看环境 | 在虚拟环境里执行pip install numpy,确认 activate 了正确环境 |
| 广播时报 ValueError: operands could not be broadcast together | 两个数组形状不兼容 | 打印两个数组的 shape | 用 reshape 把需要广播的一方补成 1 的维度 |
arr.sum()和arr.sum(axis=1)结果不同 | axis 理解不对 | 在 2x3 小数组上打印两种结果 | 记住 axis=0 压扁列,axis=1 压扁行 |
| 修改 reshape 后的数组,原数组也跟着变了 | reshape 返回的是视图,共享内存 | 用np.shares_memory(a, b)检查 | 需要独立数据时用copy() |
| 整数数组做除法结果全变成 0 | 整数数组不支持浮点除法结果,自动截断 | 打印 dtype 确认 | 先arr.astype(float)再运算 |
| 数组元素很多,print 显示不完整 | numpy 默认省略显示 | 打印 shape 确认 | 用np.set_printoptions(threshold=sys.maxsize)或切片查看 |
| 使用老版本 numpy 代码报 AttributeError | numpy 2.x 移除了一些旧 API,如 np.float_ | 查看报错中的属性名 | 升级代码写法,或按项目要求锁定 numpy 1.x 版本 |
如果安装时遇到编译报错,最直接的解决方案是换用预编译 wheel。Windows 用户建议使用官方 Python 安装包,不要使用来源不明的 Python 发行版;Linux 用户可以通过pip install --only-binary :all: numpy强制只用二进制包安装。
12. 最佳实践与下一步学习
最后给出几条工程化建议。
第一,操作数组前先确认 shape。大约一半的 numpy 报错都是形状问题。养成习惯:拿到数据先print(data.shape),再做计算,能省大量排查时间。
第二,把“对单个元素思考”改成“对整个数组思考”。遇到循环先停一下,问自己:这个操作能不能用 numpy 的一次函数调用完成?能,就写向量化版本;不能,再考虑循环。
第三,保留最小可运行示例。工作中遇到 numpy 问题,先用几行代码把问题复现出来,再贴给同事或搜索引擎,这样定位更快。
第四,批量任务一定要有日志和验证步骤。比如批量归一化之后,检查每一列均值是否接近 0、标准差是否接近 1;批量筛选之后,检查筛选前后总数是否对得上。
第五,随机实验要固定种子。用np.random.default_rng(42)而不是每次随机,结果才能复现,写文章、做报告、调参都更可靠。
第六,中间结果及时落盘。需要多次使用的数组,用np.save("data.npy", arr)保存,下次np.load("data.npy")直接读,比每次重新计算快得多,也避免内存占用过高。
今天的内容先到这里。第 51 天建议做一个练习:生成 1000 名学生的成绩表,用今天学的聚合、筛选、排序、归一化方法完成一轮完整的数据处理,再把结果用np.save保存下来。这份练习做完,再进入 pandas 会轻松很多。建议收藏这篇文章,写代码时随时回来查 API。