目录172

NumPy

概述与安装

⚠️ Jupyter 中 import numpy 失败 ≠ 没装,通常是 kernel 与命令行不是同一个 Python。

⚠️ 本笔记面向 NumPy 2.x。2.0 起 np.float_/np.NaN 等别名已被移除,用 np.float64/np.nan;随机数一律用 np.random.default_rng()。

ndarray vs Python list

Python4 行
import numpy as np

arr = np.array([1, 2, 3, 4, 5])          # 输出: [1 2 3 4 5]
type(arr)                                # 输出: <class 'numpy.ndarray'>
特性 Python list ndarray
元素类型 可混合 必须同质(dtype 统一)
大小 动态 创建后固定
存储 指针数组,分散 单块连续内存
运算 需循环 / 推导式 向量化,整块算
多维 嵌套列表,需 a[i][j] 原生支持,用 a[i, j]
内存 大(每元素带对象头) 小(仅 itemsize × size)
适用 混合类型、频繁增删 数值计算、矩阵、大数据
Python6 行
lst = [1, 2, 3]
lst + [4]                                # 输出: [1, 2, 3, 4]  ← 拼接,不是相加
a = np.array([1, 2, 3]); a + 1           # 输出: [2 3 4]       ← 逐元素
np.array([1, 2.5, 3]).dtype              # 输出: float64(混合输入被向上提升)
np.array([1, 'a']).dtype                 # 输出: <U21(数字也被提升为字符串)
np.arange(1000).nbytes                   # 输出: 8000 字节(数据本体;list 还要额外存指针与对象头)

生态位置

NumPy 是 Python 数值栈的地基:Pandas 的 DataFrame.values 就是 ndarray;SciPy(优化/积分/信号)、Matplotlib(直接吃 ndarray)、Scikit-learn(输入输出均为 ndarray)、PyTorch/TensorFlow(torch.from_numpy ↔ .numpy())都建立在它之上。

Python2 行
np.__version__                           # 查看版本
np.show_config()                         # 查看 BLAS/LAPACK 后端

最小可运行骨架

Python10 行
import numpy as np

arr = np.array([1, 2, 3, 4, 5])
arr.ndim, arr.shape, arr.size, arr.dtype   # 输出: (1, (5,), 5, dtype('int32'))
arr + 10, arr * 2, arr ** 2                # 输出: [11 12 13 14 15] [ 2  4  6  8 10] [ 1  4  9 16 25]
arr.sum(), arr.mean(), arr.max(), arr.std()# 输出: 15  3.0  5  1.414...
arr[arr > 3]                               # 输出: [4 5](布尔索引)
m = np.array([[1, 2, 3], [4, 5, 6]])
m[1, 2], m.shape, m.T.shape                # 输出: 6  (2, 3)  (3, 2)
m.sum(axis=0), m.sum(axis=1)               # 输出: [5 7 9]  [ 6 15]

⚠️ Windows 上 np.array([1,2,3]).dtype 是 int32(C long 为 32 位),Linux/macOS 是 int64。写跨平台代码时显式写 dtype=np.int64,别依赖默认推断。

环境自检与向量化提速实测

装完 NumPy 后先跑一遍自检:确认版本号、默认整型位数,并用 perf_counter 实测「Python 列表推导式 vs ndarray 向量化」在同一运算上的耗时比。

Python15 行
import numpy as np
import time

n = 2_000_000
py_list = list(range(n))
arr = np.arange(n)

t0 = time.perf_counter(); _ = [x * 2 + 1 for x in py_list]; t_list = time.perf_counter() - t0
t0 = time.perf_counter(); res = arr * 2 + 1;              t_np   = time.perf_counter() - t0

np.__version__                       # 输出: '2.x.x'
arr.dtype, arr.nbytes                # 输出: (dtype('int64'), 16000000);Windows 常为 int32 → 8000000
np.array([1, 2, 3]).dtype            # 输出: dtype('int64')(跨平台代码请显式写 dtype)
round(t_list / t_np, 1)              # 输出: 约 10(本机实测;随机器浮动,但 t_np 恒更小)
res[:5]                              # 输出: [1 3 5 7 9]

要点:同样的逐元素运算,ndarray 用一条表达式替代循环,耗时差主要来自「Python 解释器开销」被下沉到 C 层。

成绩统计小脚本

一份 10 人的成绩表,要一次给出均分、中位数、标准差、及格率、优秀人数,以及不及格名单和前三名——这是 NumPy 最典型的入门场景:全程不写 for 循环。

Python15 行
import numpy as np

names = np.array(['赵一', '钱二', '孙三', '李四', '周五',
                  '吴六', '郑七', '王八', '冯九', '陈十'])
scores = np.array([72, 88, 55, 91, 64, 79, 83, 47, 95, 68])

scores.mean(), np.median(scores), round(scores.std(), 2)   # 输出: 74.2  75.5  15.04
(scores >= 60).mean() * 100                                # 输出: 80.0(及格率,布尔当 0/1 求均值)
(scores >= 90).sum(), (scores < 60).sum()                  # 输出: 2  2(优秀 / 不及格人数)
np.percentile(scores, [25, 75])                            # 输出: [65.   86.75](四分位)

order = scores.argsort()[::-1]                             # 输出: [8 3 1 6 5 0 9 4 2 7](降序下标)
names[order[:3]], scores[order[:3]]                        # 输出: (['冯九' '李四' '钱二'], [95 91 88])
names[scores < 60]                                         # 输出: ['孙三' '王八'](不及格名单)
scores[scores > scores.mean()]                             # 输出: [88 91 79 83 95](高于均分的成绩)

要点:布尔数组既可直接 .sum() 计数、也可 .mean() 求占比;argsort()[::-1] 是做排行榜的固定套路,用同一份下标同时索引姓名和分数。

ndarray 基础

本质与属性

ndarray = 同质 + 定长 + 连续内存 + 支持向量化 的多维容器。

属性 含义 示例值
shape 各维度长度(元组) (3, 4)
ndim 维度数(= len(shape)) 2
size 元素总数(= prod(shape)) 12
dtype 元素类型 int32 / float64
itemsize 单元素字节数 4
nbytes 总字节数(= size × itemsize) 48
T 转置(视图,不复制) —
flat 一维迭代器(可 arr.flat[3] = x 改值) —
real / imag 复数实部 / 虚部(视图) —
base 若为视图,指向的源数组;副本为 None —
Python5 行
import numpy as np

a = np.arange(12).reshape(3, 4)
a.shape, a.ndim, a.size, a.dtype, a.itemsize, a.nbytes   # 输出: ((3,4), 2, 12, int32, 4, 48)
np.shares_memory(a, a.T)                                  # 输出: True(转置是视图)

创建 1D / 2D / 3D 与形状直觉

Python6 行
import numpy as np

v = np.array([1, 2, 3])                       # 1D,shape (3,)
m = np.array([[1, 2, 3], [4, 5, 6]])          # 2D,shape (2, 3)  → 2 行 3 列
t = np.arange(24).reshape(2, 3, 4)            # 3D,shape (2, 3, 4) → 2 块 3×4
np.zeros((2, 3, 4)).ndim                      # 输出: 3

语义约定(务必记住,后面 axis 全靠它):

  • 图像:(高, 宽, 通道),如 (480, 640, 3);PyTorch 常用 (通道, 高, 宽)
  • 表格:(样本数, 特征数),如 (1000, 20)
  • 视频:(帧, 高, 宽, 通道)
  • 时间序列:(天, 小时, 传感器)

索引与切片

Python18 行
import numpy as np

a = np.array([10, 20, 30, 40, 50])
a[0], a[-1]                       # 输出: 10  50
a[1:4], a[::2], a[::-1]           # 输出: [20 30 40]  [10 30 50]  [50 40 30 20 10]
a[1:3] = [200, 300]               # 切片赋值就地生效

m = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])
m[1, 3]                           # 输出: 8(推荐写法,等价于 m[1][3] 但更快)
m[0], m[0, :]                     # 输出: [1 2 3 4](整行)
m[:, 0]                           # 输出: [1 5 9](整列)
m[:2, 1:3]                        # 输出: [[2 3] [6 7]](子矩阵)
m[:, 0] = [1, 2, 3]               # 整列赋值

t = np.arange(24).reshape(2, 3, 4)
t[1, 2, 3]                        # 输出: 23
t[0]                              # 输出: shape (3, 4) 的整块
t[:, 0, 0]                        # 输出: [ 0 12](所有块的同一位置)

axis:折叠法

axis=n = 沿第 n 维折叠掉,结果保留其余维度。

Python14 行
import numpy as np

m = np.array([[1, 2, 3],
              [4, 5, 6]])         # shape (2, 3)
m.sum()                           # 输出: 21(不指定 axis = 全展平)
m.sum(axis=0)                     # 输出: [5 7 9]  shape (3,)  折叠"行" → 每列一个值
m.sum(axis=1)                     # 输出: [ 6 15]  shape (2,)  折叠"列" → 每行一个值

t = np.arange(24).reshape(2, 3, 4)
t.sum(axis=0).shape               # 输出: (3, 4)
t.sum(axis=1).shape               # 输出: (2, 4)
t.sum(axis=2).shape               # 输出: (2, 3)
t.sum(axis=-1).shape              # 输出: (2, 3)(-1 = 最后一维)
t.mean(axis=(0, 1)).shape         # 输出: (4,)(多轴同时折叠)

记忆:axis=0 往下走(跨行),axis=1 往右走(跨列)。降维结果形状 = 原 shape 去掉该轴。

⚠️ keepdims=True 保留被折叠的轴(长度 1),便于后续广播:m.sum(axis=1, keepdims=True).shape → (2, 1),可直接做 m / m.sum(axis=1, keepdims=True);不加则会广播错位或报错。

⚠️ axis 越界抛 AxisError;一维数组只有 axis=0。

dtype

类型 字节 范围 / 用途
int8/16/32/64 1/2/4/8 整数;默认 32(Win) 或 64(Unix)
uint8 1 0–255,图像像素首选
float16/32/64 2/4/8 半/单/双精度;默认 float64,ML 常用 float32
bool_ 1 掩码
complex64/128 8/16 复数
<U10 4×10 定长 Unicode 字符串
Python10 行
import numpy as np

np.array([1, 2, 3], dtype=np.float32).dtype        # 输出: float32
np.array([1.9, 2.5, 3.1]).astype(np.int32)         # 输出: [1 2 3](截断,非四舍五入!)
np.array(['1', '2', '3']).astype(np.int32)         # 输出: [1 2 3]
np.array([True, False]).astype(np.int32)           # 输出: [1 0]
a8 = np.array([100], dtype=np.int8)
a8 + a8                                  # 输出: [-56] ← 数组间运算静默环绕,无警告
np.array([200], dtype=np.int16) + 100     # 输出: [300]
# np.array([200], dtype=np.int8)         # NumPy 2 起抛 OverflowError:200 装不进 int8

⚠️ 数组之间的整数溢出不报错、静默环绕:uint8 图像做 img + 50 会绕回变黑。先 img.astype(np.int32) 再算,最后 np.clip(...).astype(np.uint8)。 ⚠️ 但 Python 整数越界在 NumPy 2 起会抛 OverflowError(np.array([200], dtype=np.int8) 直接报错),这是 NEP 50 的行为变化——装不下就报错,反而是好事。

⚠️ 需要小数时先升类型:np.array([1,2,3]) / 2 结果是 float64;但 arr // 2 保整数。np.mean 对 int 数组也返回 float。

向量化运算与广播

广播的完整规则见后续章节

Python11 行
import numpy as np

a = np.array([1, 2, 3, 4, 5]); b = np.array([10, 20, 30, 40, 50])
a + b, a * b, b / a, a ** 2, a % 2    # 逐元素;输出: [11 22 33 44 55] [10 40 90 160 250] ...
np.sqrt(a), np.exp(a), np.log(a)      # 通用函数 ufunc,逐元素
a @ a                                 # 输出: 55(内积;二维数组 @ 才是矩阵乘法)
(a > 3), (a == 3), (a > 2) & (a < 5)  # 输出布尔数组;务必用 & | ~,不用 and/or/not

m = np.array([[1, 2, 3], [4, 5, 6]])
m + np.array([10, 20, 30])            # 输出: [[11 22 33] [14 25 36]](行向量广播到每行)
m + np.array([[10], [20]])            # 输出: [[11 12 13] [24 25 26]](列向量广播到每列)

广播规则:从尾部维度对齐,长度为 1 或缺失的维被拉伸。形状不兼容(如 (3,) 与 (2,))抛 ValueError。

⚠️ A * B 是逐元素积,矩阵乘法用 A @ B 或 np.matmul(A, B)。

易错点

Python11 行
import numpy as np

a = np.array([1, 2, 3])
# a.append(4)                          # AttributeError:ndarray 没有 append
np.append(a, 4)                        # 输出: [1 2 3 4],但返回新数组(整块复制,很慢)
np.concatenate([a, np.array([5, 6])])  # 输出: [1 2 3 5 6];批量追加才用它

v = a[1:3]; v[0] = 99                  # 切片是视图!a 变成 [1 99 3]
c = a[1:3].copy(); c[0] = 7            # 原数组不变
np.array_equal(a, b)                   # 判等;if (a == b) 会抛 "ambiguous"
(a == b).all()                         # 等价写法

⚠️ 需要频繁 append 时,先用 Python list 收集,最后一次性 np.array(list)。循环里 np.append 是 O(n²)。

⚠️ 函数内 arr[0] = x 会改到调用方的数组(传引用)。不想改就 arr = arr.copy() 再操作。

彩色图像灰度化与二值化

图像本质是 (高, 宽, 通道) 的三维数组。这里把一张 4×4 的 RGB 小图压成灰度、再做阈值二值化与反色,演示 axis=2 折叠通道与 uint8 下的类型控制。

Python15 行
import numpy as np

img = np.array([[[255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 255, 255]],
                [[0, 0, 0], [128, 128, 128], [200, 100, 50], [30, 60, 90]],
                [[255, 255, 0], [0, 255, 255], [255, 0, 255], [10, 20, 30]],
                [[100, 100, 100], [150, 50, 200], [80, 160, 40], [220, 220, 20]]],
               dtype=np.uint8)

img.shape, img.dtype, img[0, 0]          # 输出: (4, 4, 3)  uint8  [255   0   0](一个像素的 RGB)
img[:, :, 0]                             # 输出: R 通道,shape (4, 4)
gray = img.mean(axis=2)                  # 输出: shape (4, 4) 的 float64:[[ 85. 85. 85. 255.] ...]
gray_u8 = gray.round().astype(np.uint8)  # 输出: [[85 85 85 255] [0 128 117 60] [170 170 170 20] [100 133 93 153]]
binary = np.where(gray > 127, 255, 0).astype(np.uint8)   # 输出: 4×4 的 0/255 二值图
(binary > 0).mean()                      # 输出: 0.4375(前景像素占比)
255 - gray_u8                            # 输出: 反色图,uint8 内不会溢出;先 astype 再算更安全

要点:mean(axis=2) 即「沿通道折叠」,是 RGB→灰度的标准写法;uint8 直接做算术会环绕,提亮要先 astype(np.int32) 再 np.clip,np.where 的结果也要转回 uint8。

销售数据按维度聚合

7 天 × 4 个产品的销量表,需要按天汇总、按产品汇总、找每个产品的峰值日,以及筛出「所有产品都过百」的日子。核心是 axis 与 .all(axis=1) 的搭配。

Python17 行
import numpy as np

sales = np.array([[100, 120, 80, 90],     # 周一
                  [110, 130, 85, 95],     # 周二
                  [120, 125, 90, 100],    # 周三
                  [130, 140, 95, 105],    # 周四
                  [125, 135, 92, 102],    # 周五
                  [140, 150, 100, 110],   # 周六
                  [150, 160, 105, 115]])  # 周日   shape (7, 4)

sales.sum(axis=1)                          # 输出: [390 420 435 470 454 500 530](每天总销量)
sales.sum(axis=0)                          # 输出: [875 960 647 717](每个产品周销量)
sales.mean(axis=0).round(1)                # 输出: [125.  137.1  92.4 102.4](产品日均)
sales.argmax(axis=0) + 1                   # 输出: [7 7 7 7](各产品销量最高的天)
sales.max(axis=0) - sales.min(axis=0)      # 输出: [50 40 25 25](各产品周内波动幅度)
(sales > 100).all(axis=1)                  # 输出: [F F F F F F T](当天所有产品都 >100)
np.where((sales > 100).all(axis=1))[0] + 1 # 输出: [7](满足条件的天号)

要点:二维表里 axis=0 折叠行(得到每个产品)、axis=1 折叠列(得到每天);.all(axis=1) / .any(axis=1) 把逐元素判断压成逐行判断。

批量欧氏距离与最近邻

有 4 个已知点和 2 个查询点,要一次算出「每个查询点到所有已知点」的距离并找最近邻。靠广播把两层循环压成一个三维数组,是 axis 与广播的综合练习。

Python11 行
import numpy as np

points = np.array([[0, 0], [1, 1], [3, 4], [5, 2]])   # 4 个已知点,shape (4, 2)
query = np.array([[0, 1], [4, 4]])                    # 2 个查询点,shape (2, 2)

diff = query[:, None, :] - points[None, :, :]   # 广播:shape (2, 4, 2),每个查询点对每个已知点的坐标差
dist = np.sqrt((diff ** 2).sum(axis=-1))        # 输出: shape (2, 4):[[1. 1. 4.243 5.099] [5.657 4.243 1. 2.236]]
np.linalg.norm(diff, axis=-1)                   # 等价写法,省掉手动 sqrt
dist.argmin(axis=1)                             # 输出: [0 2](每个查询点的最近邻下标)
dist.min(axis=1)                                # 输出: [1. 1.](最近距离)
points[dist.argmin(axis=1)]                     # 输出: [[0 0] [3 4]](最近邻坐标)

要点:插入长度为 1 的轴(a[:, None, :] / a[None, :, :])让两批点两两配对,是写「批量距离/相似度矩阵」的通用手法。

创建数组

API 速查表

类别 函数 说明
转换 np.array(obj, dtype=) 列表/元组 → ndarray(复制)
np.asarray(obj) 同 array,输入已是 ndarray 时不复制
特殊值 np.zeros/ones/empty/full(shape, val) 0 / 1 / 未初始化 / 定值
np.zeros_like/ones_like/empty_like/full_like(a, val) 沿用 a 的 shape 与 dtype
序列 np.arange(start, stop, step) 定步长,不含 stop
np.linspace(start, stop, num) 定个数,含 stop;endpoint=False 去掉终点,retstep=True 返回步长
np.logspace(a, b, num, base=10) 对数等分(10^a → 10^b)
np.geomspace(start, stop, num) 几何等分,直接给端点值
矩阵 np.eye(n, m, k) / np.identity(n) 单位阵(eye 可非方、可偏移 k) / 方阵
np.diag(v, k=0) 一维→对角阵;二维→提取对角线
随机 rng = np.random.default_rng(seed) 见 3.6,完整清单见 §14
文件 np.loadtxt/savetxt/genfromtxt/save/savez/load 见 3.7,完整用法见 §15
网格 np.meshgrid(x, y) / np.ogrid[a:b:step] / np.indices(shape) 坐标网格
np.fromfunction(fn, shape) 按索引调用 fn 生成
重复 np.tile(a, reps) / np.repeat(a, n, axis=) 整体平铺 / 逐元素重复
拼接 np.concatenate([...], axis=) / np.vstack / np.hstack / np.stack 已有轴拼接 / 纵向 / 横向 / 新建轴

np.array 注意点

Python9 行
import numpy as np

np.array([1, 2, 3])                     # 输出: [1 2 3]
# np.array([[1, 2], [3, 4, 5]])          # ✗ NumPy ≥1.24 抛 ValueError(子列表长度不齐)
np.array([[1, 2], [3, 4, 5]], dtype=object)  # 显式 object 才能建:向量化失效,尽量别用
np.array([[1, 2, 0], [3, 4, 5]])        # 正确:补齐长度
np.array([1.5, 2.7], dtype=int)         # 输出: [1 2](截断)
np.array(range(10))                     # 输出: [0 1 ... 9](range 也可直接喂)
np.asarray(existing_arr)                # 已是 ndarray 时不复制,性能更好

特殊值数组

Python9 行
import numpy as np

np.zeros((3, 4))                        # 默认 float64;np.zeros(5, dtype=int) → 整型
np.ones((2, 3), dtype=np.int8)
np.empty((2, 3))                        # 未初始化,内容是内存垃圾
np.full((3, 4), 3.14)
np.zeros_like(a)                        # 同 shape/dtype
np.zeros_like(a, dtype=float)           # 同 shape,改 dtype(整型模板做归一化时必用)
np.full_like(a, 99)

⚠️ np.empty() 不是全零。它只是跳过初始化(大数组快几倍),必须紧接着写满;忘了填就会读到随机脏数据。

⚠️ *_like 会继承原 dtype。对 uint8 图像做 np.zeros_like(img) 后存归一化小数会全变 0,记得加 dtype=float。

数值序列

Python13 行
import numpy as np

np.arange(10)                           # 输出: [0 1 ... 9]
np.arange(5, 10)                        # 输出: [5 6 7 8 9]
np.arange(0, 10, 2)                     # 输出: [0 2 4 6 8]
np.arange(10, 0, -1)                    # 输出: [10 9 ... 1](递减)
np.arange(0, 1, 0.3)                    # 输出: [0.  0.3 0.6 0.9]  浮点步长有精度误差
np.linspace(0, 10, 5)                   # 输出: [ 0.   2.5  5.   7.5 10. ]  含终点
np.linspace(0, 10, 5, endpoint=False)   # 输出: [0. 2. 4. 6. 8.]
np.linspace(0, 1, 5, retstep=True)      # 输出: (array([0., .25, .5, .75, 1.]), 0.25)
np.logspace(0, 3, 4)                    # 输出: [   1.   10.  100. 1000.]
np.logspace(-3, 0, 4, base=2)           # 输出: 2^-3 → 2^0 等比
np.geomspace(1, 1000, 4)                # 输出: [   1.   10.  100. 1000.]

选型:要「每 2 个取一个」→ arange;要「正好 N 个点」→ linspace;跨数量级采样(学习率搜索)→ logspace。

⚠️ arange 元素个数 = ceil((stop-start)/step),受浮点误差影响可能多/少一个。凡是涉及浮点边界,改用 linspace。

单位阵与对角阵

Python9 行
import numpy as np

np.eye(3)                               # 3×3 单位阵
np.eye(3, 4)                            # 非方阵,对角线为 1
np.eye(4, k=1)                          # 对角线上移一位(k=-1 下移)
np.identity(3)                          # 仅方阵,等价于 np.eye(3)
np.diag([1, 2, 3])                      # 一维 → 对角阵
np.diag(np.array([[1,2,3],[4,5,6],[7,8,9]]))   # 输出: [1 5 9]  二维 → 提取对角线
np.diag([1, 2, 3], k=1)                 # 上对角(4×4)

随机数组(NumPy 2.x 推荐写法)

Python16 行
import numpy as np

rng = np.random.default_rng(seed=42)    # 显式生成器,替代全局 np.random.seed
rng.random((3, 4))                      # [0,1) 均匀分布
rng.uniform(-1, 1, size=5)              # 指定区间均匀分布
rng.integers(0, 10, size=(3, 4))        # 随机整数,[low, high) 左闭右开
rng.normal(loc=0, scale=1, size=5)      # 正态;rng.standard_normal(5) 即标准正态
rng.standard_normal((2, 3))             # 标准正态(替代旧的 randn)
rng.choice([1, 2, 3], size=10, p=[0.5, 0.3, 0.2])   # 按概率抽样
rng.choice(10, size=3, replace=False)   # 无放回抽样
rng.shuffle(arr)                        # 原地打乱(只打乱第 0 维),无返回值
rng.permutation(10)                     # 返回新排列,不改原数组
rng.binomial(n=10, p=0.5, size=5)       # 二项
rng.poisson(lam=5, size=5)              # 泊松(计数事件)
rng.exponential(scale=2.0, size=5)      # 指数(等待时间)
rng.beta(a=2, b=5, size=5); rng.gamma(2.0, 2.0, 5); rng.chisquare(df=5, size=5)

常用范式:

Python12 行
import numpy as np
rng = np.random.default_rng(42)

# 同步打乱 X 与 y
idx = rng.permutation(len(X)); X, y = X[idx], y[idx]

# 按权重初始化(He / Xavier)
he = rng.standard_normal((n_in, n_out)) * np.sqrt(2.0 / n_in)
xavier = rng.standard_normal((n_in, n_out)) * np.sqrt(2.0 / (n_in + n_out))

# 加噪声 + 裁剪
noisy = np.clip(signal + rng.normal(0, 0.1, signal.shape), 0, 1)

⚠️ 旧 API np.random.rand/randn/randint/seed 仍可用但已不推荐:它依赖全局状态,容易被第三方库悄悄改动,破坏可复现性。新项目一律 default_rng。

⚠️ rng.shuffle 返回 None,写 x = rng.shuffle(x) 会得到 None。要新数组用 rng.permutation。

文件读写 (完整用法见后续)

Python9 行
import numpy as np

