NumPy 科学计算完全指南:从数组创建到广播机制
NumPy 是 Python 数据分析和机器学习的基石------Pandas、SciPy、Matplotlib、scikit-learn 全部构建在 NumPy 之上。本文以知识点解析 + 代码实例 为主线,所有代码输出均为真实运行结果,可直接复制验证。
文章目录
- [NumPy 科学计算完全指南:从数组创建到广播机制](#NumPy 科学计算完全指南:从数组创建到广播机制)
-
- [一、NumPy 简介与安装](#一、NumPy 简介与安装)
-
- [1.1 什么是 NumPy?](#1.1 什么是 NumPy?)
- [1.2 Python 数据分析生态圈](#1.2 Python 数据分析生态圈)
- [1.3 安装与验证](#1.3 安装与验证)
- [二、ndarray:NumPy 的核心对象](#二、ndarray:NumPy 的核心对象)
- 三、生成数列:arange、linspace、logspace
-
- [3.1 arange:按步长生成(左闭右开)](#3.1 arange:按步长生成(左闭右开))
- [3.2 linspace:按个数生成(左右都闭)](#3.2 linspace:按个数生成(左右都闭))
- [3.3 logspace:等比数列](#3.3 logspace:等比数列)
- 四、特殊数组创建
- 五、数组索引与切片
-
- [5.1 一维数组索引](#5.1 一维数组索引)
- [5.2 二维数组索引](#5.2 二维数组索引)
- [5.3 三维数组索引](#5.3 三维数组索引)
- [5.4 布尔索引](#5.4 布尔索引)
- [5.5 花式索引](#5.5 花式索引)
- 六、数组元素操作
-
- [6.1 修改元素](#6.1 修改元素)
- [6.2 np.delete:删除元素](#6.2 np.delete:删除元素)
- [6.3 np.where:条件筛选](#6.3 np.where:条件筛选)
- [6.4 np.unique:去重](#6.4 np.unique:去重)
- 七、数组组合与切割
-
- [7.1 组合](#7.1 组合)
- [7.2 切割](#7.2 切割)
- [八、算术运算与 ufunc](#八、算术运算与 ufunc)
-
- [8.1 元素级运算](#8.1 元素级运算)
- [8.2 ufunc:通用函数](#8.2 ufunc:通用函数)
- [8.3 数组与标量运算](#8.3 数组与标量运算)
- 九、矩阵运算
-
- [9.1 矩阵乘法](#9.1 矩阵乘法)
- [9.2 矩阵求逆](#9.2 矩阵求逆)
- 十、广播机制(Broadcasting)
-
- [10.1 广播规则](#10.1 广播规则)
- [10.2 三种典型情况](#10.2 三种典型情况)
- [10.3 用 np.newaxis 手动扩展维度](#10.3 用 np.newaxis 手动扩展维度)
- 十一、随机模块
-
- [11.1 随机种子](#11.1 随机种子)
- [11.2 三种常用分布](#11.2 三种常用分布)
- 十二、统计函数
-
- [12.1 keepdims:保持维度](#12.1 keepdims:保持维度)
- [12.2 bincount 与 histogram](#12.2 bincount 与 histogram)
- 十三、深拷贝与浅拷贝
- 十四、文件读写
- 十五、多项式函数
- 十六、总结
一、NumPy 简介与安装
1.1 什么是 NumPy?
NumPy(Numerical Python)是 Python 的开源数值计算扩展库,底层用 C 语言实现,核心功能包括:
- N 维数组对象(ndarray):存储单一数据类型的多维数组
- 通用函数(ufunc):对数组每个元素进行运算,速度极快
- 线性代数、傅里叶变换、随机数生成
- 与 C++/Fortran 无缝结合
🔰 为什么不用 Python 原生列表?
Python 列表的本质是一个"指针数组"------每个元素存储的是对象的内存地址 ,而不是值本身。比如
[1, 2, 3]在内存中不是连续的三个整数,而是三个指针,每个指针分别指向一个 Python 整数对象。这样做的好处是灵活(可以混合存储不同类型),但代价是:
- 内存浪费:每个 int 对象本身就要占 28 字节,而 NumPy 的 int64 只要 8 字节
- 缓存不友好:数据散落在内存各处,CPU 缓存命中率低
- 运算慢:两个列表相加需要逐个解引用、类型检查、创建新对象
NumPy 的 ndarray 直接在连续内存块 中存储原始数据(就是纯粹的数字),没有指针间接层。对大规模数值计算,速度可快 10~100 倍。这就是为什么几乎所有 Python 数据科学库(Pandas、scikit-learn、TensorFlow)底层都依赖 NumPy。
1.2 Python 数据分析生态圈
| 库 | 功能 | 依赖关系 |
|---|---|---|
| NumPy | N 维数组、矩阵运算 | 基础库 |
| SciPy | 积分、优化、插值、信号处理 | 依赖 NumPy |
| Pandas | 数据分析、表格操作 | 基于 NumPy |
| Matplotlib | 绘图 | 依赖 NumPy |
| scikit-learn | 机器学习 | 基于 NumPy/SciPy |
1.3 安装与验证
python
import numpy as np
print(np.__version__) # 查看版本
运行输出:
2.4.6
二、ndarray:NumPy 的核心对象
NumPy 最核心的东西就一个------ndarray(N-dimensional array,N维数组)。它类似 R 语言的向量和矩阵,但更强大。
ndarray 有两个基本特征:所有元素必须是相同数据类型 (这保证了内存连续和高效运算),以及支持任意维度(从一维向量到 N 维张量)。这两个特征使得 ndarray 在处理大规模矩阵运算时远比 Python 列表高效。
2.1 创建数组
从列表创建
最直接的创建方式是将 Python 列表传给 np.array()。列表的嵌套层数决定了数组的维度:一层括号是一维,两层是二维,三层是三维。
观察输出可以发现一个重要区别:Python 列表打印时元素之间有逗号 [1, 2, 3],而 NumPy 数组没有逗号 [1 2 3]------这是快速辨认两种类型的方法。
python
import numpy as np
# 一维数组
v = np.array([1, 2, 3, 4, 5])
print(v)
# 二维数组(多个一维数组构成)
m = np.array([[1,2,3,4,5],
[1,2,3,4,5],
[1,2,3,4,5]])
print(m)
# 三维数组(多个二维数组构成)
z = np.array([[ [1,2,3,4,5], [1,2,3,4,5], [1,2,3,4,5] ],
[ [1,2,3,4,5], [1,2,3,4,5], [1,2,3,4,5] ],
[ [1,2,3,4,5], [1,2,3,4,5], [1,2,3,4,5] ]])
运行输出:
一维数组:
[1 2 3 4 5]
type: <class 'numpy.ndarray'>, shape: (5,), ndim: 1, size: 5, dtype: int64
二维数组:
[[1 2 3 4 5]
[1 2 3 4 5]
[1 2 3 4 5]]
type: <class 'numpy.ndarray'>, shape: (3, 5), ndim: 2, size: 15
三维数组 shape=(3, 3, 5), ndim=3, size=45
🔰 类型规则 :ndarray 中所有元素必须是相同类型 。如果混合传入不同类型,NumPy 按优先级自动"向上转型":
str > float > int。例如np.array([1, 2.5, 'hello'])中有字符串,所以全部转为字符串['1' '2.5' 'hello']。这是 ndarray 与 Python 列表最大的区别------列表可以[1, 2.5, 'hello']混合存储,ndarray 不行。这么做的原因是性能:相同类型意味着每个元素占用相同字节数,CPU 可以直接用指针算术定位第 N 个元素,不需要类型检查。
从无到有创建
除了从列表转换,NumPy 还提供了一系列工厂函数直接生成数组。这些函数不需要你先手写数据,而是按规则自动填充------这在初始化模型参数、生成坐标轴、创建测试数据时非常常用。
python
# empty:创建未初始化的数组(内存中的随机值)
print(np.empty((2, 3)))
# arange:等差数列(左闭右开)
print(np.arange(0, 9, 3)) # [0 3 6]
# linspace:等差数列(左右都闭)
print(np.linspace(0, 1, 21))
# logspace:等比数列
print(np.logspace(0, 3, 4, base=10)) # [1, 10, 100, 1000]
运行输出:
empty((2,3)):
[[ 0. nan 0.]
[nan 0. nan]]
arange(0,9,3): [0 3 6]
linspace(0,1,21): [0. 0.05 0.1 ... 0.95 1.]
logspace(0,3,4,base=10): [ 1. 10. 100. 1000.]
💡 empty 的用途 :当你只需要一块"空地"稍后填充数据时,
empty比zeros快------因为它不初始化,直接返回内存里的旧值。但要注意:empty返回的值是不可预测的垃圾值 (看到nan是因为那块内存恰好存的是 NaN),在用于计算前必须手动赋值,否则会得到错误结果。
2.2 数据类型 dtype
Python 原生只有 int、float、str 三种数值类型,而 NumPy 提供了极其精细的类型系统。为什么需要这么多类型?因为不同的类型占用的内存不同 ------处理 100 万个数据时,用 int8(1字节)代替 int64(8字节)可以节省 7 倍内存。在深度学习、图像处理、嵌入式设备中,合理选择数据类型可以显著降低内存消耗和加速计算。
| 类型 | 说明 | 示例 |
|---|---|---|
int8/16/32/64 |
不同长度的有符号整数 | int64 占 8 字节 |
uint8/16/32/64 |
无符号整数(只能≥0) | uint8 范围 0~255 |
float16/32/64 |
不同精度的浮点数 | float64 = Python float |
bool |
布尔 | True/False |
str_ |
定长字符串 | 'S10' 表示长度10 |
complex64/128 |
复数 | 1+2j |
类型转换 astype():
python
arr = np.array([1.6, 2.3, 3.8, 4.1])
print(f"原数组: {arr}, dtype={arr.dtype}")
print(f"astype(int): {arr.astype(int)}") # 截断小数
print(f"astype(str): {arr.astype(str)}")
运行输出:
原数组: [1.6 2.3 3.8 4.1], dtype=float64
astype(int): [1 2 3 4] ← 直接截断小数部分(不是四舍五入!)
astype(str): ['1.6' '2.3' '3.8' '4.1']
🔰 注意 :
astype(int)是截断 不是四舍五入------1.6 变 1,3.8 变 3。这和 C 语言的类型转换行为一致:直接丢弃小数部分。如果要四舍五入,需要先用np.round()再转:np.round(arr).astype(int)会把 1.6→2、3.8→4。这个行为在数据预处理中容易踩坑------比如你想把连续的成绩分转换为整数分,截断会让 89.9 变成 89 而不是 90。
2.3 五大核心属性
理解数组的属性是后续一切操作的基础------shape 告诉你数组"长什么样",ndim 告诉你"几维",size 告诉你"多少个元素",dtype 告诉你"什么类型"。这四个属性在调试代码、检查数据维度是否匹配时最常用。

| 属性 | 含义 | 一维示例 | 二维示例 | 三维示例 |
|---|---|---|---|---|
shape |
形状(各维度大小) | (5,) | (3, 5) | (3, 3, 5) |
ndim |
维度数(轴数/秩) | 1 | 2 | 3 |
size |
元素总个数 | 5 | 15 | 45 |
dtype |
元素数据类型 | int64 | int64 | int64 |
type() |
Python 对象类型 | numpy.ndarray | numpy.ndarray | numpy.ndarray |
🔰 轴(axis)的概念 :NumPy 中每个维度称为一个"轴"(axis),轴的数量称为"秩"(rank)。三维数组有三个轴:第0轴(层)、第1轴(行)、第2轴(列)。从外到内编号。这个概念非常重要------后续所有按维度操作的函数(如
sum(axis=0)、concatenate(axis=1))都是基于这个编号系统的。怎么理解轴? 想象一本有 2 页、每页 4 行 6 列的表格:第 0 轴选"翻到第几页",第 1 轴选"看第几行",第 2 轴选"看第几列"。
axis=0操作就是"跨页"汇总,axis=1操作就是"跨行"汇总。
2.4 维度变换
维度变换是机器学习中极其常用的操作------例如卷积神经网络在全连接层之前需要把多维特征"展平"为一维;图像数据通常是三维的 (H, W, C),批量处理时需要升为四维 (B, H, W, C)。掌握 reshape、ravel、flatten 是进行深度学习数据处理的基本功。
升维:reshape 与 resize
python
v = np.array([1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8])
# 一维变二维,-1表示自动计算
r1 = v.reshape(4, -1)
print(r1)
# 一维变三维
r2 = v.reshape(2, 2, 4)
print(r2)
运行输出:
reshape(4,-1) 升为二维:
[[1 2 3 4]
[5 6 7 8]
[1 2 3 4]
[5 6 7 8]]
reshape(2,2,4) 二维升三维:
[[[1 2 3 4]
[5 6 7 8]]
[[1 2 3 4]
[5 6 7 8]]]
reshape vs resize:
| 方法 | 返回值 | 是否修改原数组 | 用法 |
|---|---|---|---|
reshape() |
返回新数组 | ❌ 不修改 | new = arr.reshape(2,3) |
resize() |
无返回值 | ✅ 直接修改原数组 | arr.resize(2,3) |
💡
-1的妙用 :不确定某维大小时写-1,NumPy 自动计算。16个元素 reshape(4, -1)→ 自动算出 4 列。原理很简单:元素总数 = 各维乘积,已知总数 16 和其中一维 4,另一维必然是 16÷4=4。但只能有一个维度写 -1,否则 NumPy 无法判断。reshape 的限制 :元素总数必须不变。16 个元素可以
reshape(4,4)或reshape(2,8),但不能reshape(3,5)------15≠16 会报错。
降维:ravel 与 flatten
降维是将高维数组"拍平"为一维数组的操作。这在机器学习中非常常用------例如将 28×28 的图像展平为 784 维向量输入到全连接层。NumPy 提供了两个看起来效果一样、但内存行为完全不同的函数。
python
v = np.array([1,2,3,4,5,6,7,8]).reshape(2,2,2)
# 降为一维
print(v.ravel()) # 返回视图
print(v.flatten()) # 返回副本
运行输出:
ravel() 降为一维: [1 2 3 4 5 6 7 8]
flatten() 降为一维: [1 2 3 4 5 6 7 8]
ravel vs flatten:
| 方法 | 返回的是 | 修改返回值会影响原数组吗 |
|---|---|---|
ravel() |
原数组的视图 | ✅ 会影响(共享内存) |
flatten() |
原数组的副本 | ❌ 不影响(独立内存) |
💡 记忆:flatten = flat + copy(拍平并复制),ravel 像拉拉链展开(可能共享数据)。
实际选择建议 :大多数情况下用
ravel()就够了------它更快(不复制数据)。但如果你后续要修改展平后的数组且不想影响原数组,必须用flatten()。记住这个原则:需要独立修改 → flatten,只读不改 → ravel。
转置与压缩
转置(.T)是矩阵运算的基础操作------行变列、列变行。在机器学习中,特征矩阵经常需要在 (samples, features) 和 (features, samples) 之间转置以匹配不同函数的输入要求。
squeeze 则是一个"清理工具"------它去掉所有长度为 1 的维度。这在深度学习框架中特别常见,因为模型输出往往带有冗余的 batch 维度,如 shape (1, 1, 10) 经 squeeze() 变成 (10,),更方便后续处理。
python
# 转置:行变列、列变行
a = np.arange(6).reshape(2, 3)
print(a)
print(a.T) # 或 a.transpose()
# squeeze:去掉长度为1的维度
b = np.array([[[1,2,3]]]) # shape=(1,1,3)
print(b.shape) # (1, 1, 3)
print(b.squeeze().shape) # (3,)
运行输出:
a:
[[0 1 2]
[3 4 5]]
a.T:
[[0 3]
[1 4]
[2 5]]
b.shape: (1, 1, 3)
b.squeeze().shape: (3,)
🔰 squeeze 的用途 :深度学习中模型输出 shape 可能是
(batch_size, 1, num_classes),用 squeeze 去掉中间的 1 维度变成(batch_size, num_classes)。
三、生成数列:arange、linspace、logspace
在数据分析中经常需要生成等差或等比数列------画图时生成 x 轴坐标点、划分区间边界、设置学习率衰减序列等。NumPy 提供了三个函数来满足不同需求。

3.1 arange:按步长生成(左闭右开)
python
print(np.arange(0, 9, 3))
运行输出:
[0 3 6]
类似 Python 的 range(),但返回的是 NumPy 数组。不包含终点 9 ------这是初学者最容易踩的坑:arange(0, 9, 3) 生成 [0, 3, 6] 而不是 [0, 3, 6, 9]。原因和 Python range 一样:左闭右开区间,终点不包含在内。
3.2 linspace:按个数生成(左右都闭)
python
print(np.linspace(0, 1, 21))
运行输出:
[0. 0.05 0.1 0.15 0.2 0.25 0.3 0.35 0.4 0.45 0.5 0.55 0.6 0.65 0.7 0.75 0.8 0.85 0.9 0.95 1. ]
在 0, 1 之间均匀取 21 个点,包含终点 1。步长 = (1-0)/(21-1) = 0.05。
linspace 与 arange 的核心区别是:你指定想要多少个点 ,而不是步长。NumPy 自动计算步长来均匀分布。这在画图时特别有用------np.linspace(0, 2*np.pi, 100) 可以在 0 到 2π 之间取 100 个点来画平滑的正弦曲线。
3.3 logspace:等比数列
python
print(np.logspace(0, 3, 4, base=10))
运行输出:
[ 1. 10. 100. 1000.]
生成 10^0=1, 10^1=10, 10^2=100, 10^3=1000,是以 10 为底的等比数列。
logspace 的参数是指数 ,不是实际值。logspace(0, 3, 4) 的意思是:在 10^0 到 10^3 之间取 4 个等距的指数 值(0, 1, 2, 3),然后计算 10 的这些幂次。这在机器学习调参时非常有用------比如要在 0.001 到 1000 之间搜索正则化参数,直接写 np.logspace(-3, 3, 7) 就能得到 [0.001, 0.01, 0.1, 1, 10, 100, 1000],比手动写方便得多。
| 函数 | 区间 | 控制方式 | 典型场景 |
|---|---|---|---|
arange(start, end, step) |
左闭右开 | 指定步长 | 生成 0,3,6 |
linspace(start, end, num) |
左右都闭 | 指定个数 | 画图取坐标 |
logspace(start, end, num, base) |
左右都闭 | 指定个数 | 生成 10的幂次 |
四、特殊数组创建
在实际开发中,我们经常需要初始化一块特定形状和值的数组------比如神经网络的权重初始化为零、创建单位矩阵做线性代数运算、生成全 1 数组做掩码。NumPy 提供了一组工厂函数来快速完成这些任务。
python
import numpy as np
# 全为0
print(np.zeros(5)) # 一维
print(np.zeros((2,2))) # 二维
# 全为1
print(np.ones(5))
print(np.ones((2,2)))
# 全为指定值
print(np.full((2,2), 2)) # 全部填充2
# 单位矩阵(对角线为1)
print(np.eye(5, 7))
运行输出:
np.zeros(5): [0. 0. 0. 0. 0.]
np.zeros((2,2)):
[[0. 0.]
[0. 0.]]
np.ones(5): [1. 1. 1. 1. 1.]
np.full((2,2), 2):
[[2 2]
[2 2]]
np.eye(5,7):
[[1. 0. 0. 0. 0. 0. 0.]
[0. 1. 0. 0. 0. 0. 0.]
[0. 0. 1. 0. 0. 0. 0.]
[0. 0. 0. 1. 0. 0. 0.]
[0. 0. 0. 0. 1. 0. 0.]]
| 函数 | 作用 | 常用场景 |
|---|---|---|
np.zeros(shape) |
全0数组 | 初始化权重、占位 |
np.ones(shape) |
全1数组 | 掩码、计数 |
np.full(shape, val) |
全指定值 | 自定义填充 |
np.eye(n, m) |
单位矩阵 | 线性代数 |
np.empty(shape) |
未初始化 | 追求速度的场景 |
🔰 注意 :
zeros和ones多维形状必须用元组((2,2))传入,不能写np.zeros(2,2)。原因:np.zeros(5)中的 5 被当作一维形状;如果写np.zeros(2,2),NumPy 会以为第二个 2 是dtype参数而报错。记住:多维形状永远是元组。
五、数组索引与切片
索引和切片是数组操作的核心------数据分析中几乎每一步都涉及"取某些行、选某些列、筛选满足条件的元素"。NumPy 的索引系统比 Python 列表强大得多:除了基本的整数索引和切片,还支持布尔索引(按条件筛选)和花式索引(按位置列表选取)。
5.1 一维数组索引
一维数组的索引与 Python 列表基本一致------用 [index] 取单个元素,用 [start:stop] 切片。但 NumPy 额外支持用列表做索引来选取多个不连续的元素,这是 Python 列表做不到的。
python
array1 = np.arange(1, 9) # [1 2 3 4 5 6 7 8]
# 选取
print(array1[1]) # 单个元素 → 2
print(array1[[1, 3, 5]]) # 不连续多个 → [2 4 6]
print(array1[0:6]) # 切片 → [1 2 3 4 5 6]
# 修改
array1[0] = 10
array1[[1, 3, 5]] = 20 # 批量修改
array1[0:6] = 100 # 切片修改
运行输出:
原数组: [1 2 3 4 5 6 7 8]
array1[0]=10 后: [10 2 3 4 5 6 7 8]
array1[[1,3,5]]=20 后: [10 20 3 20 5 20 7 8]
array1[0:6]=100 后: [100 100 100 100 100 100 7 8]
💡 技巧 :用列表 做索引可以选取不连续的元素,如
array1[[1,3,5]]同时选第2、4、6个。这在数据预处理中非常实用------比如你知道第 0、3、5 列是特征列,其他是标签,就可以用data[:, [0,3,5]]一步选出。
5.2 二维数组索引
二维数组索引是 NumPy 学习的关键里程碑。掌握后,三维及更高维的索引逻辑完全一样------只是多加一个维度而已。
核心规则:逗号分隔行和列 。逗号前是行索引,逗号后是列索引,: 表示"全部"。
python
array1 = np.arange(24).reshape(4, 6)
# 选取某个元素
a = array1[1, 4] # 第2行第5列 → 10
# 选取某行
b = array1[3, :] # 第4行全部
# 选取某些行(连续/不连续)
c = array1[0:2, :] # 第1~2行
d = array1[[0, 2], :] # 第1行和第3行
# 选取某列
e = array1[:, 3] # 第4列全部
# 选取某些列
f = array1[:, 0:3] # 第1~3列
g = array1[:, [0, 3]] # 第1列和第4列
运行输出:
原数组:
[[ 0 1 2 3 4 5]
[ 6 7 8 9 10 11]
[12 13 14 15 16 17]
[18 19 20 21 22 23]]
array1[1,4] = 10 ← 第2行第5列
array1[3,:] = [18 19 20 21 22 23] ← 第4行全部
array1[0:2,:] = ← 连续行
[[ 0 1 2 3 4 5]
[ 6 7 8 9 10 11]]
array1[[0,2],:] = ← 不连续行
[[ 0 1 2 3 4 5]
[12 13 14 15 16 17]]
array1[:,3] = [ 3 9 15 21] ← 第4列全部
array1[:,[0,3]] = ← 不连续列
[[ 0 3]
[ 6 9]
[12 15]
[18 21]]
索引速查表:
| 写法 | 含义 |
|---|---|
[1, 4] |
第2行第5列(单个元素) |
[1, :] |
第2行全部 |
[:, 4] |
第5列全部 |
[0:2, :] |
第1~2行 |
[:, 0:3] |
第1~3列 |
[[0,2], :] |
第1行和第3行 |
[:, [0,3]] |
第1列和第4列 |
修改元素:
python
array1[1, 4] = 100 # 改单个
array1[3, :] = 100 # 改整行
array1[[0, 2], :] = 50 # 批量改行
array1[1,4]=100 后:
[[ 0 1 2 3 4 5]
[ 6 7 8 9 100 11]
[ 12 13 14 15 16 17]
[ 18 19 20 21 22 23]]
array1[[0,2],:]=50 后:
[[ 50 50 50 50 50 50]
[ 6 7 8 9 100 11]
[ 50 50 50 50 50 50]
[100 100 100 100 100 100]]
5.3 三维数组索引
三维数组索引看起来吓人,但逻辑很简单------只是在二维的基础上多了一层"层"的概念。你可以把三维数组想象成一本书:第0轴是"第几页",第1轴是"第几行",第2轴是"第几列"。逗号前是页,中间是行,最后是列。
python
array1 = np.arange(48).reshape(2, 4, 6)
a = array1[1, 0, 0] # 第2层、第1行、第1列 → 24
b = array1[0, 1, :] # 第1层、第2行全部
c = array1[0, 1:3, :] # 第1层、第2~3行
e = array1[1, :, 1] # 第2层、所有行、第2列
运行输出:
原数组 shape=(2, 4, 6):
[[[ 0 1 2 3 4 5] ← 第1层
[ 6 7 8 9 10 11]
[12 13 14 15 16 17]
[18 19 20 21 22 23]]
[[24 25 26 27 28 29] ← 第2层
[30 31 32 33 34 35]
[36 37 38 39 40 41]
[42 43 44 45 46 47]]]
array1[1,0,0] = 24 ← 第2层第1行第1列
array1[0,1,:] = [ 6 7 8 9 10 11] ← 第1层第2行
array1[0,1:3,:] =
[[ 6 7 8 9 10 11]
[12 13 14 15 16 17]]
array1[1,:,1] = [25 31 37 43] ← 第2层所有行第2列
🔰 三维记忆法 :把三维数组想象成一叠扑克牌------第一个索引选第几叠 (层),第二个选第几行 ,第三个选第几列。
5.4 布尔索引
布尔索引是 NumPy 最强大的功能之一------它让你用条件表达式 直接筛选数组元素,不需要写 for 循环。工作原理分两步:第一步,条件表达式(如 arr > 4)生成一个布尔数组(True/False);第二步,用这个布尔数组作为索引,NumPy 返回所有 True 位置的元素。
这种方式叫做"向量化操作"------它一次性处理整个数组,而不是逐元素遍历,速度比 for 循环快几十倍。
python
arr = np.array([3, 5, 2, 8, 1, 9, 4])
mask = arr > 4 # 生成布尔数组
print(mask) # [False True False True False True False]
print(arr[mask]) # [5 8 9]
# 直接修改满足条件的元素
arr[arr > 4] = 0
print(arr) # [3 0 2 0 1 0 4]
运行输出:
原数组: [3 5 2 8 1 9 4]
arr > 4: [False True False True False True False]
arr[arr>4]: [5 8 9]
arr[arr>4]=0: [3 0 2 0 1 0 4]
💡 应用场景 :数据清洗中把异常值替换为0或均值,如
data[data > 999] = np.nan。
5.5 花式索引
花式索引(Fancy Indexing)是用整数列表/数组 作为索引来选取任意位置的元素。与切片不同,花式索引可以选取不连续的行或列,而且可以自由组合。但花式索引有一个容易混淆的陷阱:当传入两个列表时,它选取的是"坐标对"而不是"行列子矩阵"。
python
arr = np.arange(16).reshape(4, 4)
# 选第2行和第4行
print(arr[[1, 3]])
# 选 (1,2) 和 (3,1) 两个位置的元素
print(arr[[1, 3], [2, 1]]) # [6 13]
# 用 np.ix_ 选取行列子矩阵
print(arr[np.ix_([0, 2], [1, 3])])
运行输出:
原数组:
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]
[12 13 14 15]]
arr[[1,3]] 选第2和4行:
[[ 4 5 6 7]
[12 13 14 15]]
arr[[1,3],[2,1]] 选(1,2)和(3,1): [ 6 13]
arr[np.ix_([0,2],[1,3])]:
[[ 1 3]
[ 9 11]]
🔰 易错点 :
arr[[1,3],[2,1]]选的是两个点 (1,2) 和 (3,1),而不是行列子矩阵。要选子矩阵用np.ix_()。
💡 切片 vs 花式索引的内存差异 :切片产生的新数组与原数组共享存储空间 ;用列表索引会创建副本。
六、数组元素操作
6.1 修改元素
前面已展示过用索引修改元素,这里补充 np.delete 和 np.where。
6.2 np.delete:删除元素
删除元素在数据预处理中很常用------比如删除不需要的特征列、删除异常数据行。np.delete 接受三个参数:要操作的数组、要删除的索引位置、沿哪个轴操作。注意 delete 不会修改原数组,而是返回一个新数组。
python
arr = np.arange(12).reshape(3, 4)
# 删行
print(np.delete(arr, 0, axis=0)) # 删第1行
# 删列
print(np.delete(arr, 1, axis=1)) # 删第2列
运行输出:
原数组:
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
delete行(axis=0) 删第1行:
[[ 4 5 6 7]
[ 8 9 10 11]]
delete列(axis=1) 删第2列:
[[ 0 2 3]
[ 4 6 7]
[ 8 10 11]]
6.3 np.where:条件筛选
np.where 是 NumPy 中最灵活的函数之一,它有完全不同的两种用法。理解它的关键在于参数个数:三个参数时,它是一个"条件赋值"操作(满足条件取 x,否则取 y);一个参数时,它是一个"条件查找"操作(返回满足条件的元素位置)。
python
arr = np.array([3, -1, 5, -2, 8, -3])
# 用法1:where(条件, x, y) --- 满足条件输出x,否则输出y
print(np.where(arr > 0, arr, 0)) # 正数保留,负数变0 → [3 0 5 0 8 0]
# 用法2:where(条件) --- 返回满足条件的元素索引
print(np.where(arr < 0)) # → [1 3 5]
运行输出:
原数组: [ 3 -1 5 -2 8 -3]
np.where(arr>0, arr, 0): [3 0 5 0 8 0]
np.where(arr<0) 负数位置: [1 3 5]
💡 np.where 的两种用法:
- 三个参数
where(cond, x, y):相当于if cond then x else y,逐元素- 一个参数
where(cond):返回满足条件的索引位置
6.4 np.unique:去重
np.unique 看起来只是去重,但它通过可选参数可以同时返回多个信息:去重后的值、每个值首次出现的位置、反向索引(用去重结果重建原数组)、每个值的出现频次。一行代码就能完成"值频统计"------这在分析分类数据时极其有用。
python
arr = np.array([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5])
unique_vals, indices, inverse, counts = np.unique(
arr, return_index=True, return_inverse=True, return_counts=True)
print(f"去重后: {unique_vals}") # [1 2 3 4 5 6 9]
print(f"首次出现索引: {indices}") # [1 6 0 2 4 7 5]
print(f"重构数组: {unique_vals[inverse]}") # 还原回原数组
print(f"各值出现次数: {counts}") # [2 1 2 1 3 1 1]
运行输出:
去重后: [1 2 3 4 5 6 9]
首次出现索引: [1 6 0 2 4 7 5]
重构数组: [3 1 4 1 5 9 2 6 5 3 5]
各值出现次数: [2 1 2 1 3 1 1]
🔰
unique的四个返回值:去重结果 + 首次出现位置 + 反向索引(用去重结果还原原数组) + 各值频次。非常实用------一行代码完成"值频统计"。
七、数组组合与切割
在实际项目中,数据往往分散在多个数组中------比如特征和标签是分开的、训练集和验证集需要拼接、一个大数据集需要分成多个子集。NumPy 提供了组合(拼接)和切割(拆分)两类函数。
组合是将多个小数组合并为一个大数组;切割是将一个大数组拆分为多个小数组。两者互为逆操作。

7.1 组合
python
array1 = np.arange(9).reshape(3, 3)
array2 = 2 * array1
# 水平组合(左右拼)
print(np.hstack((array1, array2)))
# 垂直组合(上下拼)
print(np.vstack((array2, array1)))
# concatenate:axis=1 水平,axis=0 垂直
print(np.concatenate((array1, array2), axis=1))
运行输出:
array1: array2:
[[0 1 2] [[ 0 2 4]
[3 4 5] [ 6 8 10]
[6 7 8]] [12 14 16]]
hstack水平组合:
[[ 0 1 2 0 2 4]
[ 3 4 5 6 8 10]
[ 6 7 8 12 14 16]]
vstack垂直组合:
[[ 0 2 4]
[ 6 8 10]
[12 14 16]
[ 0 1 2]
[ 3 4 5]
[ 6 7 8]]
| 函数 | 方向 | 等价写法 |
|---|---|---|
np.hstack((a, b)) |
水平(左右) | np.concatenate((a,b), axis=1) |
np.vstack((a, b)) |
垂直(上下) | np.concatenate((a,b), axis=0) |
7.2 切割
切割是组合的逆操作------将一个大数组拆成多个小数组。在机器学习中,交叉验证时需要把数据集等分成 K 份;在处理批量数据时需要把大矩阵分成小块。NumPy 提供了 hsplit(水平切)和 vsplit(垂直切),以及更灵活的 array_split(不要求等分)。
python
array1 = np.arange(16).reshape(4, 4)
# 等分切割
print(np.hsplit(array1, 2)) # 水平2等分
print(np.vsplit(array1, 2)) # 垂直2等分
# 强制切割(不等分也能切)
print(np.array_split(array1, 3, axis=1)) # 水平3等分
print(np.array_split(array1, 3, axis=0)) # 垂直3等分
运行输出:
原数组:
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]
[12 13 14 15]]
hsplit(array1, 2) 水平2等分:
部分0: 部分1:
[[ 0 1] [[ 2 3]
[ 4 5] [ 6 7]
[ 8 9] [10 11]
[12 13]] [14 15]]
vsplit(array1, 2) 垂直2等分:
部分0: 部分1:
[[0 1 2 3] [[ 8 9 10 11]
[4 5 6 7]] [12 13 14 15]]
array_split(array1, 3, axis=0) 强制垂直3等分:
部分0(2行) 部分1(1行) 部分2(1行)
[[0 1 2 3] [[ 8 9 10 11]] [[12 13 14 15]]
[4 5 6 7]]
🔰
hsplit/vsplitvsarray_split:前者要求能整除,不能整除会报错;后者自动不等分分配。
💡 为什么"水平切割"的线是竖着的?"垂直切割"的线是横着的?这是初学者最容易混淆的地方!之所以看起来"反直觉",是因为"水平/垂直"描述的是切割后子数组的排列方向,而不是刀落下的方向。
函数 名字含义 刀的方向 结果排列 类比 hsplit水平切割Horizontal = 结果左右排列 竖着切 部分0在左,部分1在右 把法棍面包切成左右两段,刀竖着切下去 vsplit垂直切割Vertical = 结果上下堆叠 横着切 部分0在上,部分1在下 把高楼拦腰截断,刀横着切过去 记忆法:不要记"线的方向",要记"分完之后的样子":
hsplit→ 结果左右排排坐 → 刀竖着下vsplit→ 结果上下叠罗汉 → 刀横着下对应到底层 API:
hsplit等价于split(axis=1)(沿列轴切),vsplit等价于split(axis=0)(沿行轴切)。
八、算术运算与 ufunc
NumPy 的算术运算遵循一个核心原则:元素级运算 (element-wise)。也就是说,两个数组的 +、-、*、/ 都是对应位置的元素逐一运算,而不是矩阵运算。这与数学中的矩阵运算有本质区别,初学者尤其要注意 * 和 np.dot() 的差异。
8.1 元素级运算
python
array1 = np.arange(1, 5).reshape(2, 2) # [[1,2],[3,4]]
array2 = 2 * array1 # [[2,4],[6,8]]
print(array1 + array2) # 加
print(array1 - array2) # 减
print(array1 * array2) # 乘(元素级)
print(array1 / array2) # 除
print(array2 % array1) # 取余
print(array1 // array2) # 取整
运行输出:
array1: array2:
[[1 2] [[2 4]
[3 4]] [6 8]]
加法: [[ 3 6] 减法: [[-1 -2] 乘法: [[ 2 8] 除法: [[0.5 0.5]
[ 9 12]] [-3 -4]] [18 32]] [0.5 0.5]]
取余: [[0 0] 取整:
[0 0]] [[0 0]
[0 0]]
⚠️ 重要 :NumPy 的
*是元素级乘法 (对应位置相乘),不是矩阵乘法!矩阵乘法要用np.dot()。这是初学者最常犯的错误------在数学中A×B通常指矩阵乘法,但在 NumPy 中A*B只是逐元素相乘。两者结果完全不同,后续第九章会详细演示矩阵乘法。
8.2 ufunc:通用函数
NumPy 中每一个运算符(+、-、*、/)背后都对应一个 ufunc (universal function,通用函数)。ufunc 的特点是:它对数组中每个元素独立执行相同的运算,底层用 C 语言实现,避免了 Python 循环的开销。
ufunc 不仅比 Python for 循环快几十倍,还支持"广播"(下一章详解)------即使两个数组形状不同,ufunc 也能自动处理。
python
array1 = np.arange(1, 5).reshape(2, 2) # [[1,2],[3,4]]
array2 = 2 * array1 # [[2,4],[6,8]]
# 这些 ufunc 写法与运算符完全等价
np.add(array1, array2) # 等同于 array1 + array2
np.subtract(array1, array2) # 等同于 array1 - array2
np.multiply(array1, array2) # 等同于 array1 * array2
np.divide(array1, array2) # 等同于 array1 / array2
运行输出:
np.add: [[ 3 6] np.subtract: [[-1 -2] np.multiply: [[ 2 8] np.divide:
[ 9 12]] [-3 -4]] [18 32]] [[0.5 0.5]
[0.5 0.5]]
🔰 ufunc vs math 库 :
np.sin对数组操作时比math.sin快很多(C 层并行);但对单个数值,math.sin更快(调用开销小)。
8.3 数组与标量运算
当一个 NumPy 数组与一个标量(单个数字)进行运算时,标量会被"广播"到数组的每个元素上。这种机制使得你可以像写数学公式一样写代码------a * 3 + b / 2 会自动对每个元素生效,不需要写 for 循环。
这也是 NumPy 与 Python 列表行为完全不同 的地方------列表的 * 是重复,NumPy 的 * 是逐元素乘法。这个差异在从列表迁移到 NumPy 时最容易踩坑。
python
a = np.array([1, 2, 3])
print(a * 3) # 每个元素乘3
print(a + 10) # 每个元素加10
运行输出:
a * 3 = [3 6 9]
a + 10 = [11 12 13]
💡 与 Python 列表的区别 :Python 列表
[1,2,3] * 3会把列表重复成[1,2,3,1,2,3,1,2,3];而 NumPy 数组np.array([1,2,3]) * 3是每个元素乘 3。
python
# Python 列表的 * 是重复
print([1, 2, 3] * 3)
运行输出:
[1, 2, 3, 1, 2, 3, 1, 2, 3]
九、矩阵运算
矩阵运算是线性代数的基础。在机器学习中,神经网络的前向传播本质上就是一系列矩阵乘法:输出 = 权重矩阵 × 输入向量 + 偏置。理解矩阵乘法与元素级乘法的区别是使用 NumPy 做科学计算的关键。

9.1 矩阵乘法
矩阵乘法的规则是"行乘列再求和"------结果矩阵的第 i 行第 j 列等于左矩阵第 i 行与右矩阵第 j 列的对应元素乘积之和。这与元素级乘法(同位置相乘)完全不同,初学者务必通过下面的输出对比来区分。
python
a = np.arange(4).reshape(2, 2) # [[0,1],[2,3]]
b = a.copy()
# 矩阵乘法(点乘)------ 两种写法等价
print(a.dot(b))
print(np.dot(a, b))
运行输出:
a: b:
[[0 1] [[0 1]
[2 3]] [2 3]]
a.dot(b) 矩阵乘法:
[[ 2 3]
[ 6 11]]
🔰 矩阵乘法规则 :
C[i,j] = Σ A[i,k] × B[k,j],即"行乘列再求和"。例如
C[0,0] = 0×0 + 1×2 = 2;C[1,1] = 2×1 + 3×3 = 11。
9.2 矩阵求逆
矩阵求逆是线性代数中的核心操作。逆矩阵的定义是:如果 A × A⁻¹ = I(单位矩阵),则 A⁻¹ 是 A 的逆矩阵。这在解线性方程组 Ax=b 时非常关键------解就是 x = A⁻¹ × b。在机器学习中,线性回归的解析解(最小二乘法)就涉及矩阵求逆。
python
a3 = np.linalg.inv(a)
print(a3)
print(a.dot(a3)) # 验证:矩阵 × 逆 = 单位阵
运行输出:
np.linalg.inv(a) 矩阵求逆:
[[-1.5 0.5]
[ 1. 0. ]]
a.dot(inv(a)) 验证:
[[1. 0.]
[0. 1.]] ← 确实是单位矩阵!
🔰 注意 :不是所有矩阵都有逆。不可逆矩阵(奇异矩阵)
np.linalg.inv会给出"伪逆"(Moore-Penrose 伪逆),但仍可计算。
| 运算 | 函数 | 公式 |
|---|---|---|
| 矩阵乘法 | a.dot(b) 或 np.dot(a,b) |
Ci,j = Σ Ai,k × Bk,j |
| 矩阵求逆 | np.linalg.inv(a) |
A × A⁻¹ = I |
| 元素级乘法 | a * b |
Ai,j × Bi,j |
| 转置 | a.T 或 a.transpose() |
行变列、列变行 |
十、广播机制(Broadcasting)
广播是 NumPy 最强大也最容易困惑的特性之一。它允许形状不同的数组进行运算,自动将小数组"拉伸"到大数组的形状,而不需要真正复制数据。
为什么要叫"广播"?想象电视台发送信号------一个发射塔发出信号,所有接收设备都能收到。NumPy 中也是如此:一个小数组(如标量或一维向量)的运算效果会"广播"到整个大数组的每个元素上。
广播的核心价值在于避免显式复制数据------你不需要用 for 循环或 tile 函数手动扩展小数组的形状,NumPy 在底层通过"步长"机制虚拟地完成了扩展,既简洁又高效。

10.1 广播规则
从最右边的维度开始向左比较,满足以下条件之一即可广播:
- 两个维度相等
- 其中一个维度为 1
🔰 三步判断法:
- 从右到左逐个维度比较
- 维度相同 → OK
- 维度不同但有一个是 1 → 那个为 1 的被"拉伸"
- 维度不同且都不为 1 → 报错!
10.2 三种典型情况
情况1:标量 + 数组(低维有1)
python
a = np.array([[1, 2, 3],
[4, 5, 6]])
print(a + 10)
运行输出:
[[11 12 13]
[14 15 16]]
标量 10 被"拉伸"成 [[10,10,10],[10,10,10]] 再相加。
情况2:(4,3) + (3,)(后缘相符)
python
a = np.array([[0, 0, 0],
[10, 10, 10],
[20, 20, 20],
[30, 30, 30]])
b = np.array([1, 2, 3])
print(a + b)
运行输出:
a shape=(4, 3)
b shape=(3,)
a + b =
[[ 1 2 3]
[11 12 13]
[21 22 23]
[31 32 33]]
b 的 shape (3,) 与 a 的最后一维 3 相等,所以 b 被复制 4 行 变成 (4,3)。
情况3:(4,1) + (3,)(后缘不符但低维有1)
python
a2 = np.array([[0], [10], [20], [30]]) # shape (4,1)
b2 = np.array([1, 2, 3]) # shape (3,)
print(a2 + b2)
运行输出:
a2 shape=(4, 1), b2 shape=(3,)
a2 + b2 =
[[ 1 2 3]
[11 12 13]
[21 22 23]
[31 32 33]]
a2 的 1 先扩展为 3(→ shape (4,3)),b2 也扩展为 (4,3),然后相加。
💡 广播的本质:不真正复制数据,而是在内存中用"步长"技巧实现虚拟复制,非常高效。
10.3 用 np.newaxis 手动扩展维度
python
a = np.array([1, 2, 3]) # shape (3,)
print(a[:, np.newaxis].shape) # (3, 1) --- 插入新维度
print(a[np.newaxis, :].shape) # (1, 3)
# np.newaxis 和 None 等价
print(a[:, None].shape) # (3, 1)
# 看实际数组变化
print(a[:, np.newaxis]) # 变成列向量
print(a[np.newaxis, :]) # 变成行向量
运行输出:
a.shape = (3,)
a[:, np.newaxis].shape = (3, 1)
a[np.newaxis, :].shape = (1, 3)
a[:, None].shape = (3, 1)
a[:, np.newaxis] =
[[1]
[2]
[3]]
a[np.newaxis, :] =
[[1 2 3]]
🔰
np.newaxis是None的别名,用于在指定位置插入宽度为 1 的新维度。在手动触发广播时非常有用。
十一、随机模块

11.1 随机种子
随机种子(seed)控制随机数生成器的起始状态。设定相同的种子后,每次运行产生的"随机"数完全一样------这在科学实验中非常重要:别人复现你的实验时需要得到相同的数据和结果。深度学习框架(PyTorch、TensorFlow)中也有类似机制。
要注意一个陷阱:seed 只对其后的第一次随机函数调用生效。如果后续再调用其他随机函数,得到的是新的随机数(不再是种子固定的值)。
python
np.random.seed(1000)
r1 = np.random.randint(0, 10)
print(r1)
运行输出:
3
🔰 种子的作用 :设了种子后每次运行产生的随机数完全一样。调试模型、复现实验时必用。种子只对其后第一次随机函数生效。
11.2 三种常用分布
NumPy 的随机模块支持多种概率分布。最常用的三种是:randint(均匀分布的整数,每个值出现概率相等)、rand(0~1 之间的均匀分布浮点数)、normal(正态分布/高斯分布,数据集中在均值附近,越远越稀疏)。
理解分布的关键在于知道什么场景用什么:模拟掷骰子用 randint,生成随机概率用 rand,模拟身高体重等自然现象用 normal。下图展示了三种分布的频次直方图------均匀分布是"平的",正态分布是"钟形的"。
python
# 均匀分布整数 [0, 10)
print(np.random.randint(0, 10, size=(5, 5)))
# (0,1)均匀分布浮点
print(np.random.rand(5, 5))
# 正态分布 N(5, 10) --- 均值5, 标准差10
print(np.random.normal(5, 10, size=(5, 5)))
运行输出:
randint(0,10,size=(5,5)):
[[3 3 3 6 4]
[7 2 4 4 2]
[4 1 2 3 9]
[0 5 2 1 0]
[6 8 9 6 1]]
rand(5,5):
[[0.5414 0.4826 0.4765 0.4460 0.8161]
[0.2744 0.0620 0.9603 0.4581 0.7579]
...]
normal(5,10,size=(5,5)):
[[ 2.07 -1.08 11.77 27.88 8.29]
[ 5.03 3.73 -15.29 -1.84 9.94]
...]
| 函数 | 分布 | 参数说明 |
|---|---|---|
np.random.randint(low, high, size) |
均匀分布整数 | [low, high) 左闭右开 |
np.random.rand(d0, d1, ...) |
均匀分布浮点 | [0, 1) 区间 |
np.random.normal(loc, scale, size) |
正态分布 | loc=均值, scale=标准差 |
💡 参数格式差异 :
randint和normal用size=(5,5)传形状;rand直接用rand(5,5)传维度。
十二、统计函数
统计函数用于从数据中提取摘要信息------方差衡量数据波动程度、均值反映数据中心位置、中位数不受极端值影响。在数据分析中,先对数据做统计描述(均值、标准差、分位数)是最基本的第一步,帮助了解数据的整体分布和异常情况。
NumPy 的统计函数大部分支持 axis 参数,可以按行或按列分别计算------这在处理表格数据时非常实用,比如按行算每个样本的特征总和,按列算每个特征的均值。
python
np.random.seed(42)
array1 = np.random.normal(size=(3, 3))
print(array1.var()) # 方差
print(array1.std()) # 标准差
print(array1.mean()) # 均值
print(array1.sum()) # 求和
print(np.median(array1)) # 中位数
# 按行/列求和
print(array1.sum(axis=1)) # 行求和
print(array1.sum(axis=0)) # 列求和
运行输出:
数组:
[[ 0.4967 -0.1383 0.6477]
[ 1.5230 -0.2342 -0.2341]
[ 1.5792 0.7674 -0.4695]]
方差 var(): 0.521639
标准差 std(): 0.722246
均值 mean(): 0.437561
求和 sum(): 3.938051
中位数 median(): 0.496714
行求和 sum(axis=1): [1.006 1.055 1.877]
列求和 sum(axis=0): [3.599 0.395 -0.056]
| 函数 | 作用 | 公式 |
|---|---|---|
var() |
方差 | σ² = Σ(xi - μ)² / N |
std() |
标准差 | σ = √(方差) |
mean() |
均值 | μ = Σxi / N |
sum() |
求和 | Σxi |
np.median() |
中位数 | 排序后取中间值 |
sum(axis=1) |
行求和 | 每行所有列相加 |
sum(axis=0) |
列求和 | 每列所有行相加 |
🔰 axis 记忆法 :
axis=0沿行方向操作(行消失→列求和);axis=1沿列方向操作(列消失→行求和)。"axis=那个维度,那个维度就消失了"。
12.1 keepdims:保持维度
默认情况下,sum(axis=1) 会把结果"压扁"------二维数组按行求和后变成一维。但有时我们需要保持维度不变(结果是二维),这样后续可以继续用广播与其他二维数组运算。keepdims=True 就是为此设计的。
python
arr = np.array([[1, 2, 3], [4, 5, 6]])
print(f"sum(axis=1): {arr.sum(axis=1)}, shape={arr.sum(axis=1).shape}")
print(f"sum(axis=1, keepdims=True):\n{arr.sum(axis=1, keepdims=True)}, shape={arr.sum(axis=1, keepdims=True).shape}")
运行输出:
原数组 shape=(2, 3):
[[1 2 3]
[4 5 6]]
sum(axis=1): [ 6 15], shape=(2,)
sum(axis=1, keepdims=True):
[[ 6]
[15]], shape=(2, 1)
💡 keepdims 用途 :默认 sum 后维度降低,设
keepdims=True保持原维度数,方便后续广播运算。
12.2 bincount 与 histogram
bincount 和 histogram 都是"数频次"的工具,但适用场景不同:bincount 专门用于非负整数 数组,返回每个整数出现多少次,适合统计分类标签分布;histogram 更通用,可以将任意连续值划分到若干个区间(bin)中统计,适合分析数据分布。
python
arr = np.array([0, 1, 1, 3, 2, 1, 0, 3, 3, 2])
print(np.bincount(arr)) # 各值出现次数
hist, edges = np.histogram(arr, bins=4)
print(f"hist={hist}, edges={edges}")
运行输出:
原数组: [0 1 1 3 2 1 0 3 3 2]
bincount: [2 3 2 3] ← 0出现2次, 1出现3次, 2出现2次, 3出现3次
histogram bins=4: hist=[2 3 2 3], edges=[0. 0.75 1.5 2.25 3.]
| 函数 | 作用 |
|---|---|
np.bincount(arr) |
统计非负整数各值出现次数 |
np.histogram(arr, bins) |
直方图统计,返回频次和边界 |
十三、深拷贝与浅拷贝
这是 NumPy 中最容易踩坑的地方之一。很多初学者发现"我只是复制了一份,怎么改副本把原件也改了?"原因在于 NumPy 的"赋值"操作不复制数据------它只是给同一块内存贴了另一个名字。只有显式调用 .copy() 才会真正复制数据。
理解这一点对于避免隐蔽 bug 非常重要------在数据预处理流水线中,如果一个函数"不小心"修改了传入的数组,而调用者还在使用"原件",就会产生难以排查的错误。

python
array1 = np.array([1, 2, 3])
# 赋值(浅拷贝)
array2 = array1
array2[0] = 100
print(array1) # [100 2 3] ← array1 也变了!
# copy(深拷贝)
array3 = array1.copy()
array3[0] = 10
print(array1) # [100 2 3] ← array1 不变
运行输出:
赋值后 array2: [100 2 3]
赋值后 array1: [100 2 3] ← array1也被改了!
深拷贝后 array3: [10 2 3]
深拷贝后 array1: [100 2 3] ← array1不变
| 操作 | 是否共享内存 | 修改B会影响A吗 |
|---|---|---|
B = A(赋值) |
✅ 共享 | ✅ 会影响 |
B = A.copy()(深拷贝) |
❌ 独立 | ❌ 不影响 |
B = A[:](切片) |
✅ 共享 | ✅ 会影响 |
💡 记忆 :
=只是给数据贴了个新标签,两个名字指向同一块内存;.copy()是真正复制了一份新数据。这与列表的切片行为一致------切片产生的是视图而非副本。
十四、文件读写
数据分析的第一步通常是"把文件读进来"。NumPy 提供了简单的 loadtxt/savetxt 来读写纯文本文件。虽然 Pandas 的 read_csv 功能更强大(支持表头、混合类型),但 NumPy 的文件读写函数对于纯数值数据来说更轻量、更快。
loadtxt 默认按空格分隔,加 delimiter 参数可以指定分隔符。注意它要求每行有相同数量的值 ------如果文件中有缺失值或文本列,loadtxt 会报错,这时应该用 Pandas。
python
import numpy as np
# 从txt文件加载数据
data = np.loadtxt('datingTestSet2.txt', delimiter='\t')
print(data.shape)
print(data[:5])
# 将数组保存到txt文件
array = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
np.savetxt('array.txt', array)
运行输出:
loadtxt('datingTestSet2.txt', delimiter='\t'):
shape: (1000, 4)
前5行:
[[40920. 8.326976 0.953952 3. ]
[14488. 7.153469 1.673904 2. ]
[26052. 1.441871 0.805124 1. ]
[75136. 13.147394 0.428964 1. ]
[38344. 1.669788 0.134296 1. ]]
保存数组到 array.txt:
[[1 2 3]
[4 5 6]
[7 8 9]]
| 函数 | 作用 | 参数说明 |
|---|---|---|
np.loadtxt(fname, delimiter) |
从文本文件加载数据 | delimiter 指定分隔符 |
np.savetxt(fname, array) |
将数组保存为文本文件 | 默认空格分隔 |
🔰
loadtxt假定每行有相同数量的值。delimiter='\t'表示用制表符分隔。
十五、多项式函数
多项式在科学计算中无处不在------泰勒展开用多项式逼近复杂函数、信号处理用多项式做滤波器设计、数据拟合用多项式做回归。NumPy 的 poly1d 类封装了多项式的常见操作:求值、求导、求积分、求根,让多项式运算像写数学公式一样直观。
python
import numpy as np
# 创建多项式:x² + 2x + 3
p = np.poly1d([1, 2, 3])
print(p)
# 求值
print(f"p(2) = {p(2)}") # 1*4 + 2*2 + 3 = 11
# 求导
print(f"导数: {p.deriv()}") # 2x + 2
# 求积分
print(f"积分: {p.integ()}") # (1/3)x³ + x² + 3x
# 求根
print(f"根: {np.roots(p)}") # x² + 2x + 3 = 0 的解
运行输出:
poly1d([1,2,3]) =
2
1 x + 2 x + 3
p(2) = 11
导数: 2 x + 2
积分: 3 2
0.3333 x + 1 x + 3 x
根: [-1.+1.41421356j -1.-1.41421356j]
🔰 系数规则 :
np.poly1d([1, 2, 3])中,a[0]是最高次系数,a[-1]是常数项。所以[1,2,3]→1x² + 2x + 3。这个顺序与直觉可能相反------我们习惯从低次往高次写,但 NumPy 从高次往低次排。
💡 多项式拟合 :np.polyfit(x, y, deg)可以用最小二乘法拟合多项式曲线------给定一组数据点 (x, y) 和多项式次数deg,返回最佳拟合的系数。例如np.polyfit(x, y, 2)拟合二次曲线ax² + bx + c。这在简单回归分析中很常用,但更复杂的拟合建议用 scikit-learn。
十六、总结
易混淆点速查
| 易混淆 | 区别 |
|---|---|
reshape vs resize |
reshape 不改原数组返回新数组,resize 直接修改原数组 |
ravel vs flatten |
ravel 返回视图(共享内存),flatten 返回副本(独立) |
arange vs linspace |
arange 左闭右开按步长,linspace 左右都闭按个数 |
* vs dot |
* 元素级相乘,dot 矩阵乘法 |
= vs copy |
= 共享内存,copy 独立内存 |
split vs array_split |
split 要求整除,array_split 不要求 |
axis=0 vs axis=1 |
axis=0 沿行方向(行消失→列求和),axis=1 沿列方向(列消失→行求和) |
astype(int) |
截断小数,不是四舍五入 |
花式索引 [[1,3],[2,1]] |
选的是两个点 不是子矩阵,子矩阵用 np.ix_() |
切片 a[:] |
产生视图(共享内存),修改会影响原数组 |
知识体系总览
NumPy
├── 核心对象:ndarray(N维数组)+ ufunc(通用函数)
├── 创建:array / zeros / ones / full / eye / empty / arange / linspace / logspace
├── 属性:shape / ndim / size / dtype
├── 变换:reshape / resize / ravel / flatten / T / squeeze / astype
├── 索引:基本[行,列] / 布尔[条件] / 花式[[列表]]
├── 操作:delete / where / unique
├── 组合:hstack / vstack / concatenate
├── 切割:hsplit / vsplit / array_split
├── 运算:+ - * / % //(元素级)/ dot(矩阵乘法)/ inv(求逆)
├── 广播:形状不同自动拉伸,规则=从右到左逐维比较
├── 随机:randint / rand / normal / seed
├── 统计:var / std / mean / sum / median / bincount / histogram
├── 拷贝:=(浅)/ copy(深)
├── 读写:loadtxt / savetxt
└── 多项式:poly1d / roots / polyfit / deriv / integ