零基础上手Python数据分析 (23):NumPy 数值计算基础 – 数据分析的加速“引擎”

写在前面

—— 超越原生 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未初始化 (垃圾值) 的数组。 它的值取决于内存当时的状态,创建速度比 zerosones 快,但需要后续赋值。

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,结束值为 stopendpoint 参数决定是否包含 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) 之间 的随机整数。 如果 highNone,则范围是 [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 进行条件判断,生成一个与原数组 形状相同布尔数组 (包含 TrueFalse)。
使用布尔数组进行索引:布尔数组 作为索引传递给 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 官方文档进行深入学习。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容