np.loadtxt('d.csv', delimiter=',', skiprows=1, usecols=(1, 2), dtype=float)
np.genfromtxt('d.csv', delimiter=',', filling_values=0)      # 缺值填 0;默认填 nan
np.savetxt('out.csv', arr, delimiter=',', fmt='%.3f', header='a,b', comments='')
np.save('a.npy', arr); np.load('a.npy')                      # 单数组二进制,最快
np.savez('m.npz', w=W, b=b)                                  # 多数组(未压缩)
np.savez_compressed('m.npz', w=W, b=b)                       # 多数组(压缩)
d = np.load('m.npz'); d['w']; list(d.keys())                 # 惰性加载,用完 d.close()

选型:.Npy/.npz 保 dtype 与形状、无精度损失、读写快,用于中间结果和跨会话缓存;文本/CSV 用于与人或其他工具交换。loadtxt 遇到缺失值直接报错,改用 genfromtxt。

视图 vs 副本 (系统梳理见后续)

Python12 行
import numpy as np

a = np.arange(6)
b = a                    # 赋值:同一对象(b is a → True)
v = a[1:4]               # 视图:切片、reshape、ravel、transpose、T 都可能返回视图
c = a.copy()             # 副本:独立内存

v[0] = 99                # a 被改 → [0 99 2 3 4 5]
c[0] = 99                # a 不变
v.base is a              # 输出: True
c.base is None           # 输出: True
np.shares_memory(a, v)   # 输出: True(最可靠的判定方式)
  • 产生视图:基本切片、reshape(能不复制时)、ravel、T/transpose
  • 产生副本:花式索引、布尔索引、astype、flatten、copy、跨步太大无法视图化的 reshape

⚠️ 循环里复用模板数组:arr = template; arr[0] = i; lst.append(arr) 会把同一个对象 append 多次,最后全部等于最后一次的值。必须 template.copy()。

网格、重复与拼接 (拼接与分割的完整规则见后续)

Python18 行
import numpy as np

x = np.linspace(-5, 5, 10); y = np.linspace(-5, 5, 10)
X, Y = np.meshgrid(x, y)                # 输出: 均为 (10, 10),X 按行铺、Y 按列铺
Z = X**2 + Y**2                         # 二维函数直接算
oy, ox = np.ogrid[0:100, 0:100]         # 稀疏网格,形状 (100,1) 与 (1,100),省内存
np.indices((3, 3))                      # 输出: shape (2,3,3),[0] 行索引、[1] 列索引
np.fromfunction(lambda i, j: (i+1)*(j+1), (9, 9), dtype=int)   # 九九乘法表
np.outer(np.arange(1, 10), np.arange(1, 10))                   # 同上,更快

np.tile([1, 2, 3], 3)                   # 输出: [1 2 3 1 2 3 1 2 3](整体平铺)
np.repeat([1, 2, 3], 3)                 # 输出: [1 1 1 2 2 2 3 3 3](逐元素重复)
np.repeat([1, 2, 3], [2, 3, 4])         # 输出: [1 1 2 2 2 3 3 3 3]

np.concatenate([a, b], axis=0)          # 沿已有轴拼接(要求其余维度一致)
np.vstack([a, b]) / np.hstack([a, b])   # 纵向 / 横向(更直观)
np.stack([a, b], axis=0)                # 新建维度堆叠,shape 多一维
np.column_stack([x, y])                 # 一维 → (n, 2) 的列矩阵

⚠️ tile 与 repeat 别混:tile 重复整个数组,repeat 重复每个元素。

⚠️ concatenate 要求非拼接轴形状完全一致;对一维数组用 vstack 会先升成 (1, n)。批量拼接优先攒成 list 后一次性 concatenate,不要在循环里反复调用。

实战:蒙特卡洛估算圆周率

在 [-1, 1)² 里均匀撒点,落在单位圆内的比例约为 π/4,乘 4 即得 π 的估计值。这个例子同时练随机数组生成、布尔统计,以及「样本量越大越准」的直觉。

Python12 行
import numpy as np

rng = np.random.default_rng(42)

for n in (10**3, 10**5, 10**6):
    pts = rng.random((n, 2)) * 2 - 1              # shape (n, 2),两列分别是 x、y
    inside = (pts ** 2).sum(axis=1) <= 1          # 布尔数组:是否在单位圆内
    round(inside.mean() * 4, 4)                   # 输出: 3.116 → 3.1362 → 3.1446(n 越大越接近 3.1416)

pts = rng.random((2_000_000, 2)) * 2 - 1
pi_est = ((pts ** 2).sum(axis=1) <= 1).mean() * 4
round(pi_est, 4), round(abs(pi_est - np.pi), 5)   # 输出: 3.1406  0.00103(每次运行都在该量级浮动)

要点:rng.random((n, 2)) 一次生成二维样本,配合 axis=1 的聚合与 .mean(),把「模拟 + 统计」压成两行;固定 seed 才能复现同一串结果。

3.11 股价随机游走与最大回撤

用正态分布的日收益率累积成一条价格路径,再算期末价格、历史最高价和最大回撤。这是 cumprod 与 np.maximum.accumulate 的典型用法。

Python14 行
import numpy as np

rng = np.random.default_rng(2024)
ret = rng.normal(0.0005, 0.02, size=250)              # 250 个交易日收益率,shape (250,)
price = np.concatenate([[100.0], 100.0 * np.cumprod(1 + ret)])   # 输出: shape (251,),首日为 100

price[:5].round(2)                                    # 输出: [100.   102.11 105.51 107.98 105.94]
round(price[-1], 2), round(price.max(), 2), round(price.min(), 2)   # 输出: 103.0  121.29  84.01
round(ret.std() * np.sqrt(250), 4)                    # 输出: 0.3098(年化波动率)

peak = np.maximum.accumulate(price)                   # 截至每天的历史最高价,shape (251,)
drawdown = (price - peak) / peak                      # 当前回撤,≤ 0
round(drawdown.min(), 4), drawdown.argmin()           # 输出: -0.3074  213(最大回撤 30.74%,第 213 天)
(price > 100).mean().round(3)                         # 输出: 0.721(收盘价高于起点的天数占比)

要点:cumprod 把收益率序列变成价格路径,np.maximum.accumulate 是「滚动最大值」,两者相除即得回撤曲线。

生成模拟回归数据集并切分

做算法验证时经常要自己造一份带噪声的线性数据:特征来自标准正态,标签是真实权重的线性组合加噪声,最后加偏置列并按 8 随机切分。

Python14 行
import numpy as np

rng = np.random.default_rng(0)
n, d = 200, 4
X = rng.normal(size=(n, d))                       # 输出: shape (200, 4)
w_true = np.array([2.0, -1.0, 0.5, 0.0])
y = X @ w_true + 3.0 + rng.normal(0, 0.3, size=n)  # 输出: shape (200,),@ 是矩阵乘(不是逐元素)

Xb = np.column_stack([np.ones(n), X])             # 加一列 1 当截距项,shape (200, 5)
idx = rng.permutation(n)                          # 输出: 0~199 的随机排列
train_idx, test_idx = idx[:160], idx[160:]
X_train, y_train = Xb[train_idx], y[train_idx]    # 输出: shape (160, 5) / (160,)
X_test, y_test = Xb[test_idx], y[test_idx]        # 输出: shape (40, 5) / (40,)
np.corrcoef(X[:, 0], y)[0, 1].round(3)            # 输出: 0.871(第一列特征与标签的相关性)

要点:default_rng 固定种子 + permutation 一次生成索引并复用,既保证可复现,也保证 X/y 同步打乱(分别打乱两次会让特征与标签错位)。

数组属性与方法

属性速查(属性无括号,方法有括号)

属性 说明
shape / ndim / size 形状 / 维度数 / 元素总数
dtype / itemsize / nbytes 类型 / 单元素字节 / 总字节
T 转置(视图);arr.T 对 1D 无效果,3D 会完全反转轴序
flat 一维迭代器(arr.flat[0] = x 可写)
real / imag 复数实/虚部,均为视图
base / flags 视图来源 / C_CONTIGUOUS 等内存布局标志
Python5 行
import numpy as np

a = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]], dtype=np.int32)
a.shape, a.ndim, a.size, a.itemsize, a.nbytes, a.T.shape   # 输出: ((3,4), 2, 12, 4, 48, (4,3))
a.T[0, 0] = 100                                             # 改转置会改到 a(视图!)

astype:类型转换

Python8 行
import numpy as np

arr_int = np.array([1, 2, 3, 4, 5])
arr_int.astype(np.float64)               # 输出: [1. 2. 3. 4. 5.]
np.array([1.7, 2.3, 3.9]).astype(np.int32)   # 输出: [1 2 3](向零截断)
np.array([0, 1, 2]).astype(np.bool_)     # 输出: [False  True  True]
np.array(['1', '2']).astype(np.int32)    # 输出: [1 2];非数字字符串抛 ValueError
arr_int.astype('float64')                # 字符串写法等价

⚠️ astype 总是返回新数组(即使 dtype 相同也复制),大数组有可观的内存与时间开销。

⚠️ 归一化套路:img.astype(np.float32) / 255.0。直接 uint8_img / 255 也能出 float64,但多一次隐式复制。

统计方法速查 (axis 语义与各统计量的细节见后续)

方法(数组上) 函数形式 说明
sum() / prod() np.sum(a) 求和 / 求积
mean() np.mean(a) 算术平均
std() / var() 同 标准差 / 方差(std = sqrt(var))
max() / min() 同 最值
argmax() / argmin() 同 最值索引(不指定 axis 时是展平后的索引)
cumsum() / cumprod() 同 累计和 / 累计积
— np.median(a) 中位数(无方法形式)
— np.percentile(a, q) / np.quantile(a, q) 分位数(q=50 即中位数)
— np.average(a, weights=w) 加权平均
— np.corrcoef(x, y) 相关系数矩阵
Python13 行
import numpy as np

a = np.array([3, 1, 4, 1, 5, 9, 2, 6])
a.max(), a.min(), a.argmax(), a.argmin()      # 输出: 9  1  5  1
a.sum(), a.mean(), a.var(), round(a.std(), 3) # 输出: 31  3.875  6.609  2.571
a.cumsum()                                    # 输出: [ 3  4  8  9 14 23 25 31]
np.median(a), np.percentile(a, 25)            # 输出: 3.5  1.75

m = np.array([[1, 2, 3], [4, 5, 6]])
m.sum(), m.sum(axis=0), m.sum(axis=1)         # 输出: 21  [5 7 9]  [ 6 15]
m.argmax()                                    # 输出: 5(展平索引)
np.unravel_index(m.argmax(), m.shape)         # 输出: (1, 2)  ← 还原多维下标,必记
m.argmax(axis=0)                              # 输出: [1 1 1](每列最大值所在行)

⚠️ 中位数对极端值稳健、均值不稳健:[1,2,3,4,100] 均值 22、中位数 3。

布尔索引与掩码 (系统梳理见后续 )

Python16 行
import numpy as np

a = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])
mask = a > 5                          # 输出: [F F F F F T T T T]
a[mask]                               # 输出: [6 7 8 9]
a[(a > 3) & (a < 7)]                  # 输出: [4 5 6](与)
a[(a < 3) | (a > 7)]                  # 输出: [1 2 8 9](或)
a[~(a > 5)]                           # 输出: [1 2 3 4 5](非)
(a > 5).sum()                         # 输出: 4(True 计数,最常用)
(a > 5).any(), (a > 0).all()          # 输出: True  True

m2 = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
m2[m2 > 5]                            # 输出: [6 7 8 9](二维布尔索引 → 一维)
m2[m2[:, 0] > 3]                      # 输出: [[4 5 6] [7 8 9]](按行筛选)

a2 = a.copy(); a2[a2 % 2 == 0] = 0    # 输出: [1 0 3 0 5 0 7 0 9](就地改满足条件的)

⚠️ 多条件必须用 & | ~ 且每个子句加括号:(a > 3) & (a < 7)。and/or/not 会对数组求布尔值,抛 ValueError: ambiguous。

np.where 三种用法 (另见切片)

Python11 行
import numpy as np

a = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])
np.where(a > 5)                       # 输出: (array([5, 6, 7, 8]),)  ← 元组,取 [0] 得索引
np.where(a > 5, a, 0)                 # 输出: [0 0 0 0 0 6 7 8 9]   ← 三元:真取 a,假取 0
np.where(a < 4, '小', np.where(a > 6, '大', '中'))   # 输出: 多分类,嵌套

# 异常值替换为正常均值
data = np.array([10, 999, 20, 30, 999, 40])
ok = data < 100
np.where(ok, data, data[ok].mean())   # 输出: [10 25 20 30 25 40]

np.where 与布尔索引的选择:只要筛元素 → a[a > 5];要同时给两种取值 → np.where;要索引 → np.where(cond) 或 np.argwhere(cond)(argwhere 对二维返回 (n, 2) 坐标,更适合多维)。

排序、去重、裁剪、舍入

Python22 行
import numpy as np

a = np.array([3, 1, 4, 1, 5, 9, 2, 6])
a.sort()                              # 原地排序,返回 None
np.sort(a)                            # 返回新数组,原数组不变
np.sort(a)[::-1]                      # 降序(先升后反转)
a.argsort()                           # 输出: 排序后的原始索引 [1 3 6 0 2 4 7 5]
a[a.argsort()]                        # 排序结果
a.argsort()[::-1]                     # 降序排名索引;配合名字数组做排行榜

m = np.array([[3, 1, 4], [1, 5, 9], [2, 6, 5]])
m.sort(axis=1)                        # 每行内排序(原地);np.sort(m, axis=0) 每列排序

np.unique(np.array([1, 2, 2, 3, 3, 3]))                     # 输出: [1 2 3]
vals, cnts = np.unique(np.array([1,2,2,3]), return_counts=True)   # 输出: [1 2 3]  [1 2 1]
np.unique(x, return_index=True)       # 首次出现下标
np.unique(x, return_inverse=True)     # 逆索引,可用 vals[inv] 重建原数组

np.array([1, 2, 3, 4, 5, 6, 7, 8, 9]).clip(3, 7)   # 输出: [3 3 3 4 5 6 7 7 7]
np.round(np.array([1.234, 2.567]), 2)               # 输出: [1.23 2.57]
np.round(np.array([123, 456]), -2)                  # 输出: [100 500](负数位 = 整数位)
np.floor([1.7]), np.ceil([1.2]), np.trunc([1.9])    # 输出: [1.] [2.] [1.]

⚠️ a.sort() 返回 None,b = a.sort() 得到 None。要保留原数组用 np.sort(a)。

⚠️ np.round 采用银行家舍入(四舍六入五取偶):np.round(2.5) → 2.0,np.round(3.5) → 4.0。

形状变换 (系统梳理见形状操作)

Python11 行
import numpy as np

a = np.arange(6)
a.reshape(2, 3)                       # 返回视图(元素连续时),原数组不变
a.reshape(2, -1)                      # -1 表示自动推断,只能有一个 -1
a.resize(2, 3)                        # 原地改形状,无返回值(用得少)
m = np.arange(6).reshape(2, 3)
m.flatten()                           # 一维副本(改它不影响 m)
m.ravel()                             # 一维,能视图就视图(改它可能影响 m!)
m.T                                   # 转置视图
m.transpose(1, 0)                     # 显式轴重排;3D 用 transpose(2, 0, 1) 做 HWC→CHW

⚠️ 需要独立的一维副本时用 flatten();ravel() 是视图,改结果会改回原数组。

打印与调试

Python7 行
import numpy as np

np.set_printoptions(precision=3, suppress=True)   # 3 位小数 + 禁用科学计数法
np.set_printoptions(threshold=20)                 # 超过 20 个元素就省略中间
np.set_printoptions(edgeitems=3, linewidth=120)   # 省略时保留的首尾个数 / 行宽
np.set_printoptions()                             # 恢复默认
np.info(np.ndarray.reshape)                       # 查看签名的简要文档

易错点汇总

⚠️ 属性不带括号,方法带括号:arr.shape vs arr.sum()。写成 arr.shape() 会抛 TypeError: 'tuple' object is not callable。

⚠️ argmax 无 axis 时返回展平索引。多维数组要还原下标必须 np.unravel_index(arr.argmax(), arr.shape)。

⚠️ 别用 Python 内置 sum/max 处理 ndarray:sum(arr) 走 Python 循环,慢 1–2 个数量级且对多维会返回错误结果。用 arr.sum() / np.sum(arr)。

⚠️ 空数组与非数值:np.array([]).max() 抛错;含 np.nan 时 mean/max 全变 nan,改用 np.nanmean / np.nanmax / np.nan_to_num。

⚠️ sort / shuffle / resize 是原地操作,返回 None;对应返回新对象的是 np.sort / rng.permutation / reshape。

BMI 批量计算与健康分类

给一组身高体重算 BMI 并分级,再统计各类人数、挑出需要关注的人。这是嵌套 np.where 做多分类 + np.unique 做频次统计的标准组合。

Python14 行
import numpy as np

heights = np.array([1.75, 1.80, 1.65, 1.70, 1.78, 1.68, 1.82])
weights = np.array([70, 85, 55, 90, 75, 60, 95])

bmi = weights / heights ** 2                       # 输出: [22.9 26.2 20.2 31.1 23.7 21.3 28.7]
category = np.where(bmi < 18.5, '偏瘦',
                    np.where(bmi < 24, '正常',
                             np.where(bmi < 28, '偏胖', '肥胖')))
category                                           # 输出: ['正常' '偏胖' '正常' '肥胖' '正常' '正常' '肥胖']
np.unique(category, return_counts=True)            # 输出: (['偏胖' '正常' '肥胖'], [1 4 2])
np.where((bmi < 18.5) | (bmi >= 28))[0] + 1        # 输出: [4 7](需要关注的人员编号)
heights[category == '肥胖'].round(2), bmi[category == '肥胖'].round(1)   # 输出: [1.7  1.82]  [31.1 28.7]
round(bmi.mean(), 2)                               # 输出: 24.86(整体均值)

要点:np.where 从最严条件往外嵌套(边界只写一侧),分类结果交给 np.unique(..., return_counts=True) 直接出频次表。

成绩排名与分数段分布

一份 12 人的成绩,要给出降序名次、前几名,以及各分数段人数。argsort 套两次即可得到名次,np.histogram 负责分档计数。

Python11 行
import numpy as np

scores = np.array([85, 92, 78, 91, 68, 74, 88, 55, 95, 82, 79, 90])

rank = (-scores).argsort().argsort() + 1   # 输出: [ 6  2  9  3 11 10  5 12  1  7  8  4](1 = 最高分)
top3 = scores.argsort()[::-1][:3]          # 输出: [8 1 3](前三名的下标)
scores[top3]                               # 输出: [95 92 91]
np.sort(scores)[::-1][:3]                  # 输出: [95 92 91](只要分数、不要下标时的简写)
np.percentile(scores, [25, 50, 75])        # 输出: [77.   83.5  90.25]
np.histogram(scores, bins=[0, 60, 70, 80, 90, 101])[0]   # 输出: [1 1 3 3 4](<60 / 60-69 / 70-79 / 80-89 / 90+)
(scores >= scores.mean()).sum()            # 输出: 7(达到均分的人数)

要点:argsort().argsort() 把「排序下标」再排序一次即得名次;加负号实现降序排名,np.histogram 的 bins 直接给边界即可分档。

实战:Z-score 标准化与离群点检测

传感器读数里混进了异常值,先用 Z-score 找出偏离均值 2 个标准差以上的点,再用正常值均值替换(或用分位数做缩尾),最后检查清洗后的分布。

Python13 行
import numpy as np

data = np.array([23.5, 25.1, 24.8, 26.2, 23.9, 25.5, 24.1, 60.0, 22.8, 25.9])
z = (data - data.mean()) / data.std()
z.round(2)                                     # 输出: [-0.44 -0.29 -0.32 -0.19 -0.4 -0.25 -0.38 2.99 -0.5 -0.21]

outlier = np.abs(z) > 2                        # 输出: 只有下标 7(值 60.0)为 True
np.where(outlier)[0], data[outlier]            # 输出: [7]  [60.]
clean = np.where(outlier, data[~outlier].mean(), data)   # 用正常值均值 24.64 替换
zc = (clean - clean.mean()) / clean.std()
zc.round(2)                                    # 输出: [-1.12  0.44  0.15  1.52 -0.73  0.83 -0.53  0.  -1.8   1.22]
np.abs(zc).max().round(2)                      # 输出: 1.8(清洗后没有超过 2σ 的点)
np.clip(data, *np.percentile(data, [5, 95]))   # 缩尾替代方案:把超界值压到 5%/95% 分位,60.0 → 44.79

要点:(x - x.mean()) / x.std() 就是 Z-score,np.where(mask, 替代值, 原数组) 与 np.clip 是清洗异常值的两种收尾方式;注意均值/标准差本身会被异常值带偏,脏数据更严重时改用中位数与四分位距。

索引与切片

Python1 行
import numpy as np

基本索引与切片

Python14 行
arr = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

arr[2]        # 2
arr[-1]       # 9,负索引从 -1 开始
arr[2:5]      # [2 3 4],左闭右开
arr[:3]       # [0 1 2]
arr[5:]       # [5 6 7 8 9]
arr[::2]      # [0 2 4 6 8]
arr[1::2]     # [1 3 5 7 9]
arr[::-1]     # [9 8 7 6 5 4 3 2 1 0],反转
arr[-5:-2]    # [5 6 7]
arr[2:-2]     # [2 3 4 5 6 7]
arr[10:15]    # [],切片越界不报错,返回空
arr[10]       # IndexError
Python4 行
arr = np.arange(10)
arr[2:5] = 99          # [0 1 99 99 99 5 6 7 8 9],标量广播
arr[0:3] = [10, 20, 30]  # 长度必须匹配,否则 ValueError
arr[0:5] = arr[5:10]   # 切片赋切片,先算右侧(副本语义)

⚠️ 整数数组赋值会截断:a = np.array([1,2,3]); a[0] = 3.9 → 3。

多维索引

Python10 行
a = np.arange(1, 13).reshape(3, 4)

a[1, 2]      # 7,推荐 [行, 列];a[1][2] 等价但更慢
a[-1, -1]    # 12
a[0]         # [1 2 3 4],整行,等价于 a[0, :]
a[:, 0]      # [1 5 9],整列
a[1:3, 1:3]  # [[6 7], [10 11]]
a[::2, ::2]  # 隔行隔列
a[:, ::-1]   # 列反转
a[0:1, 0:1]  # [[1]],切片保维度;a[0, 0] 得到标量

三维按 [页, 行, 列] 索引;... 自动补全中间所有 ::

Python6 行
b = np.arange(24).reshape(2, 3, 4)
b[0, 1, 2]      # 6
b[0]            # 第 0 页 (3, 4)
b[:, 0, :]      # 所有页第 0 行 (2, 4)
b[..., -1]      # 等价于 b[:, :, -1],取最后一列
b[0, ...]       # 等价于 b[0]

核心直觉:索引(整数)降维,切片(:)保维。

布尔索引

布尔索引返回副本(一维化)。

Python6 行
a = np.arange(1, 11)
a[a > 5]                      # [ 6  7  8  9 10]
a[(a > 3) & (a < 8)]          # [4 5 6 7],必须加括号
a[(a < 3) | (a > 8)]          # [1 2 9 10]
a[~((a >= 3) & (a <= 7))]     # [1 2 8 9 10]
a[((a < 3) | (a > 8)) & (a % 2 == 0)]  # [2 10]
运算 数组写法 等价函数
与 a & b np.logical_and
或 a | b np.logical_or
非 ~a np.logical_not
异或 a ^ b np.logical_xor

⚠️ 布尔组合只能用 & | ~,不能用 and/or/not(对数组求真值会 ValueError);且 &/| 优先级高于比较符,每个条件必须加括号。

二维数组上布尔索引:

Python5 行
m = np.arange(1, 13).reshape(3, 4)
m[m > 6]            # [ 7  8  9 10 11 12],直接拍平成一维
m[m[:, 0] > 5]      # [[9 10 11 12]],行掩码按行筛选
m[m % 2 == 0] = 0   # 条件赋值,原地修改原数组
m[m < 0] = 0        # 数据清洗:负值截断为 0

np.where

Python11 行
a = np.arange(1, 11)
np.where(a > 5, 99, a)          # [1 2 3 4 5 99 99 99 99 99]
np.where(a > 5, 'big', 'small') # 字符串数组
np.where(a > 5)[0]              # [5 6 7 8 9],单参数形式 = 满足条件的索引
np.where(a > 5, a, -a)          # 等价于把 <=5 的取负

# 多分支嵌套(分段映射)
scores = np.array([85, 92, 78, 65])
np.where(scores >= 90, 'A',
         np.where(scores >= 80, 'B',
                  np.where(scores >= 70, 'C', 'D')))  # ['B' 'A' 'C' 'D']

⚠️ np.where(cond, x, y) 会同时计算 x 和 y,不能用来规避除零/越界;这类场景用布尔赋值 out[mask] = ...。

花式索引

整数数组索引,返回副本,可重复、可乱序。

Python10 行
a = np.array([10, 20, 30, 40, 50, 60])
a[[0, 2, 4]]      # [10 30 50]
a[[0, 0, 2]]      # [10 10 30],可重复
a[[-1, -2]]       # [60 50],支持负索引

