# NumPy

Source: https://codewiki.com/zh/datascience/numpy/

> - **what**: NumPy 用 `ndarray` 表示同质多维数组，并在整个数组上执行数值运算。
> - **when**: 数据能组成规则的数值网格，而且你需要切片、聚合、线性代数或与 Python 科学计算库交换数据时使用它。
> - **how**: 先明确每个轴的含义和 `dtype`，再用数组运算组合计算；遇到广播、切片或类型转换时检查结果的形状和内存所有权。

## 是什么，为什么存在

NumPy 是 Python 的数值数组库。它的核心对象是多维数组（ndarray）：一组同类型元素，加上一份描述维度、数据类型和内存布局的元数据。二维数组可以表示「样本 × 特征」，三维数组可以表示「图像高度 × 宽度 × 通道」，但 NumPy 本身不会替你记录这些领域含义。

Python 列表适合保存异质对象和不断变化的集合，却没有数组级算术的统一语义。`[1, 2] * 2` 会重复列表，而 NumPy 数组乘以 `2` 会逐元素计算。NumPy 为规则数值数据提供共同的数组协议，Pandas、SciPy、Matplotlib 和许多机器学习工具因此能交换数据，而不必为每次运算编写 Python 循环。

当数据是规则、稠密、同质的数值网格时，NumPy 很合适。带列名和混合类型的表格通常更适合 Pandas 或 Polars；形状高度不规则的对象集合仍可能适合 Python 容器。这个边界很实用：不要为了「向量化」而把本来不规则的数据硬塞进 `dtype=object` 数组。

这里的示例已经在 Python 3.14.3 和 NumPy 2.5.2 环境中运行。显示结果经过明确的舍入或列表转换，避免依赖数组的省略规则或终端宽度。

## 工作原理

### 一个数组的三份契约

每个数组都有形状（shape）、数据类型（dtype）和数据缓冲区。`shape` 是各轴长度组成的元组；`ndim` 是轴数；`size` 是元素总数。`dtype` 决定每个元素如何解释，例如 `int64`、`float32` 或 `bool`。

数组变量还隐含一份领域契约：每个轴代表什么。形状同为 `(32, 10)` 的两个数组，可能分别表示 32 个样本的 10 个特征，也可能表示 32 个时间点的 10 个传感器。NumPy 只验证长度能否配合，不会发现轴的业务含义颠倒。

步幅（stride）说明沿每个轴移动一个位置时跨过多少字节。切片和转置可以只改 `shape` 与 `strides`，继续引用同一缓冲区。于是两个外观不同的数组可能共享数据，这也是修改切片有时会改到原数组的原因。

### 创建数组时确定类型

`np.array()` 从现有 Python 数据创建数组，`np.zeros()`、`np.ones()` 和 `np.full()` 创建已初始化的规则数组，`np.arange()` 与 `np.linspace()` 创建数值序列。业务边界处应显式给出 `dtype`，尤其是整数宽度、浮点精度和输入可能为空时。这样可以避免类型由某一批样本偶然决定。

`np.empty()` 只分配空间，不初始化其中的值。它适合随后会覆盖每个元素的内部实现，不适合当作「空值数组」。如果任何位置可能在赋值前被读取，就应使用带有确定初值的创建函数。

`reshape()` 要求元素总数保持不变。用 `-1` 可以让 NumPy 推导一个轴的长度，但一条形状合法的语句仍可能表达错误的数据布局。重塑前后都要写清每个轴的意义，而不是只检查元素数能否整除。

常见形状操作的差异可以概括如下：

| 目的 | 表达式 | 结果要点 |
| --- | --- | --- |
| 改变维度长度 | `a.reshape(rows, cols)` | 元素数不变，可能共享内存 |
| 交换全部轴 | `a.T` | 二维时交换行列，通常是视图 |
| 指定轴顺序 | `a.transpose(order)` | `order` 是输出轴对应的输入轴 |
| 移动一个轴 | `np.moveaxis(a, source, destination)` | 适合把通道轴移到明确位置 |
| 插入长度为一的轴 | `np.expand_dims(a, axis)` | 常用于准备广播 |
| 删除长度为一的轴 | `np.squeeze(a, axis=axis)` | 指定 `axis` 可避免误删其他轴 |
| 沿新轴组合 | `np.stack(arrays, axis=axis)` | 所有输入形状必须相同 |
| 沿已有轴拼接 | `np.concatenate(arrays, axis=axis)` | 非拼接轴的长度必须相同 |

