写在前面

—— 超越原生 Python 列表,解锁高性能数值计算,深入理解 Pandas 的底层依赖
在前面一系列关于 Pandas 的学习中,我们已经领略了其在数据处理和分析方面的强大威力。我们学会了使用 DataFrame 和 Series 来高效地操作表格数据。但是,你是否好奇,Pandas 为何能够如此高效地处理大规模数据?其背后隐藏着怎样的 “秘密武器”?
答案就是我们今天要深入学习的主角——NumPy (Numerical Python)。
NumPy:Python 科学计算的基石
NumPy 是 Python 中用于 科学计算 的 基础核心库。它提供了:
一个强大的 N 维数组对象 (ndarray)。
用于操作这些数组的各种 高效函数 (例如数学运算、逻辑运算、形状操作、排序、选择等)。
用于线性代数、傅里叶变换和随机数生成的工具。
为什么在学习 Pandas 之后还要学习 NumPy?
你可能会问,既然 Pandas 已经那么好用了,为什么我们还要回过头来学习 NumPy? 原因主要有以下几点:
Pandas 的底层依赖: Pandas 的核心数据结构 Series 和 DataFrame 在底层很大程度上是建立在 NumPy 的 ndarray 之上的。理解 NumPy 的 ndarray 有助于我们更深入地理解 Pandas 的工作原理和性能特性。
高性能数值计算: NumPy 的 ndarray 专为 高性能的数值计算 而设计。相比于 Python 内置的列表 (list),ndarray 在存储和处理大规模数值数据时具有显著的优势:
更少的内存占用: ndarray 存储的是 同质数据类型 (所有元素类型相同),并且存储方式更紧凑。
更快的计算速度: NumPy 的核心运算是用 C 语言 实现的,并且支持 向量化操作 (Vectorization),可以对整个数组进行批量操作,避免了 Python 层面低效的循环,速度远超原生 Python 代码。
科学计算生态系统的基础: NumPy 是 Python 科学计算生态系统 (SciPy Stack) 的基石,许多其他重要的库,如 SciPy (科学计算库)、Matplotlib (可视化库)、Scikit-learn (机器学习库) 等,都依赖于 NumPy。掌握 NumPy 是深入学习这些库的前提。
直接应用场景: 在某些数据分析场景,特别是涉及大量 数值计算、矩阵运算、线性代数 等任务时,直接使用 NumPy 可能比 Pandas 更简洁高效。
虽然本专栏将 NumPy 放在了 Pandas 之后讲解 (因为对于初学者,直接上手 Pandas 更贴近数据分析的实际应用流程),但掌握 NumPy 的核心概念和操作,对于提升你的数据分析效率、深入理解 Pandas 以及为后续学习更高级的技术打下基础,都至关重要。
本篇博客将带你深入 NumPy 的世界,重点学习:
NumPy 的核心数据结构:ndarray (N-维数组)
创建 ndarray 的多种方法
ndarray 的重要属性
NumPy 的核心优势:向量化运算 (UFuncs)
ndarray 的索引与切片操作
布尔索引与条件筛选
掌握 NumPy,你将拥有更强大的数值计算能力,并能更深刻地理解你所使用的 Pandas 工具!
⚙️ 一、NumPy 安装与导入
与 Pandas 和 Matplotlib 类似,如果你使用 Anaconda,NumPy 通常已经预装。 若未安装,可使用 pip 或 conda 安装:
pip install numpy
# 或者
conda install numpy
在 Python 脚本或 Jupyter Notebook 中,导入 NumPy 库,并约定俗成地将其简写为 np。
import numpy as np
🔢 二、NumPy 的核心:ndarray 对象
NumPy 最核心的概念就是 ndarray (N-dimensional array),即 N 维数组。 它是一个 同质 (homogeneous) 数据类型的 多维网格。
ndarray 的关键特性:
维度 (Dimensions/Axes): ndarray 可以是一维、二维、三维甚至更高维度。 维度的数量称为 秩 (rank)。
形状 (Shape): 一个 元组 (tuple),描述了数组在 每个维度上的大小。 例如,一个 3 行 4 列的二维数组,其形状为 (3, 4)。
数据类型 (dtype): 数组中 所有元素的数据类型必须相同。 NumPy 支持多种数值数据类型,例如 int8, int16, int32, int64, uint8 (无符号整数), float16, float32, float64, complex64, complex128, bool, object (可以存储 Python 对象,但会失去 NumPy 的性能优势), string_, unicode_ 等。 这与 Python 列表可以包含不同类型元素的特性形成对比。
固定大小 (Fixed Size): ndarray 在创建时大小是固定的。 改变数组的大小会创建一个新的数组并删除原来的数组。 这有助于提高内存效率和计算性能。
1. 创建 ndarray
有多种方法可以创建 NumPy ndarray 对象:
从 Python 列表或元组创建:np.array()
这是最常用的创建方式,可以将 Python 的列表或嵌套列表转换为 ndarray。
# 创建一维数组
list1 = [1, 2, 3, 4, 5]
arr1d = np.array(list1)
print("一维数组 arr1d:
", arr1d)
print("arr1d 的类型:", type(arr1d))
print("arr1d 的数据类型:", arr1d.dtype)
# 创建二维数组 (矩阵)
list2d = [[1, 2, 3], [4, 5, 6]]
arr2d = np.array(list2d)
print("
二维数组 arr2d:
", arr2d)
print("arr2d 的数据类型:", arr2d.dtype)
# 创建指定数据类型的数组
arr_float = np.array([1, 2, 3], dtype=np.float64) # 指定为 float64 类型
print("
指定数据类型的数组 arr_float:
", arr_float)
print("arr_float 的数据类型:", arr_float.dtype)
arr_str = np.array([1, 2, 3], dtype=str) # 指定为字符串类型
print("
指定数据类型的数组 arr_str:
", arr_str)
print("arr_str 的数据类型:", arr_str.dtype)
使用 NumPy 内置函数创建特定数组:
np.zeros(shape, dtype=float): 创建指定形状 shape 且所有元素都为 0 的数组。
zeros_arr = np.zeros((2, 3)) # 创建一个 2x3 的全零浮点型数组
print("
全零数组 zeros_arr:
", zeros_arr)
np.ones(shape, dtype=float): 创建指定形状 shape 且所有元素都为 1 的数组。
ones_arr = np.ones((3, 2), dtype=int) # 创建一个 3x2 的全一整型数组
print("
全一数组 ones_arr:
", ones_arr)
np.empty(shape, dtype=float): 创建指定形状 shape 的 未初始化 (垃圾值) 的数组。 它的值取决于内存当时的状态,创建速度比 zeros 或 ones 快,但需要后续赋值。
empty_arr = np.empty((2, 2)) # 创建一个 2x2 的未初始化数组
print("
未初始化数组 empty_arr:
", empty_arr) # 输出的值是随机的
np.arange(start, stop, step, dtype=None): 创建一个 等差数列 数组,类似于 Python 的 range() 函数,但返回的是 ndarray。
range_arr = np.arange(0, 10, 2) # 创建从 0 到 10 (不包含 10),步长为 2 的数组
print("
等差数列数组 range_arr:
", range_arr) # 输出: [0 2 4 6 8]
np.linspace(start, stop, num=50, endpoint=True, dtype=None): 创建一个包含 num 个元素的 等间隔 数组,起始值为 start,结束值为 stop。 endpoint 参数决定是否包含 stop 值 (默认为 True)。
linspace_arr = np.linspace(0, 1, 5) # 创建从 0 到 1 包含 5 个等间隔元素的数组
print("
等间隔数组 linspace_arr:
", linspace_arr) # 输出: [0. 0.25 0.5 0.75 1. ]
np.eye(N, M=None, k=0, dtype=float): 创建一个 单位矩阵 (Identity Matrix),即主对角线元素为 1,其余元素为 0 的方阵。 N 是行数,M (可选) 是列数 (默认为 N),k (可选) 是对角线的偏移量。
identity_matrix = np.eye(3) # 创建一个 3x3 的单位矩阵
print("
单位矩阵 identity_matrix:
", identity_matrix)
使用随机数函数创建数组:np.random 模块
np.random 模块提供了丰富的随机数生成函数。
np.random.rand(d0, d1, ..., dn): 创建指定形状 (d0, d1, ..., dn) 的数组,元素为 [0, 1) 之间均匀分布 的随机浮点数。
rand_arr = np.random.rand(2, 3) # 创建一个 2x3 的 [0, 1) 均匀分布随机数组
print("
均匀分布随机数组 rand_arr:
", rand_arr)
np.random.randn(d0, d1, ..., dn): 创建指定形状 (d0, d1, ..., dn) 的数组,元素为 标准正态分布 (均值为 0,标准差为 1) 的随机浮点数。
randn_arr = np.random.randn(3, 2) # 创建一个 3x2 的标准正态分布随机数组
print("
标准正态分布随机数组 randn_arr:
", randn_arr)
np.random.randint(low, high=None, size=None, dtype=int): 创建指定 size 形状的数组,元素为 [low, high) 之间 的随机整数。 如果 high 为 None,则范围是 [0, low)。
randint_arr = np.random.randint(1, 10, size=(2, 4)) # 创建一个 2x4 的 [1, 10) 随机整数数组
print("
随机整数数组 randint_arr:
", randint_arr)
2. ndarray 的重要属性
可以通过 ndarray 对象的属性来查看数组的基本信息:
ndarray.ndim: 数组的 维度数量 (秩)。
arr2d = np.array([[1, 2], [3, 4]])
print("
arr2d 的维度数量 (ndim):", arr2d.ndim) # 输出: 2
ndarray.shape: 数组的 形状 (一个元组)。
print("arr2d 的形状 (shape):", arr2d.shape) # 输出: (2, 2)
ndarray.size: 数组的 元素总数。
print("arr2d 的元素总数 (size):", arr2d.size) # 输出: 4
ndarray.dtype: 数组元素的 数据类型。
print("arr2d 的数据类型 (dtype):", arr2d.dtype) # 输出: int64 (根据系统可能不同)
ndarray.itemsize: 数组中 每个元素占用的字节数。
print("arr2d 每个元素占用的字节数 (itemsize):", arr2d.itemsize) # 输出: 8 (对于 int64)
ndarray.data: 包含数组实际元素的 内存缓冲区。 通常不需要直接操作它。
⚡ 三、数组运算:向量化的威力
NumPy 最核心的优势之一就是 向量化 (Vectorization) 运算。 向量化是指 对整个数组或数组之间的元素进行批量操作,而无需编写显式的 Python 循环。
为什么向量化如此重要?
简洁性: 代码更简洁、更易读、更接近数学表达式。
高效性: NumPy 的向量化操作是在底层用 高度优化的 C 代码 实现的,避免了 Python 解释器的开销和循环的低效,计算速度通常比纯 Python 循环快几个数量级。
1. 元素级算术运算
NumPy 数组支持 元素级 (element-wise) 的算术运算 (+, -, *, /, //, %, **)。 这意味着运算符会 自动应用于数组中的每个元素。
arr_a = np.array([1, 2, 3, 4])
arr_b = np.array([10, 20, 30, 40])
# 数组与标量运算
print("arr_a + 5 =", arr_a + 5) # 输出: [ 6 7 8 9]
print("arr_a * 2 =", arr_a * 2) # 输出: [2 4 6 8]
# 数组与数组运算 (形状必须兼容,通常是相同形状)
print("arr_a + arr_b =", arr_a + arr_b) # 输出: [11 22 33 44]
print("arr_b / arr_a =", arr_b / arr_a) # 输出: [10. 10. 10. 10.]
print("arr_a ** 2 =", arr_a ** 2) # 输出: [ 1 4 9 16]
# 对比 Python 列表的循环实现
list_a = [1, 2, 3, 4]
list_result = []
for x in list_a:
list_result.append(x + 5)
print("Python 列表循环实现 a + 5:", list_result) # 输出: [6, 7, 8, 9] (代码更长,速度更慢)
2. 通用函数 (Universal Functions, ufuncs)
NumPy 提供了大量的 通用函数 (ufuncs),它们是能够对 ndarray 中的数据进行 元素级运算 的函数。 这些函数同样是基于 C 实现的,非常高效。
常见的 ufuncs 包括:
一元 ufuncs (Unary ufuncs): 对单个数组进行操作。
np.sqrt(): 计算平方根。
np.exp(): 计算指数 (e^x)。
np.log(), np.log10(), np.log2(): 计算自然对数、以 10 为底的对数、以 2 为底的对数。
np.sin(), np.cos(), np.tan(): 计算三角函数。
np.abs(): 计算绝对值。
np.ceil(), np.floor(): 向上取整、向下取整。
np.isnan(): 判断元素是否为 NaN。
… 等等。
二元 ufuncs (Binary ufuncs): 对两个数组进行操作。
np.add(), np.subtract(), np.multiply(), np.divide(), np.power(): 对应 +, -, *, /, **。
np.maximum(), np.minimum(): 计算元素级的最大值、最小值。
np.mod(): 计算元素级的模运算。
np.copysign(): 将第二个数组中元素的符号复制到第一个数组的元素上。
np.greater(), np.less(), np.equal(), np.logical_and(), np.logical_or(): 对应 >, <, ==, &, | 等比较和逻辑运算。
… 等等。
arr = np.array([1, 4, 9, 16])
print("sqrt(arr):", np.sqrt(arr)) # 输出: [1. 2. 3. 4.]
print("exp(arr):", np.exp(arr)) # 输出: [2.718... 54.598... ...]
arr1 = np.array([1, 2, 3])
arr2 = np.array([4, 0, 6])
print("maximum(arr1, arr2):", np.maximum(arr1, arr2)) # 输出: [4 2 6]
使用 ufuncs 是 NumPy 高效计算的关键,应尽量使用它们来代替手写循环。
🔪 四、索引与切片:访问数组元素
与 Python 列表类似,NumPy ndarray 也支持 索引 (Indexing) 和 切片 (Slicing) 操作来访问和修改数组中的元素或子数组。
1. 一维数组索引与切片
一维数组的索引和切片与 Python 列表非常相似:
索引: 使用方括号 [] 和整数索引 (从 0 开始) 访问单个元素。
切片: 使用 start:stop:step 的形式获取子数组。 注意:NumPy 数组切片返回的是原始数组的视图 (View),而不是副本 (Copy)!
视图 (View) vs 副本 (Copy):
视图 (View): 视图是原始数组数据的 一部分引用。 修改视图 会影响 原始数组。
副本 (Copy): 副本是原始数组数据的 一份完全独立的拷贝。 修改副本 不会影响 原始数组。
这是 NumPy 数组切片与 Python 列表切片的一个重要区别! Python 列表切片返回的是副本。 NumPy 的这种设计是为了 提高性能和节省内存,避免不必要的数据复制。 如果需要创建副本,可以使用 .copy() 方法。
arr1d = np.arange(10) # [0 1 2 3 4 5 6 7 8 9]
print("原始数组 arr1d:", arr1d)
# 索引
print("arr1d[3]:", arr1d[3]) # 输出: 3
# 切片 (获取索引 2 到 5 之前的元素)
slice_arr = arr1d[2:5]
print("切片 slice_arr:", slice_arr) # 输出: [2 3 4]
# 修改切片 (视图)
slice_arr[1] = 999
print("修改切片后的 slice_arr:", slice_arr) # 输出: [ 2 999 4]
print("修改切片后,原始数组 arr1d 也被修改了:", arr1d) # 输出: [ 0 1 2 999 4 5 6 7 8 9]
# 创建切片的副本
slice_copy = arr1d[5:8].copy()
print("
切片的副本 slice_copy:", slice_copy) # 输出: [5 6 7]
slice_copy[0] = 111
print("修改副本后的 slice_copy:", slice_copy) # 输出: [111 6 7]
print("修改副本后,原始数组 arr1d 未受影响:", arr1d) # 输出: [ 0 1 2 999 4 5 6 7 8 9]
务必注意 NumPy 切片返回视图的特性,避免意外修改原始数据。 如果需要独立修改,请使用 .copy()。
2. 多维数组索引与切片
多维数组的索引和切片使用 逗号 , 分隔不同维度的索引或切片。
索引: arr[row_index, column_index, ...]
arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print("
二维数组 arr2d:
", arr2d)
# 访问单个元素 (第 1 行,第 2 列,索引从 0 开始)
print("arr2d[0, 1]:", arr2d[0, 1]) # 输出: 2
# 也可以分步索引 (但不推荐,效率较低)
print("arr2d[0][1]:", arr2d[0][1]) # 输出: 2
切片: arr[row_start:row_stop, col_start:col_stop]
# 选取前两行
print("arr2d[0:2]:
", arr2d[0:2]) # 等价于 arr2d[0:2, :]
# 输出:
# [[1 2 3]
# [4 5 6]]
# 选取第一列
print("arr2d[:, 0]:
", arr2d[:, 0])
# 输出: [1 4 7] (返回一维数组)
# 选取子矩阵 (第 2 行到第 3 行,第 1 列到第 3 列)
print("arr2d[1:3, 0:2]:
", arr2d[1:3, 0:2]) # 行索引 1, 2; 列索引 0, 1
# 输出:
# [[4 5]
# [7 8]]
# 修改切片 (视图)
sub_arr = arr2d[1:3, 0:2]
sub_arr[:, :] = 0 # 将子数组所有元素设为 0
print("修改切片后的 arr2d:
", arr2d)
# 输出:
# [[1 2 3]
# [0 0 6]
# [0 0 9]]
多维数组的切片同样返回视图,修改切片会影响原始数组!
❓ 五、布尔索引:按条件筛选元素
布尔索引 (Boolean Indexing) 允许我们使用 布尔数组 来 选择 ndarray 中 满足特定条件的元素。 这在数据筛选中非常有用。
基本步骤:
创建布尔数组: 使用比较运算符 (>, <, ==, != 等) 或逻辑运算符 (& 与, | 或, ~ 非) 对 ndarray 进行条件判断,生成一个与原数组 形状相同 的 布尔数组 (包含 True 和 False)。
使用布尔数组进行索引: 将 布尔数组 作为索引传递给 ndarray 的方括号 [],即可 选取布尔数组中对应位置为 True 的元素。
示例:布尔索引
names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
data = np.random.randn(7, 4) # 创建一个 7x4 的随机数组
print("
Names 数组:
", names)
print("
Data 数组:
", data)
# 条件 1: 找出名字等于 'Bob' 的行
condition_bob = (names == 'Bob')
print("
条件 (names == 'Bob'):
", condition_bob) # 输出布尔数组
# 使用布尔数组筛选 data 数组的行
print("
名字等于 'Bob' 的行数据:
", data[condition_bob])
# 条件 2: 找出 data 数组中第 4 列 (索引 3) 小于 0 的行
condition_col4_neg = (data[:, 3] < 0)
print("
条件 (data[:, 3] < 0):
", condition_col4_neg)
print("
第 4 列小于 0 的行数据:
", data[condition_col4_neg])
# 组合条件: 名字等于 'Bob' 且 第 1 列 (索引 0) 大于 0 的行
condition_combined = (names == 'Bob') & (data[:, 0] > 0) # 使用 & 进行逻辑与
print("
组合条件 ((names == 'Bob') & (data[:, 0] > 0)):
", condition_combined)
print("
满足组合条件的行数据:
", data[condition_combined])
# 使用布尔索引修改元素
data[names != 'Joe'] = 7 # 将名字不等于 'Joe' 的所有行的值设为 7
print("
将名字不等于 'Joe' 的行设为 7 后的 data 数组:
", data)
布尔索引是非常强大和灵活的数据筛选工具,在数据分析中极其常用。
🐼 六、NumPy 与 Pandas 的关系:相辅相成
现在,我们再来回顾一下 NumPy 与 Pandas 的关系:
Pandas 基于 NumPy: Pandas 的 Series 和 DataFrame 在底层使用了 NumPy 的 ndarray 来存储数据。 这使得 Pandas 能够利用 NumPy 的高性能计算能力。
.values 属性: 可以通过 Series 或 DataFrame 的 .values 属性获取其底层的 NumPy ndarray 对象。
s = pd.Series([1, 2, 3])
df = pd.DataFrame({
'A': [1, 2], 'B': [3, 4]})
print("
Series 的底层 NumPy 数组:
", s.values)
print("Series 底层数组类型:", type(s.values))
print("
DataFrame 的底层 NumPy 数组:
", df.values)
print("DataFrame 底层数组类型:", type(df.values))
通用性: 许多 NumPy 的函数 (例如 ufuncs) 也可以直接应用于 Pandas 的 Series 和 DataFrame 对象。 Pandas 在内部会进行对齐和处理。
协同工作: 在实际数据分析中,NumPy 和 Pandas 通常协同工作。 NumPy 提供基础的数值计算能力,而 Pandas 提供更高级的数据结构和数据处理、分析功能。 例如,你可能会先用 NumPy 生成一些数值数据,然后用这些数据创建 Pandas DataFrame 进行分析;或者从 Pandas DataFrame 中提取 NumPy 数组进行更底层的数值计算或传递给其他需要 NumPy 数组的库 (如 Scikit-learn)。
理解 NumPy 是深入掌握 Pandas 并提升数据分析效率的关键。
📝 NumPy 基础总结
在本篇博客中,我们学习了 NumPy 的核心基础知识:
NumPy 的重要性: 它是 Python 科学计算的基础,是 Pandas 的底层依赖,提供高性能数值计算能力。
ndarray 对象: 了解了 N 维数组的特性 (维度、形状、数据类型、固定大小)。
数组创建: 掌握了使用 np.array(), np.zeros(), np.ones(), np.arange(), np.linspace(), np.random 等多种方法创建数组。
数组属性: 学会了使用 .ndim, .shape, .size, .dtype, .itemsize 查看数组信息。
向量化运算: 理解了向量化的概念及其带来的性能优势,掌握了元素级算术运算和通用函数 (ufuncs) 的使用。
索引与切片: 掌握了访问和修改一维及多维数组元素的方法,并理解了 视图 (View) 与 副本 (Copy) 的重要区别。
布尔索引: 学会了使用布尔数组进行条件筛选。
NumPy 与 Pandas 的关系: 理解了两者之间的依赖和协同关系。
掌握 NumPy 的基础知识,你不仅能更深入地理解 Pandas 的工作原理,还能在需要进行高性能数值计算或与依赖 NumPy 的其他库交互时游刃有余。
✍️ 练习一下
创建一个包含整数 1 到 10 的一维 NumPy 数组。
创建一个 3×4 的二维 NumPy 数组,所有元素初始化为 5。
创建一个包含 5 个元素的数组,元素为从 0 到 1 之间等间隔的数。
创建一个 2×3 的数组,元素为标准正态分布的随机数。
创建一个一维数组 arr = np.arange(15)。
查看该数组的形状、维度和数据类型。
计算数组中所有元素的平方根。
选取数组中索引为 5 到 10 (不包含 10) 的元素。
创建一个 4×4 的二维数组 arr2d,元素为 1 到 16 的整数。
选取数组中第 2 行 (索引为 1) 的所有元素。
选取数组中第 3 列 (索引为 2) 的所有元素。
选取数组中行索引为 1 和 2,列索引为 0 和 3 的 2×2 子数组。
使用布尔索引,选取练习题 6 中 arr2d 数组中所有大于 10 的元素。
修改练习题 6 中 arr2d 数组中所有偶数元素的值为 0。
小结一下
在本篇博客中,我们深入学习了 Python 科学计算的核心库 NumPy:
理解了 NumPy 的重要性及其与 Pandas 的关系。
掌握了 NumPy 核心数据结构 ndarray 的创建、属性和特性。
体验了向量化运算带来的简洁与高效,学会了使用算术运算和通用函数 (ufuncs)。
掌握了 ndarray 的索引、切片和布尔索引,实现了对数组数据的灵活访问和筛选。
掌握 NumPy,你不仅能够编写更高效的数值计算代码,更能深入理解 Pandas 等数据分析库的底层机制,为解决更复杂的数据分析问题和学习更高级的技术 (如机器学习) 打下了坚实的数学和计算基础。
NumPy 的内容非常丰富,本篇只是介绍了其核心基础。 如果你对线性代数、傅里叶变换、更高级的数组操作 (如广播、重塑、排序) 感兴趣,可以进一步查阅 NumPy 官方文档进行深入学习。


![[OpenWrt] 使 p910nd 支持 HP LaserJet 1020 Plus 打印机 - 宋马](https://pic.songma.com/blogimg/20250612/fef59cf653604a618f368884c117ffab.jpg)











暂无评论内容