m = np.arange(1, 17).reshape(4, 4)
m[[0, 2]]                 # 取 0、2 行
m[:, [1, 3]]              # 取 1、3 列
m[[0, 1, 2], [0, 1, 2]]   # [1 6 11],元素对:等价于 [m[0,0], m[1,1], m[2,2]]
m[[0, 2], :][:, [1, 3]]   # 链式取子矩阵(两次复制)

⚠️ m[[0,1],[0,1]] 取的是 (0,0) 和 (1,1) 两个元素,不是 2×2 子矩阵。取子矩阵用切片 m[0:2, 0:2] 或 np.ix_。

np.ix_ / take / put

Python12 行
m = np.arange(1, 17).reshape(4, 4)

m[np.ix_([0, 2], [1, 3])]   # [[2 4], [10 12]],笛卡尔积取子矩阵
np.ix_([0, 2], [1, 3])      # (array([[0],[2]]), array([[1, 3]]))

np.take(m, [0, 2], axis=0)        # 等价于 m[[0, 2]],axis 可省(按展平取)
np.take(m, [1, 3], axis=1)        # 取 1、3 列
np.take_along_axis(m, np.array([[0], [1], [2], [3]]), axis=1)  # (4,1) 逐行取指定列

a = np.arange(6)
np.put(a, [0, 2], [99, 88])  # 原地写入,无返回值 → [99 1 88 3 4 5]
a.put([0, 2], [7, 8])        # 方法形式等价

视图 vs 副本

操作 返回 说明
基本切片 a[1:4]、a[::2] 视图 改它即改原数组
整数索引 a[0]、a[0, 1] 视图(标量除外) 单行/单列仍是视图
布尔索引 a[a>5] 副本 结果形状不定,必须复制
花式索引 a[[0,2]] 副本 位置不连续,无法用视图表示
a.copy() 副本 显式复制
Python7 行
a = np.array([1, 2, 3, 4, 5])
v = a[1:4]; v[0] = 99      # a → [1 99 3 4 5],视图联动
c = a[[1, 2]].copy(); c[0] = 0   # a 不变

v.base is a       # True,视图的 base 指向源
c.base is None    # True
v.flags.owndata   # False,视图不拥有数据

⚠️ 函数内 sub = data[10:20]; sub *= 2 会改到调用方数组;要独立就写 .copy()。链式切片(视图的视图)同样指向原始数据。

⚠️ 布尔/花式索引返回副本,所以 a[a > 5] *= 2 不会改原数组;要原地改请写 a[a > 5] = a[a > 5] * 2 或 np.where 整体赋值。

常用组合技

Python11 行
a = np.arange(12)
i = np.argsort(a)          # 排序索引
a[np.random.permutation(len(a))]        # 打乱
a[np.random.choice(len(a), 5, replace=False)]  # 无放回抽样

m = np.random.randint(0, 256, (10, 10))
np.argwhere(m == 0)        # 满足条件的坐标数组,形状 (n, ndim)
np.unravel_index(np.argmax(m), m.shape)   # 扁平索引 → 多维坐标
np.ravel_multi_index((2, 3), m.shape)     # 多维坐标 → 扁平索引
m[[0, 1, 2], [0, 1, 2]]                   # 主对角线(np.diag 更快)
m[[0, 1], [1, 0]]                         # 副对角线

性能:连续区域用切片(快、零拷贝),任意位置用花式索引,条件筛选用布尔索引;避免 Python 循环。

灰度图像的区域读写

图像就是二维数组,裁剪、翻转、打码、过曝修正都可以只靠索引完成。这里演示读取子区域、提取边框、几何翻转,以及用布尔掩码批量改像素。

Python30 行
import numpy as np

rng = np.random.default_rng(0)
img = rng.integers(0, 256, size=(8, 8), dtype=np.uint8)
img[0]                       # 输出: [ 95 130 194 217 207 235  15 163]

# 1. 取区域:切片是视图,改子图会改原图
center = img[2:-2, 2:-2]     # 输出: shape (4, 4),去掉 2 像素边缘
corner = img[:3, :3]         # 左上角 3×3

# 2. 取边框:四条边拼一维,用 1:-1 避免角点重复计数
border = np.concatenate([img[0, :], img[-1, :], img[1:-1, 0], img[1:-1, -1]])
border.size                  # 输出: 28,等于 8*4-4

# 3. 几何变换全是切片/转置,零拷贝
flip_v = img[::-1, :]        # 上下翻转
flip_h = img[:, ::-1]        # 左右翻转
rot90 = img.T[::-1, :]       # 逆时针 90°:转置后上下翻

# 4. 提亮:uint8 直接加会环绕溢出,必须先升位再 clip
(img[0, :6] + np.uint8(60))                            # 输出: [155 190 254  21  11  39]  ← 溢出
np.clip(img[0, :6].astype(np.int16) + 60, 0, 255)      # 输出: [155 190 254 255 255 255]

# 5. 掩码批量改:过曝像素压成纯白
mask = img > 200
mask.sum()                   # 输出: 14,满足条件的像素数
img[mask] = 255              # 布尔索引赋值是原地写入

# 6. 打码:右下角 3×3 涂黑
img[-3:, -3:] = 0

要点:切片取区域、::-1 做翻转、布尔掩码改像素,整套图像操作无需循环,但 uint8 运算前要先 astype 防溢出。

一周逐小时温度表的切片筛选

数据是 7 天 × 24 小时的温度矩阵,行是日期、列是小时。按行切“某天”,按列切“某时段”,再用布尔条件定位高温时刻。

Python28 行
import numpy as np

rng = np.random.default_rng(42)
temp = rng.uniform(15, 30, size=(7, 24)).round(1)   # (7, 24)

# 1. 按维度切:行=天,列=小时
noon = temp[:, 12]           # 输出: [24.7 16.9 25.  15.3 16.4 23.7 19.2],每天正午
night = temp[:, :6]          # 输出: shape (7, 6),每天 0-5 点
workday = temp[:5]           # 前 5 天
temp[::2, ::6]               # 输出: shape (4, 4),隔天、每 6 小时抽样

# 2. 按行聚合定位极值
daily = temp.mean(axis=1).round(2)
daily                        # 输出: [24.04 22.18 21.79 21.46 22.93 21.79 22.21]
daily.argmax()               # 输出: 0,第 1 天最热
temp[daily.argmax()].max()   # 那天的峰值温度

# 3. 条件定位:布尔数组既能计数也能给坐标
hot = temp > 28
hot.sum()                    # 输出: 14,高温小时数
np.argwhere(hot)[:3]         # 输出: [[ 0  5] [ 0 11] [ 0 22]],(第几天, 第几小时)
hot.any(axis=1)              # 输出: [ True  True False False  True  True  True],哪天出现过高温
hot.sum(axis=1)              # 每天的高温小时数

# 4. 数据清洗:低温截断,原地生效
(temp < 18).sum()            # 输出: 35
temp[temp < 18] = 18.0
temp.min()                   # 输出: 18.0

要点:二维时序数据用 [天, 小时] 双维切片定位子集,argwhere 拿坐标、any/sum(axis=...) 按行汇总,布尔赋值做清洗。

成绩表的筛选、排名与重排

5 名学生 × 3 门课的成绩表。行掩码筛人、argwhere 定位弱科、argsort 排名后用花式索引重排整张表,take_along_axis 取每人最强科。

Python36 行
import numpy as np

scores = np.array([[85, 90, 78],    # 学生 0
                   [92, 88, 95],
                   [78, 85, 80],
                   [88, 92, 90],
                   [95, 87, 92]])   # 列:数学 / 英语 / 科学

avg = scores.mean(axis=1).round(2)
avg                            # 输出: [84.33 91.67 81.   90.   91.33]

# 1. 行掩码:一维掩码长度 = 行数,筛出整行
scores[avg >= 90]              # 输出: [[92 88 95] [88 92 90] [95 87 92]]
np.where(avg >= 90)[0]         # 输出: [1 3 4],对应学号

# 2. 元素级条件 → 坐标,定位"谁的哪门课"不及格
np.argwhere(scores < 80)       # 输出: [[0 2] [2 0]],(学号, 课程号)
(scores >= 80).all(axis=1)     # 输出: [False  True False  True  True],全科过线

# 3. 排名:argsort 取负实现降序,再用花式索引重排
total = scores.sum(axis=1)
total                          # 输出: [253 275 243 270 274]
rank = np.argsort(-total)
rank                           # 输出: [1 4 3 0 2],按总分从高到低的学号
scores[rank][:2]               # 输出: [[92 88 95] [95 87 92]],前两名整行

# 4. 每人最强科:argmax 给列号,take_along_axis 按列号取值
best = scores.argmax(axis=1)
best                           # 输出: [1 2 1 1 0]
np.take_along_axis(scores, best[:, None], axis=1).ravel()   # 输出: [90 95 85 92 95]

# 5. 补课模拟:先 copy 再改,别动原表
fixed = scores.copy()
fixed[fixed < 80] += 10
fixed[0]                       # 输出: [85 90 88]
scores.mean(axis=0).round(2)   # 输出: [87.6 88.4 87. ],各科平均分

要点:行掩码筛样本、argwhere 给坐标、argsort + 花式索引整表重排、take_along_axis 按索引数组逐行取值。

形状操作

Python1 行
import numpy as np

API 速查

函数 作用 元素数 返回
a.reshape(shape) 改变形状 必须不变 通常视图
a.resize(shape) 原地改形状 可变,补 0 None(原地)
np.resize(a, shape) 改变形状 可变,循环填充 新数组
a.ravel() 展平 不变 通常视图
a.flatten() 展平 不变 总是副本
a.T / a.transpose(axes) 转置 / 重排轴 不变 视图
a.swapaxes(i, j) 交换两轴 不变 视图
np.squeeze(a, axis) 去掉长度为 1 的轴 不变 视图
np.expand_dims(a, axis) 插入长度为 1 的轴 不变 视图
np.newaxis / None 索引位升维 不变 视图

reshape

Python8 行
a = np.arange(12)
a.reshape(3, 4)      # 3×4
a.reshape(2, 2, 3)   # 三维
a.reshape(-1, 4)     # (3, 4),-1 = 自动推断,只能写一个
a.reshape(3, -1)     # (3, 4)
a.reshape(-1)        # (12,),常用展平写法
a.reshape(5, -1)     # ValueError:12 不能被 5 整除
a.reshape(-1, -1)    # ValueError:只能有一个 -1

⚠️ reshape 不原地修改,a.reshape(2, 3) 不赋值等于白写。

⚠️ reshape 一般返回视图,但源数组非连续(如转置后)时会退化成副本;不要依赖它,需要独立就显式 .copy()。

Python3 行
imgs = np.random.rand(100, 28, 28)
imgs.reshape(100, -1)   # (100, 784),保持批次维展平
imgs.reshape(-1)        # (78400,) 全部拉平

展平:ravel vs flatten

Python5 行
a = np.array([[1, 2, 3], [4, 5, 6]])
a.ravel()               # [1 2 3 4 5 6],视图,改它改原数组
a.flatten()             # [1 2 3 4 5 6],副本
a.ravel(order='F')      # [1 4 2 5 3 6],列优先;默认 order='C' 行优先
a.reshape(-1)           # 与 ravel 同序,但优先返回视图

转置与轴交换

Python15 行
a = np.arange(6).reshape(2, 3)
a.T              # (3, 2),二维等价于 a.transpose()
a.T.strides      # 只换 stride,不搬数据,所以是视图

b = np.arange(24).reshape(2, 3, 4)
b.transpose()        # (4, 3, 2),默认反转所有轴
b.transpose(2, 0, 1) # (4, 2, 3),指定新轴顺序
b.swapaxes(0, 1)     # (3, 2, 4)
b.swapaxes(1, 2)     # (2, 4, 3)

img = np.random.rand(100, 200, 3)
img.transpose(2, 0, 1)              # HWC → CHW,(3, 100, 200)
imgs = np.random.rand(32, 224, 224, 3)
imgs.transpose(0, 3, 1, 2)          # NHWC → NCHW
imgs.swapaxes(1, 3).swapaxes(2, 3)  # 同上,分步交换

维度增删

Python12 行
a = np.array([1, 2, 3])
np.expand_dims(a, axis=0)   # (1, 3)
np.expand_dims(a, axis=1)   # (3, 1)
np.expand_dims(a, axis=-1)  # (3, 1)
a[np.newaxis, :]            # (1, 3),索引写法
a[:, None]                  # (3, 1),None 与 newaxis 等价

c = np.array([[[1, 2, 3]]])   # (1, 1, 3)
np.squeeze(c)                 # (3,)
np.squeeze(c, axis=0)         # (1, 3)
np.squeeze(c, axis=(0, 1))    # (3,)
np.squeeze(np.zeros((2, 2)))  # (2, 2),无长度为 1 的轴则原样返回

⚠️ np.squeeze 不加 axis 会去掉所有长度为 1 的轴,batch=1 时会把批次维挤没;安全做法是指定 axis。

分块与窗口(reshape 典型用法)

Python8 行
m = np.arange(1_000_000).reshape(1000, 1000)
blocks = m.reshape(10, 100, 10, 100).swapaxes(1, 2)   # (10, 10, 100, 100)
blocks[3, 5].shape       # (100, 100),与 m[300:400, 500:600] 等价
blocks.sum(axis=(2, 3))  # (10, 10) 每块求和

ts = np.arange(1000); w = 10
idx = np.arange(w) + np.arange(len(ts) - w + 1)[:, None]   # (991, 10)
ts[idx]                  # 滑动窗口矩阵,比循环快得多

视图与副本速判

返回视图 返回副本
切片、转置、reshape(通常)、ravel(通常)、swapaxes、单行/列索引、squeeze、expand_dims flatten、.copy()、花式索引、布尔索引、算术运算结果、np.resize
Python12 行
x = np.arange(10)
y = x[2:5]
y.base is x        # True
y.flags.owndata    # False

def is_view(src, obj):        # 兼容链式视图
    cur = obj
    while cur.base is not None:
        if cur.base is src:
            return True
        cur = cur.base
    return False

⚠️ b = a 只是绑定同一个对象,不是复制;要 a.copy() 或 np.array(a, copy=True)。

易错点

  • resize 两套语义别混:np.resize(a, (3,3)) 返回新数组、循环重复填充;a.resize((3,3)) 原地改、缺位补 0。
  • 需要填充/截断时优先 np.pad、np.tile、np.repeat、切片,少用 resize。
  • 转置本身几乎免费,但按非连续轴聚合(如大矩阵 sum(axis=0))缓存不友好;高频列操作可先 a.T.copy()。

图像增强并堆成训练批次

数据增强的翻转、转置、通道调序全是切片,几乎零成本;最后用 np.stack 新建一个批次轴把变体合成一批,再按框架需要转成 NCHW 或展平。

Python21 行
import numpy as np

rng = np.random.default_rng(1)
img = rng.random((32, 32, 3))     # 单张 RGB 图 (H, W, C)

variants = [
    img,                          # 原图
    img[:, ::-1, :],              # 水平翻转(沿宽度反向)
    img[::-1, :, :],              # 垂直翻转(沿高度反向)
    img.transpose(1, 0, 2),       # 对角翻转:只换 H/W,通道不动
    img[:, :, ::-1],              # 通道翻转 RGB → BGR
]
aug = np.stack(variants)                  # 输出: shape (5, 32, 32, 3),stack 新建轴
aug.transpose(0, 3, 1, 2).shape           # 输出: (5, 3, 32, 32),NHWC → NCHW
aug.reshape(len(aug), -1).shape           # 输出: (5, 3072),保批次维展平送全连接层

np.shares_memory(aug, img)                # 输出: False,stack 一定复制
np.allclose(aug[1].mean(), img.mean())    # 输出: True,翻转不改变统计量

# 追加一张到已有批次:concatenate 沿已有轴接,需要先补出批次维
np.concatenate([img[None], aug]).shape    # 输出: (6, 32, 32, 3)

要点:stack 造新轴(得到批次)、concatenate 接已有轴(扩批次)、transpose 换通道顺序,三者配合就是完整的批数据整形链。

滑动窗口构造序列模型输入

时序预测要把一维序列切成“窗口 → 下一时刻”的样本对。sliding_window_view 直接返回零拷贝视图,比手写索引矩阵更省内存。

Python23 行
import numpy as np
from numpy.lib.stride_tricks import sliding_window_view

ts = np.arange(1000, dtype=float)     # 1000 个时间步
w = 24                                # 窗口长度

win = sliding_window_view(ts, w)
win.shape                             # 输出: (977, 24),等于 1000-24+1
np.shares_memory(win, ts)             # 输出: True,视图不占额外内存
win.flags.writeable                   # 输出: False,只读;要改先 .copy()

# 对齐标签:win[i] = ts[i:i+24],预测目标是 ts[i+24]
X, y = win[:-1], ts[w:]
X.shape, y.shape                      # 输出: ((976, 24), (976,))
X[..., None].shape                    # 输出: (976, 24, 1),补特征维给 LSTM

# 按 batch 分组:先截成整数倍再 reshape
bs = 32
n = len(X) // bs                      # 输出: 30
X[:n * bs][..., None].reshape(n, bs, w, 1).shape   # 输出: (30, 32, 24, 1)

win.mean(axis=1)[:3]                  # 输出: [11.5 12.5 13.5],每个窗口均值
win[-1][-1]                           # 输出: 999.0,最后一个窗口覆盖到序列末尾

要点:sliding_window_view 零拷贝生成窗口矩阵(只读),配 [..., None] 补特征维、reshape 分批,就是序列模型的标准输入流程。

灰度图升维成 RGB 与单样本推理

模型输入常要求固定维数:单张灰度图得补出 batch 与 channel 轴,推理完再降回来。squeeze 不指定 axis 会一次挤掉所有长度 1 的轴,这是最常见的坑。

Python22 行
import numpy as np

gray = np.arange(16, dtype=float).reshape(4, 4)     # 单通道图 (H, W)

# 1. 升维成三通道:先补通道轴,再沿该轴复制
rgb = np.repeat(gray[:, :, None], 3, axis=2)
rgb.shape                                    # 输出: (4, 4, 3)
np.array_equal(rgb, np.stack([gray] * 3, axis=-1))    # 输出: True,两种写法等价

# 2. 三通道转回灰度:末维与权重 (3,) 对齐,矩阵乘直接降维
back = rgb @ np.array([0.299, 0.587, 0.114])
back.shape, np.allclose(back, gray)          # 输出: ((4, 4), True),权重和为 1

# 3. 补 batch 与 channel 轴喂模型
x = gray[None, ..., None]
x.shape                                      # 输出: (1, 4, 4, 1)
pred = x * 2                                 # 占位:模型前向

# 4. 降维三种写法,注意差别
pred[0, ..., 0].shape                        # 输出: (4, 4),显式索引最安全
np.squeeze(pred).shape                       # 输出: (4, 4),batch 和 channel 一起被挤掉
np.squeeze(pred, axis=-1).shape              # 输出: (1, 4, 4),只去通道,保住批次

要点:[None, ..., None] 一次补齐首尾轴,降维时指定 axis 或用显式索引,避免 squeeze() 把 batch=1 的批次维一起挤没。

数组运算与广播

Python1 行
import numpy as np

运算符与等价 ufunc

类别 运算符 等价函数
算术 + - * / // % ** add subtract multiply divide floor_divide mod power
比较 == != < <= > >= equal not_equal less less_equal greater greater_equal
逻辑 & | ~ ^ logical_and logical_or logical_not logical_xor
Python15 行
a = np.array([1, 2, 3, 4])
b = np.array([10, 20, 30, 40])

a + b        # [11 22 33 44]
a * b        # [10 40 90 160],元素级
a ** 2       # [1 4 9 16]
a / 2        # [0.5 1. 1.5 2.],结果转 float
a // 2       # [0 1 1 2]
a + 10       # [11 12 13 14],标量广播

a > 2                 # [False False  True  True]
(a > 1) & (a < 4)     # [False  True  True False]
np.all(a > 0), np.any(a > 3)   # (True, True)

a += 10               # 原地;a = a + 10 会新建数组

⚠️ 原地运算会改变 dtype 之外的语义:a //= 2 对浮点数组合法,但对整数数组赋浮点值会被截断。

广播规则

规则(三步)

  1. 把两个形状右对齐,维度少的在左侧补 1。
  2. 从右向左逐维比较:两维相等,或其中一个是 1 → 兼容;否则报错。
  3. 结果为各维的最大值;维度为 1 的轴沿该轴复制(不真的复制数据)。
Python5 行
(3, 4) + (4,)      # → (3,4)+(1,4) → (3,4)   ✓
(3, 1) + (4,)      # → (3,1)+(1,4) → (3,4)   ✓ 两轴都扩展
(2, 3, 4) + (4,)   # → (2,3,4)+(1,1,4) → (2,3,4)  ✓
(3, 4) + (3,)      # → (3,4)+(1,3):末维 4 vs 3 ✗ ValueError
(2, 3) + (4, 3)    # 首维 2 vs 4 ✗ ValueError

典型报错

ValueError: operands could not be broadcast together with shapes (3,4) (3,)

修法:把 (3,) 变成 (3, 1)(列方向)或 (1, 3)(行方向),再运算。

Python5 行
m = np.ones((3, 4))
m + np.arange(4)                    # (3, 4),末维对齐 → 按行加
m + np.array([1, 2, 3])             # ValueError:末维 4 vs 3
m + np.array([1, 2, 3])[:, None]    # (3, 4),升维成 (3,1) → 按列加
m + np.array([1, 2, 3]).reshape(3, 1)   # 同上,reshape 写法

升维:np.newaxis / None

Python8 行
v = np.array([1, 2, 3])
v.shape             # (3,)
v[np.newaxis, :]    # (1, 3) 行向量
v[:, None]          # (3, 1) 列向量
v[None, :, None]    # (1, 3, 1)

v + v[:, None]      # (3, 3) 外积式加法
v.reshape(-1, 1) * v.reshape(1, -1)   # (3, 3) 外积

方向语义速记

Python3 行
m = np.arange(1, 7).reshape(2, 3)
m + np.array([10, 20, 30])        # 按行走:[[11 22 33], [14 25 36]]
m + np.array([[10], [20]])        # 按列走:[[11 12 13], [24 25 26]]

⚠️ 一维数组 (3,) 默认对齐到最后一维,即按行广播;想按列必须显式升维。

典型应用

Python21 行
data = np.arange(12).reshape(4, 3).astype(float)

# 每列中心化:mean 形状 (3,),天然按行广播
data - data.mean(axis=0)
# 每行中心化:用 keepdims 或 [:, None]
data - data.mean(axis=1, keepdims=True)

# 标准化
(data - data.mean(0)) / data.std(0)

# 距离矩阵(成对欧氏距离)
P = np.array([[0, 0], [1, 1], [2, 2]])
d = np.sqrt(((P[:, None, :] - P[None, :, :]) ** 2).sum(-1))   # (3, 3)

# RGB 通道增益
img = np.random.rand(5, 5, 3)
img * np.array([1.2, 0.8, 1.0])      # (5,5,3),最后一维对齐

# 加权总分
scores = np.array([[80, 90, 85], [75, 85, 90]])
scores * np.array([0.3, 0.4, 0.3])   # 按列权重

矩阵乘法

需求 写法
元素级乘 a * b / np.multiply
矩阵乘 A @ B / np.matmul / np.dot
向量点积 v1 @ v2 / np.dot(v1, v2) / (v1*v2).sum()
向量外积 np.outer(v1, v2)
Python9 行
A = np.array([[1, 2], [3, 4]]); B = np.array([[5, 6], [7, 8]])
A * B        # [[5 12] [21 32]]
A @ B        # [[19 22] [43 50]]
np.dot(A, B) # 同上

v1, v2 = np.array([1, 2, 3]), np.array([4, 5, 6])
v1 @ v2                 # 32
np.outer(v1, v2)        # (3, 3)
np.linalg.norm(v1)      # 3.7416...,等价 np.sqrt((v1**2).sum())

⚠️ * 绝不是矩阵乘法。np.matmul 与 np.dot 在 二维以上行为不同:matmul 按栈批量矩阵乘(广播前导维),dot 做最后一轴与前一阵倒数第二轴的点积。高维运算优先 @。

聚合与 axis

Python10 行
m = np.arange(1, 10).reshape(3, 3)
m.sum()                  # 45
m.sum(axis=0)            # [12 15 18] 按列
m.sum(axis=1)            # [6 15 24] 按行
m.sum(axis=1, keepdims=True)   # [[6] [15] [24]],保维度便于广播
m.mean(), m.std(), m.var()
m.max(axis=1), m.argmax(), m.argmin()
np.cumsum(m)             # 展平累积和
np.cumprod(np.arange(1, 5))    # [1 2 6 24]
np.diff(m, axis=1)       # 沿列方向一阶差分

比较与裁剪

Python6 行
a = np.array([1, 5, 3, 8, 2]); b = np.array([2, 3, 6, 4, 7])
np.maximum(a, b)        # [2 5 6 8 7]
np.minimum(a, b)        # [1 3 3 4 2]
np.maximum(0, a)        # ReLU
np.clip(a, 2, 6)        # [2 5 3 6 2]
np.clip(a, 2, None)     # 只限下界

易错点

Python7 行
0.1 + 0.2 == 0.3        # False
np.isclose(0.1 + 0.2, 0.3)   # True
np.allclose(x, y)            # 数组整体近似比较

a = np.array([1, 2, 3]); b = a   # 同一对象
b += 10                          # a 也变成 [11 12 13]
b = a.copy()                     # 正确做法