### 移动与组合轴

转置不是「把任何数组的行列互换」。二维数组的 `.T` 确实交换两个轴，但三维数组的 `.T` 会反转全部轴顺序。处理批次、通道和空间维度时，`transpose()` 或 `moveaxis()` 能把意图写得更明确。

`stack()` 与 `concatenate()` 解决不同问题。`stack()` 新建一个轴，例如把十个形状为 `(64, 64)` 的图像组合成 `(10, 64, 64)`；`concatenate()` 沿已有轴接长数组。生成代码很容易在这里多出或丢掉一个批次轴。

不带 `axis` 的 `squeeze()` 会删除所有长度为 `1` 的轴。这在测试数据中看似方便，却可能让批量大小为 `1` 时的返回值突然少一个维度。公共 API 应指定要删除的轴，或者干脆保留稳定的批次维度。

`np.newaxis` 就是 `None` 的别名，用在索引中可以插入长度为 `1` 的轴。`values[:, np.newaxis]` 把 `(n,)` 变成 `(n, 1)`，`values[np.newaxis, :]` 则变成 `(1, n)`。两者元素相同，但广播方向完全不同。

组合数组前先验证除目标轴以外的长度。错误消息只能指出某些维度不匹配，不知道一个输入是否使用「通道优先」，另一个却使用「通道最后」。接口中的轴命名比修复一条 `ValueError` 更重要。

### 向量化、通用函数与聚合

向量化（vectorization）是把计算表达为整个数组上的操作。例如，`prices * quantities` 表示逐元素乘法，`np.sqrt(values)` 把同一个通用函数应用到每个元素。向量化首先改变的是表达方式：循环由 NumPy 处理，形状与类型规则集中在数组操作中。

`sum()`、`mean()`、`max()` 等聚合通过 `axis` 指定要消去的轴。对于形状为「样本 × 特征」的二维数组，`axis=0` 会消去样本轴，留下每个特征的结果；`axis=1` 会消去特征轴，留下每个样本的结果。`keepdims=True` 保留长度为 `1` 的轴，后续广播时通常更清楚。

普通算术运算是逐元素的。两个二维数组之间的 `*` 不是矩阵乘法；线性代数乘法使用 `@` 或 `np.matmul()`。`np.linalg.solve()` 用于求解方阵线性方程组，通常比显式计算逆矩阵再相乘更直接。

### 广播从尾轴开始

广播（broadcasting）让不同形状的数组参与逐元素运算。NumPy 从最右侧轴开始比较：两个长度相等，或者其中一个长度为 `1`，这一对轴就兼容；缺失的左侧轴按长度 `1` 处理。否则运算会抛出形状不兼容错误。

例如，形状为 `(4, 3)` 的样本矩阵可以减去形状为 `(3,)` 的特征均值，因为尾轴长度都是 `3`。形状为 `(4,)` 的样本均值不能直接从它减去，因为尾轴 `3` 与 `4` 不兼容。把样本均值保留为 `(4, 1)` 后，长度为 `1` 的尾轴才会沿特征方向广播。

广播描述的是逻辑上的扩展，不等于先创建一份重复数据。不过，算术结果通常仍会分配自己的数组。先预测结果形状，再写表达式；「没有报错」只能证明形状兼容，不能证明轴的含义正确。

### 索引决定共享还是复制

基本切片，例如 `array[1:4]` 或 `matrix[:, ::2]`，通常返回共享数据的数组视图（array view）。修改视图中的元素会在原数组中可见。`view.base` 有助于探索，但一串视图的 `base` 不一定直接就是手里的原数组，因此判断两个数组是否重叠应使用 `np.shares_memory()`。

整数数组索引和布尔索引属于高级索引，结果是副本。`array[[0, 2]]` 与 `array[array > 0]` 后续可以独立修改，但复制会分配新缓冲区。混合基本索引与高级索引时，结果形状也可能与逐轴切片的直觉不同，应该用小输入先确认。

