目录172
- 概述与安装
- ndarray vs Python list
- 生态位置
- 最小可运行骨架
- 环境自检与向量化提速实测
- 成绩统计小脚本
- ndarray 基础
- 本质与属性
- 创建 1D / 2D / 3D 与形状直觉
- 索引与切片
- axis:折叠法
- dtype
- 向量化运算与广播
- 易错点
- 彩色图像灰度化与二值化
- 销售数据按维度聚合
- 批量欧氏距离与最近邻
- 创建数组
- API 速查表
- np.array 注意点
- 特殊值数组
- 数值序列
- 单位阵与对角阵
- 随机数组(NumPy 2.x 推荐写法)
- 文件读写 (完整用法见后续)
- 视图 vs 副本 (系统梳理见后续)
- 网格、重复与拼接 (拼接与分割的完整规则见后续)
- 实战:蒙特卡洛估算圆周率
- 3.11 股价随机游走与最大回撤
- 生成模拟回归数据集并切分
- 数组属性与方法
- 属性速查(属性无括号,方法有括号)
- astype:类型转换
- 统计方法速查 (axis 语义与各统计量的细节见后续)
- 布尔索引与掩码 (系统梳理见后续 )
- np.where 三种用法 (另见切片)
- 排序、去重、裁剪、舍入
- 形状变换 (系统梳理见形状操作)
- 打印与调试
- 易错点汇总
- BMI 批量计算与健康分类
- 成绩排名与分数段分布
- 实战:Z-score 标准化与离群点检测
- 索引与切片
- 基本索引与切片
- 多维索引
- 布尔索引
- np.where
- 花式索引
- np.ix_ / take / put
- 视图 vs 副本
- 常用组合技
- 灰度图像的区域读写
- 一周逐小时温度表的切片筛选
- 成绩表的筛选、排名与重排
- 形状操作
- API 速查
- reshape
- 展平:ravel vs flatten
- 转置与轴交换
- 维度增删
- 分块与窗口(reshape 典型用法)
- 视图与副本速判
- 易错点
- 图像增强并堆成训练批次
- 滑动窗口构造序列模型输入
- 灰度图升维成 RGB 与单样本推理
- 数组运算与广播
- 运算符与等价 ufunc
- 广播规则
- 矩阵乘法
- 聚合与 axis
- 比较与裁剪
- 易错点
- 特征矩阵按列 Min-Max 归一化
- 按行 softmax 与交叉熵(数值稳定版)
- 样本到聚类中心的距离矩阵与最近邻分配
- 外积构造价目表与函数网格
- 多通道图像白平衡与逐通道标准化
- 数学函数
- 三角函数
- 指数与对数
- 取整
- 幂与开方
- 绝对值与符号
- 聚合 / 累积 / 差分
- 取余与整除
- 复数
- 自定义 ufunc
- 易错点
- 抛体运动的轨迹与射程
- 取整函数在计费、打包与时间换算里的选择
- 信号平滑与主频提取
- 小量对数变换与 log-sum-exp
- 统计函数
- API 速查
- axis 的语义(重点)
- ddof:总体 vs 样本
- 最值与位置
- 分位数
- 相关与协方差
- histogram 与 bincount
- 累积、差分与变化率
- NaN 处理
- 易错点
- 成绩表的多轴统计与加权平均
- 传感器数据的异常值检测
- 订单金额分箱与分渠道汇总
- 排序与搜索
- 排序 API 速查
- axis 行为
- 稳定性与 kind
- lexsort:多键排序
- partition:只要 TopK
- searchsorted:有序数组二分查找
- where / nonzero / argwhere / extract
- 易错点
- 排行榜 TopK 与名次列
- 员工表按多列排序出报表
- 价格区间查找与档位打标
- 拼接与分割
- 拼接 API 速查
- 维度匹配规则
- 分割 API 速查
- tile 与 repeat
- append / insert / delete
- 常用配方
- 多来源上报数据合并成一张表
- 训练集 / 验证集 / 测试集切分
- 图片通道合并、加边与组 batch
- 视图与复制
- 三种“复制”
- 判别方法
- 哪些操作返回视图,哪些返回副本
- base 的传递性
- 视图的语义:共享数据、独立元数据
- 陷阱清单
- 选择建议
- 就地修改的三个坑
- 用视图分块处理大数组省内存
- 写不污染入参的函数
- 线性代数
- 乘法怎么选:* / @ / matmul / dot
- 点积、余弦相似度、范数、归一化
- np.linalg 速查表
- 转置、迹、协方差
- 批量解线性方程组(配料/配方问题)
- 多项式拟合与岭回归(过拟合 / 正则化)
- SVD 低秩近似做图像压缩
- 随机数
- 14.1 旧 API → 新 API 对照(旧 API 不再展开,照表替换即可)
- 常用生成
- choice / permutation / shuffle
- 蒙特卡洛模拟股价路径与风险度量
- Bootstrap 置信区间
- 置换检验判断 A/B 实验是否显著
- 文件读写
- 格式怎么选
- 二进制:save / load / savez
- 文本:savetxt / loadtxt
- CSV 与缺失值:genfromtxt
- 大文件与性能
- npz 缓存特征 + 断点续训
- 混合类型 CSV 的清洗与分组透视
- mmap 分块流式统计与原地写回
- 实战套路
- 通用套路(跨项目复用)
- 图像处理骨架
- 数据分析骨架
- 神经网络骨架(前向 + 反向 + 训练)
- K-Means 聚类(向量化距离矩阵 + 多次重启)
- 滑动窗口特征 + 监督样本构造(时序预测)
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
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) |
| 适用 | 混合类型、频繁增删 | 数值计算、矩阵、大数据 |
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())都建立在它之上。
np.__version__ # 查看版本
np.show_config() # 查看 BLAS/LAPACK 后端最小可运行骨架
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 向量化」在同一运算上的耗时比。
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 循环。
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 |
— |
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 与形状直觉
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) - 视频:
(帧, 高, 宽, 通道) - 时间序列:
(天, 小时, 传感器)
索引与切片
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 维折叠掉,结果保留其余维度。
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 字符串 |
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。
向量化运算与广播
广播的完整规则见后续章节
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)。
易错点
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 下的类型控制。
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) 的搭配。
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 与广播的综合练习。
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 注意点
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 时不复制,性能更好特殊值数组
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。
数值序列
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。
单位阵与对角阵
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 推荐写法)
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)常用范式:
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。
文件读写 (完整用法见后续)
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 副本 (系统梳理见后续)
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()。
网格、重复与拼接 (拼接与分割的完整规则见后续)
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 即得 π 的估计值。这个例子同时练随机数组生成、布尔统计,以及「样本量越大越准」的直觉。
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 的典型用法。
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 随机切分。
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 等内存布局标志 |
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:类型转换
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) |
相关系数矩阵 |
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。
布尔索引与掩码 (系统梳理见后续 )
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 三种用法 (另见切片)
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) 坐标,更适合多维)。
排序、去重、裁剪、舍入
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。
形状变换 (系统梳理见形状操作)
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()是视图,改结果会改回原数组。
打印与调试
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.shapevsarr.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 做频次统计的标准组合。
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 负责分档计数。
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 个标准差以上的点,再用正常值均值替换(或用分位数做缩尾),最后检查清洗后的分布。
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 是清洗异常值的两种收尾方式;注意均值/标准差本身会被异常值带偏,脏数据更严重时改用中位数与四分位距。
索引与切片
import numpy as np基本索引与切片
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] # IndexErrorarr = 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。
多维索引
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] 得到标量三维按 [页, 行, 列] 索引;... 自动补全中间所有 ::
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]核心直觉:索引(整数)降维,切片(:)保维。
布尔索引
布尔索引返回副本(一维化)。
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);且&/|优先级高于比较符,每个条件必须加括号。
二维数组上布尔索引:
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 # 数据清洗:负值截断为 0np.where
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] = ...。
花式索引
整数数组索引,返回副本,可重复、可乱序。
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
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() |
副本 | 显式复制 |
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整体赋值。
常用组合技
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 循环。
灰度图像的区域读写
图像就是二维数组,裁剪、翻转、打码、过曝修正都可以只靠索引完成。这里演示读取子区域、提取边框、几何翻转,以及用布尔掩码批量改像素。
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 小时的温度矩阵,行是日期、列是小时。按行切“某天”,按列切“某时段”,再用布尔条件定位高温时刻。
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 取每人最强科。
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 按索引数组逐行取值。
形状操作
import numpy as npAPI 速查
| 函数 | 作用 | 元素数 | 返回 |
|---|---|---|---|
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
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()。
imgs = np.random.rand(100, 28, 28)
imgs.reshape(100, -1) # (100, 784),保持批次维展平
imgs.reshape(-1) # (78400,) 全部拉平展平:ravel vs flatten
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 同序,但优先返回视图转置与轴交换
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) # 同上,分步交换维度增删
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 典型用法)
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 |
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 或展平。
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 直接返回零拷贝视图,比手写索引矩阵更省内存。
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 的轴,这是最常见的坑。
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 的批次维一起挤没。
数组运算与广播
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 |
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 → 兼容;否则报错。
- 结果为各维的最大值;维度为 1 的轴沿该轴复制(不真的复制数据)。
(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)(行方向),再运算。
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
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) 外积方向语义速记
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,)默认对齐到最后一维,即按行广播;想按列必须显式升维。
典型应用
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) |
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
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) # 沿列方向一阶差分比较与裁剪
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) # 只限下界易错点
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。
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)),结果不变但绝不溢出,这是所有框架的标准实现。
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 的分配步骤。样本量大时改用平方展开式,避免生成三维中间数组。
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 直接算二元函数值。
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) 才能广播回去。
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),才能同时套用到单图和批量数据。
数学函数
import numpy as np全部为向量化 ufunc,直接作用于数组,支持广播与 out= 原地写入(np.sin(a, out=a))。
三角函数
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 常用 |
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取整
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)。
幂与开方
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# 均方误差 / 平均绝对误差
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绝对值与符号
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) |
一维卷积(可算移动平均) |
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取余与整除
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。
复数
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
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) 生成各角度按自身飞行时长归一化的时间网格。
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。选错方向就是钱算错。
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 找主频。窗口长度是关键:太短去噪不够,太长会把高频成分一起削掉。
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 就是为这类场景准备的。
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(或手动减最大值)处理极小概率求和,把乘法链换成对数加法是数值稳定的通用手段。
统计函数
import numpy as npAPI 速查
| 类别 | 函数 | 说明 |
|---|---|---|
| 集中趋势 | 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() 同理。
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 = 被“折叠/消掉”的那个维度,其余维度保留。
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]]
三维与多轴:
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 | 无偏样本估计 | 用样本推断总体,统计学默认 |
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。
最值与位置
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= 插值方式)。
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 异常值相关与协方差
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
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传序列时是边界值,传整数时是箱子个数,语义完全不同。
累积、差分与变化率
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 处理
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() # 1nan* 系列:nansum nanmean nanmedian nanstd nanvar nanmin nanmax nanargmin nanargmax nanpercentile nanquantile —— 签名与去掉 nan 的版本一致。
填充:
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 就能得到完全不同口径的指标,不需要任何循环。
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 不受影响。
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=) 一次完成「按组求和」。
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) 是最快的分组求和。
排序与搜索
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=) |
— | — | 方法形式同样可用 |
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 行为
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) # 每行排序后的列号按某一列给整表排序(表格排序的标准写法):
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) | 自动挑最优稳定算法 |
np.sort(a, kind='stable')
np.argsort(keys, kind='stable') # 多次排序(先 A 后 B)时必须稳定
np.sort(stu, order=['class', 'score']) # 结构化数组多字段排序lexsort:多键排序
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的键顺序是反的:最后一个键才是主键(类似 SQLORDER BY k2, k1)。
partition:只要 TopK
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:有序数组二分查找
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 更直观):
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
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 的逆排列」一次算出。
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 把键从后往前当主键,降序键取负号即可,一次调用得到整表的重排索引。
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),不用扫全表。
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 只在有序数组上有效,返回插入位置——打标签直接当索引用,判存在必须再比一次值。
拼接与分割
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 输入先转列向量再拼 |
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]) |
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) |
否 |
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] |
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
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一批。
常用配方
# 加一行/一列常量
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 补齐再拼,否则会触发维度不匹配错误。
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 的索引列表形式一次给出,比连续两次切片更清晰。
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 时通道轴不能补。
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)。
视图与复制
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 |
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 不变 副本:完全独立判别方法
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 连续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] |
视图 | 增维不复制 |
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 一律指向最原始的根数组,不是上一层。
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 全部同步变化视图的语义:共享数据、独立元数据
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。
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 可以随时验证自己拿到的是哪种。
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() 固化。
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()。
线性代数
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) 会报错(不支持标量)。
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) |
# 批量矩阵乘:(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))]。
点积、余弦相似度、范数、归一化
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' |
— | 核范数(奇异值之和) |
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) |
矩阵幂 |
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=...) |
# 最小二乘拟合 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,别用==。
转置、迹、协方差
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。
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 当成额外的若干行样本再做最小二乘,不需要额外的求解器。
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 字节)。
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),否则存图溢出。
随机数
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 持有独立状态。
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)。
多流(并行/多线程)
# 每个线程一个独立 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]常用生成
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) |
一次试验多类别 |
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
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 轴 |
# 训练/验证/测试划分(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 循环。
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 次,用重抽样统计量的分位数直接给出置信区间——对均值、中位数、相关系数等任意统计量都成立,不依赖分布假设。
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 值。不需要任何正态假设。
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。
文件读写
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
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
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
# 数值 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。
大文件与性能
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,中断后从上次轮数继续。
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、缺失值用中位数填补,最后做分组汇总。
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()。
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()。
实战套路
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返回的是视图(零拷贝),别对它原地写入。
图像处理骨架
# 图像即数组:彩色 (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)。
数据分析骨架
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 是所有分布类分析的基础。
神经网络骨架(前向 + 反向 + 训练)
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 最小的一次,避免掉进局部最优。
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 的滑动窗口切成 (特征, 目标) 样本:窗口统计量做特征,窗口后一天做目标。时序数据必须按时间切分训练/测试,并用“用今天预测明天”的朴素基线做对照。
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_ 一次性拼出特征矩阵和偏置列;目标与窗口要错开一行对齐;时序切分按时间顺序,且一定要和朴素基线比,否则不知道模型到底有没有学到东西。