⚠️ 聚合后一维化会让后续广播方向出错,习惯性加 keepdims=True。

⚠️ 对整数数组做 / 得到 float;大整数 ** 可能溢出,先 .astype(float) 或 np.int64。

特征矩阵按列 Min-Max 归一化

样本按行、特征按列存放时,列统计量形状是 (1, n_feat),能直接广播回原矩阵。常量列的区间为 0,必须做除零保护,否则整列变 nan。

Python24 行
import numpy as np

X = np.array([[1.0,  50.0, 3.0],
              [2.0, 100.0, 3.0],
              [4.0, 200.0, 3.0],
              [8.0, 400.0, 3.0]])     # 第 3 列是常量列

lo = X.min(axis=0, keepdims=True)     # 输出: [[ 1. 50.  3.]],形状 (1, 3)
hi = X.max(axis=0, keepdims=True)
span = hi - lo                        # 输出: [[  7. 350.   0.]],常量列区间为 0

safe = np.where(span == 0, 1.0, span)  # 除零保护:区间为 0 的列除以 1
Xn = (X - lo) / safe                   # (4,3) 与 (1,3) 广播
Xn[:, 0]                               # 输出: [0.       0.142857 0.428571 1.      ]
Xn[:, 2]                               # 输出: [0. 0. 0.],常量列归零而非 nan
Xn.min(axis=0), Xn.max(axis=0)         # 输出: ([0. 0. 0.], [1. 1. 0.])

# 反归一化:保存 lo / safe 就能还原,推理阶段必须复用训练集的统计量
np.allclose(Xn * safe + lo, X)         # 输出: True

# 换成按行归一化:一维结果无法按列对齐,必须 keepdims
X.min(axis=1, keepdims=True).shape     # 输出: (4, 1)
np.ptp(X, axis=1, keepdims=True).ravel()   # 输出: [ 49.  98. 197. 397.]
X - X.min(axis=1)                      # ✗ ValueError: shapes (4,3) (4,)  ← 少 keepdims

要点:列统计量 (1, n) 天然按行广播,行统计量必须 keepdims=True 变 (n, 1);除零处理用 np.where 替换分母而不是事后补 nan。

按行 softmax 与交叉熵(数值稳定版)

logits 数值一大,np.exp 直接溢出成 inf。先减去每行最大值(广播 (n,1)),结果不变但绝不溢出,这是所有框架的标准实现。

Python24 行
import numpy as np

logits = np.array([[   1.0,    2.0,    3.0],
                   [1000.0, 1001.0, 1002.0]])   # 第 2 行数值极大

np.exp(logits).sum(axis=1)        # 输出: [30.192875  inf],直接算就溢出

# 稳定写法:减行最大值,(2,3) - (2,1) 广播
z = logits - logits.max(axis=1, keepdims=True)
p = np.exp(z)
p /= p.sum(axis=1, keepdims=True)
p[0]                              # 输出: [0.090031 0.244728 0.665241]
p[1]                              # 输出: [0.090031 0.244728 0.665241],softmax 平移不变
p.sum(axis=1)                     # 输出: [1. 1.]

# 交叉熵:花式索引只取标签位置的概率
labels = np.array([2, 0])
picked = p[np.arange(len(labels)), labels]
picked                            # 输出: [0.665241 0.090031]
-np.log(picked).mean()            # 输出: 1.4076059644443804

# log-softmax:连 exp 归一化都省了,训练里更常用
logp = z - np.log(np.exp(z).sum(axis=1, keepdims=True))
np.allclose(logp, np.log(p))      # 输出: True

要点:x - x.max(axis=1, keepdims=True) 是 softmax 的防溢出标准前处理,配 arange + 标签的花式索引即可取出每行目标概率。

样本到聚类中心的距离矩阵与最近邻分配

(N,1,D) 与 (1,K,D) 广播出 (N,K,D) 的全部差向量,一步得到 N×K 距离矩阵,argmin 就是 K-means 的分配步骤。样本量大时改用平方展开式,避免生成三维中间数组。

Python22 行
import numpy as np

pts = np.array([[0.0, 0.0], [0.2, 0.1], [5.0, 5.0], [5.2, 4.8], [10.0, 0.0]])
ctr = np.array([[0.0, 0.0], [5.0, 5.0], [9.0, 1.0]])     # 3 个中心

diff = pts[:, None, :] - ctr[None, :, :]
diff.shape                        # 输出: (5, 3, 2),(5,1,2) 与 (1,3,2) 广播
d = np.sqrt((diff ** 2).sum(axis=-1))
d.round(3)                        # 输出: [[ 0.  7.071  9.055] ... [10. 7.071 1.414]],(5,3)

label = d.argmin(axis=1)
label                             # 输出: [0 0 1 1 2],每个样本归到最近中心
d.min(axis=1).round(3)            # 输出: [0.    0.224 0.    0.283 1.414]
np.bincount(label, minlength=3)   # 输出: [2 2 1],各簇样本数

# 更新中心(K-means 的一次迭代)
new_ctr = np.stack([pts[label == k].mean(axis=0) for k in range(len(ctr))])
new_ctr                           # 输出: [[ 0.1  0.05] [ 5.1  4.9 ] [10.   0.  ]]

# 大数据量写法:|a-b|² = |a|² - 2a·b + |b|²,只需 (N,K) 内存
d2 = (pts ** 2).sum(1)[:, None] - 2 * pts @ ctr.T + (ctr ** 2).sum(1)[None, :]
np.allclose(np.sqrt(np.maximum(d2, 0)), d)   # 输出: True,clip 负数抵消浮点误差

要点:升维广播得到全部样本对的差值,(N,1,D)-(1,K,D) 是距离矩阵的通用套路;内存吃紧时换平方展开式用矩阵乘替代三维中间量。

外积构造价目表与函数网格

一个 (m,1) 乘一个 (1,n) 就是外积,天然得到 m×n 的“组合表”。这种写法可以替代双层循环,也能替代 meshgrid 直接算二元函数值。

Python24 行
import numpy as np

qty = np.array([1, 5, 10, 50])          # 数量档
price = np.array([2.5, 8.0, 19.9])      # 三种单价

table = qty[:, None] * price[None, :]   # (4,1) × (1,3) → (4,3)
table                                   # 输出: [[2.5 8. 19.9] ... [125. 400. 995.]]
np.allclose(table, np.outer(qty, price))    # 输出: True,np.outer 是同一件事

# 再叠一层按数量档的折扣:折扣是列向量 (4,1),沿列广播
disc = np.array([1.0, 0.98, 0.95, 0.9])[:, None]
final = (table * disc).round(2)
final[1]                                # 输出: [12.25 39.2  97.51]
final.sum(axis=0)                       # 输出: [ 151.    483.2  1201.96],各单价档合计
final.sum(axis=1)                       # 输出: [  30.4   148.96  288.8  1368.  ],各数量档合计

# 二元函数网格:无需 meshgrid,(1,5) 与 (3,1) 直接广播
x = np.linspace(-1, 1, 5)
y = np.array([1.0, 2.0, 3.0])
Z = np.exp(-x[None, :] ** 2) * y[:, None]
Z.shape                                 # 输出: (3, 5)
Z[0].round(4)                           # 输出: [0.3679 0.7788 1.     0.7788 0.3679]
xx, yy = np.meshgrid(x, y)
np.allclose(Z, np.exp(-xx ** 2) * yy)   # 输出: True,但 meshgrid 会真的展开成两个 (3,5)

要点:a[:, None] * b[None, :] 就是外积,一行生成组合表;算二元函数时用广播代替 meshgrid 可省两份完整网格的内存。

多通道图像白平衡与逐通道标准化

(H, W, 3) 的末维是通道,与 (3,) 的增益天然对齐,一行完成逐通道调色。求通道统计量时对 axis=(0,1) 聚合并 keepdims,得到 (1,1,3) 才能广播回去。

Python31 行
import numpy as np

rng = np.random.default_rng(7)
img = rng.integers(0, 256, size=(4, 4, 3)).astype(np.float32)
img[0, 0]                          # 输出: [241. 160. 175.]

# 1. 白平衡:末维 3 与增益 (3,) 对齐 → 逐通道乘系数
gain = np.array([1.15, 1.0, 0.9], dtype=np.float32)
wb = np.clip(img * gain, 0, 255)
wb[0, 0]                           # 输出: [255.  160.  157.5],R 提亮后被 clip 截住

# 2. 逐通道标准化:在 H、W 上聚合,保住通道轴
cm = img.mean(axis=(0, 1), keepdims=True)
cs = img.std(axis=(0, 1), keepdims=True)
cm.shape                           # 输出: (1, 1, 3)
cm.ravel().round(2)                # 输出: [175.81  97.44 144.19]
cs.ravel().round(2)                # 输出: [67.41 56.04 75.25]
norm = (img - cm) / cs
norm.mean(axis=(0, 1)).round(6)    # 输出: [ 0. -0.  0.]

# 3. 转灰度:通道加权求和,末维被吃掉
gray = img @ np.array([0.299, 0.587, 0.114], dtype=np.float32)
gray.shape, gray[0].round(2)       # 输出: ((4, 4), [185.93 177.92  98.74  90.41])

# 4. 批量图像 (N,H,W,C):右对齐后 (1,1,3) 自动补成 (1,1,1,3)
batch = rng.integers(0, 256, size=(2, 4, 4, 3)).astype(np.float32)
(batch - cm).shape                             # 输出: (2, 4, 4, 3),全局均值直接用
per = batch.mean(axis=(1, 2), keepdims=True)   # 输出: shape (2, 1, 1, 3),每张图各自的通道均值
(batch - per).shape                            # 输出: (2, 4, 4, 3)

img * np.array([1.15, 1.0])        # ✗ ValueError: shapes (4,4,3) (2,)  ← 长度 2 无法对齐

要点:通道在末维时 (3,) 系数可直接广播;聚合出的通道统计量要用 axis=(0,1) + keepdims=True 保成 (1,1,3),才能同时套用到单图和批量数据。

数学函数

Python1 行
import numpy as np

全部为向量化 ufunc,直接作用于数组,支持广播与 out= 原地写入(np.sin(a, out=a))。

三角函数

Python9 行
d = np.array([0, 30, 45, 60, 90])
r = np.radians(d)            # 等价 np.deg2rad;反向 np.degrees / np.rad2deg
np.sin(r)                    # [0. 0.5 0.707 0.866 1.]
np.cos(r)                    # [1. 0.866 0.707 0.5 ~0]
np.tan(r)                    # 90° 处为 1.6e16(cos→0)
np.arcsin / np.arccos / np.arctan        # 反函数,返回弧度
np.arctan2(y, x)             # 带象限的反正切,值域 (-π, π]
np.sinh / np.cosh / np.tanh  # 双曲函数;cosh²-sinh²=1
np.degrees(np.arctan(1))     # 45.0

⚠️ 三角函数只吃弧度,传入角度要先 np.radians。

指数与对数

函数 含义 备注
np.exp(x) eˣ
np.exp2(x) 2ˣ
np.expm1(x) eˣ−1 x 极小时精度远高于 exp(x)-1
np.log(x) ln x
np.log10 / np.log2 log₁₀ / log₂
np.log1p(x) ln(1+x) x 极小时精度远高于 log(1+x)
np.logaddexp(a,b) log(eᵃ+eᵇ) 数值稳定,softmax 常用
Python5 行
np.exp(np.arange(4))         # [1. 2.718 7.389 20.086]
np.log(np.array([1, np.e]))  # [0. 1.]
np.log(27) / np.log(3)       # 3.0,换底公式 log_a(b)=ln b / ln a
np.log(0)                    # -inf,RuntimeWarning
np.log(-1)                   # nan

取整

Python7 行
x = np.array([1.2, 1.5, 1.8, 2.5, -1.5, -2.5])
np.round(x)      # [1. 2. 2. 2. -2. -2.],银行家舍入(5 取偶)
np.floor(x)      # [1. 1. 1. 2. -2. -3.],向 -∞
np.ceil(x)       # [2. 2. 2. 3. -1. -2.],向 +∞
np.trunc(x)      # [1. 1. 1. 2. -1. -2.],向 0
np.round(np.array([123, 456]), -1)   # [120. 460.],负数位 = 舍到十位/百位
(np.floor(x * 10) / 10)              # 手动保留 1 位小数(向下)

⚠️ np.round(2.5) → 2 不是 3(四舍六入五取偶);要“四舍五入”手写 np.floor(x + 0.5)。

幂与开方

Python7 行
np.power([2, 3, 4], [1, 2, 3])    # [2 9 64],逐元素
np.square([1, 2, 3])              # [1 4 9],快于 **2
np.sqrt([1, 4, 9])                # [1. 2. 3.]
np.cbrt([1, 8, -27])              # [1. 2. -3.],支持负数
np.sqrt(-1.0)                     # nan + RuntimeWarning;复数用 np.sqrt(-1+0j)
np.hypot(3, 4)                    # 5.0,√(a²+b²),比手写稳定
np.power(2.0, -2)                 # 0.25
Python4 行
# 均方误差 / 平均绝对误差
err = np.array([2.5, 3.8]) - np.array([3.0, 4.0])
np.mean(np.square(err))   # MSE
np.mean(np.abs(err))      # MAE

绝对值与符号

Python4 行
np.abs / np.absolute       # 绝对值;复数给模
np.fabs                    # 只处理浮点,更快
np.sign([-5, 0, 3])        # [-1 0 1],判断涨跌方向
np.abs(3 + 4j)             # 5.0

聚合 / 累积 / 差分

函数 作用
np.sum / np.prod 求和 / 求积,支持 axis、keepdims
np.cumsum / np.cumprod 累积和 / 累积积
np.diff(a, n=1, axis=-1) n 阶差分,长度减 1
np.gradient 梯度(中心差分,长度不变)
np.trapz(y, x) 梯形法数值积分
np.convolve(a, v, mode) 一维卷积(可算移动平均)
Python10 行
a = np.array([1, 2, 3, 4, 5])
np.cumsum(a)         # [1 3 6 10 15]
np.diff(a)           # [1 1 1 1]
np.diff(a, n=2)      # [0 0 0]

prices = np.array([100, 102, 105, 103])
ret = np.diff(prices) / prices[:-1] * 100      # 日收益率 %

ma = np.convolve(prices, np.ones(3) / 3, mode='valid')   # 3 日移动平均
np.trapz(np.exp(np.linspace(0, 1, 1000)), dx=1/999)      # ≈ e - 1

取余与整除

Python7 行
np.mod / np.remainder([10, 15], [3, 4])   # [1 3]
np.fmod([-7], [3])                        # [-1],符号随被除数(mod 随除数 → 2)
np.divmod([10, 15], [3, 4])               # (商 array([3,3]), 余 array([1,3]))
np.gcd([12, 18], [8, 12])                 # [4 6]
np.lcm([12, 18], [8, 12])                 # [24 36]
np.gcd.reduce([12, 18, 24])               # 6
np.reciprocal(np.array([1, 2, 4]), dtype=float)   # [1. 0.5 0.25]

⚠️ 整数数组 np.reciprocal 默认返回整数(截断),求倒数必须指定 dtype=float。

复数

Python9 行
z = np.array([1 + 2j, 3 + 4j])
z.real, z.imag             # 实部 / 虚部(可写)
np.real(z), np.imag(z)     # 函数形式
np.abs(z)                  # 模 [2.236 5.]
np.angle(z)                # 辐角(弧度)
np.conj(z)                 # 共轭;z * conj(z) == |z|²
np.exp(1j * np.pi)         # (-1+0j),欧拉公式 e^{iθ} = cosθ + i·sinθ
r * np.exp(1j * theta)     # 极坐标 → 复数
np.fft.fft(sig)            # 傅里叶变换;配合 np.fft.fftfreq

自定义 ufunc

Python2 行
np.frompyfunc(lambda x: x**2 + 1, 1, 1)(np.arange(3))   # dtype=object,需 astype
np.vectorize(lambda x: max(x, 0), otypes=[float])(np.array([-1, 2]))

⚠️ vectorize / frompyfunc 只是语法糖,内部仍是 Python 循环,没有性能收益;能写成 ufunc 表达式就不要用它们,纯标量逻辑考虑 numba @njit。

易错点

  • np.log(0) → -inf、np.log(负数) → nan,均带 RuntimeWarning;先 np.clip(x, eps, None)。
  • 大数组用 np.float32 可省一半内存;累加前确认精度需求。
  • np.round 是银行家舍入,floor 与 trunc 对负数不同。
  • np.diff 结果比输入短 1,与 [:-1] 对齐运算时别搞错长度。
  • 一维 (3,)、行向量 (1,3)、列向量 (3,1) 广播行为完全不同。

抛体运动的轨迹与射程

三角函数配广播,一次算完多个发射角的轨迹:角度数组转弧度后分解速度,再用 (101,1) × (1,3) 生成各角度按自身飞行时长归一化的时间网格。

Python27 行
import numpy as np

v0, g = 50.0, 9.8                       # 初速度 m/s,重力加速度
ang = np.array([30.0, 45.0, 60.0])
th = np.radians(ang)                    # 三角函数只吃弧度

vx, vy = v0 * np.cos(th), v0 * np.sin(th)
vx.round(3)                             # 输出: [43.301 35.355 25.   ]
vy.round(3)                             # 输出: [25.    35.355 43.301]
np.hypot(vx, vy)                        # 输出: [50. 50. 50.],分解后合速度不变

# 解析解:飞行时间、最大高度、射程
t_fly = 2 * vy / g
h_max = vy ** 2 / (2 * g)
reach = v0 ** 2 * np.sin(2 * th) / g
t_fly.round(3)                          # 输出: [5.102 7.215 8.837]
h_max.round(2)                          # 输出: [31.89 63.78 95.66]
reach.round(2)                          # 输出: [220.92 255.1  220.92],30° 与 60° 射程相同

# 采样轨迹:每个角度用自己的 t_fly 归一化 → (101,1) 广播 (1,3)
t = np.linspace(0, 1, 101)[:, None] * t_fly[None, :]
t.shape                                 # 输出: (101, 3)
xs = vx * t
ys = vy * t - 0.5 * g * t ** 2
ys.max(axis=0).round(2)                 # 输出: [31.89 63.78 95.66],与 h_max 一致
xs.max(axis=0).round(2)                 # 输出: [220.92 255.1  220.92],与 reach 一致
np.degrees(np.arctan2(vy, vx)).round(2) # 输出: [30. 45. 60.],反推发射角

要点:np.radians + 广播能一次性算多组参数的轨迹,np.hypot/np.arctan2 用来做速度合成与反解角度比手写公式更稳。

取整函数在计费、打包与时间换算里的选择

取整不是只有“四舍五入”:算箱数必须向上、算收费必须防止多收、算时分秒要靠 divmod。选错方向就是钱算错。

Python27 行
import numpy as np

# 1. 打包箱数:不满一箱也占一箱 → ceil
items = np.array([1, 12, 13, 25])
np.ceil(items / 12).astype(int)          # 输出: [1 1 2 3]

# 2. 停车计费:前 30 分钟免费,之后不足 1 小时按 1 小时
mins = np.array([15, 45, 61, 125])
np.ceil(np.maximum(mins - 30, 0) / 60).astype(int)   # 输出: [0 1 1 2]

# 3. 打折保留到分:向下取整,宁少收不多收
price = np.array([19.99, 5.55, 100.0])
np.floor(price * 0.8 * 100) / 100        # 输出: [15.99  4.44 80.  ]

# 4. 秒 → 时分秒:divmod 一次拿到商和余数
sec = np.array([59, 3661, 86399])
h, rest = np.divmod(sec, 3600)
m, s = np.divmod(rest, 60)
np.stack([h, m, s], axis=1)              # 输出: [[ 0  0 59] [ 1  1  1] [23 59 59]]

# 5. 两个坑:银行家舍入 + 浮点表示
np.round([0.5, 1.5, 2.5, -2.5])          # 输出: [ 0.  2.  2. -2.],五取偶而非五进一
np.floor(np.array([0.5, 1.5, 2.5]) + 0.5)    # 输出: [1. 2. 3.],要"四舍五入"就手写
np.round(np.array([2.675, 1.005, 0.125]), 2) # 输出: [2.68 1.   0.12],二进制表示导致方向不定

# 6. 舍入到 5 的倍数:先缩放再还原
np.round(np.array([12.3, 17.8, 22.5]) / 5) * 5   # 输出: [10. 20. 20.]

要点:向上取整算容量、向下取整算金额、divmod 做进位换算;涉及钱的场景不要依赖 np.round,它是银行家舍入且受浮点表示影响。

信号平滑与主频提取

含噪信号先用 np.convolve 做移动平均去噪,再用 rfft + rfftfreq 找主频。窗口长度是关键:太短去噪不够,太长会把高频成分一起削掉。

Python31 行
import numpy as np

fs = 200                                  # 采样率 200 Hz
t = np.arange(0, 2.0, 1 / fs)             # 输出: shape (400,)
rng = np.random.default_rng(3)
sig = 2 * np.sin(2 * np.pi * 5 * t) + 0.8 * np.sin(2 * np.pi * 20 * t)
noisy = sig + rng.normal(0, 0.5, t.size)