赋值时规则不同：`array[mask] = 0` 会写回原数组，因为索引出现在赋值目标上。相反，先保存 `selected = array[mask]`，再改 `selected`，只会修改那份副本。代码审查时需要看完整的数据流，不能只凭一个索引表达式判断最终写到了哪里。

### 掩码、条件与顺序

比较数组会得到布尔数组。多个条件必须使用逐元素运算符 `&`、`|` 和 `~`，而且每个比较通常要放在括号中；Python 的 `and` 与 `or` 需要把整个数组解释成一个真假值，因此会抛出歧义错误。

布尔索引只保留满足条件的元素，常常会压平原有的结构。`np.where(condition, left, right)` 则在两个同形或可广播的值之间逐元素选择，并保留广播后的形状。选择哪一个取决于下游是否还需要原来的轴。

缺失值不只有 `NaN` 一种表示。浮点数组能直接保存 `NaN`，整数数组不能；把整数输入与 `NaN` 放在一起创建数组，通常会提升到浮点类型。若缺失与零、空字符串或某个哨兵值混用，聚合前必须先统一语义。

`np.argsort()` 返回能重排数组的索引，而不是已经排序的值。需要同步重排标签与特征时，先计算一份顺序索引，再把它应用到所有关联数组。只排序一列会破坏样本内部的对应关系。

掩码和顺序索引的长度也属于数组契约。一个来自过滤前数据的旧掩码，不能安全地复用到过滤后的数组。数据管道应让掩码与它对应的数据一起流动，或者在每个阶段重新计算。

### 错误与数值警告

形状不兼容通常会抛出 `ValueError`。先读取异常中列出的输入形状，再从尾轴向左比较，比盲目插入 `reshape()` 更安全。能让代码运行的形状修补不一定保留数据含义。

不安全的原地类型转换会失败。例如，浮点除法结果不能按 `same_kind` 规则写回整数数组。修复方向通常是创建所需浮点类型的结果，而不是强制使用会截断信息的转换。

除零、无效运算和溢出的浮点操作默认可能发出 `RuntimeWarning`，同时产生 `inf` 或 `NaN`。如果调用方忽略警告，这些特殊值还能继续流入后续计算，所以只看函数是否抛出异常并不够。

`np.errstate()` 可以在一个明确的代码块内改变浮点错误处理方式。测试关键数值路径时，可以把相关警告临时提升为异常；预期会产生特殊值的算法则可以在局部处理，并立即验证结果。

`np.seterr()` 改变的是当前线程的 NumPy 错误设置，不适合由普通库函数随意修改。局部上下文比隐藏的环境变化更容易推理，也不会要求调用者在函数返回后恢复设置。

`np.isfinite()` 能同时筛出 `NaN` 与正负无穷。输入验证和关键中间结果都可以使用它，但是否拒绝、替换或保留这些值仍应由领域契约决定。

警告策略不能替代边界测试。要分别覆盖除零、极大值、极小值和全缺失切片，因为它们触发的类型提升、警告和返回形状可能不同。

## 示例

### 读懂形状、类型和轴

这个二维数组的行表示日期，列表示传感器。`axis=1` 消去传感器轴，得到每日均值；`axis=0` 消去日期轴，得到每个传感器的峰值。

<!-- quick -->

```python
# file: array_axes.py
import numpy as np

temperatures = np.array(
    [[18.5, 20.0, 19.5], [21.0, 23.5, 22.0]],
    dtype=np.float64,
)

print(f"shape={temperatures.shape}")
print(f"ndim={temperatures.ndim}, dtype={temperatures.dtype}")
print("daily mean:", np.round(temperatures.mean(axis=1), 2).tolist())
print("sensor peak:", temperatures.max(axis=0).tolist())
```

```text
shape=(2, 3)
ndim=2, dtype=float64
daily mean: [19.33, 22.17]
sensor peak: [21.0, 23.5, 22.0]
```

<!-- /quick -->

输出形状 `(2, 3)` 本身不能说明哪个轴是日期。变量名、邻近注释和接口文档要保存这份语义。聚合后也应检查结果形状是否与消费方的预期一致。

### 用广播标准化特征

下面把每列当作一个特征。`keepdims=True` 让均值与标准差保持 `(1, 3)`，所以减法和除法都明确沿样本轴广播。第三列是常数，代码把它的零标准差替换为 `1.0`，避免除零。

