Python 数据分析与机器学习入门 (二):NumPy 核心教程,玩转多维数组
引言:为什么需要 NumPy?
在开始学习数据分析时,一个自然的问题是:“Python 已经有了列表(list),为什么我们还需要一个新的数据结构,比如 NumPy 数组?” 答案在于性能和效率。Python 列表非常灵活,可以存储不同类型的元素(如整数、字符串、甚至其他对象),但这种灵活性是以牺牲数值计算性能为代价的。
当处理大规模数值数据,尤其是进行数学运算时,Python 列表的性能瓶颈会变得非常明显。NumPy(Numerical Python 的简称)的出现正是为了解决这个问题。它提供了核心的数据结构——ndarray
(N-dimensional array),一个为高性能数值计算而优化的多维数组对象。
基石地位:几乎所有 Python 数据科学生态系统中的高级库,包括 Pandas 和 Scikit-learn,都是构建在 NumPy 之上的。因此,深刻理解 NumPy 是您数据科学之旅的基石。
性能优势的深层原因:不只是“快”
简单地说 NumPy 比 Python 列表快是远远不够的。理解其背后的原因,能帮助您建立一个关于高性能计算的坚实心智模型。NumPy 的速度优势主要源于以下三个方面:
1. 内存布局 (Contiguous Memory Layout)
- Python 列表:元素在内存中是分散存储的。列表本身只存储指向各个 Python 对象的指针,而这些对象可以散布在内存的任何地方。当您遍历列表时,CPU 需要在内存中“跳来跳去”地访问数据,这会导致大量的“缓存未命中”(cache misses),严重影响效率。
- NumPy 数组:
ndarray
是一个同质(所有元素类型相同)且连续的内存块。数据紧凑地存储在一起,使得 CPU 可以利用其高速缓存(cache locality)一次性加载一大块数据,极大地提升了数据访问速度。
2. 向量化操作 (Vectorization)
- Python 列表:对列表进行元素级运算(例如,将两个列表的对应元素相加)需要显式的
for
循环。Python 的解释器在执行循环时,每一次迭代都会产生开销。 - NumPy 数组:通过向量化彻底改变了这一点。当您写下
array_a + array_b
这样的代码时,NumPy 并没有在 Python 层面进行循环。相反,它将这个操作委托给底层用 C 或 Fortran 语言编写的高度优化的、预编译的代码库(如 BLAS 和 LAPACK)来执行。这些底层代码可以直接操作连续的内存块,并利用现代 CPU 的 SIMD(Single Instruction, Multiple Data,单指令多数据流)指令集,实现对整个数组的并行计算。
3. 更少的内存占用
- 由于 Python 列表中的每个元素都是一个完整的 Python 对象,它包含了类型信息、引用计数等额外的元数据。而 NumPy 数组直接存储原始数据类型(如 32位整数或 64位浮点数),没有这些额外的开销,因此在存储大规模数据时更为节省内存。
理解了这三点,您就会明白为什么整个科学计算栈都建立在 NumPy 之上。它不仅仅是“方便”,更是性能的保证。
创建 NumPy 数组
首先,确保导入 NumPy 库,业界通用惯例是将其简写为 np
。
import numpy as np
从 Python 列表创建
最直接的创建方式是从 Python 列表或元组转换而来。
# 从列表创建一维数组
my_list = [1, 2, 3, 4, 5]
my_array = np.array(my_list)
print(my_array)
# 输出: [1 2 3 4 5]# 从嵌套列表创建二维数组
nested_list = [[1, 2, 3], [4, 5, 6]]
two_d_array = np.array(nested_list)
print(two_d_array)
# 输出:
# [[1 2 3]
# [4 5 6]]
使用内置函数创建
NumPy 提供了多种便捷的函数来创建特定类型的数组,这在初始化时非常有用。
-
np.arange()
: 类似于 Python 的range()
函数,但返回的是一个 NumPy 数组。# 创建一个从 0 到 9 的数组 arr_arange = np.arange(10) print(arr_arange) # 输出: [0 1 2 3 4 5 6 7 8 9]
-
np.zeros()
和np.ones()
: 创建指定形状且全为 0 或全为 1 的数组。这对于创建占位符数组非常方便。# 创建一个 3x4 的全零矩阵 zeros_arr = np.zeros((3, 4)) print(zeros_arr) # 输出: # [[0. 0. 0. 0.] # [0. 0. 0. 0.] # [0. 0. 0. 0.]]# 创建一个 2x3 的全一矩阵 ones_arr = np.ones((2, 3)) print(ones_arr) # 输出: # [[1. 1. 1.] # [1. 1. 1.]]
-
np.linspace()
: 在指定的间隔内返回均匀间隔的数字。# 创建一个从 0 到 10 之间,包含 5 个均匀间隔点的数组 linspace_arr = np.linspace(0, 10, 5) print(linspace_arr) # 输出: [ 0. 2.5 5. 7.5 10. ]
检查您的数组
了解数组的属性对于后续操作至关重要。NumPy 数组有几个非常有用的属性:
.shape
: 返回一个元组,表示数组的维度(行数、列数等)。.ndim
: 返回数组的维数。.size
: 返回数组中元素的总数。.dtype
: 返回数组中元素的数据类型。
arr = np.array([[1, 2, 3], [4, 5, 6]])print(f"Shape: {arr.shape}") # 输出: Shape: (2, 3)
print(f"Dimensions: {arr.ndim}") # 输出: Dimensions: 2
print(f"Size: {arr.size}") # 输出: Size: 6
print(f"Data type: {arr.dtype}") # 输出: Data type: int64
基础数组运算
元素级算术运算
这是 NumPy 最强大的功能之一。所有标准的算术运算符(+
, -
, *
, /
)都可以在数组上进行元素级的操作,无需编写循环。
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])# 元素级加法
print(f"a + b = {a + b}") # 输出: a + b = [5 7 9]# 标量乘法 (Broadcasting)
print(f"a * 2 = {a * 2}") # 输出: a * 2 = [2 4 6]# 元素级乘法
print(f"a * b = {a * b}") # 输出: a * b = [ 4 10 18]
通用函数 (ufuncs)
NumPy 的通用函数(ufunc)是对 ndarray
中的数据执行元素级操作的函数。它们是实现向量化操作的核心。常见的 ufuncs 包括数学运算(如 np.sqrt()
, np.exp()
, np.sin()
)和聚合函数。
arr = np.array([1, 4, 9])# 计算每个元素的平方根
print(np.sqrt(arr)) # 输出: [1. 2. 3.]
基本索引与切片
NumPy 数组的索引和切片语法与 Python 列表非常相似,这使得它易于上手。
arr = np.arange(10) # [0 1 2 3 4 5 6 7 8 9]# 获取单个元素
print(arr[5]) # 输出: 5# 切片获取子数组
print(arr[2:5]) # 输出: [2 3 4]# 从头开始切片
print(arr[:5]) # 输出: [0 1 2 3 4]# 修改数组中的值
arr[0:3] = 99
print(arr) # 输出: [99 99 99 3 4 5 6 7 8 9]
重要区别: NumPy 数组的切片是原始数组的视图(view),而不是副本(copy)。这意味着,如果您修改了切片,原始数组也会被修改。这是一种为了性能和内存效率而做的设计。如果需要副本,必须显式使用
.copy()
方法。
arr = np.arange(10)
slice_of_arr = arr[0:5]
slice_of_arr[:] = 99 # 修改切片的所有元素print(f"Slice: {slice_of_arr}") # 输出: Slice: [99 99 99 99 99]
print(f"Original array: {arr}") # 输出: Original array: [99 99 99 99 99 5 6 7 8 9]
总结与展望
在本篇中,我们不仅学习了 NumPy 的基本操作,更重要的是理解了它作为科学计算基石的深层原因——基于 C 的性能、连续内存布局和向量化操作。这些概念是理解后续所有数据科学工具的关键。
掌握了如何用 NumPy 高效地处理同质化的数值数据后,我们自然会遇到更复杂的需求:如何处理带有不同数据类型、带有标签(列名)的表格数据?这正是 Pandas 的用武之地。在下一篇文章中,我们将学习 Pandas,看看它是如何建立在 NumPy 之上,为我们提供处理真实世界结构化数据的强大武器。