# 1. 移动平均 = 与等权核卷积;mode='valid' 只保留完全重叠部分
k = 5
sm = np.convolve(noisy, np.ones(k) / k, mode='valid')
sm.size                                   # 输出: 396,等于 400-5+1
aligned = sig[k // 2: k // 2 + sm.size]   # 卷积结果整体后移 k//2,比较前要对齐
np.abs(noisy - sig).mean().round(4)       # 输出: 0.3969,原始噪声水平
np.abs(sm - aligned).mean().round(4)      # 输出: 0.248,平滑后误差下降

# 窗口过长的反效果:20 Hz 周期只有 10 个采样点,k=11 直接把它抹平
sm11 = np.convolve(noisy, np.ones(11) / 11, mode='valid')
np.abs(sm11 - sig[5:5 + sm11.size]).mean().round(4)   # 输出: 0.5692,比不平滑还差

# 2. 频谱:实信号用 rfft,只留非负频率
spec = np.abs(np.fft.rfft(noisy))
freq = np.fft.rfftfreq(t.size, 1 / fs)
freq[np.argsort(spec)[-2:]]               # 输出: [20.  5.],两个主频(幅度升序,5 Hz 最强)
freq[-1]                                  # 输出: 100.0,奈奎斯特频率 = fs/2

# 3. 数值导数:gradient 用中心差分,长度不变
grad = np.gradient(sig, t)
grad.size                                 # 输出: 400
grad.max().round(2)                       # 输出: 156.62,解析极值 163.36,差分略有平滑
(np.diff(np.sign(sig)) != 0).sum()        # 输出: 40,符号变化次数 = 过零点个数

要点:np.convolve 等权核做移动平均要注意 mode 带来的长度与相位偏移;rfft + rfftfreq 配 argsort 就能定位主频,窗口长度需按最高频成分的周期挑。

小量对数变换与 log-sum-exp

收益率、概率这类“接近 0 或极小”的数,直接 log(1+x)、exp 会丢精度甚至下溢。log1p/expm1/logaddexp 就是为这类场景准备的。

Python24 行
import numpy as np

# 1. log1p:x 极小时 1+x 先被舍入,精度全丢
r = np.array([1e-8, 1e-4, 0.01])
np.log(1 + r) - np.log1p(r)        # 输出: [-6.08e-17 -1.10e-17  8.67e-18],双精度差异已可见
np.log(np.float32(1) + np.float32(1e-8))   # 输出: 0.0,单精度直接归零
np.log1p(np.float32(1e-8))                 # 输出: 1e-08,正确

# 2. 对数收益可加:连乘换成求和,避免长序列累乘溢出/失精
rets = np.array([0.01, -0.02, 0.015])
lr = np.log1p(rets)
lr                                 # 输出: [ 0.00995033 -0.02020271  0.01488861]
np.expm1(lr.sum())                 # 输出: 0.004647,累计收益率
np.prod(1 + rets) - 1              # 输出: 0.004646999999999846,数学等价、精度略差

# 3. log-sum-exp:概率取对数后求和,直接 exp 会下溢成 0
logp = np.array([-1000.0, -1001.0, -1002.0])
np.log(np.exp(logp).sum())         # 输出: -inf,exp(-1000) 下溢为 0
m = logp.max()
m + np.log(np.exp(logp - m).sum()) # 输出: -999.5923940355556,减最大值后稳定
np.logaddexp.reduce(logp)          # 输出: -999.5923940355557,一步到位

# 4. 归一化成概率:减去 logsumexp 再 exp
np.exp(logp - np.logaddexp.reduce(logp))   # 输出: [0.66524096 0.24472847 0.09003057]

要点:log1p/expm1 处理接近 0 的量,logaddexp(或手动减最大值)处理极小概率求和,把乘法链换成对数加法是数值稳定的通用手段。

统计函数

Python1 行
import numpy as np

API 速查

类别 函数 说明
集中趋势 np.mean/median/average 均值 / 中位数 / 加权均值(weights=)
离散程度 np.std/var, np.ptp 标准差 / 方差(ddof=)/ 极差
求和累积 np.sum/prod, np.cumsum/cumprod, np.diff 和积 / 累积和积 / 相邻差
最值 np.max/min, np.argmax/argmin 值 / 位置索引
分位 np.percentile/quantile 0-100 / 0-1 参数
相关 np.corrcoef, np.cov 相关系数矩阵 / 协方差矩阵
分布 np.histogram, np.bincount 分箱计数 / 非负整数频次
缺失值 np.isnan, np.nan* 系列, np.nan_to_num 检测 / 忽略 NaN 计算 / 填充
计数 np.sum(cond), np.count_nonzero 布尔 True 即 1

方法形式等价:a.mean() == np.mean(a),a.sum()、a.max()、a.std() 同理。

Python5 行
a = np.array([85, 92, 78, 96, 88])
a.mean()                       # 87.8
np.median(a)                   # 88.0
np.average([85, 90, 78], weights=[4, 3, 2])   # 85.111... = (85*4+90*3+78*2)/9
np.ptp(a)                      # 18  (max-min)

axis 的语义(重点)

axis = 被“折叠/消掉”的那个维度,其余维度保留。

Python5 行
a = np.array([[10, 20, 30],
              [40, 50, 60]])          # shape (2, 3)
np.sum(a)            # 210        全部元素 → 标量
np.sum(a, axis=0)    # [50 70 90] 沿行方向折叠掉 → 剩 3 列(每列求和)
np.sum(a, axis=1)    # [60 150]   沿列方向折叠掉 → 剩 2 行(每行求和)

图示:

        axis=1 →(沿列走)
      ┌───────────────┐
      │  10  20  30   │ → 60
axis=0│               │
 ↓    │  40  50  60   │ → 150
      └───────────────┘
        ↓   ↓   ↓
       50  70  90        ← axis=0 的结果
  • axis=0:竖着扫,跨行聚合 → 结果长度 = 列数(“每列一个值”)
  • axis=1:横着扫,跨列聚合 → 结果长度 = 行数(“每行一个值”)
  • axis=None(默认):全部展平成一个值
  • keepdims=True:保留被折叠的轴(长度 1),便于广播 np.sum(a, axis=1, keepdims=True) → [[60], [150]]

三维与多轴:

Python5 行
x = np.random.rand(3, 5, 4)      # (班级, 学生, 考试)
np.mean(x, axis=0).shape         # (5, 4)   折叠班级
np.mean(x, axis=1).shape         # (3, 4)   折叠学生
np.mean(x, axis=2).shape         # (3, 5)   折叠考试
np.mean(x, axis=(0, 1)).shape    # (4,)     同时折叠班级+学生

记忆:结果的 shape = 原 shape 去掉 axis 指定的那些轴。

ddof:总体 vs 样本

方差分母 = n - ddof。

ddof 分母 含义 场景
0(默认) n 总体方差/标准差 描述已有全部数据、ML 特征标准化
1 n-1 无偏样本估计 用样本推断总体,统计学默认
Python4 行
d = np.array([1, 2, 3, 4, 5])
np.std(d)            # 1.4142...  sqrt(10/5)
np.std(d, ddof=1)    # 1.5811...  sqrt(10/4)
np.var(d)            # 2.0  —— 恒等于 np.std(d)**2

⚠️ np.std(d, ddof=1) != np.std(d),混用会导致论文/报表数值对不上。pandas 的 .std() 默认 ddof=1,NumPy 默认 ddof=0。

最值与位置

Python9 行
a = np.array([85, 92, 78, 96, 88])
i = np.argmax(a)     # 3
a[i]                 # 96
m = np.array([[10, 20, 30], [15, 25, 35], [12, 22, 32]])
np.argmax(m)             # 5   不带 axis:展平后的索引
np.argmax(m, axis=0)     # [1 1 1]  每列最大值所在的行号
np.argmax(m, axis=1)     # [2 2 2]  每行最大值所在的列号
np.unravel_index(5, m.shape)         # (1, 2)  展平索引 → 多维索引
np.argmax(m, axis=1)[0], np.max(m, axis=1)[0]   # 配对使用

⚠️ np.argmax 只返回第一个最大值的索引;遇并列/NaN 需另处理(np.nanargmax 遇全 NaN 会报错)。

分位数

percentile 参数 0-100,quantile 参数 0-1,其余完全一致(都支持 axis、method= 插值方式)。

Python5 行
s = np.array([60, 65, 70, 75, 80, 85, 90, 95, 100])
np.percentile(s, [25, 50, 75])    # [70. 80. 90.]   一次算多个
np.quantile(s, 0.5)               # 80.0   == np.median(s)
np.percentile(s, 90)              # 96.0  = 95 + 0.2*(100-95) 线性插值
q1, q3 = np.percentile(s, [25, 75]); iqr = q3 - q1   # 箱线图/IQR 异常值

相关与协方差

Python7 行
t = np.array([15, 18, 22, 25, 28, 32, 35, 38])
ice = np.array([50, 60, 80, 100, 120, 150, 180, 200])
coat = np.array([100, 90, 70, 50, 30, 20, 10, 5])
np.corrcoef(t, ice)[0, 1]     # ≈ 0.996  强正相关
np.corrcoef(t, coat)[0, 1]    # ≈ -0.992 强负相关
np.corrcoef([math, phys, eng]) # 传序列 → n×n 相关矩阵,对角线恒为 1
np.cov(t, ice)                # 2×2 协方差矩阵,对角线是各自方差
  • r ∈ [-1, 1];|r| 大只说明线性强弱,不代表斜率大小,更不等于因果。
  • 协方差受量纲影响(乘以 1000 就变 10⁶ 倍),相关系数做了标准化,跨组可比。

histogram 与 bincount

Python12 行
scores = np.array([55, 65, 72, 78, 85, 88, 92, 95, 58, 70, 82, 90])
counts, edges = np.histogram(scores, bins=[0, 60, 80, 90, 100])
# counts: [2 4 3 3]   edges: [  0  60  80  90 100]  (len(edges) == len(counts)+1)
# 区间左闭右开,只有最后一个区间右端闭合

counts, edges = np.histogram(scores, bins=5)      # 整数 → 等宽 5 箱,边界自动取 [min, max]
counts, edges = np.histogram(scores, bins=5, density=True)   # 归一化为概率密度(积分=1)

votes = np.array([0, 1, 2, 1, 0, 2, 1, 1, 0, 2, 1])
np.bincount(votes)                          # [3 5 3]  索引即取值,长度 = max+1
np.bincount(votes, minlength=5)             # [3 5 3 0 0]  固定长度,便于对齐
np.bincount([0,1,2,1,0,2,1], weights=[3,2,4,2,3,4,2])   # [6. 6. 8.]  加权求和

⚠️ bincount 只接受非负整数;值域从 0 开始,骰子点数 1-6 会产生一个无用的 0 号桶。 ⚠️ histogram 的 bins 传序列时是边界值,传整数时是箱子个数,语义完全不同。

累积、差分与变化率

Python9 行
u = np.array([100, 150, 120, 180, 200])
np.cumsum(u)                 # [100 250 370 550 750]  长度不变
np.cumprod(np.array([1,2,3,4]))    # [ 1  2  6 24]
np.diff(np.array([1, 3, 6, 10]))   # [2 3 4]   长度 n-1

p = np.array([100, 102, 101, 98, 95])
ret = np.diff(p) / p[:-1]                  # 日收益率,长度 n-1
run_max = np.maximum.accumulate(p)         # 截至当前的 running max
drawdown = (p - run_max) / run_max         # 回撤序列;np.min(drawdown) 即最大回撤

NaN 处理

Python5 行
d = np.array([10, 20, np.nan, 30, 40])
np.sum(d)                    # nan  一个 NaN 污染全部结果
np.nanmean(d)                # 25.0 = (10+20+30+40)/4
np.isnan(d)                  # [False False  True False False]
np.isnan(d).sum()            # 1

nan* 系列:nansum nanmean nanmedian nanstd nanvar nanmin nanmax nanargmin nanargmax nanpercentile nanquantile —— 签名与去掉 nan 的版本一致。

填充:

Python2 行
np.nan_to_num(d, nan=0.0)                        # [10. 20.  0. 30. 40.]
np.where(np.isnan(d), np.nanmean(d), d)          # 用均值填充

⚠️ np.nan == np.nan 是 False,判断缺失值只能用 np.isnan();整数数组里无法存 NaN,会先被提升为 float。

易错点

⚠️ axis=0/1 用反:想算“每个学生平均分”(逐行)要用 axis=1。 ⚠️ 有极端值时 mean 被带偏,median 更稳健——工资/房价类数据优先看中位数与分位数。 ⚠️ np.corrcoef 返回矩阵,取两个数的值要写 [0, 1];对常数序列返回 nan(除以 0 标准差)。 ⚠️ 布尔条件计数用 np.sum(cond) / np.count_nonzero(cond),不要写 Python 循环。 ⚠️ 多维统计优先一次传 axis,比 [f(row) for row in arr] 快一个量级。

成绩表的多轴统计与加权平均

一张「学生 × 科目」的成绩矩阵,要同时算出每人平均、每科平均、按学分加权的 GPA,以及每科的离散程度。关键是同一份数据换 axis 就能得到完全不同口径的指标,不需要任何循环。

Python25 行
import numpy as np

scores = np.array([[88, 92, 75],
                   [95, 78, 84],
                   [70, 85, 90],
                   [82, 91, 68]])          # 4 名学生 × 3 门课
credits = np.array([4, 3, 2])              # 数学 / 物理 / 英语 的学分

per_student = scores.mean(axis=1)          # 折叠科目 → 每人一个值
per_student.round(2)                       # 输出: [85.   85.67 81.67 80.33]

per_course = scores.mean(axis=0)           # 折叠学生 → 每科一个值
per_course.round(2)                        # 输出: [83.75 86.5  79.25]

gpa = np.average(scores, axis=1, weights=credits)   # 按学分加权,权重沿被折叠的轴
gpa.round(2)                               # 输出: [86.44 86.89 79.44 81.89]

spread = scores.std(axis=0, ddof=1)        # 每科的样本标准差,看哪科分化大
spread.round(2)                            # 输出: [10.59  6.45  9.71]

high = np.count_nonzero(scores >= 85, axis=0)      # 每科 85 分以上人数
high                                       # 输出: [2 3 1]

flat = np.argmax(scores)                   # 全表最高分的展平索引
np.unravel_index(flat, scores.shape)       # 输出: (1, 0)  第 2 个学生的第 1 门课

要点:axis 决定聚合口径,average(weights=) 做加权,count_nonzero(cond, axis=) 代替循环计数。

传感器数据的异常值检测

传感器读数里混进了几个明显离谱的值,需要先定位再处理。这里对比 IQR 法和 3σ 法:极端值会把 mean/std 自己拉偏,导致 3σ 法失效,而基于分位数的 IQR 不受影响。

Python28 行
import numpy as np

sensor = np.array([23.5, 24.1, 23.8, 99.9, 24.2, -10.0,
                   23.9, 24.0, 100.0, 23.6, 24.3, 23.7])

# 方法一:IQR(稳健)
q1, q3 = np.percentile(sensor, [25, 75])
iqr = q3 - q1
lo, hi = q1 - 1.5 * iqr, q3 + 1.5 * iqr
round(lo, 3), round(hi, 3)                 # 输出: (22.85, 25.05)
mask = (sensor < lo) | (sensor > hi)
np.where(mask)[0]                          # 输出: [3 5 8]
sensor[mask]                               # 输出: [ 99.9 -10.  100. ]

# 方法二:3σ(被极端值自身污染,这里一个都抓不到)
mu, sd = sensor.mean(), sensor.std()
round(mu, 2), round(sd, 2)                 # 输出: (33.75, 31.03)
np.count_nonzero(np.abs(sensor - mu) > 3 * sd)     # 输出: 0

# 处理方式 A:截断到上下界
clipped = np.clip(sensor, lo, hi)
clipped[[3, 5, 8]]                         # 输出: [25.05 22.85 25.05]

# 处理方式 B:用干净数据的中位数替换
med = np.median(sensor[~mask])
med                                        # 输出: 23.9
filled = np.where(mask, med, sensor)
round(filled.mean(), 2)                    # 输出: 23.9   (原始均值 33.75)

要点:异常值检测优先用分位数(IQR),mean/std 本身会被异常值带偏;np.clip 截断、np.where 替换二选一。

订单金额分箱与分渠道汇总

要出一份销售分布报表:金额落在哪个区间各有多少单,以及每个渠道的总额和均单价。前者用 histogram 自定义边界,后者用 bincount(weights=) 一次完成「按组求和」。

Python24 行
import numpy as np

orders  = np.array([35, 128, 260, 89, 540, 45, 310, 150, 78, 205, 620, 95, 42, 180, 275])
channel = np.array([0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2])   # 0=门店 1=线上 2=分销

# 1) 金额分箱:传序列时 bins 是边界值,左闭右开
counts, edges = np.histogram(orders, bins=[0, 100, 300, 500, 10000])
counts                                     # 输出: [6 6 1 2]
(counts / counts.sum() * 100).round(2)     # 输出: [40.   40.    6.67 13.33]

# 2) 按渠道求和:weights 让 bincount 从「计数」变成「分组求和」
total = np.bincount(channel, weights=orders)
total                                      # 输出: [ 681. 1618.  753.]
n = np.bincount(channel)
n                                          # 输出: [5 5 5]
(total / n).round(2)                       # 输出: [136.2 323.6 150.6]

# 3) 渠道冠军
np.argmax(total)                           # 输出: 1   线上渠道总额最高

# 4) 等宽分箱看整体形态(bins 传整数 = 箱子个数)
c2, e2 = np.histogram(orders, bins=4)
c2                                         # 输出: [9 4 0 2]
e2.round(2)                                # 输出: [ 35.   181.25 327.5  473.75 620.  ]

要点:bins 传序列是边界、传整数是箱数;bincount(group, weights=value) 是最快的分组求和。

排序与搜索

Python1 行
import numpy as np

排序 API 速查

函数 返回 是否改原数组 用途
np.sort(a, axis=-1) 排序后的副本 否 只要排序结果
a.sort(axis=-1) None 是(就地) 大数组省内存
np.argsort(a) 排序后的原索引 否 需要同步重排多个关联数组
np.lexsort((k1, k2)) 索引 否 多键排序,最后一个键为主键
np.partition(a, k) 部分排序副本 否 只要 TopK,O(n)
np.argpartition(a, k) 索引 否 TopK 的索引
a.argsort() / a.sort(kind=) — — 方法形式同样可用
Python7 行
s = np.array([85, 92, 78, 95, 88])
np.sort(s)                   # [78 85 88 92 95],s 不变
s.sort()                     # s 就地变为 [78 85 88 92 95]
idx = np.argsort(s)          # 升序索引
s[idx]                       # == np.sort(s)
s[idx][::-1]                 # 降序值
np.sort(s)[::-1]             # 降序(等价于 -np.sort(-s))

⚠️ a.sort() 返回 None。写 b = a.sort() 会得到 None,这是最常见的坑。

axis 行为

Python8 行
m = np.array([[85, 92, 78],
              [90, 88, 95],
              [78, 85, 82]])
np.sort(m, axis=0)     # 每列内部排序 → [[78 85 78], [85 88 82], [90 92 95]]
np.sort(m, axis=1)     # 每行内部排序 → [[78 85 92], [88 90 95], [78 82 85]]
np.sort(m, axis=None)  # 展平后整体排序,返回一维
np.argsort(m, axis=0)  # [[2 2 0], [0 1 2], [1 0 1]]  每列排序后的行号
np.argsort(m, axis=1)  # 每行排序后的列号

按某一列给整表排序(表格排序的标准写法):

Python2 行
order = np.argsort(m[:, 1])      # 以第 1 列为键
m[order]                         # 整表按该列升序重排

稳定性与 kind

kind 稳定 平均/最坏 空间 何时用
quicksort(默认) 否 O(n log n) / O(n²) O(log n) 一般情况
mergesort 是 O(n log n) / O(n log n) O(n) 需要稳定
heapsort 否 O(n log n) / O(n log n) O(1) 内存受限
stable 是 O(n log n) O(n) 自动挑最优稳定算法
Python3 行
np.sort(a, kind='stable')
np.argsort(keys, kind='stable')    # 多次排序(先 A 后 B)时必须稳定
np.sort(stu, order=['class', 'score'])   # 结构化数组多字段排序

lexsort:多键排序

Python5 行
total = np.array([85, 92, 85, 92, 78])
math_ = np.array([90, 88, 85, 90, 80])
idx = np.lexsort((math_, total))       # 主键=total,并列时按 math_
# idx: [4 2 0 1 3]
idx = np.lexsort((-math_, -total))     # 全部降序:对数值取负

⚠️ lexsort 的键顺序是反的:最后一个键才是主键(类似 SQL ORDER BY k2, k1)。

partition:只要 TopK

Python7 行
s = np.array([85, 92, 78, 95, 88, 76, 90, 82])
p = np.partition(s, 3)      # 第 4 小的元素就位;左侧 ≤ 它 ≤ 右侧,两侧内部无序
p[3]                        # 85,第 4 小的值
top3 = np.partition(s, -3)[-3:]              # 最大的 3 个(顺序不定)
np.sort(top3)[::-1]                          # [95 92 90]
idx = np.argpartition(s, -3)[-3:]            # 对应索引
idx[np.argsort(s[idx])[::-1]]                # 索引也按分数从高到低排好
  • 复杂度 O(n) vs sort 的 O(n log n),数组越大、K 越小优势越明显。
  • K 接近 n 一半时不如直接 sort。

searchsorted:有序数组二分查找

Python9 行
a = np.array([1, 3, 5, 7, 9, 11, 13])     # 前提:已升序
np.searchsorted(a, 5)          # 2   命中时返回左插入点
np.searchsorted(a, 6)          # 3   未命中返回"插入后仍有序"的位置
np.searchsorted(a, [2, 6, 10, 14])        # [1 3 5 7]  支持向量化

b = np.array([1, 3, 5, 5, 5, 7, 9])
np.searchsorted(b, 5, side='left')        # 2
np.searchsorted(b, 5, side='right')       # 5
np.searchsorted(b, 5, 'right') - np.searchsorted(b, 5, 'left')   # 3 = 5 的出现次数

典型用法 —— 分箱/打标签(比 np.digitize 更直观):

Python7 行
scores = np.array([45, 67, 82, 91, 75])
edges = np.array([60, 70, 80, 90])
labels = np.array(['不及格', '及格', '中等', '良好', '优秀'])
labels[np.searchsorted(edges, scores)]     # ['不及格' '及格' '良好' '优秀' '中等']

lo = np.searchsorted(a, 200, 'left'); hi = np.searchsorted(a, 600, 'right')
a[lo:hi]                                   # 落在 [200, 600] 的元素

⚠️ 输入数组必须有序,否则结果无意义且不报错。 ⚠️ 返回的是“插入位置”不是“存在与否”,判断元素是否存在要再检查 a[pos] == v。

where / nonzero / argwhere / extract

Python15 行
s = np.array([85, 92, 78, 95, 88, 76, 90, 82])
np.where(s >= 90)                # (array([1, 3, 6]),)   永远返回元组
np.where(s >= 90)[0]             # [1 3 6]
s[np.where(s >= 90)]             # [92 95 90]
s[(s >= 80) & (s < 90)]          # [85 88 82]   布尔索引更简洁
s[~((s >= 80) & (s < 90))]       # 取反

np.where(s >= 60, s, 0)          # 三元:满足条件取 s,否则取 0
np.where(s >= 90, '优', np.where(s >= 60, '及格', '不及格'))   # 嵌套多分支

r, c = np.where(m < 60)          # 2D:分别得到行号数组和列号数组
np.argwhere(m < 60)              # [[2 1]]  shape (n, ndim),与 where 互为转置形式
np.nonzero(m)                    # 非零元素索引 == np.where(m != 0)
np.count_nonzero(m < 60, axis=1) # 每行不及格科目数,比 len(np.where(...)) 快
np.extract(s >= 90, s)           # [92 95 90],等价于 s[s >= 90](总是返回一维)

⚠️ 多条件必须用 & | ~ 且每个子条件加括号:(a > 1) & (b < 2)。写 and/or/not 会抛 ValueError。 ⚠️ 一维时也要写 np.where(cond)[0],直接用元组做索引虽然能跑但语义混乱。 ⚠️ np.where(cond, x, y) 会同时求值 x 和 y,若其中有除零/越界运算照样报错(广播场景用 np.divide(..., where=) 或掩码赋值)。

易错点

⚠️ np.sort 返回副本、a.sort() 就地修改,别把返回值赋给变量后还以为原数组没动。 ⚠️ 需要同步重排“姓名/价格/销量”等多列时,只能用 argsort 的索引去 fancy index,不能各自 sort。 ⚠️ [::-1] 只是反转视图,配合 argsort 得到降序;但对 kind='quicksort' 的并列元素,反转后顺序不可预期。 ⚠️ 对结构化数组排序用 order=[...],对普通 2D 数组排序列用 argsort(m[:, k])。

排行榜 TopK 与名次列

排行榜只需要前几名,用 argpartition 拿到候选再局部排序,比全量 argsort 快。同时要给每一行标上名次,用「argsort 的逆排列」一次算出。

Python26 行
import numpy as np

names = np.array(['A组', 'B组', 'C组', 'D组', 'E组', 'F组', 'G组', 'H组', 'I组', 'J组'])
score = np.array([812, 950, 733, 941, 688, 1024, 877, 733, 902, 795])

# 1) TopK:先 O(n) 粗筛出 k 个,再只排这 k 个
k = 3
idx = np.argpartition(score, -k)[-k:]          # 这 k 个是最大的,但内部无序
idx = idx[np.argsort(score[idx])[::-1]]        # 局部降序
names[idx]                                     # 输出: ['F组' 'B组' 'D组']
score[idx]                                     # 输出: [1024  950  941]

# 2) 末位 K:同理换成正的 k
tail = np.argpartition(score, k)[:k]
np.sort(score[tail])                           # 输出: [688 733 733]

# 3) 名次列:把「排序后的位置」写回原顺序
rank = np.empty(score.size, dtype=int)
rank[np.argsort(-score, kind='stable')] = np.arange(1, score.size + 1)
rank                                           # 输出: [ 6  2  8  3 10  1  5  9  4  7]
names[rank == 1]                               # 输出: ['F组']

# 4) 分数线:超过 900 分的组,按分数降序
above = np.where(score > 900)[0]
above[np.argsort(score[above])[::-1]]          # 输出: [5 1 3 8]
names[above[np.argsort(score[above])[::-1]]]   # 输出: ['F组' 'B组' 'D组' 'I组']

要点:TopK 用 argpartition + 局部 argsort;名次列靠 rank[argsort(...)] = arange(1, n+1) 反写。

员工表按多列排序出报表

报表要求「部门升序 → 绩效降序 → 工号升序」,三个键混合升降。lexsort 把键从后往前当主键,降序键取负号即可,一次调用得到整表的重排索引。

Python25 行
import numpy as np

emp_id = np.array([1007, 1002, 1015, 1003, 1009, 1011])
dept   = np.array([   2,    1,    2,    1,    3,    1])
perf   = np.array([  88,   92,   95,   92,   79,   85])

# 最后一个键是主键:dept 升 → -perf(即 perf 降)→ emp_id 升
order = np.lexsort((emp_id, -perf, dept))
order                          # 输出: [1 3 5 2 0 4]

emp_id[order]                  # 输出: [1002 1003 1011 1015 1007 1009]
dept[order]                    # 输出: [1 1 1 2 2 3]
perf[order]                    # 输出: [92 92 85 95 88 79]

# 各部门第一名:在已排好的表上取每个 dept 的首次出现位置
_, first = np.unique(dept[order], return_index=True)
first                          # 输出: [0 3 5]
emp_id[order][first]           # 输出: [1002 1015 1009]
perf[order][first]             # 输出: [92 95 79]

# 若数据已在 2D 表里,按第 2 列排整表
table = np.column_stack((emp_id, dept, perf))
order2 = np.argsort(table[:, 2], kind='stable')[::-1]
table[order2][:2]              # 输出: [[1015    2   95]
                               #        [1003    1   92]]

要点:lexsort 键顺序是反的(最后一个才是主键),降序对该键取负,得到的索引可同步重排所有关联列。

价格区间查找与档位打标

有序价格表上要做两件事:给每个商品打上价位档标签,以及取出落在某个价格区间的全部商品。两者都是 searchsorted 的标准用法,复杂度 O(log n),不用扫全表。

Python26 行
import numpy as np

price = np.array([19, 45, 88, 129, 199, 259, 340, 480, 599, 899, 1299])   # 前提:已升序
edges = np.array([50, 200, 500])
tier  = np.array(['低价', '中价', '高价', '奢华'])

# 1) 打标签:searchsorted 返回落在第几个区间
slot = np.searchsorted(edges, price)
slot                           # 输出: [0 0 1 1 1 2 2 2 3 3 3]
tier[slot][:5]                 # 输出: ['低价' '低价' '中价' '中价' '中价']
np.bincount(slot, minlength=4) # 输出: [2 3 3 3]   各档位商品数

# 2) 区间查询 [100, 500]:左端用 left,右端用 right
lo = np.searchsorted(price, 100, side='left')
hi = np.searchsorted(price, 500, side='right')
lo, hi                         # 输出: (3, 8)
price[lo:hi]                   # 输出: [129 199 259 340 480]
hi - lo                        # 输出: 5

# 3) 判断某个价格是否真的存在(返回的是插入位置,不是存在性)
def exists(a, v):
    p = np.searchsorted(a, v)
    return p < a.size and a[p] == v

exists(price, 259)             # 输出: True
exists(price, 300)             # 输出: False

要点:searchsorted 只在有序数组上有效,返回插入位置——打标签直接当索引用,判存在必须再比一次值。

拼接与分割

Python1 行
import numpy as np

拼接 API 速查

函数 等价写法 说明
np.concatenate((a, b), axis=0) — 通用,沿已存在的轴;维度不变
np.vstack((a, b)) concatenate(axis=0)(1D 先升为 2D) 加行
np.hstack((a, b)) concatenate(axis=1)(1D 沿 axis=0) 加列
np.dstack((a, b)) concatenate(axis=2) 加第 3 维(通道)
np.stack((a, b), axis=0) — 新建轴堆叠,维度 +1,要求 shape 全等
np.r_[...] 行方向(axis=0) 切片语法糖,支持 np.r_[1:4, 0, 1]
np.c_[...] 列方向(axis=1) 1D 输入先转列向量再拼
Python15 行
a = np.array([[1, 2, 3], [4, 5, 6]])       # (2, 3)
b = np.array([[7, 8, 9], [10, 11, 12]])    # (2, 3)
np.concatenate((a, b), axis=0).shape       # (4, 3)
np.concatenate((a, b), axis=1).shape       # (2, 6)

x = np.array([1, 2, 3]); y = np.array([4, 5, 6])
np.vstack((x, y)).shape       # (2, 3)  1D → 2D,每个输入变一行
np.hstack((x, y))             # [1 2 3 4 5 6]  仍是 1D!
np.dstack((x, y)).shape       # (1, 3, 2)  1D 输入 → (1, len, 个数)
np.stack((x, y), axis=0).shape   # (2, 3)
np.stack((x, y), axis=1).shape   # (3, 2)  == np.column_stack((x, y))
np.r_[x, y]                   # [1 2 3 4 5 6]
np.c_[x, y]                   # [[1 4], [2 5], [3 6]]  shape (3, 2)

np.stack([img1, img2, img3], axis=0).shape    # (3, H, W)  组 batch 的标准做法

⚠️ concatenate 维度不变,stack 维度 +1 且要求所有输入 shape 完全相同。混淆这两者是本章第一大坑。 ⚠️ hstack 对 1D 输入不会升维(结果还是 1D),而 vstack 会。想拼成两列请用 np.column_stack / np.stack(..., axis=1)。 ⚠️ np.concatenate 的第一个参数是数组序列(元组/列表):np.concatenate([a, b]) ✓,np.concatenate(a, b) ✗。

维度匹配规则

操作 约束
concatenate(axis=k) 除第 k 轴外,其余所有轴长度必须相等;结果第 k 轴 = 各长度之和
vstack 列数相等(1D 输入长度相等)
hstack 行数相等(1D 无此约束)
dstack 前两维相等
stack(axis=k) 所有输入 shape 完全相等;结果在位置 k 插入长度为 n 的新轴(k ∈ [-ndim-1, ndim])
Python3 行
np.concatenate((np.zeros((2, 3)), np.zeros((5, 3))), axis=0).shape   # (7, 3) ✓ 列数相同
np.concatenate((np.zeros((2, 3)), np.zeros((2, 7))), axis=1).shape   # (2, 10) ✓ 行数相同
np.concatenate((np.zeros((2, 3)), np.zeros((5, 7))), axis=0)         # ValueError

⚠️ 报错信息 all the input array dimensions except for the concatenation axis must match 就是这个规则;用 np.pad 或手工补零把非拼接轴对齐即可。

分割 API 速查

函数 等价 是否允许不均等
np.split(a, n_or_indices, axis=0) — 否,除不尽直接 ValueError
np.array_split(a, n, axis=0) 宽松版 是,前面的份儿更大
np.vsplit(a, n) split(axis=0) 否(≥2D)
np.hsplit(a, n) split(axis=1) 否(1D/2D)
np.dsplit(a, n) split(axis=2) 否
Python9 行
a = np.arange(10)
np.split(a, 2)                 # [array([0..4]), array([5..9])]
np.split(a, [3, 7])            # 切点 → [0:3], [3:7], [7:]  共 len(indices)+1 段
np.array_split(a, 3)           # 长度 4, 3, 3(不报错)

m = np.arange(24).reshape(6, 4)
np.vsplit(m, 3)                # 3 份 (2, 4)
np.hsplit(m, [1, 3])           # 形状 (6,1), (6,2), (6,1)
train, val, test = np.vsplit(m, [4, 5])      # 数据集划分惯用法

⚠️ 返回的是 list of ndarray,不是 ndarray;需要数组请再 np.array(...) 或 np.stack(...)。 ⚠️ split 的第二个参数:整数 = 份数,列表 = 切点索引,语义完全不同。 ⚠️ vsplit 不能用于 1D(会报 vsplit only works on arrays of 1 or more dimensions),1D 用 split/hsplit。

tile 与 repeat

函数 重复单位 例子([1,2,3] ×2)
np.repeat(a, n, axis=None) 元素 [1 1 2 2 3 3]
np.tile(a, reps) 整个数组 [1 2 3 1 2 3]
Python9 行
np.repeat([1, 2, 3], 3)              # [1 1 1 2 2 2 3 3 3]
np.repeat([1, 2, 3], [2, 3, 4])      # [1 1 2 2 2 3 3 3 3]  逐元素不同次数
np.repeat([[1, 2], [3, 4]], 2, axis=0)   # 每行原地重复 → (4, 2)
np.repeat([[1, 2], [3, 4]], [2, 3], axis=0)   # 第 0 行 ×2、第 1 行 ×3 → (5, 2)

np.tile([1, 2, 3], 2)                # [1 2 3 1 2 3]
np.tile([[1, 2], [3, 4]], (2, 3))    # 竖向 ×2、横向 ×3 → (4, 6)
np.tile([[0, 1], [1, 0]], (4, 4))    # 8×8 棋盘
np.tile(labels, 3)                   # 数据增强时同步放大标签

⚠️ np.repeat(a, n) 不带 axis 会先展平再重复,2D 数组会得到一维结果——想保持形状务必写 axis=。 ⚠️ tile 的 reps 元组长度 > 输入 ndim 时,会在前面补新轴(np.tile(a, (3, 2, 1)) 对 (2,2) 输入得 (3,4,2))。

append / insert / delete

Python9 行
A = np.arange(6).reshape(2, 3)
np.append([1, 2, 3], [4, 5])                  # [1 2 3 4 5]  默认展平
np.append(A, [[7, 8, 9]], axis=0)             # 加行 → (3, 3),需显式给 (1, 3) 形状
np.append(A, [[7], [8]], axis=1)              # 加列 → (2, 4),需显式给 (2, 1) 形状
np.insert([1, 2, 3, 4], 2, 99)                # [1 2 99 3 4]  在索引 2 前插入
np.insert([1, 2, 3, 4], [1, 3], [88, 99])     # 多位置多值
np.delete([1, 2, 3, 4, 5], [0, -1])           # [2 3 4]
np.delete(A, 1, axis=0)                       # 删第 1 行 → (1, 3)
np.delete([1, 2, 3, 4, 5], slice(1, 4))       # [1 5]  支持切片对象

⚠️ 这三个函数总是返回新数组(整块拷贝),在循环里反复 append 是 O(n²),应改用 list 收集后一次 np.array(),或直接 np.concatenate 一批。

常用配方

Python9 行
# 加一行/一列常量
np.vstack((m, np.zeros(m.shape[1])))          # 加零行
np.hstack((m, np.ones((m.shape[0], 1))))      # 加全 1 列(截距项)
# 变长序列对齐后批拼
padded = [np.pad(s, (0, max_len - len(s))) for s in seqs]
batch = np.vstack(padded)
# 图像加边框:np.pad(img, ((1,1),(1,1),(0,0))) 或 np.pad(img, 1)
# RGB 通道合成:np.dstack((r, g, b))  → (H, W, 3)
# 2×2 图块拼全景:np.vstack((np.hstack((i1, i2)), np.hstack((i3, i4))))

多来源上报数据合并成一张表

三个门店各自上报一张表,列含义相同但行数不等,用 concatenate(axis=0) 纵向堆起来。若某个来源缺列,先 np.pad 补齐再拼,否则会触发维度不匹配错误。

Python26 行
import numpy as np

# 列含义:[门店号, 销量, 退货]
shop_a = np.array([[1, 120, 3], [1,  98, 1]])
shop_b = np.array([[2, 150, 5], [2, 133, 2], [2, 141, 4]])
shop_c = np.array([[3,  87, 0]])

table = np.concatenate((shop_a, shop_b, shop_c), axis=0)
table.shape                    # 输出: (6, 3)
table[:, 0]                    # 输出: [1 1 2 2 2 3]

# 新增一列「净销量」:先算再 column_stack
net = table[:, 1] - table[:, 2]
net                            # 输出: [117  97 145 131 137  87]
full = np.column_stack((table, net))
full.shape                     # 输出: (6, 4)

# 某来源只上报了两列 → 补零成 3 列再拼,避免 ValueError
shop_d = np.array([[4, 76]])
padded = np.pad(shop_d, ((0, 0), (0, 1)))      # 只在列方向右侧补 1 列
padded                         # 输出: [[ 4 76  0]]
table = np.concatenate((table, padded), axis=0)
table.shape                    # 输出: (7, 3)

# 加一列全 1(回归里的截距项)
np.hstack((table, np.ones((table.shape[0], 1), dtype=int))).shape   # 输出: (7, 4)

要点:纵向合并只要求「除拼接轴外其余轴长度一致」,列数对不上先 np.pad 补齐再 concatenate。

训练集 / 验证集 / 测试集切分

数据集要按 70/15/15 切开,且特征和标签必须用同一个打乱索引,否则样本和标签就对不上了。切点用 split 的索引列表形式一次给出,比连续两次切片更清晰。

Python25 行
import numpy as np

rng = np.random.default_rng(0)
X = np.arange(100 * 4).reshape(100, 4)     # 100 样本 × 4 特征
y = np.arange(100) % 3                     # 3 分类标签

perm = rng.permutation(100)                # 同一个 perm 作用于 X 和 y
X, y = X[perm], y[perm]

X_tr, X_val, X_te = np.vsplit(X, [70, 85])         # 切点写法:[0:70] [70:85] [85:]
y_tr, y_val, y_te = np.split(y, [70, 85])          # 1D 用 split,vsplit 会报错
X_tr.shape, X_val.shape, X_te.shape                # 输出: ((70, 4), (15, 4), (15, 4))
y_tr.shape, y_val.shape, y_te.shape                # 输出: ((70,), (15,), (15,))
np.array_equal(X_tr[0], X[0])                      # 输出: True   顺序未被打乱二次

# K 折交叉验证:array_split 容忍除不尽,再把其余折拼回训练集
folds_X = np.array_split(X, 5)
folds_y = np.array_split(y, 5)
[f.shape[0] for f in folds_X]                      # 输出: [20, 20, 20, 20, 20]

i = 2
val_X = folds_X[i]
tr_X = np.concatenate(folds_X[:i] + folds_X[i + 1:], axis=0)
tr_y = np.concatenate(folds_y[:i] + folds_y[i + 1:], axis=0)
tr_X.shape, val_X.shape, tr_y.shape                # 输出: ((80, 4), (20, 4), (80,))

要点:特征与标签共用一个 permutation 索引;切点用 split(a, [i, j]),折数除不尽用 array_split。

图片通道合并、加边与组 batch

图像处理里三件最常见的形状操作:把 R/G/B 三个 (H, W) 平面合成 (H, W, 3)、给图加边框、把多张图堆成 batch。注意 dstack 拼通道、stack 组 batch、pad 时通道轴不能补。

Python28 行
import numpy as np

h, w = 2, 3
r = np.full((h, w), 200, dtype=np.uint8)
g = np.full((h, w), 120, dtype=np.uint8)
b = np.full((h, w), 60,  dtype=np.uint8)

img = np.dstack((r, g, b))         # 三个平面 → 通道维
img.shape                          # 输出: (2, 3, 3)
img[0, 0]                          # 输出: [200 120  60]

# 拆回单通道:dsplit 得 (H, W, 1),squeeze 去掉长度 1 的轴
ch_r, ch_g, ch_b = np.dsplit(img, 3)
ch_r.shape, ch_r.squeeze().shape   # 输出: ((2, 3, 1), (2, 3))
img[..., 0].shape                  # 输出: (2, 3)   切片写法更常用

# 组 batch:stack 新建 axis=0,要求各图 shape 完全一致
batch = np.stack([img, img, img, img], axis=0)
batch.shape                        # 输出: (4, 2, 3, 3)

# 加 1 像素黑边:H/W 各补,通道轴写 (0, 0) 不补
bordered = np.pad(img, ((1, 1), (1, 1), (0, 0)))
bordered.shape                     # 输出: (4, 5, 3)
bordered[0, 0]                     # 输出: [0 0 0]

# 2×2 拼贴大图
tiled = np.vstack((np.hstack((img, img)), np.hstack((img, img))))
tiled.shape                        # 输出: (4, 6, 3)

要点:dstack 合通道、stack(axis=0) 组 batch、pad 的每个轴各给一对宽度且通道轴填 (0, 0)。

视图与复制

Python1 行
import numpy as np

三种“复制”

方式 写法 共享数据 共享 shape/strides b is a b.base OWNDATA
赋值 b = a 是 是 True 同 a(原数组为 None) True
视图 b = a.view() / a[1:4] / a.T 是 否 False 指向根数组 a False
副本 b = a.copy() 否 否 False None True
Python4 行
a = np.array([1, 2, 3, 4, 5])
b = a;      b[0] = 999    # a → [999 2 3 4 5]   赋值:同一个对象
v = a.view(); v[0] = 888  # a → [888 2 3 4 5]   视图:共享数据,改 shape 互不影响
c = a.copy(); c[0] = 777  # a 不变              副本:完全独立

判别方法

Python7 行
b is a                      # 是否同一个对象(赋值)
np.shares_memory(a, b)      # 内存块是否重叠(严格)
np.may_share_memory(a, b)   # 保守估计,宁可说"可能"
b.base is a                 # b 是否为 a 的视图(base 指向最顶层的根)
b.base is None              # b 自己拥有数据(原数组或副本)
b.flags['OWNDATA']          # False ⇒ 数据是借来的(视图)
b.flags['C_CONTIGUOUS']     # 是否 C 连续
Python5 行
def relation(a, b):
    if b is a: return 'same'
    if b.base is a or np.shares_memory(a, b): return 'view'
    if np.array_equal(a, b): return 'copy'
    return 'independent'

⚠️ shares_memory 只看内存区间是否重叠,a[:4] 与 a[4:] 不重叠会正确返回 False,但涉及零长数组/相邻块时要小心;判断“是不是视图”优先用 .base。

哪些操作返回视图,哪些返回副本

操作 结果 备注
a[1:4]、a[::2]、a[::-1]、a[:] 视图 基本切片永远是视图
a[2](单元素索引) 标量/降维数组 不共享内存
a.view() 视图 显式
a.T / a.transpose() / a.swapaxes() / np.moveaxis() 视图 只换 strides
a.reshape(...) 视情况 能用 strides 表达即视图,否则副本
a.ravel() 视情况 连续时视图;flatten() 恒为副本
a.squeeze() 视图(通常)
a.flatten() 副本 总是
a.copy() / np.array(a) 副本 np.array(a, copy=False) 可能返回视图
花式索引 a[[0, 2, 4]] 副本 即使索引重复
布尔索引 a[a > 2] 副本 a[a > 2] = 0 才是就地赋值
a.astype(...)(含同类型转换) 副本
算术/比较 a + 1、a * 2、a > 0 副本(新数组) 就地请用 a += 1
a[np.newaxis, :] / a[..., None] 视图 增维不复制
Python5 行
a = np.array([[1, 2, 3], [4, 5, 6]])
a.reshape(6).base is a            # True   连续 → 视图
a[:, ::2].reshape(6).base is a    # False  非连续 reshape 会拷贝
a.T.ravel().base is a             # False  ravel 也可能拷贝
np.shares_memory(a, a.flatten())  # False

⚠️ 不要用 reshape 的返回值是否视图来写逻辑。要视图就先 np.ascontiguousarray(a);要副本就明写 .copy()。

base 的传递性

视图的视图,.base 一律指向最原始的根数组,不是上一层。

Python5 行
a = np.arange(8)
b = a[1:7]      # b.base is a → True
c = b[::2]      # c.base is a → True(不是 b!)
d = c.view()    # d.base is a → True
d[0] = 999      # a, b, c 全部同步变化

视图的语义:共享数据、独立元数据

Python4 行
a = np.arange(6)          # strides (8,)
b = a[::2]                # strides (16,),仍是 a 的视图
b[0] = 999                # a → [999 1 2 3 4 5]
v = a.view(); v.shape = (2, 3)    # 改视图形状不影响 a.shape
  • 数组 = 元数据(shape / dtype / strides)+ 数据缓冲区。视图共享缓冲区、各持一份元数据。
  • C 连续:行优先,最后一维变化最快;F 连续:列优先。a.T 把 C 连续变 F 连续。
  • 视图几乎零开销(几百纳秒 + 0 额外内存);副本是 O(n) 拷贝。大数组切片 + 只读取时,用视图可省掉整块内存。

陷阱清单

⚠️ b = a 不是备份。要备份写 a.copy();[base] * n 或 [base for _ in range(n)] 里全是同一个数组。 ⚠️ 函数参数是引用传递,def f(x): x[0] = 0 会改到外部数组;不想改就 x = x.copy() 或返回新数组。 ⚠️ 返回切片的函数(return matrix[:, 0])返回的是视图,调用方一改就改到原矩阵。 ⚠️ 循环里把切片 append 进列表,保存的是视图,原数组后续被清零时全部“消失”——要固化就 .copy()。 ⚠️ dtype=object 的数组 copy() 只复制容器,不复制里面的 Python 对象(改字典仍会互相影响),需 copy.deepcopy。 ⚠️ 布尔索引产生副本,所以 a[a > 2][0] = 0 无效(改的是临时副本);就地改必须写 a[a > 2] = 0。

选择建议

场景 选
只读取子区域、处理大数组、需要不同视角(转置/降维) 视图(快、省内存)
要保护原数据、作为函数返回值、多线程各自持有 副本
明确要就地改原数组 赋值 / 切片 + [:] =
拿不准 显式 .copy(),正确性优先

就地修改的三个坑

同一份数据,是视图还是副本决定了「改了到底有没有生效」。这三段分别对应最常踩的三种情况:切片视图意外穿透、布尔索引副本修改无效、sort() 就地返回 None。

Python34 行
import numpy as np

raw = np.array([[10, 20, 30],
                [40, 50, 60]])

# 坑 1:切片是视图,+= 会穿透到原数组
col = raw[:, 1]
col += 5
raw                            # 输出: [[10 25 30]
                               #        [40 55 60]]

# 坑 2:布尔索引是副本,改它对原数组无效
sub = raw[raw > 50]
sub[:] = 0
raw                            # 输出: [[10 25 30]
                               #        [40 55 60]]   完全没变
raw[raw > 50] = 0              # 这才是就地赋值
raw                            # 输出: [[10 25 30]
                               #        [40  0  0]]

# 坑 3:a = a + x 是重新绑定,a += x 才是就地
base = np.arange(6)
v = base[::2]
v = v + 100                    # v 指向新数组,base 不受影响
base                           # 输出: [0 1 2 3 4 5]
v2 = base[::2]
v2 += 100                      # 就地写入共享缓冲区
base                           # 输出: [100   1 102   3 104   5]

# 坑 4:sort() 就地排序返回 None
s = np.array([3, 1, 2])
t = s.sort()
t                              # 输出: None
s                              # 输出: [1 2 3]

要点:切片/视图上的 +=、[:] = 会写回原数组,布尔索引产生副本必须写成 a[cond] = v 才生效。

用视图分块处理大数组省内存

大数组分块处理时,切片是视图、零拷贝,所以循环里不会额外吃内存;一旦写 .copy(),每块都要真实分配一份。用 .base / shares_memory 可以随时验证自己拿到的是哪种。

Python33 行
import numpy as np

big = np.zeros((2000, 2000), dtype=np.float64)
big += np.arange(2000)                  # 广播就地填充,big 自己拥有数据
big.base is None                        # 输出: True
round(big.nbytes / 1024 ** 2, 2)        # 输出: 30.52   (约 30.5 MB)

# 分块求和:每块都是视图,全程 0 额外内存
chunk, sums = 500, []
for start in range(0, big.shape[0], chunk):
    view = big[start:start + chunk]     # 视图
    sums.append(view.sum())
len(sums)                               # 输出: 4
np.isclose(np.sum(sums), big.sum())     # 输出: True

# 验证视图 vs 副本
sub_view = big[:500]
sub_copy = big[:500].copy()             # 额外分配约 7.63 MB
sub_view.base is big                    # 输出: True
sub_copy.base                           # 输出: None
np.shares_memory(big, sub_view)         # 输出: True
np.shares_memory(big, sub_copy)         # 输出: False
round(sub_copy.nbytes / 1024 ** 2, 2)   # 输出: 7.63

# 转置也是视图,换个方向遍历不产生拷贝
big.T.base is big                       # 输出: True
big.T.flags['C_CONTIGUOUS']             # 输出: False   (F 连续)

# 标准化就地做,避免两个 30 MB 的临时数组
big -= big.mean()
big /= big.std()
round(float(big.mean()), 6)             # 输出: 0.0 附近(-0.0 或极小值)
big.flags['OWNDATA']                    # 输出: True

要点:分块处理用切片视图(零拷贝),只在需要固化数据时才 .copy();标量运算用 -=//= 就地做省掉临时数组。

写不污染入参的函数

函数拿到的数组是引用,直接改就会打到调用方的数据上。三条规则:入参先 copy 再改、getter 返回副本、循环里收集切片必须 .copy() 固化。

Python52 行
import numpy as np

def clip_negative_bad(a):
    a[a < 0] = 0               # 直接改入参,副作用外泄
    return a

def clip_negative(a):
    out = a.copy()             # 先固化再改
    out[out < 0] = 0
    return out

data = np.array([3, -1, 4, -5])
r = clip_negative(data)
data                           # 输出: [ 3 -1  4 -5]   原数组完好
r                              # 输出: [3 0 4 0]
clip_negative_bad(data)
data                           # 输出: [3 0 4 0]        已被污染

# getter 返回副本,内部状态才安全
class Series:
    def __init__(self, v):
        self._v = np.asarray(v)
    def raw(self):
        return self._v         # 返回内部数组本体
    def values(self):
        return self._v.copy()  # 返回副本

s = Series([1, 2, 3])
s.raw()[0] = 99
s.values()                     # 输出: [99  2  3]   内部被改了
t = Series([1, 2, 3])
t.values()[0] = 99
t.values()                     # 输出: [1 2 3]      内部安全

# 循环里收集切片:不 copy 存的是视图,原数组一变全变
buf = np.arange(6)
views = [buf[i:i + 3] for i in range(3)]
snaps = [buf[i:i + 3].copy() for i in range(3)]
buf[:] = 0
views[0]                       # 输出: [0 0 0]   视图跟着清零了
snaps[0]                       # 输出: [0 1 2]   副本保住了快照

# 边界处显式转换:统一 dtype + 强制拷贝,整数入参不会被就地截断
def zscore(a):
    x = np.array(a, dtype=float)
    x -= x.mean()
    x /= x.std()
    return x

ints = np.array([1, 2, 3, 4])
zscore(ints).round(2)          # 输出: [-1.34 -0.45  0.45  1.34]
ints                           # 输出: [1 2 3 4]   未被改动

要点:函数入参、getter 返回值、循环里保存的切片,只要不想让改动穿透就显式 .copy()。

线性代数

Python1 行
import numpy as np

乘法怎么选:* / @ / matmul / dot

写法 语义 关键差异
A * B、np.multiply 元素级相乘 只需广播兼容
A @ B、np.matmul(A, B) 矩阵乘法 2D 完全等价;>2D 视为批量矩阵乘,batch 维可广播
np.dot(A, B)、A.dot(B) 点积 1D→标量;2D→矩阵乘;含标量→标量乘;>2D 沿最后轴求和,batch 维不广播
np.vdot(a, b) 展平后共轭内积 复数取共轭
np.inner(a, b) 最后轴内积
np.outer(a, b) 外积 结果是秩 1 矩阵
np.cross(a, b) 叉积 仅 2D/3D

选择规则:2D 一律用 @;要「标量×数组」或「1D 点积」才用 dot;np.matmul(2, A) 会报错(不支持标量)。

Python6 行
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
A * B          # 输出: [[ 5 12] [21 32]]
A @ B          # 输出: [[19 22] [43 50]]
np.matmul(A, B)  # 输出: 与 @ 相同
np.dot(2, A)   # 输出: [[2 4] [6 8]]  # matmul 做不到

形状规则

输入 输出
(m,n) @ (n,p) (m,p)
(n,) @ (n,) 标量(内积)
(m,n) @ (n,) (m,)
(n,) @ (n,p) (p,)
(...,m,n) @ (...,n,p) (...,m,p)
Python2 行
# 批量矩阵乘:(3,2,2) @ (3,2,2) -> (3,2,2),逐对相乘
batch_A @ batch_B  # 深度学习里对一个 batch 做变换就是这一行

⚠️ A @ B != B @ A(矩阵乘法不可交换),且 (m,n) @ (n,p) 要求 A.shape[1] == B.shape[0],否则 ValueError。 ⚠️ (n,) 和 (n,1) 不是一回事:X @ w 得 (m,),X @ w.reshape(-1,1) 得 (m,1)。构建设计矩阵用 np.c_[X, np.ones(len(X))]。

点积、余弦相似度、范数、归一化

Python11 行
a = np.array([1, 2, 3]); b = np.array([4, 5, 6])
a @ b              # 输出: 32          点积
np.sum(a * b)      # 输出: 32          等价写法
np.outer(a, np.array([4, 5]))  # 输出: [[4 5] [8 10] [12 15]]
np.cross([1,0,0], [0,1,0])         # 输出: [0 0 1]

# 余弦相似度:先归一化再点积(去掉尺度影响)
def cosine_similarity(X, Y):
    Xn = X / np.linalg.norm(X, axis=-1, keepdims=True)
    Yn = Y / np.linalg.norm(Y, axis=-1, keepdims=True)
    return Xn @ Yn.T          # (n_x, n_y) 相似度矩阵

范数 np.linalg.norm(x, ord=...)

ord 向量 矩阵
默认 None L2 Frobenius(不是谱范数!)
1 绝对值和 最大列和
2 L2 谱范数 = 最大奇异值
np.inf 最大绝对值 最大行和
'fro' — Frobenius
'nuc' — 核范数(奇异值之和)
Python9 行
v = np.array([3, -4, 0, 5])
np.linalg.norm(v)              # 输出: 7.07   L2
np.linalg.norm(v, ord=1)       # 输出: 12     绝对值和
np.linalg.norm(v, ord=np.inf)  # 输出: 5      最大值
np.count_nonzero(v)            # 输出: 3      "L0"

# 逐行归一化(机器学习最常见):keepdims=True 才能正确广播
Xn = X / np.linalg.norm(X, axis=1, keepdims=True)
np.linalg.norm(Xn, axis=1)     # 输出: 全 1

⚠️ np.linalg.norm(A) 对矩阵返回 Frobenius,想要最大奇异值必须写 np.linalg.norm(A, 2)。 ⚠️ 归一化忘了 keepdims=True → 形状 (n,) 无法与 (n,d) 广播。

np.linalg 速查表

函数 作用 返回 / 备注
inv(A) 逆矩阵 奇异矩阵抛 LinAlgError
solve(A, b) 解 Ax = b 优先于 inv(A) @ b
lstsq(A, b, rcond=None) 最小二乘 x, residuals, rank, s
det(A) 行列式 浮点,判 0 用 np.isclose
slogdet(A) 符号 + log|det| 大矩阵防溢出
matrix_rank(A) 秩 = 非零奇异值个数
cond(A) 条件数 = σmax/σmin,越大越病态
norm(x, ord) 范数 见上表
eig(A) 特征值分解 w, V;V[:, i] 是第 i 个特征向量
eigh(A) 对称/厄米矩阵 更快更稳,特征值升序
svd(A, full_matrices=True) 奇异值分解 U, s, Vt;s 是一维,需 np.diag
qr(A) QR 分解 Q 正交、R 上三角
cholesky(A) Cholesky 需对称正定,A = L @ L.T
pinv(A) 伪逆 基于 SVD,秩亏也能用
matrix_power(A, n) 矩阵幂
Python26 行
A = np.array([[2, 3], [3, 4]], dtype=float)
b = np.array([8, 11], dtype=float)
np.linalg.solve(A, b)      # 输出: [1. 2.]
np.linalg.inv(A) @ b       # 输出: [1. 2.] —— 结果相同但更慢更不稳
np.linalg.det(np.array([[1,2],[3,4]]))  # 输出: -2.0

# 特征值分解:A v = λ v
Ae = np.array([[2., 1.], [1., 2.]])
w, V = np.linalg.eig(Ae)                    # w: 特征值, V[:, i]: 对应特征向量
w                                           # 输出: [3. 1.]
np.allclose(Ae @ V[:, 0], w[0] * V[:, 0])   # 输出: True(验证 Av = λv)

# SVD:A = U Σ V^T,任意形状(不必是方阵)都能分解
M = np.arange(1, 13, dtype=float).reshape(4, 3)
U, s, Vt = np.linalg.svd(M)                 # s 是一维!需要自己补成 Σ
U.shape, s.shape, Vt.shape                  # 输出: (4, 4) (3,) (3,)
Sigma = np.zeros(M.shape); Sigma[:3, :3] = np.diag(s)
np.allclose(U @ Sigma @ Vt, M)              # 输出: True

# full_matrices=False:U 压成 (4, 3),省内存
U2, s2, Vt2 = np.linalg.svd(M, full_matrices=False)   # U2.shape -> (4, 3)

# 低秩近似(图像压缩/推荐系统):只留前 k 个奇异值
k = 2
M_k = (U2[:, :k] * s2[:k]) @ Vt2[:k, :]     # 比 np.diag 省内存
err = np.linalg.norm(M - M_k) / np.linalg.norm(M)

解方程组

情况 条件 方法
唯一解 A 满秩方阵 solve
超定(方程 > 未知数) 无精确解 lstsq 最小二乘
欠定(方程 < 未知数) 无穷多解 lstsq 给最小范数解
秩亏/病态 条件数大 pinv 或 lstsq(rcond=...)
Python7 行
# 最小二乘拟合 y = a x + b
x = np.linspace(0, 5, 50)
y = 2 * x + 1 + np.random.default_rng(0).normal(0, 1, 50)
A = np.c_[x, np.ones_like(x)]                    # 设计矩阵 (50, 2)
coef, *_ = np.linalg.lstsq(A, y, rcond=None)     # 输出: coef ≈ [2., 1.]
y_hat = A @ coef
R2 = 1 - np.sum((y - y_hat)**2) / np.sum((y - y.mean())**2)

⚠️ 永远不要用 inv(A) @ b 解方程组:慢且数值误差被放大。用 solve(LU 分解)。 ⚠️ det(A) 接近 0 不代表 det 恰好为 0,判断可逆用 np.isclose(det, 0) 或 matrix_rank。 ⚠️ 复数/浮点比较用 np.allclose,别用 ==。

转置、迹、协方差

Python14 行
A.T                        # 转置,1D 数组的 .T 是它自己
np.array_equal((A @ B).T, B.T @ A.T)   # 输出: True(乘积转置反序)
np.trace(A)                # 对角线之和
np.isclose(np.trace(A), np.sum(np.linalg.eigvals(A)).real)   # 迹 = 特征值之和

# 协方差矩阵(样本×特征),与 np.cov(X, rowvar=False) 一致
Xc = X - X.mean(axis=0)
cov = Xc.T @ Xc / (len(X) - 1)

# PCA:中心化 → 协方差 → eigh → 取最大特征向量
w, V = np.linalg.eigh(cov)
pc1 = V[:, ::-1][:, 0]                     # eigh 升序,倒序取第一主成分
X_pca = Xc @ V[:, ::-1][:, :k]             # 降到 k 维
w[::-1] / w.sum()                          # 各主成分解释的方差比例

批量解线性方程组(配料/配方问题)

三种原料按不同比例混合,要同时满足多张订单的营养指标。设用量向量 x,约束是 x @ M = b(M 行=原料、列=营养素),转置成标准形式后,多张订单可以作为多个右端项一次解出,不必循环调用 solve。

Python18 行
import numpy as np

# 三种原料每千克提供的营养素(行=原料,列=蛋白/脂肪/碳水)
M = np.array([[0.30, 0.10, 0.50],
              [0.20, 0.40, 0.30],
              [0.10, 0.20, 0.60]])
# 4 张订单要求达到的营养素总量(行=订单)
orders = np.array([[30., 20., 50.],
                   [45., 15., 40.],
                   [25., 25., 60.],
                   [60., 30., 20.]])

# 约束 x @ M = b  =>  M.T @ x = b,一次解出全部订单
X = np.linalg.solve(M.T, orders.T)        # (3, 4):第 j 列是第 j 张订单的用量
X.T.round(2)                              # 输出: [[80. 28.89 2.22] [150. 38.89 -77.78] [50. 27.78 44.44] [180. 117.78 -175.56]]
np.abs(X.T @ M - orders).max()            # 输出: 7.1e-15(残差,验证解正确)
np.linalg.cond(M.T)                       # 输出: 5.78(远离病态,解可信)
(X.T < 0).any(axis=1)                     # 输出: [False  True False  True] → 第 2、4 单需要"负用量",配方无可行解

要点:多个右端项直接堆成 B 的二维列一次 solve(A, B);约束写成 x @ M = b 时要转置成 M.T @ x = b;解出负数代表需求不可行;用 cond 判断结果是否值得相信。

多项式拟合与岭回归(过拟合 / 正则化)

只有 24 个带噪声的观测点,用多项式去拟合 sin(2πx)。次数升高会把噪声一起拟合进来(设计矩阵条件数爆炸),此时给最小二乘加一个 λ‖w‖² 惩罚项就能压住,且岭回归等价于把 sqrt(λ)·I 当成额外的若干行样本再做最小二乘,不需要额外的求解器。

Python29 行
import numpy as np
rng = np.random.default_rng(42)

x = np.linspace(0, 1, 24)
y = np.sin(2 * np.pi * x) + rng.normal(0, 0.15, 24)        # 24 个带噪声观测
xt = np.linspace(0, 1, 300)
yt = np.sin(2 * np.pi * xt) + rng.normal(0, 0.15, 300)     # 同分布的验证集

def polyfit(x, y, deg, lam=0.0):
    """岭回归:在范德蒙德矩阵下面补 sqrt(lam)*I,目标补 0,再做最小二乘"""
    V = np.vander(x, deg + 1)                              # (n, deg+1)
    A = np.vstack([V, np.sqrt(lam) * np.eye(deg + 1)])
    b = np.concatenate([y, np.zeros(deg + 1)])
    return np.linalg.lstsq(A, b, rcond=None)[0]

def rmse(x, y, coef):
    return np.sqrt(np.mean((np.vander(x, len(coef)) @ coef - y) ** 2))

c1 = polyfit(x, y, 1)
(rmse(x, y, c1), rmse(xt, yt, c1))      # 输出: 训练 0.477 / 验证 0.472  欠拟合
c3 = polyfit(x, y, 3)
(rmse(x, y, c3), rmse(xt, yt, c3))      # 输出: 训练 0.143 / 验证 0.159  最佳
c15 = polyfit(x, y, 15)
(rmse(x, y, c15), rmse(xt, yt, c15))    # 输出: 训练 0.066 / 验证 0.172  过拟合(训练更好、验证更差)
np.linalg.cond(np.vander(x, 16))        # 输出: 2.8e+11(deg=15 时设计矩阵严重病态)

rmse(xt, yt, polyfit(x, y, 15, 1e-4))   # 输出: 0.149  适度正则化,验证误差回落
rmse(xt, yt, polyfit(x, y, 15, 1e-2))   # 输出: 0.217
rmse(xt, yt, polyfit(x, y, 15, 1.0))    # 输出: 0.420  λ 过大 → 又变成欠拟合

要点:np.vander 一步构造多项式设计矩阵;岭回归 = np.vstack([V, sqrt(λ)I]) + 目标补零 + lstsq;判断过拟合看的是验证误差与训练误差的差距,不是训练误差本身。

SVD 低秩近似做图像压缩

灰度图就是一个矩阵,SVD 后只保留前 k 个奇异值即可重建。合成一张 256×256 的图,比较不同 k 下的相对误差与存储占用(系数按 float32 存,原图 uint8 每像素 1 字节)。

Python22 行
import numpy as np

h = w = 256
yy, xx = np.mgrid[0:h, 0:w]                       # 合成"图像":条纹背景 + 两个圆盘
img = (0.5 + 0.5 * np.sin(xx / 18) * np.cos(yy / 24)) * 160
img = img + 60 * ((xx - 70) ** 2 + (yy - 90) ** 2 < 40 ** 2)
img = img - 50 * ((xx - 180) ** 2 + (yy - 160) ** 2 < 30 ** 2)
img = np.clip(img, 0, 255).astype(np.uint8).astype(float)

U, s, Vt = np.linalg.svd(img, full_matrices=False)
(s[:6] / s[0]).round(3)                           # 输出: [1. 0.509 0.102 0.053 0.042 0.026] 能量高度集中
(s[:10] ** 2).sum() / (s ** 2).sum()              # 输出: 0.9991(前 10 项已带走 99.9% 能量)

for k in (5, 10, 30):
    comp = (U[:, :k] * s[:k]) @ Vt[:k]            # 秩 k 重建,避免构造 diag(s) 省内存
    err = np.linalg.norm(img - comp) / np.linalg.norm(img)
    ratio = k * (h + w + 1) * 4 / (h * w)         # 存 U[:, :k]、s[:k]、Vt[:k] 所需的字节占比
    # k=5  → 相对误差 0.050,占用 15.7%
    # k=10 → 相对误差 0.030,占用 31.3%
    # k=30 → 相对误差 0.011,占用 93.9%(此时已不划算)

comp = np.clip((U[:, :10] * s[:10]) @ Vt[:10], 0, 255).astype(np.uint8)   # 存回图像必须 clip + 转 uint8

要点:(U[:, :k] * s[:k]) @ Vt[:k] 是低秩重建的标准写法(广播替代 np.diag);用 (s**2).cumsum() / (s**2).sum() 挑 k;重建后必须 clip + astype(np.uint8),否则存图溢出。

随机数

Python2 行
import numpy as np
rng = np.random.default_rng(42)   # 新 API,全文只用它

14.1 旧 API → 新 API 对照(旧 API 不再展开,照表替换即可)

功能 旧(全局,不推荐) 新(Generator)
设置种子 np.random.seed(42) rng = np.random.default_rng(42)
[0,1) 均匀 np.random.rand(5) rng.random(5)
标准正态 np.random.randn(5) rng.standard_normal(5)
随机整数 np.random.randint(0, 10, 5) rng.integers(0, 10, 5)
均匀分布 np.random.uniform(a, b, n) rng.uniform(a, b, n)
正态分布 np.random.normal(m, s, n) rng.normal(m, s, n)
随机选择 np.random.choice(a, 3) rng.choice(a, 3)
随机排列 np.random.permutation(a) rng.permutation(a)
原地打乱 np.random.shuffle(a) rng.shuffle(a)
字节 np.random.bytes(32) rng.bytes(32)

旧 API 的问题:全局状态(任何地方调用都会推进同一序列)、多线程不安全、无法并行独立流。新 API 每个 rng 持有独立状态。

Python6 行
a = np.random.default_rng(42).random(3)
b = np.random.default_rng(42).random(3)     # 同种子重新创建
np.array_equal(a, b)                        # 输出: True(可复现)

r1 = np.random.default_rng(42); r2 = np.random.default_rng(123)
r1.random(3), r2.random(3)                  # 两个流互不影响,各自推进

种子与可复现:科研/调试/测试固定种子;生产、游戏、密码学不要固定(密码学用 secrets 模块,别用 NumPy)。

多流(并行/多线程)

Python11 行
# 每个线程一个独立 rng
def worker(seed):
    return np.random.default_rng(seed).random(1000).sum()

# NumPy ≥ 1.25:从一个 rng 派生多个互不重叠的子流
rngs = np.random.default_rng(42).spawn(4)

# 更底层:SeedSequence 显式管理
ss = np.random.SeedSequence(123)
children = ss.spawn(4)
rngs = [np.random.default_rng(s) for s in children]

常用生成

Python15 行
rng = np.random.default_rng(42)
rng.random(5)                          # 输出: [0.774 0.439 0.859 0.697 0.094]
rng.random((2, 3))                     # 形状参数可直接给元组
rng.uniform(0, 10, size=5)             # [0, 10) 浮点
rng.uniform(0, 10, size=(3, 3))        # 也支持多维

rng = np.random.default_rng(42)        # 重置(下面这行才是种子 42 的首次抽取)
rng.integers(0, 10, size=5)            # 输出: [0 7 6 4 4]  注意不含上界
rng.integers(1, 6, size=5, endpoint=True)   # [1, 6] 含上界
rng.integers(0, 256, size=10, dtype=np.uint8)
rng.standard_normal(5)                 # 标准正态
rng.normal(loc=100, scale=15, size=1000)

# 参数广播:每列不同均值/标准差
rng.normal([10, 20, 30], [1, 2, 3], size=(100, 3)).mean(axis=0)  # 输出: ≈ [10 20 30]

各分布速查

分布 调用 用途
均匀 uniform(low, high, size) 权重初始化(Xavier)
标准正态 standard_normal(size) 噪声、He 初始化
正态 normal(loc, scale, size) 自然现象、测量误差
二项 binomial(n, p, size) n 次试验成功次数;n=1 即伯努利
泊松 poisson(lam, size) 单位时间/空间的事件计数
指数 exponential(scale, size) 事件间隔、寿命(scale = 均值)
伽马 gamma(shape, scale, size) k 个事件的总等待时间
贝塔 beta(a, b, size) [0,1] 上的概率分布/CTR
卡方 chisquare(df, size) 假设检验
多项 multinomial(n, pvals, size) 一次试验多类别
Python13 行
rng.binomial(10, 0.5, 1000).mean()     # 输出: ≈ 5
rng.poisson(3, 1000).mean()            # 输出: ≈ 3
rng.exponential(5, 1000).mean()        # 输出: ≈ 5

# 蒙特卡洛求 π
n = 1_000_000
x, y = rng.uniform(-1, 1, n), rng.uniform(-1, 1, n)
4 * np.sum(x**2 + y**2 <= 1) / n       # 输出: ≈ 3.1416(误差 ∝ 1/√n)

# 生成指定协方差的样本:X = Z @ L.T
L = np.linalg.cholesky(np.array([[4., 2.], [2., 4.]]))
X = rng.standard_normal((1000, 2)) @ L.T
np.cov(X.T)                            # 输出: ≈ [[4, 2], [2, 4]]

choice / permutation / shuffle

Python17 行
rng.choice(['a', 'b', 'c', 'd'])                      # 输出: 单个元素
rng.choice(['a', 'b', 'c', 'd'], size=3)              # 有放回
rng.choice(['a', 'b', 'c', 'd'], size=3, replace=False)   # 无放回
rng.choice(100, size=10, replace=False)               # 从 0~99 取 10 个不重复
rng.choice(items, size=1000, p=[0.1, 0.2, 0.3, 0.4])  # 加权(p 必须和为 1)

# 加权抽奖:任意权重先归一化
probs = np.array([100, 50, 30, 15, 5]); probs = probs / probs.sum()
rng.choice(articles, size=10, p=probs)

rng.permutation(10)          # 返回打乱的副本,原数组不变
rng.permutation(X)           # 多维只打乱第 0 轴
rng.shuffle(X)                # 原地打乱,返回 None

# 打乱 X / y 且保持对应关系
idx = rng.permutation(len(X))
X, y = X[idx], y[idx]
permutation shuffle
返回 新数组 None
原数组 不变 被修改
多维 只打乱第 0 轴 只打乱第 0 轴
Python4 行
# 训练/验证/测试划分(70/15/15)
idx = rng.permutation(n)
tr, va = int(0.7 * n), int(0.85 * n)
train_idx, val_idx, test_idx = idx[:tr], idx[tr:va], idx[va:]

⚠️ rng.shuffle(arr) 没有返回值,写 x = rng.shuffle(x) 会得到 None。 ⚠️ integers(low, high) 默认不含 high;要含上界加 endpoint=True。 ⚠️ choice 的 p 之和必须≈1,否则抛异常;权重不必归一化时自己除一下。 ⚠️ 拒绝采样别用 while 循环逐个生成,一次生成大量候选再布尔筛选更快: cands = rng.random(10_000); samples = cands[cands > 0.5][:10]

蒙特卡洛模拟股价路径与风险度量

用几何布朗运动一次性模拟 2000 条一年的日频价格路径,统计终值分布、盈利概率、VaR/CVaR 与最大回撤。所有路径并行推进,全程无 Python 循环。

Python21 行
import numpy as np
rng = np.random.default_rng(42)

S0, mu, sigma, T, steps, n = 100.0, 0.08, 0.25, 1.0, 252, 2000
dt = T / steps
Z = rng.standard_normal((n, steps))                              # 一次抽完全部随机增量
inc = (mu - 0.5 * sigma ** 2) * dt + sigma * np.sqrt(dt) * Z     # 对数收益增量
paths = S0 * np.exp(np.cumsum(inc, axis=1))                      # (2000, 252) 沿时间轴累乘
paths = np.c_[np.full(n, S0), paths]                             # 补上初始价 → (2000, 253)

final = paths[:, -1]
final.mean()                              # 输出: 108.12(理论均值 S0*exp(mu*T)=108.33)
np.median(final)                          # 输出: 104.75(对数正态右偏,中位数低于均值)
(final > S0).mean()                       # 输出: 0.574  盈利概率

var95 = np.percentile(final, 5)           # 输出: 69.46  VaR95:最差 5% 情形的价格阈值
final[final <= var95].mean()              # 输出: 63.75  CVaR:跌破 VaR 后的平均价格

peak = np.maximum.accumulate(paths, axis=1)              # 逐路径的历史最高价
mdd = ((paths - peak) / peak).min(axis=1)                # 每条路径的最大回撤
(mdd.mean(), mdd.min())                   # 输出: (-0.235, -0.606)

要点:exp(cumsum(...)) 把增量变路径;np.maximum.accumulate 是算历史峰值/最大回撤的向量化写法;分位数 percentile 直接给出 VaR,再用布尔掩码取尾部均值得到 CVaR。

Bootstrap 置信区间

样本是偏态的指数分布(如工单处理时长),正态近似不可靠。有放回重抽样 5000 次,用重抽样统计量的分位数直接给出置信区间——对均值、中位数、相关系数等任意统计量都成立,不依赖分布假设。

Python15 行
import numpy as np
rng = np.random.default_rng(42)

x = rng.exponential(scale=8.0, size=120)               # 偏态样本
B = 5000
idx = rng.integers(0, len(x), size=(B, len(x)))        # (B, n) 一次抽完 B 个重抽样集
boot = x[idx]                                          # (B, n),等价于 B 次有放回抽样
means = boot.mean(axis=1)
meds = np.median(boot, axis=1)

x.mean(), np.percentile(means, [2.5, 97.5])            # 输出: 7.42, [6.20 8.71]
np.median(x), np.percentile(meds, [2.5, 97.5])         # 输出: 5.36, [3.34 7.65](偏态下中位数区间与均值区间差别很大)

se = x.std(ddof=1) / np.sqrt(len(x))
(x.mean() - 1.96 * se, x.mean() + 1.96 * se)           # 输出: (6.16, 8.68) 正态近似:只能给均值,且区间偏窄

要点:rng.integers(0, n, size=(B, n)) 用一次索引矩阵替代 B 次 choice 循环(快一个数量级);区间取 [2.5, 97.5] 分位;B*n 是真实内存开销,样本很大时改成循环分批累加统计量。

置换检验判断 A/B 实验是否显著

对照组 40 人、实验组 45 人,观测到均值差 5.89,这个差异是真有效还是抽样波动?把两组混在一起反复随机重新分组,看“偶然出现这么大差异”的比例——即 p 值。不需要任何正态假设。

Python14 行
import numpy as np
rng = np.random.default_rng(42)

ctrl = rng.normal(100, 15, 40)          # 对照组
test = rng.normal(107, 15, 45)          # 实验组
obs = test.mean() - ctrl.mean()         # 输出: 5.89  实际观测到的差异

pool = np.concatenate([ctrl, test])
n1 = len(ctrl)
B = 5000
perm = np.argsort(rng.random((B, len(pool))), axis=1)          # 一次生成 B 个排列
diffs = pool[perm[:, n1:]].mean(1) - pool[perm[:, :n1]].mean(1) # (B,) 零假设下的差异分布
diffs.std()                             # 输出: 2.55  纯随机时的波动尺度
(np.abs(diffs) >= abs(obs)).mean()      # 输出: 0.0168 → p<0.05,差异显著

要点:np.argsort(rng.random((B, n)), axis=1) 一次拿到 B 个独立排列(比循环 permutation 快得多);p 值 = 置换统计量绝对值 ≥ 观测值的比例;双尾用 abs,单尾去掉 abs。

文件读写

Python1 行
import numpy as np

格式怎么选

格式 API 何时用
.npy 单数组 save / load 中间结果、最快、保精度
.npz 多数组 savez / load 一次存 train/test/标签
.npz 压缩 savez_compressed 长期归档、稀疏/有规律数据(随机数据压不动,白耗 CPU)
.txt/.csv savetxt / loadtxt 人可读、与 Excel/其他语言交换
.csv 带缺失值 genfromtxt 缺列/缺值/混合类型
原始二进制 tofile / fromfile 跨语言裸数据(不存 shape 和 dtype)
大文件 load(..., mmap_mode='r') 超过内存,按需分页读

二进制:save / load / savez

Python14 行
arr = np.array([[1, 2, 3], [4, 5, 6]])
np.save('a.npy', arr)           # 自动补 .npy 后缀;save 会覆盖同名文件
a = np.load('a.npy')            # dtype/shape 自动恢复
a.dtype, a.shape                # 输出: (dtype('int32'), (2, 3))

# 多个数组:命名保存
np.savez('ds.npz', train_X=X, train_y=y, test_X=X2)
np.savez_compressed('ds_c.npz', train_X=X, train_y=y)

# 惰性加载:.npz 打开时并不读入数据,取 key 时才读
with np.load('ds.npz') as d:
    print(d.files)              # 输出: ['train_X', 'train_y', 'test_X']
    X = d['train_X']            # 这一刻才真正读取
# 离开 with 自动关闭;不用 with 则记得 d.close()

⚠️ .npz 是惰性加载:把 np.load(...) 的句柄存起来长期持有会占着文件句柄,用 with 或 close()。 ⚠️ 不命名的 np.savez(f, a, b) 会用默认键 arr_0, arr_1,难维护,一律用关键字参数。 ⚠️ tofile 会展平数组且不记录 shape/dtype,fromfile 必须手动指定 dtype 并 reshape。

文本:savetxt / loadtxt

Python17 行
data = np.array([[1.23456, 2.34567], [3.45678, 4.56789]])

np.savetxt('a.txt', data)                       # 默认 fmt='%.18e',很长
np.savetxt('b.txt', data, fmt='%.2f')           # 输出文件: 1.23 2.35 / 3.46 4.57
np.savetxt('c.csv', data, delimiter=',', fmt='%.4f')
np.savetxt('d.txt', data, fmt='%d')             # 整数
np.savetxt('e.txt', data, fmt=['%d', '%.3f'])   # 每列不同格式
np.savetxt('f.csv', data, delimiter=',',
           header='Math,English', comments='', fmt='%.2f')   # comments='' 去掉 '# '
np.savetxt('g.txt', data, footer='end', comments='# ')

np.loadtxt('c.csv', delimiter=',')              # 读回 (2, 2)
np.loadtxt('f.csv', delimiter=',', skiprows=1)  # 跳过表头
np.loadtxt('a.txt', usecols=(0, 1))             # 只要第 0、1 列
np.loadtxt('a.txt', max_rows=100)               # 只读前 100 行
np.loadtxt('a.txt', dtype=int)
c0, c1 = np.loadtxt('a.txt', unpack=True)       # 按列解包

⚠️ savetxt 只支持 1D/2D,3D 数组会报错(先 reshape 或用 .npy)。 ⚠️ header 默认带 # 前缀,要标准 CSV 表头必须 comments=''。 ⚠️ 文本文件有精度损失。需要精确往返用 fmt='%.18e' 或直接上 .npy。

CSV 与缺失值:genfromtxt

Python24 行
# 数值 CSV
np.savetxt('scores.csv', data, delimiter=',', header='Math,English', comments='', fmt='%d')
np.loadtxt('scores.csv', delimiter=',', skiprows=1)

# 带表头 + 按列名访问(结构化数组)
d = np.genfromtxt('scores.csv', delimiter=',', names=True)
d.dtype.names      # 输出: ('Math', 'English')
d['Math']          # 按列取值

# 缺失值:genfromtxt 默认把空缺填 nan
d = np.genfromtxt('m.csv', delimiter=',')
np.isnan(d).sum()                      # 缺失个数
d[~np.isnan(d).any(axis=1)]            # 删掉含缺失的行

# 自定义缺失标记 + 填充值
d = np.genfromtxt('m.csv', delimiter=',',
                  missing_values=['NA', 'N/A', '-999'],
                  filling_values=np.nan)
d = np.genfromtxt('m.csv', delimiter=',', filling_values=(0, -1, 999))  # 每列不同

# 混合类型(字符串+数字)需 dtype=None, encoding=None
rec = np.genfromtxt('people.csv', delimiter=',', names=True,
                    dtype=None, encoding=None)
rec['Name'], rec['Age']

⚠️ loadtxt 遇到缺失值直接报错;有空字段就用 genfromtxt。 ⚠️ dtype=None 时字符串默认是 bytes,加 encoding=None 才是 str。

大文件与性能

Python10 行
m = np.load('big.npy', mmap_mode='r')   # 内存映射,几乎不占内存
chunk = m[0:1000]                        # 真正切片时才载入
result = m.mean(axis=0)                  # 可当普通数组用

# 分块遍历
for i in range(0, len(m), 10000):
    process(m[i:i+10000])

# 降 dtype 省空间
arr.astype(np.float32).nbytes            # 输出: 比 float64 少一半

性能排序:.npy ≫ .csv/.txt(二进制快一个数量级、体积小、无精度损失);.npz 一次存多个数组比逐个 save 快。

npz 缓存特征 + 断点续训

特征工程通常很慢,把结果缓存成 .npz,第二次运行直接读盘;长训练任务再把参数和优化器状态连同“已跑轮数”存进 checkpoint,中断后从上次轮数继续。

Python44 行
import numpy as np
import os, tempfile

rng = np.random.default_rng(42)
d = tempfile.mkdtemp()
cache_p = os.path.join(d, "features.npz")
ckpt_p = os.path.join(d, "ckpt.npz")

def build_features():                     # 模拟昂贵的预处理(读图、分词、归一化…)
    X = rng.standard_normal((400, 8))
    w = np.array([1.5, -2.0, 0, 0, 3.0, 0, 0, 0.5])
    return X, (X @ w + rng.normal(0, 0.3, 400) > 0).astype(float)

def get_features():
    if os.path.exists(cache_p):
        with np.load(cache_p) as f:       # with 保证句柄被关闭
            return f["X"], f["y"], "cache"
    X, y = build_features()
    np.savez_compressed(cache_p, X=X, y=y, n=len(X))    # 顺手存样本数,便于校验缓存是否有效
    return X, y, "compute"

X, y, src = get_features(); src           # 输出: 'compute'(首次:算完再存)
X, y, src = get_features(); src           # 输出: 'cache'(第二次:直接读盘)
os.path.getsize(cache_p) / 1024           # 输出: 24.8 KB

# 断点续训:ckpt 里存参数 + 已跑轮数
EPOCHS, LR = 30, 0.5
for stop in (20, 30):                     # 第一次跑到 20 轮"中断",重启后继续到 30
    if os.path.exists(ckpt_p):
        with np.load(ckpt_p) as f:
            W, b, start = f["W"], float(f["b"]), int(f["epoch"])
    else:
        W, b, start = np.zeros(X.shape[1]), 0.0, 0
    # start  # 输出: 第一次 0,第二次 20
    for epoch in range(start, stop):
        p = 1 / (1 + np.exp(-(X @ W + b)))
        g = (p - y) / len(X)
        W -= LR * (X.T @ g); b -= LR * g.sum()
        if (epoch + 1) % 10 == 0:
            np.savez(ckpt_p, W=W, b=b, epoch=epoch + 1, acc=((p > 0.5) == y).mean())
            # 输出: 依次存下 epoch=10 acc=0.968 / 20 acc=0.970 / 30 acc=0.970

with np.load(ckpt_p) as f:
    f.files                               # 输出: ['W', 'b', 'epoch', 'acc']

要点:缓存要带可校验的元信息(样本数/数据版本/hash),否则改了上游数据却读到旧缓存;.npz 一律用 with 或 close();checkpoint 里存 epoch 才能从断点接着跑,而非从头开始。

混合类型 CSV 的清洗与分组透视

订单 CSV 里有日期、中文城市、缺失金额、渠道四类列。先用显式 dtype 的结构化数组读进来(dtype=None 遇到 NA 会因类型推断失败而报错),再把日期转成 datetime64、缺失值用中位数填补,最后做分组汇总。

Python36 行
import numpy as np
import os, tempfile

csv_text = """date,city,amount,channel
2024-01-05,北京,1200,app
2024-01-05,上海,860,web
2024-01-06,北京,,app
2024-01-06,广州,1500,app
2024-01-07,上海,NA,web
2024-01-07,北京,940,web
2024-01-08,广州,2100,app
2024-01-08,上海,1750,app
"""
p = os.path.join(tempfile.mkdtemp(), "orders.csv")
with open(p, "w", encoding="utf-8") as f:
    f.write(csv_text)

dtype = [("date", "U10"), ("city", "U8"), ("amount", "f8"), ("channel", "U8")]
raw = np.genfromtxt(p, delimiter=",", names=True, dtype=dtype, encoding=None,
                    missing_values=["NA", ""], filling_values=np.nan)
raw["amount"]                             # 输出: [1200. 860. nan 1500. nan 940. 2100. 1750.]
np.isnan(raw["amount"]).sum()             # 输出: 2

days = raw["date"].astype("datetime64[D]")                        # 字符串 → 日期,可直接相减/比较
days.max() - days.min()                   # 输出: 3 days(覆盖 4 天)
amt = np.where(np.isnan(raw["amount"]), np.nanmedian(raw["amount"]), raw["amount"])
amt                                       # 输出: [1200. 860. 1350. 1500. 1350. 940. 2100. 1750.]

cities, ci = np.unique(raw["city"], return_inverse=True)          # 类目字符串 → 整数编码
chans, hi = np.unique(raw["channel"], return_inverse=True)
np.bincount(ci, weights=amt)              # 输出: [3960. 3490. 3600.]  依次为 上海/北京/广州
np.bincount(ci)                           # 输出: [3 3 2]  每城订单数

pivot = np.zeros((len(cities), len(chans)))
np.add.at(pivot, (ci, hi), amt)           # 二维分组求和(bincount 的高维替代)
pivot          # 输出: [[1750. 2210.] [2550. 940.] [3600. 0.]]  行=城市(上海/北京/广州),列=渠道(app/web)

要点:混合类型列里若含 NA,别用 dtype=None(类型推断会崩),显式给结构化 dtype 最稳;astype("datetime64[D]") 一步把日期字符串变成可运算的日期;np.unique(..., return_inverse=True) + bincount(一维)/ np.add.at(多维)是纯 NumPy 的 groupby。

mmap 分块流式统计与原地写回

数组大到内存放不下时,用 mmap_mode='r' 打开,按块累加求和得到全局均值/标准差,峰值内存始终只有一块;需要改写时用 'r+' 原地标准化后 flush()。

Python25 行
import numpy as np
import os, tempfile

rng = np.random.default_rng(42)
p = os.path.join(tempfile.mkdtemp(), "big.npy")
n, d = 20000, 50
np.save(p, (rng.standard_normal((n, d)) * 3 + 7).astype(np.float32))
os.path.getsize(p) / 1e6                  # 输出: 4.0 MB(真实场景可能是 40 GB)

mm = np.load(p, mmap_mode="r")            # 几乎不占内存,切片时才真正读盘
s1 = np.zeros(d); s2 = np.zeros(d); cnt = 0
for i in range(0, n, 2000):
    blk = np.asarray(mm[i:i + 2000], dtype=np.float64)   # 用 float64 累加,避免精度损失
    s1 += blk.sum(0); s2 += (blk * blk).sum(0); cnt += len(blk)
mean = s1 / cnt
std = np.sqrt(s2 / cnt - mean ** 2)       # 一次遍历同时得到均值和方差
np.allclose(mean, mm.mean(axis=0), atol=1e-4)   # 输出: True(与整列统计一致)
np.allclose(std, mm.std(axis=0), atol=1e-3)     # 输出: True

mw = np.load(p, mmap_mode="r+")           # 需要写回时用 'r+'
for i in range(0, n, 2000):
    mw[i:i + 2000] = (np.asarray(mw[i:i + 2000]) - mean) / std
mw.flush()                                # 确保落盘
re = np.load(p, mmap_mode="r")
(np.abs(re.mean(axis=0)).max(), re.std(axis=0).mean())   # 输出: (0.0, 1.0)  已标准化

要点:E[x²] − E[x]² 只需一次遍历就能同时得到均值和标准差(块内用 float64 累加防精度损失);mmap_mode='r+' 支持原地改写超大数组,写完记得 flush()。

实战套路

Python1 行
import numpy as np

通用套路(跨项目复用)

需求 写法
添加偏置列 X1 = np.c_[X, np.ones(len(X))]
one-hot Y = np.eye(C)[y];反向 y = Y.argmax(1)
广播加偏置 Z = X @ W + b,b 形状 (1, out),自动广播到 (batch, out)
滑动窗口 np.lib.stride_tricks.sliding_window_view(a, w) → 再 mean/std/max(axis=-1)
边界填充 np.pad(a, pad_width, mode='edge'/'constant'/'reflect')
1D 卷积/移动平均 np.convolve(x, np.ones(k)/k, mode='valid')
裁剪到合法范围 np.clip(x, 0, 255).astype(np.uint8)
按条件替换 np.where(cond, a, b)
分组统计 np.add.reduceat、np.bincount(y, weights=...)
分箱计数 np.histogram(x, bins) / np.digitize(x, bins)
排名/取 Top-k np.argsort(s)[::-1][:k](大数据用 np.argpartition)
打乱并保持配对 idx = rng.permutation(n); X[idx], y[idx]
成对距离/相似度 Xn @ Xn.T(归一化后即余弦相似度矩阵)
网格坐标 np.meshgrid(...), np.c_[xx.ravel(), yy.ravel()]

⚠️ 用 Python for 循环遍历像素/样本是最大性能陷阱,一律换成切片/窗口/矩阵运算。 ⚠️ sliding_window_view 返回的是视图(零拷贝),别对它原地写入。

图像处理骨架

Python40 行
# 图像即数组:彩色 (H, W, 3),灰度 (H, W)
img = np.array(Image.open('a.jpg'))          # dtype uint8

# 1) 灰度化:加权平均(人眼对绿最敏感),一行搞定
gray = img @ np.array([0.299, 0.587, 0.114])   # 输出: (H, W) float
gray = gray.astype(np.uint8)

# 2) 卷积/滤波:pad + sliding_window_view,全向量化
def conv2d(img, kernel):
    kh, kw = kernel.shape
    padded = np.pad(img, ((kh//2, kh//2), (kw//2, kw//2)), mode='edge')
    win = np.lib.stride_tricks.sliding_window_view(padded, (kh, kw))
    return np.sum(win * kernel, axis=(-2, -1))     # 输出: 与原图同形状

mean_k = np.ones((3, 3)) / 9
blur = conv2d(gray, mean_k)
# 中值滤波(去椒盐噪声):把 np.sum(...) 换成 np.median(win, axis=(-2,-1))

# 3) 高斯核
ax = np.arange(-(3//2), 3//2 + 1)
xx, yy = np.meshgrid(ax, ax)
gk = np.exp(-(xx**2 + yy**2) / (2 * 1.0**2)); gk /= gk.sum()

# 4) 边缘检测:Sobel 两个方向 → 梯度幅值
Kx = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]])
Ky = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]])
gx, gy = conv2d(gray, Kx), conv2d(gray, Ky)
edges = np.sqrt(gx**2 + gy**2)
edges = (edges / edges.max() * 255).astype(np.uint8)   # 归一化到 0-255
edges = np.where(edges > 100, 255, 0).astype(np.uint8) # 阈值二值化

