数据分析基础numpy
前面章节都是讲述python基础的,因为自己基本已经掌握,所以跳过。
一、numpy简介
- numpy:开源的python科学计算模块,用于数据快速处理;
- numpy支持矩阵与数组操作,计算速度快,是Python中科学计算的基础库;
- numpy优点:
- 底层使用C语言实现,计算速度快
- numpy支持均值,累积和,方差等运算,可以直接使用;
- numpy处理数据方式灵活,支持excel, csv等多种方式数据导入;
二、numpy安装
方式1:pip install numpy
方式2:anaconda环境:自带numpy,不用安装
numpy官方文档:https://numpy.org/doc/
numpy源码:https://github.com/numpy/numpy
三、numpy使用
1 | |
3.1 ndarray
- 1.numpy中基本数据结构;
- 2.ndarray对象索引从0开始
- 3.所有元素是同一种类型;
- 4.与列表类似,支持切片等操作;
在NumPy中,ndarray是核心的多维数组对象,其名称是N-dimensional
array的缩写,直译为“N维数组”
3.2 创建ndarray对象
3.2.1 array方法
格式:numpy.array(object, dtype=None, copy=True, order='K', subok=False, ndmin=0)
object(输入对象,必填)
- 这是你想要转换的原始数据。它可以是 Python 列表、元组、多维嵌套列表,甚至是另一个现有的 NumPy 数组。
dtype(数据类型,选填)
指定生成的数组内部元素的硬性数据类型(如
np.int32,np.float64,np.bool_)。- 如果不传(默认):NumPy
会启动自动推断机制。如果列表里同时有整数和浮点数,它会自动统一提升为
float64;如果混入了None或字符串,则会像我们之前讨论过的那样,降级退化为object类型。
- 如果不传(默认):NumPy
会启动自动推断机制。如果列表里同时有整数和浮点数,它会自动统一提升为
copy(是否复制,默认 True)
决定是否在内存中开辟一块全新的空间来复制这份数据。
True:无论如何,都在内存里完整复制一份新数据,修改新数组绝对不会影响原对象。False:如果输入对象本身已经是一个 ndarray,NumPy 会尽可能直接引用原内存地址(不复制),省内存且速度极快。
order(内存布局顺序,默认 'K')
决定多维数组在计算机底层内存底层的行与列是怎么一维线性排列的。
'C':C 语言风格。在内存中按行连续存放(一行一行存)。'F':Fortran 语言风格。在内存中按列连续存放(一列一列存)。'K'(默认):保留原有风格。自动检测输入对象的内存布局,尽可能贴近并维持它原本的顺序。
subok(是否允许子类,默认 False)
False(默认):强行把返回的对象转换为最标准的np.ndarray基本类型。True:如果输入对象是 ndarray 的子类(例如矩阵子类np.matrix),则会保留子类的身份,允许子类传递。
ndmin(最小维度,默认 0)
强制指定生成的数组必须满足的最小维度。
- 如果你传入一个普通的扁平列表
[1, 2, 3](原本是一维数组),若设置ndmin=2,NumPy 会自动在外面给你套一层中括号,强行将其升维拉伸成一个二维矩阵[[1, 2, 3]](形状为(1, 3))。
- 如果你传入一个普通的扁平列表
1 | |
3.2.2 ndarray轴与秩
- 轴(axis):每一个线性的数组称为是一个轴;
- 第一个轴(axis=0):第一层数组,
- 第二个轴(axis=1):数组里的数组
- 依次类推;
- 秩(rank):维度
- 示意图:
3.2.3 ndarray相关属性
| 参数 | 说明 | 用途说明或示例 |
|---|---|---|
| object | 类似数组对象,例如:序列、range对象等 | 用于构造数组的原始数据。 示例: np.array([1, 2, 3]),其中
[1, 2, 3] 是 object 参数 |
| dtype | 元素类型 | 指定数组中元素的数据类型。 示例: np.array([1, 2, 3], dtype=float)
生成 float 类型数组 |
| order | 数据在内存排列形式 | 'C' 表示按行优先(C语言风格),'F'
表示按列优先(Fortran风格)。示例: np.array([[1,2],[3,4]], order='F') |
| ndmin | 指定维度 | 指定数组的最小维度。 示例: np.array([1, 2, 3], ndmin=2)
结果为 [[1, 2, 3]](二维) |
1 | |
为什么 shape:(1, 3)?
- 你使用了参数
ndmin=2,这是指定最小维度为2维。 - 原始数据
[1, 2, 3]是一维数组(形状为(3,))。 - 使用
ndmin=2后,NumPy 自动在最前面加一维,使其变为二维数组:形状变成(1, 3),即 1行3列。
1 | |
为什么 size: 3?
size表示数组中总元素个数。- 虽然
nd的形状是(1, 3),它的元素还是1, 2, 3,总共 3 个数。 - 所以:
size = 1 × 3 = 3
1 | |
3.2.4 创建ndarray对象常用方法
| 方法 | 说明 |
|---|---|
np.zeros(shape, dtype=float, order='C') |
根据指定shape创建默认值为0的ndarray对象 |
np.empty(shape, dtype=float, order='C') |
根据指定shape创建默认值为随机数的ndarray对象 |
np.ones(shape, dtype=float, order='C') |
根据指定shape创建默认值为1的ndarray对象 |
np.full(shape, fill_value, dtype=None, order='C') |
根据指定shape创建默认值为fill_value的ndarray对象 |
np.arange([start, ]stop, [step, ]dtype=None) |
类似于range, 返回ndarray对象 |
np.linspace(start, stop, num=50, endpoint=True, retstep=False, dtype=None, axis=0) |
根据给定起始值与数量,返回ndarray对象 |
np.zeros_like/empty_like/ones_like(a, dtype=None, order='K', subok=True) |
根据给定array返回相同形状的ndarray对象 |
1 | |
为什么b的值与c = np.linspace(1, 5, num=5); print('linspace1:\n', c)
一样?
empty_like的预期行为np.empty_like(a)会创建一个与a形状和数据类型相同的数组,但 不初始化内存(即内存中的值是随机的)。理论上,输出应为随机值。实际输出全1的可能原因
- 内存复用:NumPy 可能复用了
a的内存空间(尤其是当a刚被释放时),导致b直接继承了a的值。 - 内存清零优化:某些系统或环境(如 Docker、特定版本的 NumPy)可能在分配内存时自动清零,导致“未初始化”的值表现为0或重复之前的数据。
- 缓存机制:操作系统的内存管理可能将之前存储
a的内存块快速分配给b,而未覆盖原有数据。
- 内存复用:NumPy 可能复用了
关键点
empty_like不保证输出随机值,它只是 **不主动初始化内存释放时),导致b直接继承了a的值。- 实际值取决于内存的当前状态。
生成随机数时指定形状:
1
2
3
4
5
6
7
8
9a = np.ones((2,10))
b = np.random.rand(*a.shape) # 生成随机数组
print(b)
#执行结果:
[[0.2423814 0.31150916 0.03527437 0.83015459 0.82835642 0.41430738
0.3986594 0.60127557 0.31258882 0.85706738]
[0.8419892 0.37580216 0.93050322 0.87510586 0.03349094 0.66328732
0.96759445 0.15272788 0.32526041 0.97141136]]
3.2.5 np.radom相关方法
可以用来产生随机小数或整数。
| 方法 | 说明 |
|---|---|
np.random.rand(d0, d1, ..., dn) |
根据给定形状产生随机值 |
np.random.randint(low, high=None, size=None, dtype='l') |
根据指定范围产生整数 |
np.random.randint 方法 dtype
参数的意义:
- 作用
dtype控制输出随机整数的数据类型,例如int32、int64、np.uint8等。默认值为'l'(即np.int_,通常对应int64或int32,具体取决于平台) - 可选值
- 支持标准的整数类型字符串(如
'i'、'l'、'u')或具体的 NumPy 类型(如np.int32)。 - 例如:
dtype='int64'会生成 64 位有符号整数
- 支持标准的整数类型字符串(如
- 默认行为 若未显式指定
dtype,函数会使用默认的'l'(长整型),其实际位宽由系统决定 - 注意事项
- 如果指定的
dtype范围无法覆盖[low, high)区间(如dtype='uint8'但high=1000),可能导致溢出或未定义行为 - 在需要跨平台一致性时,建议显式指定
dtype(如dtype=np.int64)
- 如果指定的
1 | |
3.2.6 reshape方法
格式:array.reshape(shape, order='C')
作用:调整array的新装,返回新的ndarray对象
1 | |
- 理解下order中的C与F:
- 二维array对象:
a = [[1,2],[3,4]]
- C代表在C语言中数据在内存存储方式;
a[0][0],a[0][1],a[1][0],a[1][1]
- F代表在Fortran语言中数据在内存存储方式;
a[0][0],a[1][0],a[0][1],a[1][1]
- 二维array对象:
1 | |
3.2.7 ndarray对象转其他数据结构
a = np.arange(10)
| 方法 | 说明 |
|---|---|
a.tolist() |
转成列表 |
a.tostring(order='C') |
转成 bytes。此方法在NumPy 1.19.0中已被移除 |
a.tobytes(order='C') |
转成 bytes |
a.tofile(fid, sep="", format="%s") |
保存到文件,fid:打开文件或者路径 |
1 | |
1 | |
3.3 numpy的数据类型
| 类型 | 类型代码 | 说明 |
|---|---|---|
| int8/16/32/64 | i1/i2/i4/i8 | 有符号8/16/32/64位整数 |
| uint8/16/32/64 | u1/u2/u4/u8 | 无符号8/16/32/64位整数 |
| float16/32/64 | f2/f4/f8 | 半精度16位浮点数 / 单精度32位浮点数 / 双精度64位浮点数 |
| complex64/128 | c8/c16 | 单/双精度复数(实部虚部各占一半) |
| bool | b1 | 布尔类型(True/False) |
1 | |
3.4 numpy访问与修改
numpy访问与列表类似,支取切片操作。
3.4.1 一维array
1 | |
1 | |
3.4.2 理解numpy中的轴
一维array,轴:0
二维array,轴:0,1
三维array,轴:0,1,2
3.4.3 二维array
1 | |
3.4.4 三维array
1 | |
3.4.5 多维数据取值
1 | |
1 | |
1 | |
1 | |
1 | |
| 索引方式 | 示例 | 作用 | 返回值类型 |
|---|---|---|---|
| 花式索引 | a[[1,3,4],[2,3,4]] |
提取多个指定坐标的元素 | 一维数组 |
| 多维索引(单个) | a[1,2] |
提取单个元素 | 标量值 |
| 切片索引 | a[1:4, 2:5] |
提取连续的行和列的子数组 | 二维数组 |
| 操作类型 | 语法示例 | 功能说明 | 返回值类型 |
|---|---|---|---|
| 单行访问 | a[0] |
获取第1行所有元素 | 一维数组 |
| 单元素访问 | a[0][1] 或
a[0,1] |
获取第1行第2列的元素(两种写法等价) | 标量值 |
| 多行访问 | a[[1,2,4]] |
获取第2、3、5行的所有元素(花 | 返回值类型 |
| :--------------: | :----------------式索引) | 二维数组 | |
| 切片取行 | a[:5:2] |
每隔一行取一次(步长2),获取第1、3、5行 | 二维数组 |
| 单列访问 | a[:,1] |
获取所有行的第2列 | 一维数组 |
| 多列访问 | a[:,[1,3]] |
获取所有行的第2、4列(花式索引) | 二维数组 |
| 切片取列 | a[:,::2] |
获取所有行的偶数列(步长2) | 二维数组 |
| 坐标点访问 | a[[1,3,4],[2,3,4]] |
获取(1,2)、(3,3)、(4,4)位置的元素(行/列索引一一配对) | 一维数组 |
| 矩形区域切片 | a[1:4, 2:5] |
获取第2-4行、第3-5列的子矩阵(左闭右开) | 二维数组 |
| 混合切片 | a[1:3,:3] |
获取第2-3行、前3列的子矩阵 | 二维数组 |
关键说明:
花式索引:通过列表指定不连续的位置(如
a[[1,3,4]]),返回的是副本而非视图切片语法:
start:stop:step,左闭右开区间,可用于行/列维度多维索引:逗号分隔不同维度的索引(如
a[0,1]比a[0][1]更高效)类型区别:
- 单行/单列返回一维数组
- 多行/多列返回二维数组
- 标量访问返回Python原生类型
扩展应用:
1
2
3
4
5
6
7
8
9
10
11布尔索引(条件筛选)布尔索引(条件筛选)# 高级花式索引(获取矩形区域)
a[np.ix_([1,3,4], [2,3,4])] # 等价于a[1:5:2, 2:5]
#执行结果:
[[ 7 8 9]
[17 18 19]
[22 23 24]]
# 布尔索引(条件筛选)
a[a > 20] # 获取所有大于20的元素
#执行结果:
[21 22 23 24]
3.4.6 array修改
1 | |
3.5 numpy计算
3.5.1 numpy广播:boardcasting
基本运算被应用到array所有的元素中。
1 | |
1 | |
3.5.2 array之间计算
1 | |
1 | |
3.5.3 多维array之间计算
1 | |
1 | |
3.5.4 基本计算
主要包括:求和,均值,方差,累积和等;
numpy模块与array对象都支持这些方法,使用方式也类似,我们来看一种即可;
这些方法参数类似,我们以sum为例:
1 | |
numpy中常用计算相关方法:
| 方法 | 说明 |
|---|---|
| np.mean() | 计算均值 |
| np.max() | 最大值 |
| np.min() | 最小值 |
| np.cumsum() | 计算累加和 |
| np.std() | 计算标准差 |
| np.var() | 计算方差(方差是各数据点与均值之差的平方的平均值) |
| np.cov() | 计算协方差 |
| np.average() | 计算均值 |
| np.max() | 均值最大值 |
| np.median() | 计算中位数 |
| np.ptp() | 计算极值(最大值与最小值差) |
1 | |
1 | |
3.5.5 多维array计算
3.5.5.1 示例
多维array指定axis,可以得到不同效果,在计算常用指标,发挥奇效。
1 | |
1 | |
1 | |
3.5.5.2 np.sum(b, axis=2)语句的字面含义与运行结果
这两行代码展示了 NumPy 中最核心的两个操作:高维张量的构建(维度重塑) 与 沿指定轴的降维聚合(求和)。
执行这段代码后的最终输出结果为一个 (2, 3)
的二维矩阵:
Python
1 | |
3.5.5.3
第一步:reshape(2, 3, 4) 的高维空间构建
首先,np.arange(24) 生成了一个包含 0 到 23
的一维连续整型数组。接下来的 .reshape(2, 3, 4) 将这 24
个数字重新揉成了一个
三维张量(你可以把它想象成一本书,或者一栋建筑)。
可以把这三个维度(形状为
(2, 3, 4))拆解为如下的空间结构:
axis=0(长度为 2):代表有 2 个页面(或 2 个独立矩阵,即“层”)。axis=1(长度为 3):代表每个页面里有 3 行。axis=2(长度为 4):代表每行里面有 4 列(即一维线条上的 4 个连续数字)。
在内存中,三维数组 b 的物理分布和逻辑排布如下:
1 | |
3.5.5.4
第二步:np.sum(b, axis=2) 的沿轴降维机制
这是最关键的聚合运算。在 NumPy 中,“沿着某个 axis 操作”的本质含义是:把该 axis 对应的维度彻底“压扁、熔化”,而保持其他维度的结构不改变。
由于指定了 axis=2(列维,长度为 4):
- NumPy 会锁定
axis=0(页)和axis=1(行)的相对位置不变。 - 拿着扫描仪进入每一个具体的“行”,把该行内部的 4 个列数字全部加在一起,融合成一个单一的数字。
3.5.5.4.1 底层具体的求和演变路径
- 在第 0 页:
- 第 0 行:将
[0, 1, 2, 3]熔化求和 \(\rightarrow 0 + 1 + 2 + 3 = \mathbf{6}\) - 第 1 行:将
[4, 5, 6, 7]熔化求和 \(\rightarrow 4 + 5 + 6 + 7 = \mathbf{22}\) - 第 2 行:将
[8, 9, 10, 11]熔化求和 \(\rightarrow 8 + 9 + 10 + 11 = \mathbf{38}\)
- 第 0 行:将
- 在第 1 页:
- 第 0 行:将
[12, 13, 14, 15]熔化求和 \(\rightarrow 12 + 13 + 14 + 15 = \mathbf{54}\) - 第 1 行:将
[16, 17, 18, 19]熔化求和 \(\rightarrow 16 + 17 + 18 + 19 = \mathbf{70}\) - 第 2 行:将
[20, 21, 22, 23]熔化求和 \(\rightarrow 20 + 21 + 22 + 23 = \mathbf{86}\)
- 第 0 行:将
3.5.5.5 形状消减公式(记忆捷径)
在进行任意高维数组的聚合计算(如 sum, mean,
max)时,可以通过一个纯数学的“消减规则”来瞬间推导输出结果的形状:
- 原始形状:
(2, 3, 4) - 指定轴:
axis=2 - 消减结果:直接把索引为 2
的那个数字从元组里无情擦除 \(\rightarrow\) 剩下
(2, 3)。
所以,原本包含 24 个元素的三维张量,经过 axis=2
的洗礼后,成功“退化”并坍缩为一个拥有 2 页、每页 3 行的
(2, 3) 二维矩阵。
3.5.6 numpy其他计算相关方法
| 方法 | 说明 |
|---|---|
| np.sqrt() | 计算平方根(对每个元素计算平方根) |
| np.log() | 计算对数 |
| np.exp() | 计算自然数的指数值 |
| np.cos/sin/tan/ | 三角函数 |
| np.std() | 计算标准差 |
1 | |
3.6 numpy数据拼分割
3.6.1 多个array拼接
3.6.1.1 拼接的方法concatenate
拼接多个数组:concatenate((a1, a2, ...), axis=0, out=None)
1 | |
3.6.1.2 多个array拼接时的要求
比如要求它们的形状必须是一样的吗?
在 NumPy 中,多个数组(array)进行拼接时,不一定要求它们的形状(shape)完全一样,但它们必须满足一个非常严格的数学和空间几何规则。
这个规则可以概括为一句话:除了指定的拼接轴(axis)之外,其他维度的长度必须完全相同。
为了深刻理解拼接,可以从“一维、二维以及高维”三个场景由浅入深地来看:
3.6.1.2.1. 一维数组(Vectors):最宽松
对于一维数组,拼接(np.concatenate 或
np.append)完全不要求形状一样,可以把任意长度的一维数组像接火车一样串联起来。
1 | |
3.6.1.2.2. 二维数组(Matrices):最经典的“对齐”规则
二维数组有两根轴:axis=0(纵向拼接,向下堆叠)和
axis=1(横向拼接,向右横摆)。
场景
A:纵向堆叠(axis=0)
如果想把两个表格上下拼在一起,它们必须满足:列数(width)必须完全一样。至于行数(height),多长多短都可以。
- 数组 A 的形状是
(行数1, 列数) - 数组 B 的形状是
(行数2, 列数)
1 | |
场景
B:横向拼接(axis=1)
如果想把两个表格左右拼在一起,它们必须满足:行数(height)必须完全一样。
- 数组 A 的形状是
(行数, 列数1) - 数组 B 的形状是
(行数, 列数2)
1 | |
3.6.1.3. 高维数组(三维及以上):推广通用公式
在处理大模型中的 Embedding、图片数据(如
[Batch, Channel, Height, Width])时,拼接的规则依然遵循相同的逻辑。
假设有两个四维数组想要拼接:
- 数组 A 的形状是:
(2, 3, 4, 5) - 数组 B 的形状是:
(2, 3, 9, 5)
观察它们的维度,只有 第三个维度(索引为2)
的数字不同(一个是 4,一个是
9)。因此,这两个数组只能在 axis=2
上进行拼接。
1 | |
如果尝试在 axis=0 或 axis=1
上拼接它们,NumPy
就会直接抛出经典的错误:ValueError: all the input array dimensions except for the concatenation axis must match exactly。
1 | |
3.6.1.4 记忆小绝招
在实际工程开发(比如处理 RAG 向量召回矩阵、或者特征拼接)时,可以这样在脑海中脑补:
axis=0(纵向):像在积木塔往上叠积木,新积木和旧积木的底面积(其余维度)必须严丝合缝,高度(指定轴)无所谓。axis=1(横向):像两个人并排站立,两人的身高(其余维度)必须一样高,身材胖瘦(指定轴)无所谓。 array at index 1 has size 9
1 | |
3.6.2 多个数组的堆叠
3.6.2.1 堆叠示例
np.stack(arrays, axis=0, out=None) 基本理解:arrays,沿着axis进行堆叠,类似穿起来,而不是拼接 例如:
1 | |
1 | |
3.6.2.2 堆叠时的运行机制
为了彻底看清 axis=0 和 axis=1
这两根轴在底层是如何像“隐形魔术手”一样操纵数据排列的,现在把这两个堆叠过程拆解为准备、升级、对齐、组合四个核心机制。
先看下三个初始的一维数组(形状都是
(2,)):
a = [0, 1]b = [2, 3]c = [4, 5]
3.6.2.2.1
机制一:d1 = np.stack((a, b, c), axis=0) 详细拆解
当 axis=0 时,NumPy
的核心机制是:在最外层(索引为 0
的位置)强行塞入一个全新的轴(维度),然后沿着行的方向,把数组一个接一个“上下堆叠”起来。
3.6.2.2.1.1 底层演变三步走:
开辟新维度(轴 0):
NumPy 发现要在第 0 维(最外层)建新维度,于是先把
a, b, c从一维的(2,)转换为临时的二维结构(1, 2):a变成[[0, 1]](第 0 轴长度为 1,第 1 轴长度为 2)b变成[[2, 3]]c变成[[4, 5]]
纵向堆叠(上下落盘子):
因为新轴建在最外层(控制行数),NumPy 就像叠盘子一样,把这三份数据在纵向(向下)的维度上合并:
- 把
a放在第 0 行 - 把
b放在第 1 行 - 把
c放在第 2 行
- 把
最终成型:
新轴(第 0 轴)容纳了 3 个数组,长度变成了 3;原来的轴(第 1 轴)长度依然是 2。
所以,
d1.shape变成了(3, 2)(3行2列)。数据呈现为:
1
2
3array([[0, 1], # 这一行来自 a
[2, 3], # 这一行来自 b
[4, 5]]) # 这一行来自 c
3.6.2.2.2
机制二:d2 = np.stack((a, b, c), axis=1) 详细拆解
这是最让人头疼、也最容易混淆的操作。当 axis=1 时,NumPy
的核心机制是:把全新开辟的轴塞在内部(索引为 1
的位置),然后沿着列的方向,把数组一列一列“横向并排立起来”。
3.6.2.2.2.1 底层演变三步走:
开辟新维度(轴 1):
NumPy 发现要在第 1 维(里层)建新维度。它会把
a, b, c从(2,)转换成形状为(2, 1)的列向量结构:a变成[[0], [1]](第 0 轴长度为 2,第 1 轴长度为 1)b变成[[2], [3]]c变成[[4], [5]]
横向堆叠(并排立木棍):
因为新轴建在内部(控制列数),NumPy 就像并排立木棍一样,在横向(向右)的维度上把它们组合在一起:
- 在第一行:把
a的第 1 个元素0、b的第 1 个元素2、c的第 1 个元素4横向拼成一行[0, 2, 4]。 - 在第二行:把
a的第 2 个元素1、b的第 2 个元素3、c的第 2 个元素5横向拼成一行[1, 3, 5]。
- 在第一行:把
最终成型:
原先的轴(第 0 轴)长度依然是 2(保持 2 行不变);新生成的轴(第 1 轴)因为塞进了 3 个数组,长度变成了 3(变成 3 列)。
所以,
d2.shape变成了(2, 3)(2行3列)。数据呈现为:
1
2array([[0, 2, 4], # 第一行:包含了 a, b, c 的各自第一个元素
[1, 3, 5]]) # 第二行:包含了 a, b, c 的各自第二个元素
3.6.2.2.3 黄金总结:一张表看清本质区别
为了方便你在后续开发(如拼接深度学习特征向量)时瞬间反应过来,请牢记这个对照:
| 操作命令 | 新轴插入位置 | 几何动作印象 | 最终形状 (Shape) | 最终数据特点 |
|---|---|---|---|---|
np.stack(..., axis=0) |
最外层(前面) | 叠盘子(上下堆叠) | (3, 2) |
每一个原始数组占一行 |
np.stack(..., axis=1) |
最内层(后面) | 立栅栏(左右并排) | (2, 3) |
3.6.2.2.4 np.stack
的硬性要求
无论是沿着哪根轴堆叠,np.stack()
要求参与堆叠的所有数组的形状(Shape)必须完全一模一样。
比如在你的例子中,a, b, c 的形状全部都是
(2,),所以它们能完美堆叠。如果突然加入一个
x = np.array([6, 7, 8])(形状是
(3,)),程序就会直接报错,因为大小不一致的物体是无法整齐地“叠”成规则的高维实体的。
3.6.3 np.hstack与np.vstack
3.6.3.1. 示例操作
1 | |
3.6.3.2. hstack与vstack运行机制
在 NumPy 中,np.vstack 和 np.hstack
是为了让开发者不需要死记硬背
axis(轴)的数字,而设计的两个高度直观的函数。它们的名字非常具象:
np.vstack:v代表 Vertical(纵向),意思是垂直地、上下地堆叠数组。np.hstack:h代表 Horizontal(横向),意思是水平地、左右地拼接数组。
在底层,它们其实是
np.concatenate(拼接)的包装函数。下面分别来看一维、二维以及高维数据在这两个函数下的底层机制。
3.6.3.2.1. 经典二维数据下的机制(最直观)
假设有两个二维矩阵 \(A\) 和 \(B\):
- \(A\) 形状为
(2, 3)(2行3列) - \(B\) 形状为
(2, 3)(2行3列)
① np.vstack((A, B)) 机制
- 几何动作:把 \(B\) 直接放到 \(A\) 的正下方。
- 对齐要求:既然是上下放,它们的宽度(列数)必须完全一样。
- 最终形状:变成
(4, 3)(行数相加,列数不变)。 - 等价命令:
np.concatenate((A, B), axis=0)
② np.hstack((A, B)) 机制
- 几何动作:把 \(B\) 靠在 \(A\) 的右侧。
- 对齐要求:既然是左右并排,它们的高度(行数)必须完全一样。
- 最终形状:变成 `(2,*几何动作:把 \(B\) 直接放到 \(A\) 的正下方**。
- 6)`(行数不变,列数相加)。
- 等价命令:
np.concatenate((A, B), axis=1)
(注:如果处理的是一维数组 (N,),vstack
会把它们变成二维的上下行;而 hstack
则会把它们接成一条更长的一维线。)
1 | |
3.6.3.2.2. 3维甚至更高维度的数据也可以进行吗?
答案是:完全可以! 当数据升到 3 维、4
维甚至更高维度时,np.vstack 和 np.hstack
的核心底层机制依然死死锚定在第 0 轴和第 1
轴上,绝对不会因为维度的拔高而发生改变。
可以用一个通用的公式来总结高维数据下的变化机制:
| 函数 | 永远等价于 | 形状变化规则 | 硬性对齐要求 |
|---|---|---|---|
np.vstack |
np.concatenate(..., axis=0) |
只有第 0 轴的长度相加,其余轴不变 | 除了第 0 轴外,其余维度的数字必须完全相同 |
np.hstack |
np.concatenate(..., axis=1) |
只有第 1 轴的长度相加,其余轴不变 | 除了第 1 轴外,其余维度的数字必须完全相同 |
3.6.3.2.3. 高维情况下的具体机制与实例演示
以大模型特征或图像处理中常见的 3维数组 (Depth, Height, Width) 为例:
假设有两个三维数组:
- 数组 \(A\) 的形状是
(2, 3, 5)(可以想象成有 2 层,每层是 3行5列 的表格) - 数组 \(B\) 的形状是
(2, 3, 5)
场景一:高维下的
np.vstack((A, B))
根据公式,vstack 永远在
axis=0(最外层维度) 上做加法。
1 | |
- 机制解释:最外层的数字从
2变成了4(\(2+2\)),而内层的3和5保持岿然不动。在空间上,这相当于你手里有两个由立体方块组成的“集装箱”,vstack直接把第二个集装箱放在第一个集装箱的正下方。
场景二:高维下的
np.hstack((A, B))
根据公式,hstack 永远在
axis=1(次外层维度,即矩阵的行维度)
上做加法。
1 | |
- 机制解释:中间那层的数字从
3变成了6(\(3+3\)),而最外层的2和最内层的5完美保持不变。在空间上,这相当于把两个集装箱在它们各自内部的行方向上进行延伸和横向加宽。
3.6.3.2.4. 高维拼接时的“翻车”陷阱
因为 vstack 和 hstack
的作用轴是被牢牢焊死的(一个是 axis=0,一个是
axis=1),所以如果遇到更深的维度(比如第 2 轴、第 3
轴)不同,这两个函数就会直接瘫痪。
举个例子:
- 数组 \(X\) 的形状是
(2, 3, 4) - 数组 \(Y\) 的形状是
(2, 3, 9)—— 注意:只有最内层(axis=2)的长度不同(一个是4,一个是9)
此时:
- 执行
np.vstack((X, Y))❌ 报错(因为要求除了 axis=0 外其他必须相同,但最后一位 4 和 9 无法对齐)。 - 执行
np.hstack((X, Y))❌ 报错(因为要求除了 axis=1 外其他必须相同,4 和 9 依然对齐失败)。
💡 这种时候该怎么办?
遇到这种超越了 h 和 v
传统管辖范围的更高轴拼接,就必须请出它们的底层老大哥
np.concatenate,并明确点名你想拼接的那根轴:
1 | |
3.6.3.2.5. 核心记忆终极法则
在处理任何维度的 NumPy 数组时:
- 只要听到
vstack,脑海里就画一个垂直向下的箭头,它只负责在最外层开辟的第 0 个数字上做加法。 - 只要听到
hstack,脑海里就画一个水平向右的箭头,它只负责在括号进来第二层的第 1 个数字上做加法。
3.6.4 numpy分割
split方法:将指定的array按照aixs分割成制定值。
3.6.4.1 numpy分割示例
格式:np.split(ary, indices_or_sections, axis=0)
1 | |
1 | |
3.6.4.2 机制一:等量切分(传入一个整数 N)
当给第二个参数传入一个整数 \(N\) 时,NumPy 会尝试把数组平均分成 \(N\) 等份。
3.6.4.2.1 基础代码示例(一维数组)
1 | |
3.6.4.2.2 核心陷阱:不能整除时会直接报错
等量切分有一个硬性要求:总长度必须能被 \(N\) 整除。
如果上述数组 x 只有 5 个元素,而你尝试
np.split(x, 3),NumPy 绝不会自作聪明地分成
(2, 2, 1),而是会直接抛出经典的错误:
1ValueError: array split does not result in an equal division
3.6.4.3 机制二:指定位置切分(传入一个索引列表 [i, j, ...])
如果想切出大小不等的数组,或者想在特定的位置“切刀”,就需要传入一个排好序的整数列表。
NumPy 会把这些数字当作切刀的下标(索引位置)。其底层的切分区间符合 Python 典型的左闭右开原则:
- 第一块:
[:i] - 第二块:
[i:j] - 第三块:
[j:]
3.6.4.3.1 基础代码示例(一维数组)
1 | |
这种方式非常安全,不会出现因为指定维度上的长度无法被N整除而报错。
3.6.4.4 机制三:二维及高维数组下的切分(引入 axis)
在处理多维数据(如特征矩阵、图像矩阵)时,axis
参数决定了你是横着切(按行) 还是 竖着切(按列)。
假设有一个 (4, 4) 的二维矩阵 A:
1 | |
3.6.4.4.1 axis=0(默认值):纵向切(切断行)
拿着刀水平横着砍过去,把行给切开。
1 | |
3.6.4.4.2 axis=1:横向切(切断列)
拿着刀垂直竖着劈下来,把列给切开。
1 | |
3.6.4.5 机制四:衍生的小兄弟:np.vsplit 与 np.hsplit
为了省去写 axis 的麻烦,NumPy
同样提供了两个具象化函数,它们的底层完全是基于 np.split
实现的:
np.vsplit(A, 2):等价于np.split(A, 2, axis=0)(Vertical,垂直切开,把行拆散)。np.hsplit(A, 2):等价于np.split(A, 2, axis=1)(Horizontal,水平切开,把列拆散)。
3.6.4.6 核心总结
- 想平均分:第二个参数传整数(注意必须能整除)。
- 想自由分:第二个参数传包含索引的列表。
- 控制方向:
axis=0砍断行(上下分家),axis=1砍断列(左右分家)。
3.6.5 vsplit与hsplit
vsplit沿着垂直轴切分
- ```python a = np.arange(10) b = np.hsplit(a, 5) # 等价于 np.split(a,
5, axis=1) print(b) [array([0, 1]), array([2, 3]), array([4, 5]),
array([6, 7]), array([8, 9])]
1
2
3
4
5
6
7
8
9
10
- hsplit沿着水平轴切分
- ```python
a = np.arange(10).reshape(2,5)
b = np.vsplit(a, 2) # 等价于 np.split(a, 2, axis=0)
print(b)
[array([[0, 1, 2, 3, 4]]), array([[5, 6, 7, 8, 9]])]
- ```python a = np.arange(10) b = np.hsplit(a, 5) # 等价于 np.split(a,
5, axis=1) print(b) [array([0, 1]), array([2, 3]), array([4, 5]),
array([6, 7]), array([8, 9])]
3.7 numpy其他操作
3.7.1 nan与缺省值处理
- nan是numpy和pandas中用于标识缺失数据
- none:Python 中对象,不能与Nan混淆一起
- 一个例子:某同学两次考试(每次考两门),第1次、第2次考试中都有一门考试因为某些情况没有参加,数据格式如下:
1 | |
3.7.2 nan判断
3.7.2.1 nan判断示例
1 | |
3.7.3 boolean索引
3.7.2.1 boolean索引示例
1 | |
3.7.2.2 语句的字面含义与运行结果
最后一个语句 v1[np.isnan(v1)] 在 NumPy
中被称为布尔索引(Boolean Indexing) 或
掩码(Mask)提取。
它的字面意思是:“从矩阵 v1 中,把所有满足
np.isnan(v1) 为 True
的元素挑选出来,并组合成一个新数组返回。”
因为在你的矩阵中,第 1 行第 2 列、第 2 行第 1 列的元素是
nan(空值/非数字),所以执行该语句后,NumPy
把它们给抓了出来,返回了 array([nan, nan])。
3.7.2.3 底层图解与执行机制
这个语句的底层演变可以拆解为以下三个步骤:
3.7.2.3.1 第一步:生成布尔掩码(Mask)
首先,np.isnan(v1)
会对矩阵中的每个位置进行逻辑判断,生成一个形状完全一模一样、由
True 和 False 组成的“布尔矩阵”:
1 | |
3.7.2.3.2 第二步:位置对齐与筛选
接着,NumPy 会把这个布尔矩阵像一件“镂空的衣服”一样平铺在原始矩阵
v1 上。只有布尔矩阵中对应位置为 True
的地方,底下的数据才会被允许“漏出来”:
1 | |
3.7.2.3.3 第三步:维度展平(Flattening)
这是最核心的一点机制:无论�
会把这个布尔矩阵像一件“镂空的衣服”一样平�始数组 v1
是二维、三维还是更高维度,一旦使用布尔索引进行数据提取,返回的结果永远会退化(展平)为一维数组。
因为符合 True 条件的元素在空间上可能是不连续的,NumPy
无法保证它们还能拼成一个规则的二维矩阵,所以统一将它们抽离出来,在线性队列中排好。即使最终只找到了一个
nan,它也会被包裹在一维数组 array([nan])
当中。
3.7.2.4 这种语法的实际开发应用场景
在日常的数据清洗、临床数据集构建或机器学习特征工程中,这种“数组[布尔条件]”的语法出镜率极高。它的真正威力和正确用法通常有以下两种:
3.7.2.4.1 场景一:统计缺失值的数量
在清洗大型矩阵时,我们想快速知道里面一共有多少个 nan
损坏了数据,可以配合 len() 或者是 .size
属性:
1 | |
3.7.2.4.2 场景二:空值非法数据的清洗与替换(最核心用法)
在实际写算法时,nan
参与计算会导致整个矩阵的均值、标准差全部变成
nan。我们通常会利用这个语法把 nan
揪出来,并直接原地安全替换为 0 或者是中位数:
1 | |
3.7.4 np.all与np.any
3.7.4.1 np.all与np.any示例
1 | |
3.7.4.2 方法的基本概念与核心区别
在 NumPy 中,np.all() 和 np.any()
是专门用来对布尔数组(或者可以隐式转换为布尔值的逻辑数组)进行全局逻辑组合判断的函数。它们类似于
Python 原生的 all() 和
any(),但在处理大规模多维矩阵时,由于底层经过了 C
语言优化,性能要高效得多。
它们的本质区别可以总结为一句话:
np.all():全真才为真。只有当数组中的所有元素都为True(或非零值)时,才返回True;只要有一个是False,就返回False。np.any():一真即为真。只要数组中存在至少一个True(或非零值),就返回True;只有当全盘皆为False时,才返回False。
3.7.4.3 一维数组下的基础行为
在一维数组中,这两个函数会将整个数组压扁进行单一的条件判定。
3.7.4.3.1 基础代码示例
1 | |
3.7.4.3.2 隐式数值转换规则
如果传入的不是布尔数组,而是普通的数值数组,NumPy
会自动遵循标准计算机逻辑:0 代表 False,任何非 0
数字(包括负数)都代表 True。
1 | |
3.7.4.4 二维及高维矩阵下的轴向判断(引入 axis)
当处理矩阵或高维张量时,如果不指定 axis
参数,np.all 和 np.any
依然会默认把整个矩阵展平为一条线,最终只吐出一个总的 True
或 False。
但通过指定
axis,我们可以实现按行或按列的批量逻辑扫描。
假设我们有以下二维布尔矩阵 mask:
1 | |
3.7.4.4.1 axis=0:纵向扫描(按列压缩)
拿着扫描仪从上往下看每一列。
1 | |
3.7.4.4.2 axis=1:横向扫描(按行压缩)
拿着扫描仪从左往右看每一行。
1 | |
3.7.4.5 数据清洗与验证中的经典应用场景
这两个函数在数据清洗、数据工程(例如过滤非法数据或空值)中极为常用。
3.7.4.5.1 场景一:批量检查矩阵中是否存在任何缺失值(NaN)
这是最经典的高频组合拳:
1 | |
3.7.4.5.2 场景二:安全比对两个大型矩阵是否完全一致
在测试算法或检查前后数据导出是否发生损坏时,不要使用
if A == B:(这会报错),而要用 np.all:
1 | |
3.7.4.6 注意None
3.7.4.6.1 None示例
1 | |
3.7.4.6.2 为什么数组形状是 [1 None]
当在 NumPy 中把数字 1 和 Python 的原生 None
强行混在一个列表里创建数组时:
- NumPy 无法找到一个统一的数字类型(如
int或float)来同时兼容它们。 - 为了不丢失信息,NumPy
只能采取妥协策略:将整个数组的类型提升为最底层的
object(对象类型)。
此时,数组 a
内部存储的不再是连续的底层数字,而是两个指向 Python
内存对象的指针。所以打印出来就是原汁原味的
[1 None]。
3.7.4.6.3 为什么 np.any(a) 的输出是 1
np.any()
的底层机制是“一真即为真”,它在对象数组上会退化为 Python
原生的布尔逻辑短路求值。
3.7.4.6.3.1 执行演变过程
np.any()开始逐个检查数组里面的元素。- 遇到第一个元素
1,根据 Python 的隐式类型转换,非零数字1的布尔值是True。 - 触发短路逻辑:既然已经找到了一个为
True的元素,np.any()认为没有必要再往下看了。它直接把当前这个判定为真的元素本身(也就是1) 作为结果返回了,而不是返回标准的布尔值True。
3.7.4.6.3.2 为什么纯数字数组会返回标准的 True 或 False
在例子 np.array([1, None])
中,数组的底层数据类型(dtype)是
object(对象类型)。因为里面装的是原生
Python 对象,NumPy 只能被迫调用 Python 内置的逻辑,从而触发了 Python
对象独有的“返回元素本身”的短路特性。
而在现在的语句中:
1 | |
这是一个纯整型数组,其底层的 dtype
是标准的 int64(或
int32)。
当对一个纯数字类型的 NumPy 数组调用 np.any()
时,它不会再去调用慢吞吞的 Python 对象短路逻辑,而是直接在底层走
C 语言级别的向量化优化操作。
3.7.4.6.3.3 C 语言层面的执行机制
对于强类型的数字数组,np.any()
在底层的执行演变过程可以精简地拆解为两步:
NumPy 根本不关心里面是几,它直接把整个整型数组看作一个布尔状态。
在 C 语言的底层运算里:
0被视为0(False)任何非
0数字(如1, 2, 3)都被统一等价视为1(True)
因为底层是强类型的 C
运算,它的函数签名已经严格固定了返回值类型。只要在其高效的并行扫描中发现有任何一个非零元素,它在
C 层面就会直接吐出标准的布尔信号,映射回 Python 层面就是标准的
True 或
False,而绝对不会把中间的数字
1、2 或 3 扔出来。
3.7.4.6.3.4 核心对比总结
为了让防止在数据工程和算法开发中混淆,可以用一张简单的对比表来彻底分清它们的本质:
| 数组类型 (dtype) | 数组内容示例 | np.any() / np.all() 的返回值 | 底层行为机制 |
|---|---|---|---|
object (对象数组) |
[1, None] 或 ['a', False] |
元素本身 (1, None,
'a') |
调用 Python 原生对象逻辑,触发对象短路求值 |
数字类型 (int/float) |
[1, 2, 3] 或 [0, 0.0] |
标准布尔值 (True /
False) |
经过 C 语言向量化高能优化,严格限定布尔输出 |
3.7.4.6.4 为什么 np.all(a) 的输出是 None
np.all() 的底层机制是“全真才为真”,同样遵循 Python
对象的逻辑短路求值。
3.7.4.6.4.1 执行演变过程
np.all()开始挨个扫描数组。- 第一个元素
1是真值(True),满足条件,于是继续往下看。 - 遇到第二个元素
None。在 Python 中,None的布尔值是False。 - 触发短路逻辑:一旦撞到了假值,整个“全真”的假设就破灭了。
np.all()立刻停止扫描,并将这个导致判定失败的罪魁祸首本身(也就是None) 直接返回。
3.7.4.7 np.where
形式:
where(condition, [x, y])如果给定x,y,满足条件condition,输出x,不满足输出y;
- 如果没有x,y,返回满足条件对应的值的索引。
示例:随机生成成绩单,判断是否及格
1 | |
1 | |
1 | |
3.8 numpy与ndarray的关系
在 Python 数据科学和 AI 开发中,numpy
和 ndarray
是密不可分的两个核心概念。简单来说,它们的关系是“工具箱”与“主打工具”的关系,或者说是“模块(Module)”与“数据类型(Data
Type)”的关系。
可以从以下几个维度来彻底理清它们的关系:
3.8.1. 概念上的根本区别
numpy是一个库 / 模块 (Library / Module)- 它是 Python 的一个第三方开源数学计算库(全称 Numerical Python)。
- 它是一个容纳了各种功能的大集合,里面不仅包含高效的数据结构,还包含了成百上千个用于线性代数、傅里叶变换、随机数生成的函数和方法(如
np.sin(),np.dot(),np.linalg.solve())。
ndarray是一个类 / 数据类型 (Class / Data Type)- 它是 NumPy 库中定义的核心多维数组对象(全称 N-dimensional array object)。
- 它是真实在内存中开辟空间、存放你数据的实体数据结构。在
NumPy 中创建的所有矩阵、向量、多维数组,在 Python
里的实际类型(
type())都是numpy.ndarray。
3.8.2. 代码中的代码关联(代码演示)
通过几行简单的代码,就能直观地看到它们在程序中的位置:
Python
1 | |
输出结果:
1 | |
- 解释:你通过
import numpy引入了整个工具箱,当你调用np.array()函数去生产一个数组时,这个函数返回给你的成品,就是一个ndarray类型的对象。
3.8.3.
命名上的小陷阱:np.array 与 np.ndarray
像很多刚接触数据科学的开发者一样,我产生过一个疑问:为什么创建时用
np.array,而类型显示的却是 np.ndarray?
np.array()是一个便利的“工厂函数(Factory Function)”。它负责接收传给它的 Python 列表或元组,并在后台对其进行格式检查、内存分配,最终实例化并返回一个ndarray对象。np.ndarray是具体的“类(Class)”。虽然也可以直接通过np.ndarray(...)的方式去实例化一个数组,但那种方式属于底层构造,参数非常晦涩难懂(需要手动指定内存步长、数据类型指针等),非常不直观。因此官方推荐一律使用np.array()来创建。
3.8.4. 为什么 numpy
必须依赖 ndarray?
NumPy 之所以能够跑得比 Python 原生列表快几十甚至上百倍,全靠
ndarray 的底层设计:
- 连续内存分配:Python
列表里存的是对象的指针,数据在内存里是散落的;而
ndarray在底层(C语言层面)开辟的是一段完全连续的原始内存块。 - 单一数据类型(Homogeneous):
ndarray要求内部所有元素必须是同一种类型(比如全是float32或全是int64)。这样 CPU 就可以利用 SIMD(单指令流多数据流)指令集进行并行向量化计算,这也是为什么在做 RAG 应用或者大模型特征处理时,向量加减乘除能瞬间完成的原因。
3.8.5. 总结
numpy是写的代码里import的那个名字,是整个庞大的计算生态。ndarray是numpy里面专门用来装载多维矩阵数据的容器。- 用
numpy(工具箱)提供的np.array()方法