什么是 NumPy?
- 全称:Numerical Python(数值 Python)
- 定义:它是 Python 中用于科学计算的基础包。
- 核心对象:Ndarray(N-dimensional array,N维数组对象)。
为什么要学 NumPy?(核心优势)
优势一:运算速度极快
- Python List:只能通过
for 循环逐个元素处理,速度慢。 - NumPy:底层由 C 语言编写,支持向量化运算(Vectorization)。
例子:两个数组相加,NumPy 只需一行代码 arr1 + arr2,无需循环,速度提升百倍。
优势二:节省空间
- Python List:存储的是“指针”,每个元素是一个独立的对象(包含类型、值等信息),内存占用大。
- NumPy:存储的是连续的同质数据(所有元素类型一致),内存占用小,读取效率高。
优势三:功能强大
- 提供了大量数学函数(线性代数、傅里叶变换、随机数生成等)。
- 是后续数据分析库(如 Pandas、Matplotlib、Scikit-learn)的底层基石。
️ NumPy 的应用场景
总结
什么是 dtype?
- 定义:NumPy 中的数据类型对象(dtype),用于描述数组中元素的数据类型(如整数、浮点数、布尔值等)。
为什么要关注 dtype?
- 节省内存:NumPy 默认的
int64 或 float64 占用空间大,如果数据只需要 int8 或 int16,可以大幅节省内存。 - 特殊需求:如图像处理通常需要
uint8(0-255)。
NumPy 的 7 大类基本数据类型
| | | |
|---|
bool_ | | | |
int8 | | | |
int16 | | | |
int32 | | | |
int64 | | | |
float32 | | | |
float64 | | | |
注意:
- 位数越高,能表示的数值范围越大,精度越高,但占用内存也越大。
定义和转换数据类型
方法一:创建时直接指定 (dtype)
import numpy as np
# 1. 默认情况下,带小数的数会被转为 float64
arr1 = np.array([1.1, 2.2, 3.3])
print('默认类型:', arr1.dtype) # float64
# 2. 强制创建为整数类型 (会截断小数)
arr2 = np.array([1, 2, 3], dtype='int32')
print('指定 int32:', arr2.dtype) # int32
# 3. 创建布尔类型
arr3 = np.array([0, 1, 2, -1], dtype='bool')
print('转布尔:', arr3) # [False True True True]
# 规则:0为False,非0为True
方法二:使用 astype() 转换现有数组
# 将 float64 转为 int32 (注意:这里会发生截断,不是四舍五入)
arr4 = arr1.astype('int32')
print('float转int:', arr4) # [1 2 3]
# 将整数转为字符串 (视频中提到的特殊用法)
arr5 = np.array([1, 2, 3], dtype='str')
print('转字符串:', arr5) # ['1' '2' '3']
方法三:使用类型字符代码
# 简写方式
arr6 = np.array([1, 2, 3], dtype='f4') # f4 代表 float32
arr7 = np.array([1, 2, 3], dtype='i8') # i8 代表 int64
print('f4 类型:', arr6.dtype) # float32
常见“坑点”与注意事项
坑点一:精度丢失与截断
- 现象:当把
float 转成 int 时,NumPy 直接丢弃小数部分,而不是四舍五入。
arr8 = np.array([3.9, 4.1], dtype='int32')
print('精度丢失:', arr8) # 结果是 [3, 4],不是 [4, 4]
坑点二:溢出 (Overflow)
- 现象:指定的数据类型太小,装不下你的数据,会发生“溢出”。
- 注意:在 NumPy 1.x 版本结果可能变成负数或奇怪的数字;在 NumPy 2.x 版本会直接报错。
# 错误示范:用 int8 存 300
# int8 的最大值仅为 127
arr_err = np.array(300, dtype='int8')
print(arr_err) # 可能输出 44 或 -12 (取决于系统,因为溢出了)
总结
- 默认规则:NumPy 默认倾向于使用
float64,因为它精度高,不容易出错。 - 优化规则:当你处理海量数据(如百万级以上的数组)时,显式指定
dtype(如 float32 或 int16)能节省一半甚至更多的内存。 - 转换规则:使用
.astype() 时,要时刻警惕数据是否会被截断或溢出。
什么是 Ndarray?
- NumPy 的核心对象是 Ndarray,它是一个多维数组容器。
- 它由 数据 (Data)、描述信息 (Metadata) 组成。
- 固定大小:创建后大小固定(改变大小其实是创建了新对象)。
Ndarray 内存由两部分组成:
- strides:步长(下一集 p=118 详细讲,这里先知道有这个概念)。
这里其实可以解释为什么 NumPy 比 Python 列表快:
- 因为 Python 列表存储的是指针,指针指向散落在内存各处的对象;
- 而 Ndarray 存储的是连续的值,CPU 读取连续内存的速度极快(缓存命中率高)。
代码验证
print('---验证内存连续性---')
import numpy as np
# 创建一个二维数组
arr = np.array([, ])[[source_group_web_1]]
# 1. 查看数据类型 (Metadata)
print('数据类型 dtype:', arr.dtype) # int32 或 int64
# 2. 查看数组形状 (Metadata)
print('数组形状 shape:', arr.shape) # (2, 3)
# 3. 查看数据在内存中的地址 (Buffer)
print('数据内存地址 data:', arr.data)
# <memory at 0x...> 这是一个内存缓冲区对象
# 4. 查看数据的字节大小 (验证连续性)
print('每个元素字节 itemsize:', arr.itemsize) # 例如 8 (bytes)
print('总字节数 nbytes:', arr.nbytes) # 例如 48 (6个元素 * 8字节)
创建 Ndarray 对象
创建 Ndarray 对象最基础、最常用的方法:
- 语法:
numpy.array(object, dtype=None, copy=True, order='K', subok=False, ndmin=0) dtype:指定生成数组的数据类型(如 int32, float64)。
代码示例
基础创建(一维数组)
import numpy as np
print('---基础创建(一维数组)---')
# 从列表创建
data1 = [1, 2, 3, 4]
arr1 = np.array(data1)
print(arr1) # 输出: [1 2 3 4]
print(type(arr1)) # 输出: <class 'numpy.ndarray'>
二维数组创建
print('---二维数组创建---')
# 从嵌套列表创建二维数组 (矩阵)
data2 = [[1, 2, 3],
[4, 5, 6]]
arr2 = np.array(data2)
print(arr2)
# 输出:
# [[1 2 3]
# [4 5 6]]
print('维度:', arr2.ndim) # 输出: 2
print('形状:', arr2.shape) # 输出: (2, 3)
指定数据类型 (dtype)
print('---指定数据类型 (dtype)---')
# 强制创建为浮点型
arr_float = np.array([1, 2, 3], dtype=float)
print(arr_float) # 输出: [1. 2. 3.]
# 强制创建为整型 (浮点转整型会截断)
arr_int = np.array([1.1, 2.2, 3.3], dtype=int)
print(arr_int) # 输出: [1 2 3]
强制指定最小维度 (ndmin)
print('---强制指定最小维度 (ndmin)---')
# 即使数据是一维的,也强制创建为 2 维或 3 维
arr_2d = np.array([1, 2, 3], ndmin=2)
print(arr_2d) # 输出: [[1 2 3]]
print(arr_2d.shape) # 输出: (1, 3) -> 1行3列
arr_3d = np.array([1, 2, 3], ndmin=3)
print(arr_3d) # 输出: [[[1 2 3]]]
print(arr_3d.shape) # 输出: (1, 1, 3)
特殊创建方式
print('---特殊创建方式---')
# 创建 3 行 4 列的全 0 数组
zeros_arr = np.zeros((3, 4))
print(zeros_arr)
# 输出:
# [[0. 0. 0. 0.]
# [0. 0. 0. 0.]
# [0. 0. 0. 0.]]
# 创建 2 行 3 列的全 1 数组
ones_arr = np.ones((2, 3))
print(ones_arr)
# 输出:
# [[1. 1. 1.]
# [1. 1. 1.]]
总结
- 控制:通过
dtype 控制精度,通过 ndmin 控制维度。 - 快捷:使用
np.zeros 和 np.ones 快速初始化特定结构的数据(常用于占位或初始化权重)。
创建 Ndarray 对象的特定矩阵方法
以下是 NumPy 中常用的快捷创建函数及其适用场景:
| | |
|---|
np.zeros(shape) | | |
np.ones(shape) | | |
np.full(shape, fill_value) | | |
np.eye(N) | | |
np.arange([start, ]stop, [step]) | | |
np.linspace(start, stop, num) | | |
关键属性 (Attributes)
在创建数组的过程中,需要关注数组的“形状”信息:
- shape (形状): 一个元组,表示数组在各个维度上的长度。例如
(3, 4) 代表3行4列。 - dtype (数据类型): 数组中元素的类型(如
int32, float64)。在创建时可以通过参数指定。
代码示例
生成全0和全1矩阵 (用于初始化)
import numpy as np
print('生成全0和全1矩阵 (用于初始化)')
# 1. 创建一个 3行4列 的全0矩阵
zeros_matrix = np.zeros((3, 4))
print("全0矩阵:\n", zeros_matrix)
# 2. 创建一个 2x2x2 的全1矩阵 (三维)
ones_matrix = np.ones((2, 2, 2))
print("全1矩阵:\n", ones_matrix)
# 3. 创建一个 3x3 的单位矩阵 (对角线为1)
identity_matrix = np.eye(3)
print("单位矩阵:\n", identity_matrix)
生成特定数值的矩阵与等差数列
print('生成特定数值的矩阵与等差数列')
# 4. 创建一个 2x3 矩阵,所有元素填充为 666
filled_matrix = np.full((2, 3), 666)
print("填充矩阵:\n", filled_matrix)
# 5. 生成从 0 到 10 的等差一维数组 (步长为2)
arange_array = np.arange(0, 10, 2)
print("等差数组:", arange_array) # 输出: [0 2 4 6 8]
# 6. 生成从 0 到 1 的 5 个等间隔数值 (常用于数学函数绘图)
linspace_array = np.linspace(0, 1, 5)
print("等间隔数组:", linspace_array) # 输出: [0. 0.25 0.5 0.75 1. ]
指定数据类型 (dtype)
print('指定数据类型 (dtype)')
# 7. 创建全1矩阵,但指定数据类型为整数 (默认可能是浮点数)
int_ones = np.ones((2, 2), dtype=np.int32)
print("整数矩阵类型:", int_ones.dtype) # 输出: int32
创建随机数矩阵
以下是使用 NumPy 生成不同类型随机数矩阵的常用方法。
代码示例
import numpy as np
# 1. 创建 2x3 的随机矩阵 (0到1之间的均匀分布)
arr1 = np.random.rand(2, 3)
print('2x3 的随机矩阵: \n', arr1)
# 2. 创建 3x3 的正态分布随机矩阵 (标准正态分布)
arr2 = np.random.randn(3, 3)
print('3x3 的正态分布随机矩阵: \n', arr2)
# 3. 创建 0-10 之间的随机整数 (1个)
arr3 = np.random.randint(10)
print('0-10 之间的随机整数 (1个): \n', arr3)
# 4. 创建 2x3 的 0-10 随机整数矩阵
# 参数说明: low=0, high=10, size=(2, 3)
arr4 = np.random.randint(0, 10, size=(2, 3))
print('2x3 的 0-10 随机整数矩阵: \n', arr4)
核心函数速查
| | |
|---|
np.random.rand(d0, d1, ...) | | |
np.random.randn(d0, d1, ...) | | |
np.random.randint(low, high, size) | | |
注意:np.random.randint 的范围是左闭右开,即包含 low 但不包含 high。
NumPy 数组与 Python 列表的对比
- Python 列表像一个通用的“工具箱”,什么都能往里装,非常灵活;
- NumPy 数组则像一个高度优化的“零件盒”,虽然只能装同一种零件,但存取速度极快,还能批量处理。
| | |
|---|
| 数据类型 | 异构:可以包含任意类型的对象(如整数、字符串、字典等)。 | 同构:所有元素必须是相同的数据类型(如全是 int 或全是 float)。 |
| 内存布局 | 分散存储:存储的是指向实际对象的指针,对象本身分散在内存各处。 | 连续存储 |
| 计算性能 | 慢:不支持向量化运算,需使用循环逐个处理,效率较低。 | 极快:支持向量化操作,可对整个数组进行批量运算,底层由C语言优化。 |
| 主要功能 | 通用 | 专业 |
| 适用场景 | | |
为什么 NumPy 数组更快、更省内存?
内存效率:连续 vs. 分散
- Python 列表:你可以把它想象成一个通讯录,里面存的不是联系人本身,而是他们的电话号码(指针)。当你访问一个元素时,Python 需要根据这个“电话号码”去内存里找到真正的对象。每个对象还有额外的“元数据”(如类型、引用计数等),非常占内存。
- NumPy 数组:它像一个连续的储物柜,所有数据(比如全是整数)都紧挨着存放在一起。访问时,CPU 可以高效地预读取数据(缓存友好),并且没有额外的指针和对象开销,因此内存占用小得多。
计算性能:向量化 vs. 循环
- Python 列表:对列表中的每个元素进行运算(例如,每个元素乘以2),你必须写一个
for 循环。在循环中,Python 解释器需要反复进行类型检查、函数调用等,这些都会带来巨大的性能开销。 - NumPy 数组:得益于其同构和连续存储的特性,NumPy 可以将整个运算操作(如
array * 2)交给底层高度优化的 C 语言代码一次性完成。这个过程被称为**向量化 (Vectorization)**,它避免了 Python 循环的开销,速度可以提升数十甚至上百倍。
代码验证
import time
import numpy as np
# --- 数据类型:灵活 vs. 严格 ---
# Python 列表:可以混合任意类型
my_list = [1, "hello", 3.14, True]
print(f"列表: {my_list}") # 输出: [1, 'hello', 3.14, True]
# NumPy 数组:强制转换为同一类型
my_array = np.array([1, "hello", 3.14, True])
print(f"数组: {my_array}") # 输出: ['1' 'hello' '3.14' 'True'] (全部转为字符串)
# --- 计算性能:循环 vs. 向量化 ---
# 创建一个包含100万个元素的列表
size = 1000000
py_list = list(range(size))
np_array = np.arange(size)
# 使用 Python 列表进行运算
start_time = time.time()
# 必须使用循环或列表推导式
result_list = [x * 2for x in py_list]
list_time = time.time() - start_time
print(f"Python列表耗时: {list_time:.4f}秒")
# 使用 NumPy 数组进行运算
start_time = time.time()
# 直接向量化运算,语法简洁
result_array = np_array * 2
array_time = time.time() - start_time
print(f"NumPy数组耗时: {array_time:.4f}秒")
print(f"NumPy 比列表快 {list_time / array_time:.1f} 倍")
总结:如何选择?
使用 Python 列表
- 当你需要存储不同类型的数据时(例如,一个包含姓名、年龄、分数的记录)。
- 当你需要频繁地添加或删除元素时(使用
append(), pop() 等方法)。
使用 NumPy 数组
- 作为 Pandas、Scikit-learn 等数据分析/机器学习库的数据基础。
实际建议:在实际的数据分析项目中,一个常见的模式是:先用列表收集和整理原始数据,然后将其转换为 NumPy 数组进行高效的计算和分析。
Ndarray 数组的属性
核心属性速查表
| | | |
|---|
.ndim | | | |
.shape | | | |
.size | | | |
.dtype | | | |
.itemsize | | | 一个元素占多少字节(比如 int64 占 8 字节)。 |
.nbytes | | | 整个数组占多少字节 (= size × itemsize)。 |
代码示例
import numpy as np
# 补全后的代码:3行4列,数据类型为 int32
arr = np.array([
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]
], dtype=np.int32)
print(arr)
# 输出:
# [[ 1 2 3 4]
# [ 5 6 7 8]
# [ 9 10 11 12]]
print(arr.ndim) # 输出: 2
# 因为这是一个矩阵(有行有列),所以是2维
print(arr.shape) # 输出: (3, 4)
# 表示有 3 行,每行 4 个元素
print(arr.size) # 输出: 12
# 3行 * 4列 = 12个元素
print(arr.dtype) # 输出: int32
# 我们创建时指定了 int32,所以这里显示 int32
print(arr.itemsize) # 输出: 4
# 因为 int32 占 4 字节
print(arr.nbytes) # 输出: 48
# 因为 12个元素 × 4字节 = 48字节
两个特殊的属性
- 例如:
arr.T.shape 会变成 (4, 3)。
- 通常我们不需要直接访问它,因为我们用索引(如
arr[0])就能取数据。 - 直接看它只会得到一个
<memory at ...> 的地址信息。
NumPy 切片语法详解
NumPy 的切片语法和 Python 列表非常类似,但在多维数组(二维、三维)上的扩展更强大。
一、核心语法速查表
| | |
|---|
arr[n] | | |
arr[start:stop] | | |
arr[start:] | | |
arr[:stop] | | |
arr[::step] | | |
arr[-1] | | |
二、一维数组切片(基础)
import numpy as np
arr = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
print(arr[2:5]) # [2 3 4]
print(arr[0:8:2]) # [0 2 4 6]
print(arr[:5]) # [0 1 2 3 4]
print(arr[5:]) # [5 6 7 8 9]
print(arr[-1]) # 9
print(arr[::-1]) # [9 8 7 6 5 4 3 2 1 0]
三、二维数组切片(进阶)
arr = np.array([
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]
])
print(arr[1, :]) # [5 6 7 8]
print(arr[:, 2]) # [3 7 11]
四、切片区域(切块)
print(arr[0:2, 1:3])
输出:
[[2 3]
[6 7]]
说明:
五、整数数组索引(花式索引)
arr = np.array([10, 20, 30, 40, 50, 60])
indices = [0, 2, 4]
print(arr[indices]) # [10 30 50]
六、布尔索引(条件筛选)
arr = np.array([1, 6, 3, 8, 5, 9])
mask = arr > 5
print(arr[mask]) # [6 8 9]
print(arr[arr > 5]) # 简写
七、重要注意事项
1. Python 列表 vs NumPy
示例:
slice_arr = arr[0:3]
slice_arr[0] = 999
原数组会被修改。
2. 避免影响原数组
slice_arr = arr[0:3].copy()
八、总结
- 二维切片:
arr[行, 列],用逗号隔开,: 表示全部
📡 NumPy 广播机制详解
广播(Broadcasting)是 NumPy 最强大的功能之一。你可以把它想象成一个“智能拉伸”的过程。
在没有广播之前,两个形状不同的数组是无法直接运算的。但有了广播,NumPy 会自动把“小数组”在逻辑上进行拉伸、复制,让它变得和“大数组”一样大,从而能够进行计算。
💡 核心优势这不仅让代码更简洁,而且速度极快——因为它只是在逻辑上拉伸,并没有真的在内存里复制数据。
📏 广播的核心规则
广播并不是乱来的,它遵循一套严格的“从后往前,对齐比较”的规则。
核心原则
- 维度补齐:如果两个数组维度数量不同,维度少的那个数组,形状会在前面补 1。
- 维度兼容:从后往前对比两个数组的维度长度,只有满足以下任一条件,才能广播:
- 结果形状:运算结果的形状,取两个数组在该维度上的最大值。
💻 代码实战场景
场景 1:标量与数组(0维 vs 1维)
这是最简单的广播。标量会被视为形状为 () 的数组,在运算时自动拉伸以匹配数组。
import numpy as np
print('场景 1:标量与数组(0维 vs 1维)')
arr = np.array([1, 2, 3])
# 标量 10 会自动广播成 [10, 10, 10] 然后与 arr 相加
result = arr + 10
print(result)
# 输出: [11 12 13]
场景 2:二维数组与一维数组(最经典!)
这是数据科学中最常见的操作,例如给矩阵的每一行加上同一个向量。
print('场景 2:二维数组与一维数组(最经典!)')
matrix = np.array([
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]
])
vector = np.array([10, 20, 30, 40])
print("矩阵形状:", matrix.shape) # (3, 4)
print("向量形状:", vector.shape) # (4,)
result = matrix + vector
print("结果:\n", result)
# 解析:vector 里的 [10, 20, 30, 40] 被逻辑复制了 3 份,
# 分别加到了矩阵的每一行上。
场景 3:双维拉伸(外积运算)
如果两个数组在某个维度上都是 1,它们会互相拉伸,形成一个更大的矩阵。这常用于计算坐标网格。
| | | | |
|---|
| 第 0 维 | | | 3 | |
| 第 1 维 | | | 4 | |
| 最终 | (3, 1) | (1, 4) | (3, 4) | |
print('场景 3:双维拉伸(外积运算)')
# 列向量 (3, 1)
col = np.array([[1], [2], [3]])
# 行向量 (4,) -> 视为 (1, 4)
row = np.array([10, 20, 30, 40])
result = col + row
print(result)
"""
输出结果:
[[11 21 31 41]
[12 22 32 42]
[13 23 33 43]]
"""
⚠️ 广播失败的情况
并不是所有数组都能广播。如果两个数组在同一个维度上,长度既不相等,也不是 1,就会报错。
错误示例分析:
分析过程:
- 看第 1 维(列):都是 4,符合条件(相等),没问题。
- 结果:
ValueError: operands could not be broadcast together。NumPy 不知道该把 B 拉伸成 3 还是把 A 拉伸成 2,所以直接报错。
📌 总结
- 广播的本质:让小数组在逻辑上“变大”,去匹配大数组,从而实现逐元素运算。