# 5) 亮度/对比度
bright = np.clip(img * 1.2, 0, 255).astype(np.uint8)
contrast = np.clip((img - img.mean()) * 1.5 + img.mean(), 0, 255).astype(np.uint8)

# 6) 直方图均衡化:CDF 映射
hist, bins = np.histogram(gray.ravel(), 256, [0, 256])
cdf = hist.cumsum()
eq = np.interp(gray.ravel(), bins[:-1], cdf * 255 / cdf[-1])
eq = eq.reshape(gray.shape).astype(np.uint8)

关键技巧:加权灰度系数 [0.299, 0.587, 0.114];卷积 = pad + sliding_window_view + 末两轴求和;中值/最大/最小值滤波只换归约函数;直方图均衡化本质是 np.interp(像素, bin, 归一化CDF);所有改动后必须 clip + astype(np.uint8)。

数据分析骨架

Python36 行
G = np.array(grades, dtype=float)     # (n_students, n_subjects)

# 1) 沿轴统计:axis=0 每科,axis=1 每人
G.mean(), G.mean(axis=0), G.mean(axis=1)
np.median(G, axis=0), G.std(axis=0), G.min(0), G.max(0), G.ptp(0)
np.percentile(G, [25, 50, 75], axis=0)

# 2) 相关系数矩阵:corrcoef 算行之间,样本在行 → 必须转置
C = np.corrcoef(G.T)                  # 输出: (n_subjects, n_subjects)
np.fill_diagonal(C, 0)
np.argwhere(np.abs(C) > 0.7)           # 输出: 强相关科目的 (i, j) 下标对