```python
# file: standardize_features.py
import numpy as np

samples = np.array(
    [[1.0, 10.0, 7.0], [2.0, 20.0, 7.0], [3.0, 30.0, 7.0]]
)

means = samples.mean(axis=0, keepdims=True)
scales = samples.std(axis=0, keepdims=True)
safe_scales = np.where(scales == 0, 1.0, scales)
standardized = (samples - means) / safe_scales

np.set_printoptions(precision=2, suppress=True)
print("mean shape:", means.shape)
print(standardized)
print("constant feature:", standardized[:, 2].tolist())
```

```text
mean shape: (1, 3)
[[-1.22 -1.22  0.  ]
 [ 0.    0.    0.  ]
 [ 1.22  1.22  0.  ]]
constant feature: [0.0, 0.0, 0.0]
```

把零标准差替换为 `1.0` 是这里明确选择的策略，不是普遍规则。另一个系统可能要删除常数特征或拒绝输入。数值代码必须把这种领域决定写进接口和测试。

### 区分视图与副本

基本切片 `scores[:, 1:]` 与原数组共享数据，布尔索引则创建副本。两次赋值看起来相似，却只有第一次回写到 `scores`。

```python
# file: view_copy.py
import numpy as np

scores = np.array(
    [[70, 80, 90], [60, 75, 85], [88, 92, 95]],
    dtype=np.int64,
)

last_two = scores[:, 1:]
selected = scores[scores[:, 0] >= 70]

print("slice shares memory:", np.shares_memory(scores, last_two))
print("mask shares memory:", np.shares_memory(scores, selected))

last_two[0, 0] = 999
selected[0, 0] = -1
print("source first row:", scores[0].tolist())
print("selected first row:", selected[0].tolist())
```

```text
slice shares memory: True
mask shares memory: False
source first row: [70, 999, 90]
selected first row: [-1, 80, 90]
```

需要隔离修改时，调用 `.copy()` 明确取得独立缓冲区。需要原地更新时，也要在函数名或文档中写明副作用。依赖调用者猜测所有权，会让一次看似局部的清洗污染上游数据。

### 隔离随机状态并求最小二乘解

`default_rng()` 返回独立的随机数生成器，不会重置模块级随机状态。这里用它生成训练与测试索引，再由 `np.linalg.lstsq()` 拟合含截距的一元线性模型。

```python
# file: random_lstsq.py
import numpy as np

rng = np.random.default_rng(42)
feature = np.array([0, 1, 2, 3, 4, 5], dtype=np.float64)
target = 1.0 + 2.0 * feature

order = rng.permutation(feature.size)
train_index = order[:4]
test_index = order[4:]

design = np.column_stack(
    [np.ones(train_index.size), feature[train_index]]
)
intercept, slope = np.linalg.lstsq(
    design, target[train_index], rcond=None
)[0]
predicted = intercept + slope * feature[test_index]

print("train index:", train_index.tolist())
print(f"model: y = {intercept:.1f} + {slope:.1f}x")
print("test index:", test_index.tolist())
print("prediction:", np.round(predicted, 6).tolist())
```

```text
train index: [3, 2, 5, 4]
model: y = 1.0 + 2.0x
test index: [1, 0]
prediction: [3.0, 1.0]
```

固定种子让这次运行可重复，但长期实验还应保存实际索引、NumPy 版本和所选位生成器。不要把「同一个种子」当作跨所有版本与算法的完整数据溯源。真实建模还要在分割前明确分层、分组和时间顺序等约束。

## 陷阱

> **陷阱:** 把「广播成功」当作「轴对齐正确」。形状 `(100, 1)` 与 `(100,)` 相加会产生 `(100, 100)`，即使本意只是给每个样本加一个值。

**修复方法：** 在边界处断言形状，并为每个轴写出含义。使用 `keepdims=True`、`np.newaxis` 或明确的 `reshape()` 表达要扩展的轴，再用一个很小的手算样本验证结果。

> **陷阱:** 认为切片总是独立数据，或者认为所有索引都返回视图。基本切片通常共享内存，高级索引通常复制数据，混合索引则更难凭外观判断。

**修复方法：** 需要隔离时显式 `.copy()`，需要确认重叠时调用 `np.shares_memory()`。对原地修改函数写测试，既检查返回值，也检查输入数组是否按契约改变。

