一、NumPy概述
NumPy(Numerical Python) 是Python科学计算生态中最为核心的基础库,提供了高性能的多维数组对象 ndarray 以及丰富的数组操作函数。几乎所有Python数据分析与科学计算的高级库(如Pandas、SciPy、scikit-learn、TensorFlow、PyTorch)都建立在NumPy的基础之上。
为什么学习NumPy数组?
- 高性能: NumPy的底层使用C语言实现,向量化操作比纯Python循环快数十倍甚至上百倍
- 内存效率: ndarray在内存中连续存储,访问速度快且内存占用远低于Python列表
- 广播机制: 支持不同形状数组之间的算术运算,无需显式编写循环
- 通用性强: 几乎所有Python数据科学生态工具都兼容NumPy数组
安装NumPy非常简单,只需在终端执行:
# 使用pip安装
pip install numpy
# 使用conda安装
conda install numpy
安装完成后,通过以下方式导入:
import numpy as np
# 验证安装版本
print(np.__version__) # 输出类似 1.26.3
二、创建NumPy数组
NumPy提供了多种创建数组的方式,覆盖了从基础数据转换到特殊数组生成的各类场景。
2.1 从Python列表创建:array()
np.array() 是最基础的数组创建函数,将Python列表或元组转换为ndarray对象。可以创建一维、二维甚至更高维度的数组。
# 一维数组
arr1d = np.array([1, 2, 3, 4, 5])
print(arr1d)
# 输出: [1 2 3 4 5]
# 二维数组(矩阵)
arr2d = np.array([[1, 2, 3], [4, 5, 6]])
print(arr2d)
# 输出:
# [[1 2 3]
# [4 5 6]]
# 三维数组
arr3d = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
print(arr3d)
# 输出:
# [[[1 2]
# [3 4]]
# [[5 6]
# [7 8]]]
# 显式指定数据类型
arr_float = np.array([1, 2, 3], dtype=np.float64)
print(arr_float.dtype) # float64
最佳实践
创建数组时,如果数据已经是列表形式,优先使用 np.array();如果需要生成特殊结构的数组,使用下面介绍的专用函数效率更高、代码更简洁。
2.2 特殊数组:zeros / ones / eye / full
这些函数用于创建全零数组、全一数组、单位矩阵以及指定填充值的数组,是初始化权重矩阵、占位数组时的常用工具。
# 全零数组
zeros_1d = np.zeros(5)
print(zeros_1d) # [0. 0. 0. 0. 0.]
zeros_2d = np.zeros((3, 4))
print(zeros_2d.shape) # (3, 4)
# 全一数组
ones_2d = np.ones((2, 3))
print(ones_2d)
# [[1. 1. 1.]
# [1. 1. 1.]]
# 单位矩阵
eye_mat = np.eye(4)
print(eye_mat)
# [[1. 0. 0. 0.]
# [0. 1. 0. 0.]
# [0. 0. 1. 0.]
# [0. 0. 0. 1.]]
# 指定填充值的数组
full_arr = np.full((3, 3), fill_value=7)
print(full_arr)
# [[7 7 7]
# [7 7 7]
# [7 7 7]]
2.3 序列生成:arange / linspace / logspace
这三个函数用于生成数值序列,arange 类似于Python内置的 range() 但支持浮点数步长;linspace 在指定区间内生成等间隔的固定数量数值;logspace 则按对数刻度生成序列。
# arange: 类似range(),支持浮点数
arr_a = np.arange(10)
print(arr_a) # [0 1 2 3 4 5 6 7 8 9]
arr_b = np.arange(2, 10, 2)
print(arr_b) # [2 4 6 8]
arr_c = np.arange(0, 1, 0.2)
print(arr_c) # [0. 0.2 0.4 0.6 0.8]
# linspace: 指定起始、结束和数量(包含结束点)
arr_d = np.linspace(0, 1, 5)
print(arr_d) # [0. 0.25 0.5 0.75 1. ]
arr_e = np.linspace(0, 10, 4, endpoint=False)
print(arr_e) # [0. 2.5 5. 7.5]
# logspace: 按对数刻度生成
arr_f = np.logspace(0, 3, 4)
print(arr_f) # [ 1. 10. 100. 1000.]
2.4 随机数组:random模块
NumPy的 random 模块提供了丰富的随机数生成函数,包括均匀分布、正态分布、随机整数、随机排列等,是模拟数据和机器学习初始化的重要工具。
# 设置随机种子(保证结果可复现)
np.random.seed(42)
# [0,1)均匀分布随机数
rand_arr = np.random.rand(3, 4)
print(rand_arr)
# 标准正态分布随机数
randn_arr = np.random.randn(2, 3)
print(randn_arr)
# 指定范围内的随机整数
rand_int = np.random.randint(0, 100, size=(3, 3))
print(rand_int)
# 随机抽样(无放回)
sample = np.random.choice(np.arange(10), size=5, replace=False)
print(sample) # 例如 [1 7 4 9 3]
# 随机排列
shuffled = np.random.permutation(10)
print(shuffled) # 例如 [3 8 0 5 2 9 1 7 4 6]
# 自定义分布:指定概率
probs = np.array([0.1, 0.2, 0.7])
weighted = np.random.choice(['A', 'B', 'C'], size=10, p=probs)
print(weighted) # 多数为'C'
随机种子说明
np.random.seed() 用于设置随机数生成器的种子。使用相同的种子,每次生成的随机数序列完全相同,这在调试和复现实验结果时非常关键。在机器学习的实验中,固定随机种子是一项基本要求。
三、数组核心属性
ndarray对象拥有一组丰富的内置属性,可以快速获取数组的维度信息、形状、数据类型、元素个数、内存占用等关键元信息。熟练使用这些属性是高效操作NumPy数组的基础。
| 属性 |
含义 |
返回值示例 |
| ndim |
数组的维度数量(轴的数量) |
2 |
| shape |
数组的形状,元组形式 |
(3, 4) |
| dtype |
数组元素的数据类型 |
float64 |
| size |
数组中元素的总个数 |
12 |
| itemsize |
每个元素占用的字节数 |
8 |
| nbytes |
数组占用的总字节数 |
96 |
# 创建一个测试数组
arr = np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]], dtype=np.float64)
print(f"数组维度: {arr.ndim}") # 2
print(f"数组形状: {arr.shape}") # (3, 4)
print(f"数据类型: {arr.dtype}") # float64
print(f"元素总数: {arr.size}") # 12
print(f"每元素字节: {arr.itemsize}") # 8
print(f"总字节数: {arr.nbytes}") # 96
数组维度: 2
数组形状: (3, 4)
数据类型: float64
元素总数: 12
每元素字节: 8
总字节数: 96
3.1 ndim — 维度数量
ndim 返回数组的轴数(秩)。一维数组的 ndim=1,二维数组 ndim=2,以此类推。标量值(0维数组)的 ndim=0。
scalar = np.array(5)
print(scalar.ndim) # 0(标量)
vector = np.array([1, 2, 3])
print(vector.ndim) # 1(向量)
matrix = np.array([[1, 2], [3, 4]])
print(matrix.ndim) # 2(矩阵)
tensor = np.ones((2, 3, 4))
print(tensor.ndim) # 3(三维张量)
3.2 shape — 数组形状
shape 返回一个元组,表示数组在每个维度上的大小。这是NumPy中最常用的属性之一,理解 shape 对于数组操作至关重要。
arr = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
print(arr.shape) # (2, 2, 2)
# 含义:2个"页面",每个页面2行2列
# shape可以用于reshape(见后文)
# 也常用于条件判断
if arr.shape[0] == arr.shape[1]:
print("这是一个方阵")
3.3 dtype — 数据类型
dtype 描述了数组中每个元素的类型。NumPy的数据类型体系比Python原生类型更细粒度,支持从8位到128位的各类整数和浮点数。
dtype的重要性
- 精确控制内存占用:例如 int32 比 int64 节省一半内存
- 确保计算精度:科学计算中 float64(双精度)是默认选择
- 与外部系统互操作:读写二进制文件、与C/Fortran代码交互时需要匹配数据类型
- 性能优化:使用更小精度的类型可提升缓存利用率和计算速度
3.4 size / itemsize / nbytes
size 返回数组中的总元素数量(等于 shape 中各维度值的乘积)。itemsize 返回每个元素的字节数,nbytes 是 size * itemsize,表示数组占用的总内存字节数。
# 内存占用对比
arr_int32 = np.ones((1000, 1000), dtype=np.int32)
arr_float64 = np.ones((1000, 1000), dtype=np.float64)
print(f"int32 总字节: {arr_int32.nbytes}") # 4,000,000(约4MB)
print(f"float64 总字节: {arr_float64.nbytes}") # 8,000,000(约8MB)
print(f"int32 itemsize: {arr_int32.itemsize}") # 4
print(f"float64 itemsize: {arr_float64.itemsize}") # 8
四、数据类型详解
NumPy的数据类型体系非常丰富,比Python原生的 int、float 更加精细化,提供了精确的位宽控制。
4.1 常用数据类型一览
| 类型 |
类型代码 |
描述 |
字节数 |
| int8 |
'i1' |
有符号8位整数 |
1 |
| int32 |
'i4' |
有符号32位整数 |
4 |
| int64 |
'i8' |
有符号64位整数 |
8 |
| uint8 |
'u1' |
无符号8位整数(0-255) |
1 |
| float16 |
'f2' |
半精度浮点数 |
2 |
| float32 |
'f4' |
单精度浮点数 |
4 |
| float64 |
'f8' |
双精度浮点数(默认) |
8 |
| complex64 |
'c8' |
单精度复数(实部+虚部各32位) |
8 |
| complex128 |
'c16' |
双精度复数(实部+虚部各64位) |
16 |
| bool |
'?' |
布尔类型(True/False) |
1 |
# 不同类型数组的创建
int_arr = np.array([1, 2, 3], dtype=np.int32)
print(int_arr.dtype) # int32
float_arr = np.array([1, 2, 3], dtype=np.float64)
print(float_arr.dtype) # float64
complex_arr = np.array([1+2j, 3+4j], dtype=np.complex128)
print(complex_arr.dtype) # complex128
bool_arr = np.array([True, False, True], dtype=np.bool_)
print(bool_arr.dtype) # bool
4.2 astype() 类型转换
astype() 方法用于将数组从一种数据类型转换为另一种,返回一个新的数组(不会修改原数组)。类型转换在数据预处理、内存优化和精度控制中非常常用。
# 基础类型转换
arr_float = np.array([1.2, 2.7, 3.5, 4.9])
arr_int = arr_float.astype(np.int32)
print(arr_int) # [1 2 3 4](向下取整截断)
# 浮点数转布尔
arr_bool = arr_float.astype(np.bool_)
print(arr_bool) # [ True True True True](非零为True)
# 降低精度以节省内存
arr_high = np.random.randn(1000, 1000).astype(np.float32)
print(f"float32内存: {arr_high.nbytes / 1024 / 1024:.2f} MB")
# 相比float64节省一半内存
# 类型代码简写
arr_code = np.array([1, 2, 3]).astype('f4')
print(arr_code.dtype) # float32
# 字符串转数字
str_arr = np.array(['1.5', '2.5', '3.5'])
num_arr = str_arr.astype(np.float64)
print(num_arr) # [1.5 2.5 3.5]
类型转换注意事项
- 浮点转整数: 会截断小数部分(不是四舍五入),2.9 转为 2,而不是 3
- 溢出风险: 将大范围类型转为小范围类型时可能溢出,如 int64 转 int8 会截断高位字节
- 布尔转换: 0 和 0.0 转 False,非零值全部转为 True
- 精度损失: float64 转 float32 会损失约7位有效数字的精度
五、数组维度操作
维度操作是NumPy中最实用且最常使用的功能之一,涵盖形状变换(reshape)、展平(ravel/flatten)和转置(transpose)等核心操作。掌握这些操作是进行数据预处理和矩阵运算的前提。
5.1 reshape — 重塑数组形状
reshape() 在不改变数据的前提下,改变数组的维度结构。新形状的元素总数必须与原数组相同。
# 一维转二维
arr = np.arange(12)
reshaped = arr.reshape(3, 4)
print(reshaped)
# [[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
# 使用-1自动推断维度
auto = arr.reshape(2, -1) # -1自动计算为6
print(auto.shape) # (2, 6)
# 一维转三维
arr3d = arr.reshape(2, 2, 3)
print(arr3d)
# [[[ 0 1 2]
# [ 3 4 5]]
# [[ 6 7 8]
# [ 9 10 11]]]
# reshape返回的是视图(view),修改会影响原数组
view = arr.reshape(3, 4)
view[0, 0] = 999
print(arr[0]) # [999 1 2 3 4 5 6 7 8 9 10 11]
reshape的-1技巧
在 reshape 中使用 -1 作为某一维度的大小,NumPy会自动根据总元素数和其他维度值计算该维度的大小。例如 arr.reshape(-1, 1) 可以将一维数组转为列向量,arr.reshape(1, -1) 转为行向量。这是数据预处理中非常常用的技巧。
5.2 ravel / flatten — 数组展平
两者都将多维数组变为一维,区别在于 ravel() 返回的是原数组的视图(共享内存),而 flatten() 返回的是新分配的数组(独立内存)。
# 创建一个二维数组
arr2d = np.array([[1, 2, 3], [4, 5, 6]])
# ravel: 返回视图(不复制数据)
r = arr2d.ravel()
print(r) # [1 2 3 4 5 6]
# flatten: 返回副本(分配新内存)
f = arr2d.flatten()
print(f) # [1 2 3 4 5 6]
# 关键区别:修改ravel会影响原数组
r[0] = 100
print(arr2d)
# [[100 2 3]
# [ 4 5 6]]
# 修改flatten不影响原数组
f[1] = 200
print(arr2d)
# [[100 2 3]
# [ 4 5 6]](不受影响)
# 展平时的顺序控制
arr_c = np.array([[1, 2, 3], [4, 5, 6]])
print(arr_c.ravel(order='C')) # [1 2 3 4 5 6](行优先,默认)
print(arr_c.ravel(order='F')) # [1 4 2 5 3 6](列优先)
5.3 transpose / .T — 转置
转置操作交换数组的维度。对于二维数组,transpose() 和 .T 会将行和列互换;对于高维数组,可以指定轴的排列顺序。
# 二维矩阵转置
mat = np.array([[1, 2, 3],
[4, 5, 6]])
print(mat.T)
# [[1 4]
# [2 5]
# [3 6]]
# transpose() 与 .T 等价
print(np.all(mat.T == mat.transpose())) # True
# 高维数组的转置:指定轴顺序
arr3d = np.arange(24).reshape(2, 3, 4)
print(arr3d.shape) # (2, 3, 4)
# 交换维度:将第0轴和第2轴互换
transposed = arr3d.transpose(2, 1, 0)
print(transposed.shape) # (4, 3, 2)
# 实际应用:图像数据的通道转置
# 假设图像数据形状为 (H, W, C) 转为 (C, H, W)
img_hwc = np.random.rand(224, 224, 3)
img_chw = img_hwc.transpose(2, 0, 1)
print(img_chw.shape) # (3, 224, 224)
5.4 新增与删除维度
在实际的数据处理中,经常需要增加或减少数组的维度以满足特定函数的输入要求。
# np.newaxis: 增加新维度
arr = np.array([1, 2, 3])
col = arr[:, np.newaxis] # (3,) -> (3, 1)
row = arr[np.newaxis, :] # (3,) -> (1, 3)
print(col.shape) # (3, 1)
print(row.shape) # (1, 3)
# np.expand_dims: 在指定位置增加维度
expanded = np.expand_dims(arr, axis=1)
print(expanded.shape) # (3, 1)
# np.squeeze: 删除长度为1的维度
arr_sq = np.array([[[1], [2], [3]]])
print(arr_sq.shape) # (1, 3, 1)
squeezed = np.squeeze(arr_sq)
print(squeezed.shape) # (3,)
# 只删除指定轴上的单维度
squeezed_axis = np.squeeze(arr_sq, axis=0)
print(squeezed_axis.shape) # (3, 1)
六、综合案例实战
下面通过一个完整的综合案例,将本章学习的数组创建、属性查询和维度操作融会贯通。
案例:学生成绩数据处理
模拟一个班级的学生成绩数据,通过NumPy操作完成成绩创建、信息查询和数据重塑。
# 步骤1:创建成绩数据(5名学生,4门课程)
np.random.seed(2024)
scores = np.random.randint(50, 100, size=(5, 4))
print("成绩矩阵 (5x4):")
print(scores)
# 步骤2:查询数据基本属性
print(f"\n数据维度: {scores.ndim}")
print(f"数据形状: {scores.shape}")
print(f"数据类型: {scores.dtype}")
print(f"数据总字节: {scores.nbytes} 字节")
# 步骤3:计算每名学生的总分和平均分
total_scores = scores.sum(axis=1)
mean_scores = scores.mean(axis=1)
print(f"\n每名学生总分: {total_scores}")
print(f"每名学生平均分: {mean_scores}")
# 步骤4:计算每门课程的平均分
course_means = scores.mean(axis=0)
print(f"每门课程平均分: {course_means}")
# 步骤5:数据重塑
# 将成绩表展平为一维
flat_scores = scores.flatten()
print(f"\n展平后的成绩: {flat_scores}")
# 重塑为不同的结构
class1 = scores[:3, :].reshape(-1)
class2 = scores[3:, :].reshape(-1)
print(f"前三名学生成绩: {class1}")
print(f"后两名学生成绩: {class2}")
# 步骤6:成绩转置(课程作为行,学生作为列)
scores_T = scores.T
print(f"\n转置后形状: {scores_T.shape}")
print("课程成绩分布:")
print(scores_T)
成绩矩阵 (5x4):
[[89 52 80 54]
[67 94 50 91]
[58 52 76 64]
[53 50 52 71]
[66 71 84 93]]
数据维度: 2
数据形状: (5, 4)
数据类型: int32
数据总字节: 80 字节
每名学生总分: [275 302 250 226 314]
每名学生平均分: [68.75 75.5 62.5 56.5 78.5 ]
每门课程平均分: [66.6 67.8 68.4 74.6]
展平后的成绩: [89 52 80 54 67 94 50 91 58 52 76 64 53 50 52 71 66 71 84 93]
前三名学生成绩: [89 52 80 54 67 94 50 91 58 52 76 64]
后两名学生成绩: [53 50 52 71 66 71 84 93]
转置后形状: (4, 5)
课程成绩分布:
[[89 67 58 53 66]
[52 94 52 50 71]
[80 50 76 52 84]
[54 91 64 71 93]]
通过这个案例可以看到,NumPy的数组操作使得批量数据处理变得异常简洁。如果使用纯Python的列表和循环来实现上述功能,代码量至少会多出3-5倍,而且执行速度也会慢很多。
七、常见错误与调试
易错点1:reshape的元素总数不匹配
这是用NumPy时最常见的错误之一。reshape要求新形状的元素总数与原数组完全一致,否则会抛出 ValueError。
# 错误示例
arr = np.arange(10)
# arr.reshape(3, 4) # 报错!10个元素无法重排为12个元素
# 正确做法:使用-1自动推断
reshaped = arr.reshape(2, -1) # 自动计算为5
print(reshaped.shape) # (2, 5)
易错点2:视图与副本的混淆
reshape() 和 ravel() 返回的是视图,修改会影响原数组;flatten() 返回的是副本,修改不会影响原数组。如果不确定,使用 .copy() 显式创建副本。
arr = np.arange(6).reshape(2, 3)
# 显式创建副本确保安全
safe_copy = arr.reshape(3, 2).copy()
易错点3:astype不修改原数组
astype() 返回新数组,不会修改原数组。如果需要覆盖原数组,必须重新赋值。
arr = np.array([1.5, 2.5, 3.5])
arr.astype(np.int32) # 没赋值,不影响原数组
print(arr.dtype) # float64(未改变)
arr = arr.astype(np.int32) # 正确方式:重新赋值
print(arr.dtype) # int32
八、核心要点总结
- 数组创建方式多样: np.array() 从已有列表创建;np.zeros()、np.ones()、np.eye()、np.full() 创建特殊结构数组;np.arange() 和 np.linspace() 生成数值序列;np.random 模块生成随机数组
- 六大核心属性: ndim(维度数)、shape(形状)、dtype(数据类型)、size(元素个数)、itemsize(元素字节数)、nbytes(总字节数)
- 数据类型精准控制: NumPy提供从 int8 到 complex128 的丰富数据类型,astype() 实现类型转换,需要注意精度损失和溢出问题
- 维度操作灵活: reshape() 重塑形状,ravel()/flatten() 展平数组,transpose()/.T 转置;newaxis/squeeze 增删维度;理解视图与副本的区别是关键
- 实际应用场景: NumPy数组操作广泛应用于数据处理、机器学习特征工程、图像处理、信号分析等各个领域
九、进一步思考
掌握数组创建与属性是NumPy学习的第一步,也是最重要的一步。在此基础上可以进一步探索以下方向:
进阶学习路径
- 数组运算: 向量化算术运算、广播机制、矩阵乘法(np.dot / @)、通用函数(ufunc)
- 索引与切片: 花式索引、布尔索引、切片赋值、np.where() 条件筛选
- 统计与聚合: sum()、mean()、std()、min()、max() 以及 axis 参数的深入理解
- 文件IO: np.loadtxt()、np.save()、np.savetxt() 实现数据持久化
- 线性代数: np.linalg 模块中的矩阵分解、特征值、线性方程组求解
- 性能优化: 理解内存布局(C-order vs Fortran-order)、使用 np.einsum() 高效表达张量运算
NumPy的高性能计算能力远不止于此。在后续的学习中,建议结合实际的数据分析项目来练习,例如使用NumPy处理CSV数据、计算统计指标、实现简单的机器学习算法(如KNN、线性回归)等。只有通过大量的编码实践,才能真正掌握NumPy的精髓。
课后练习建议
- 创建一个形状为 (6, 8) 的随机整数数组,将其重塑为 (4, 12),再转置为 (12, 4)
- 创建两个形状不同的数组,使用 np.newaxis 使它们的形状兼容并做加法运算
- 从 np.linspace(0, 100, 31) 生成的序列中,筛选出大于50的元素(提示:研究布尔索引)
- 对比使用Python列表解析和NumPy向量化计算100万个数的平方所需的时间,观察性能差异