# 3) 分箱分布
bins = [0, 60, 70, 80, 90, 100]
hist, _ = np.histogram(G.ravel(), bins=bins)
hist / G.size * 100                   # 输出: 各段百分比

# 4) 异常值:IQR 或 z-score
q1, q3 = np.percentile(G, [25, 75], axis=0)
iqr = q3 - q1
mask = (G < q1 - 1.5 * iqr) | (G > q3 + 1.5 * iqr)     # 广播逐列判断
z = np.abs((G - G.mean(0)) / G.std(0)); outliers = z > 3

# 5) 排名与百分位
total = G.sum(axis=1)
order = np.argsort(total)[::-1]                      # 降序索引
rank_pct = (total[:, None] > total[None, :]).mean(1) * 100   # 向量化百分位

# 6) 布尔掩码聚合
pass_rate = (G >= 60).mean()                          # 及格率
top = G[G[:, 1] > 80]                                 # 数学 > 80 的行

# 7) 趋势(多次考试 (n_exams, n_students, n_subjects))
H = np.array(history)
overall = H.mean(axis=(1, 2))                         # 每次考试总均分
improve = H[-1].mean(1) - H[0].mean(1)                # 每人进步幅度

关键技巧:axis 的语义是“被折叠的那一维”;corrcoef 计算行与行相关,样本在行就要 .T;百分位/排名用广播比较向量化,不要写双层循环;np.histogram + cumsum 是所有分布类分析的基础。