> **陷阱:** 忽略固定宽度整数的溢出，或把浮点结果写回整数数组。数组运算遵循 NumPy 的类型规则，不会自动获得 Python 整数的任意精度。

**修复方法：** 在入口处选择足够宽的 `dtype`，用 `np.result_type()` 检查组合运算的结果类型。除法、归一化和累计求和前尤其要确认类型，并对最大值、最小值与负数编写边界测试。

> **陷阱:** 把 `np.empty()` 当作填充了空值或零的数组。它可能暴露该内存块原有的任意位模式，未覆盖位置的结果没有业务意义。

**修复方法：** 只有能证明每个元素都会在读取前赋值时才用 `empty()`。否则使用 `zeros()`、`full()` 或一个能表达缺失策略的明确初值。

> **陷阱:** 在库函数里调用全局 `np.random.seed()`。它会重置同一进程中其他代码共享的旧式随机状态，使测试结果依赖调用顺序。

**修复方法：** 接收一个 `Generator` 参数，或在函数内部用明确种子创建局部 `Generator`。实验需要跨进程重放时，还要保存抽样结果和位生成器信息。

> **陷阱:** 用 `==` 比较浮点计算结果，或让一个 `NaN` 悄悄传播到整列聚合中。二进制浮点舍入和缺失值策略是两个不同问题。

**修复方法：** 近似比较使用 `np.isclose()` 或 `np.allclose()`，并根据领域设置容差。先决定 `NaN` 应当拒绝、忽略还是传播，再选择 `mean()`、`nanmean()` 或显式验证；不要为了得到数字而默认忽略缺失值。

<!-- deep -->

## 内存布局和数据所有权

`ndarray` 把数据缓冲区与解释缓冲区的元数据分开。元数据包括 `shape`、`dtype`、`strides` 和从缓冲区起点算起的偏移。一个视图可以更改这些字段而不复制元素，所以转置、反向切片和间隔切片仍能看到同一批底层数据。

这种设计也意味着「数组连续」不是恒真条件。C 连续数组的最后一个轴通常相邻，Fortran 连续数组的第一个轴通常相邻；转置后的数组可能只满足其中一种，也可能都不满足。调用外部库前应查看其契约，只有接口确实要求时才用 `np.ascontiguousarray()` 物化一份连续副本。

`ravel()` 会尽量返回视图，但必要时也会复制；`flatten()` 总是返回副本。`reshape()` 在布局允许时可以返回视图，否则可能复制，某些形状与顺序组合还会失败。所有权重要时，不要用「这个函数一般不会复制」代替检查和测试。

广播视图可能让多个逻辑位置指向同一个元素。`np.broadcast_to()` 返回只读视图，避免一次赋值含糊地影响多个位置。算术表达式的输出通常是新数组，但 `out=` 参数和原地运算会改变所有权与类型转换要求，使用时应在接口中写明。

内存消耗可以从数组本身核对：`size * itemsize` 等于元素缓冲区的 `nbytes`。这不包含 Python 数组对象的少量元数据，也不代表一个视图拥有同等大小的新缓冲区。排查峰值内存时，要区分视图的逻辑大小、实际拥有的缓冲区，以及表达式创建的临时结果。

## 类型提升和数值边界

`dtype` 是数组运算的一部分，不只是存储选项。两个输入参与运算时，NumPy 根据其数据类型与 Python 标量的类别确定共同结果类型；`np.result_type()` 可以在不执行完整计算的情况下检查该决定。赋值回现有数组还要满足转换规则，因此浮点结果不能安全地原地写回整数数组。

固定宽度整数会在边界处溢出。累计值的范围往往比单个输入大，所以计数、像素和小整数特征即使能存进 `int8`，它们的和也可能需要更宽的类型。应根据最终运算范围选择类型，而不是只根据原始值选择。

浮点类型需要同时考虑范围、精度和特殊值。`NaN` 不等于自身，正负无穷可以通过 `np.isfinite()` 检出，接近零的分母会放大舍入误差。容差与缺失值策略来自问题本身；NumPy 提供检查工具，但不会替你决定什么结果可以接受。

数组 API 还会区分逐元素语义与线性代数语义。`a * b`、`a @ b` 和 `np.dot(a, b)` 在高维输入上不一定相同。公共函数应约束接受的维度，并对非方形批次编写测试，这比依赖二维示例推断高维行为可靠。

随机数也有状态和算法边界。把 `Generator` 作为参数传入，可以让函数的随机状态归调用者所有，测试也能注入固定生成器。需要精确复现实验时，应保存输入、实际抽样结果、位生成器与库版本，而不只记录一个整数种子。

## API 边界上的数组契约

### 输入验证

接收数组的公共函数应先决定它接受「任意 array-like 对象」，还是只接受 `ndarray`。`np.asarray()` 可以把列表等输入规范化为数组，并在输入已经兼容时避免不必要的复制。规范化之后再检查 `ndim`、`shape`、`dtype` 与有限值，错误信息才能对应统一表示。

不要只写 `assert array.shape[1] == 3`。普通断言可以在优化模式下移除，而且如果输入是一维数组，访问第二个轴会先抛出索引错误。应按顺序验证类型、维度、各轴长度和数值范围，并给调用者一条能修正输入的异常消息。

空数组需要单独的领域决定。某些逐元素变换可以自然返回空结果，而 `max()` 之类没有单位元的聚合无法从空输入得到答案。API 应说明空输入是被拒绝、返回空数组，还是使用显式的 `initial` 值。

可写性也是契约的一部分。把 `array.flags.writeable` 设为 `False` 可以阻止意外原地修改，但它不是安全边界，也不会冻结其他共享视图。真正需要隔离时仍要创建副本，并控制谁持有可写引用。

### 输出形状与所有权

函数文档应给出相对于输入的输出形状，例如「输入 `(samples, features)`，输出 `(features,)`」。只写「返回均值」不足以区分全局标量、逐样本均值和逐特征均值。类型注解目前也不能替代运行时的具体轴契约。

返回视图可以避免复制，但会把输入的生命周期和可变性暴露给调用者。返回副本则提供隔离，却分配新缓冲区。两种选择都合理，关键是稳定、可见，并由测试覆盖修改输入和修改输出后的行为。

如果函数接受 `out=` 参数，就要验证目标形状、可写性和转换安全性。重叠的输入与输出还可能改变计算含义。除非底层操作明确支持这种重叠，不要假设原地写入与先计算完整结果再赋值等价。

标量返回值也需要留意。某些 NumPy 聚合返回 NumPy 标量，例如 `np.float64`，它能参与大多数 Python 运算，但序列化库可能要求原生 `float` 或 `int`。在 JSON 或数据库边界做明确转换，比在数值核心到处调用 `.item()` 更清楚。

### 保存与交换

`.npy` 格式保存一个数组的形状、`dtype` 和数据，`.npz` 可以把多个具名数组放进同一归档。它们适合 NumPy 工作流中的精确往返，但不是供所有语言通用的表格协议。跨系统交换前要根据消费者选择格式，而不是只考虑写出是否方便。

对象数组可能依赖 Python pickle。加载不可信 pickle 数据会执行构造对象所需的代码，因此不应把 `allow_pickle=True` 当作修复未知文件错误的通用开关。优先保存数值、布尔或定长字符串类型，并保持加载不可信文件时禁用 pickle。

文本格式会丢失一部分类型与形状信息。CSV 不能自然表示任意维数组，也无法完整保留浮点格式、字节序或结构化 `dtype`。写出文本时应同时定义列、缺失值、精度和重建形状的规则。

与 Pandas、机器学习框架或本地扩展交换数组时，要检查是否复制、是否共享可写内存，以及设备和字节序要求。名称相似的转换方法可能有不同所有权语义。最可靠的接口测试会修改交换前后的一侧，并验证另一侧是否应当变化。

<!-- /deep -->

[检查点: datascience/numpy](https://codewiki.com/zh/datascience/numpy/#checkpoint)

## 延伸阅读

- [NumPy：初学者基础](https://numpy.org/doc/stable/user/absolute_beginners.html)
- [NumPy：广播](https://numpy.org/doc/stable/user/basics.broadcasting.html)
- [NumPy：副本与视图](https://numpy.org/doc/stable/user/basics.copies.html)
- [NumPy：随机数生成器](https://numpy.org/doc/stable/reference/random/generator.html)
- [NumPy：线性代数](https://numpy.org/doc/stable/reference/routines.linalg.html)