神经网络骨架(前向 + 反向 + 训练)

Python46 行
rng = np.random.default_rng(42)

def softmax(z):                       # 减去最大值防指数溢出
    e = np.exp(z - z.max(axis=1, keepdims=True))
    return e / e.sum(axis=1, keepdims=True)

def relu(z):    return np.maximum(0, z)
def drelu(z):   return (z > 0).astype(float)

sizes = [784, 128, 64, 10]
# He 初始化:对 ReLU 友好
W = [rng.standard_normal((sizes[i], sizes[i+1])) * np.sqrt(2.0 / sizes[i])
     for i in range(len(sizes) - 1)]
b = [np.zeros((1, sizes[i+1])) for i in range(len(sizes) - 1)]   # (1, out) 便于广播

def forward(X):
    A, Z, out = [X], [], X
    for i in range(len(W) - 1):                  # 隐藏层
        z = out @ W[i] + b[i]                    # 广播加偏置
        Z.append(z); out = relu(z); A.append(out)
    z = out @ W[-1] + b[-1]                      # 输出层
    Z.append(z); A.append(softmax(z))
    return A, Z

def backward(X, Y, A, Z, lr):
    m = X.shape[0]
    delta = A[-1] - Y                            # softmax + 交叉熵:梯度直接是 ŷ - y
    for i in range(len(W) - 1, -1, -1):
        dW = A[i].T @ delta / m
        db = delta.sum(axis=0, keepdims=True) / m
        if i > 0:                                    # 先算 delta(用更新前的 W)
            delta = (delta @ W[i].T) * drelu(Z[i-1]) # 链式法则往回传
        W[i] -= lr * dW; b[i] -= lr * db             # 再更新参数

def cross_entropy(Y_hat, Y):
    return -np.sum(Y * np.log(Y_hat + 1e-8)) / len(Y)

for epoch in range(epochs):
    idx = rng.permutation(len(X))                # 每轮打乱
    Xs, Ys = X[idx], Y[idx]
    for s in range(0, len(X), batch_size):       # mini-batch
        xb, yb = Xs[s:s+batch_size], Ys[s:s+batch_size]
        A, Z = forward(xb)
        backward(xb, yb, A, Z, lr)
    A, _ = forward(X)
    acc = (A[-1].argmax(1) == Y.argmax(1)).mean()

关键技巧:一层就是 Z = X @ W + b;偏置形状 (1, out) 靠广播覆盖整个 batch;softmax 必减 z.max 防溢出;softmax + 交叉熵 的输出层梯度恰好是 ŷ - y;dW = A_prev.T @ delta / m,db = delta.sum(0, keepdims=True) / m;one-hot 用 np.eye(C)[y],预测用 argmax(1);输入务必先标准化,否则训练不收敛。

⚠️ 偏置写成形状 (out,) 也能广播,但用 (1, out) 更不容易与 batch 维混淆。 ⚠️ 交叉熵里的 + 1e-8 不能省,否则 log(0) → nan。 ⚠️ 必须先算出传给下一层的 delta 再更新 W[i],否则用的是已经改过的权重(上面代码已按正确顺序写)。

K-Means 聚类(向量化距离矩阵 + 多次重启)

对 600 个二维样本做聚类。核心是 (n,1,2) 与 (1,k,2) 广播出 (n,k) 距离矩阵——不写任何双重循环;再用多次随机初始化取 inertia 最小的一次,避免掉进局部最优。

Python30 行
import numpy as np
rng = np.random.default_rng(42)

centers = np.array([[0., 0.], [5., 5.], [-4., 4.]])
X = np.vstack([c + rng.standard_normal((200, 2)) * 0.8 for c in centers])   # (600, 2)

def kmeans(X, k, iters=100, seed=0):
    r = np.random.default_rng(seed)
    C = X[r.choice(len(X), k, replace=False)]          # 随机挑 k 个样本作初始中心
    for it in range(iters):
        dist = ((X[:, None, :] - C[None, :, :]) ** 2).sum(-1)    # (n, k) 广播,无循环
        lab = dist.argmin(1)
        newC = np.array([X[lab == j].mean(0) if np.any(lab == j) else C[j]
                         for j in range(k)])                      # 空簇保留原中心
        if np.allclose(newC, C, atol=1e-9):
            break
        C = newC
    dist = ((X[:, None, :] - C[None, :, :]) ** 2).sum(-1)
    return dist.argmin(1), C, float(dist.min(1).sum()), it + 1

runs = [kmeans(X, 3, seed=s) for s in range(5)]
[r[2] for r in runs]        # 输出: [744.6, 3908.7, 744.6, 744.6, 744.6] ← 有一次初始化掉进局部最优
lab, C, inertia, it = min(runs, key=lambda r: r[2])               # 取 inertia 最小的一次
np.bincount(lab)            # 输出: [200 200 200]
it                          # 输出: 7(迭代 7 次收敛)
C.round(2)                  # 输出: [[4.95 4.97] [0.02 -0.03] [-4.03 4.02]](簇编号顺序是任意的)
inertia                     # 输出: 744.6

# 肘部法选 k:超过真实簇数后 inertia 下降明显变缓
[round(kmeans(X, k, seed=0)[2], 1) for k in (2, 3, 4, 5)]   # 输出: [4018.5, 744.6, 640.3, 557.9]

要点:(X[:, None, :] - C[None, :, :])**2).sum(-1) 是成对距离的通用写法(样本量大时按块算,避免 (n,k,d) 中间数组);必须处理空簇;多次重启取最小 inertia;argmin(1) 给标签、min(1).sum() 给 inertia。

滑动窗口特征 + 监督样本构造(时序预测)

把一条有趋势和周期的时间序列,用长度为 7 的滑动窗口切成 (特征, 目标) 样本:窗口统计量做特征,窗口后一天做目标。时序数据必须按时间切分训练/测试,并用“用今天预测明天”的朴素基线做对照。

Python28 行
import numpy as np
rng = np.random.default_rng(42)

n = 500
t = np.arange(n)
series = 20 + 0.02 * t + 5 * np.sin(2 * np.pi * t / 30) + rng.normal(0, 1.0, n)

W = 7
win = np.lib.stride_tricks.sliding_window_view(series, W)   # (494, 7) 视图,零拷贝
win = win[:n - W]                                           # 与目标对齐
y = series[W:]                                              # 目标:窗口之后那一天
tt = np.arange(W, W + len(y))                               # 目标日的时间索引

X = np.c_[win.mean(1),                      # 窗口均值
          win.std(1),                       # 窗口波动
          win[:, -1],                       # 窗口最后一天
          win[:, -1] - win[:, 0],           # 窗口净变化
          np.sin(2 * np.pi * tt / 30),      # 周期特征(知道 30 天周期时很有用)
          np.cos(2 * np.pi * tt / 30),
          np.ones(len(y))]                  # 偏置列
X.shape                                     # 输出: (493, 7)

cut = int(0.8 * len(y))                     # 按时间切分,绝不 shuffle
coef, *_ = np.linalg.lstsq(X[:cut], y[:cut], rcond=None)
pred = X[cut:] @ coef
np.sqrt(np.mean((pred - y[cut:]) ** 2))               # 输出: 1.071  模型
np.sqrt(np.mean((win[cut:, -1] - y[cut:]) ** 2))      # 输出: 1.478  基线"用今天预测明天"
coef.round(3)   # 输出: [0.719 -0.032 0.255 -0.125 2.208 3.225 0.745]

要点:sliding_window_view 返回视图(零拷贝、别原地写),配 np.c_ 一次性拼出特征矩阵和偏置列;目标与窗口要错开一行对齐;时序切分按时间顺序,且一定要和朴素基线比,否则不知道模型到底有没有学到东西。