Python 数据科学与可视化工具箱 (一) - NumPy 介绍与 ndarray 对象


创作不易,请各位看官顺手点点关注,不胜感激 。

作为数据科学的基石,NumPy (Numerical Python) 是 Python 中处理数值计算最重要的库。它提供了强大的多维数组对象 ndarray (N-dimensional array),以及用于操作这些数组的工具。理解 NumPy 和 ndarray 是进行高效数据分析、科学计算以及机器学习的基础。


1. 为什么需要 NumPy?

Python 列表 (List) 固然灵活,但在处理大量数值数据时,存在显著的性能瓶颈:

  • 性能低下:Python 列表是异构的,可以存储不同类型的数据。这意味着每个元素都需要单独存储其类型信息和值,导致内存占用大,且操作时无法进行连续内存访问优化。
  • 功能有限:Python 列表不直接支持像向量加法、矩阵乘法等高级数学运算。你需要编写循环来实现这些操作,这不仅繁琐,而且效率低下。

NumPy 的出现正是为了解决这些问题:

  • 高效存储ndarray 对象将所有元素存储在连续的内存块中,且所有元素必须是同一数据类型,这极大地提高了内存利用率和访问速度。
  • 强大的数学运算能力:NumPy 底层由 C 和 Fortran 实现,提供了高度优化的函数,可以对整个数组进行批量操作 (向量化操作),避免了显式的 Python 循环,从而大幅提升计算速度。
  • 广播机制 (Broadcasting):NumPy 能够对形状不同的数组执行算术运算,这在处理多维数据时非常方便。

简而言之,NumPy 是 Python 进行高性能科学计算的事实标准


2. ndarray 对象:NumPy 的核心

ndarray 是 NumPy 库的核心数据结构,它是一个多维同类型数组

2.1 ndarray 的重要属性

一个 ndarray 对象包含以下重要属性:

  • ndim: 数组的维数(轴的数量,axes)。
  • shape: 一个元组,表示数组在每个维度上的大小。例如,一个 2x3 的矩阵 shape(2, 3)
  • size: 数组中元素的总个数,等于 shape 中所有元素的乘积。
  • dtype: 数组中元素的类型。NumPy 支持多种数据类型,如 int32, float64, bool, complex 等。所有元素的数据类型必须相同。
  • itemsize: 数组中每个元素占用的字节数。
  • data: 包含实际数组元素的缓冲区。通常不直接使用。
2.2 创建 ndarray

创建 ndarray 的方式有多种:

  1. 从 Python 列表或元组创建np.array() 是最常用的方法。

    import numpy as np
    
    # 从列表创建一维数组
    arr1d = np.array([1, 2, 3, 4, 5])
    print("一维数组:", arr1d)
    print("维数:", arr1d.ndim)      # 1
    print("形状:", arr1d.shape)     # (5,)
    print("元素类型:", arr1d.dtype) # int64 (或根据系统而定)
    
    # 从嵌套列表创建二维数组 (矩阵)
    arr2d = np.array([[1, 2, 3], [4, 5, 6]])
    print("\n二维数组:\n", arr2d)
    print("维数:", arr2d.ndim)      # 2
    print("形状:", arr2d.shape)     # (2, 3)
    print("元素总数:", arr2d.size)  # 6
    
    # 创建时指定数据类型
    arr_float = np.array([1, 2, 3], dtype=np.float32)
    print("\n指定 float32 类型数组:", arr_float)
    print("元素类型:", arr_float.dtype) # float32
    
  2. 使用内置函数创建特定形状的数组

    • np.zeros(shape): 创建指定形状的全零数组。
    • np.ones(shape): 创建指定形状的全一数组。
    • np.empty(shape): 创建指定形状的空数组(元素值随机,取决于内存状态)。
    • np.full(shape, fill_value): 创建指定形状并填充指定值的数组。
    • np.eye(N): 创建 NxN 的单位矩阵。
    • np.identity(N): 同 np.eye(N)
    # 全零数组
    zeros_arr = np.zeros((2, 4))
    print("\n全零数组:\n", zeros_arr)
    
    # 全一数组
    ones_arr = np.ones((3, 2), dtype=np.int8)
    print("\n全一数组 (int8):\n", ones_arr)
    
    # 空数组
    empty_arr = np.empty((2, 2))
    print("\n空数组 (随机值):\n", empty_arr) # 每次运行可能不同
    
    # 填充特定值的数组
    full_arr = np.full((3, 3), 7)
    print("\n填充 7 的数组:\n", full_arr)
    
    # 单位矩阵
    identity_matrix = np.eye(3)
    print("\n3x3 单位矩阵:\n", identity_matrix)
    
  3. 使用序列生成函数创建

    • np.arange(start, stop, step): 类似于 Python 的 range(),返回一个数组。
    • np.linspace(start, stop, num): 在指定区间内返回均匀分布的 num 个数字。
    • np.logspace(start, stop, num): 在对数刻度上生成均匀间隔的数字。
    # arange
    arr_range = np.arange(0, 10, 2) # [0, 2, 4, 6, 8]
    print("\narange:", arr_range)
    
    # linspace
    arr_linspace = np.linspace(0, 1, 5) # 包含 start 和 stop
    print("\nlinspace (0到1之间5个均匀分布的数):", arr_linspace)
    
  4. 随机数生成numpy.random 模块提供了各种生成随机数的函数。

    • np.random.rand(d0, d1, ...): 生成指定形状的 [0, 1) 区间内的浮点数。
    • np.random.randn(d0, d1, ...): 生成标准正态分布 (均值为0,方差为1) 的浮点数。
    • np.random.randint(low, high, size): 生成指定范围内的随机整数。
    • np.random.normal(loc, scale, size): 生成指定均值和标准差的正态分布随机数。
    # [0, 1) 均匀分布随机数
    rand_arr = np.random.rand(2, 3)
    print("\n随机 (0-1) 数组:\n", rand_arr)
    
    # 标准正态分布随机数
    randn_arr = np.random.randn(2, 2)
    print("\n标准正态分布随机数组:\n", randn_arr)
    
    # 1到10之间的随机整数 (5个)
    randint_arr = np.random.randint(1, 11, size=5)
    print("\n随机整数数组 (1-10):", randint_arr)
    

3. ndarray 的索引和切片

NumPy 数组的索引和切片与 Python 列表类似,但更强大,支持多维索引。

3.1 一维数组索引和切片

与 Python 列表完全相同。

arr = np.arange(10) # [0 1 2 3 4 5 6 7 8 9]
print("\n原始一维数组:", arr)
print("第一个元素:", arr[0])     # 0
print("第三个到第五个元素:", arr[2:5]) # [2 3 4]
print("从索引5开始到结尾:", arr[5:]) # [5 6 7 8 9]
print("步长为2:", arr[::2])     # [0 2 4 6 8]
print("反转数组:", arr[::-1])   # [9 8 7 6 5 4 3 2 1 0]
3.2 多维数组索引和切片

多维数组的索引和切片使用逗号 , 分隔每个维度的索引。

  • 基本索引arr[row_index, col_index]
  • 切片arr[row_slice, col_slice]
arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print("\n原始二维数组:\n", arr2d)

# 访问单个元素 (第1行第2列,索引从0开始)
print("arr2d[0, 1]:", arr2d[0, 1]) # 2

# 访问整行 (第1行)
print("arr2d[0, :]:", arr2d[0, :]) # [1 2 3] (等同于 arr2d[0])

# 访问整列 (第2列)
print("arr2d[:, 1]:", arr2d[:, 1]) # [2 5 8]

# 访问子矩阵 (前两行,后两列)
print("arr2d[:2, 1:]:\n", arr2d[:2, 1:]) # [[2 3], [5 6]]

# 混合索引 (选择第0行和第2行,所有列)
print("arr2d[[0, 2], :]:\n", arr2d[[0, 2], :]) # [[1 2 3], [7 8 9]]
3.3 布尔索引 (Boolean Indexing)

使用布尔数组来选择元素,非常强大。

arr = np.array([10, 20, 30, 40, 50])
# 选出大于 30 的元素
mask = arr > 30
print("\n布尔掩码:", mask) # [False False False  True  True]
print("大于 30 的元素:", arr[mask]) # [40 50]

# 或者直接写条件
print("等于 20 的元素:", arr[arr == 20]) # [20]
3.4 花式索引 (Fancy Indexing)

使用整数数组来选择任意行或列。

arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]])
print("\n原始二维数组:\n", arr2d)

# 选择第0行、第2行、第1行
print("花式索引选择行 (0, 2, 1):\n", arr2d[[0, 2, 1]])

# 选择第0行和第2行,然后从这些行中选择第1列和第0列
# 注意:这会返回 (len(rows), len(cols)) 形状的数组
print("花式索引选择特定元素 arr2d[[0, 2], [1, 0]]:", arr2d[[0, 2], [1, 0]]) # [2 7] (arr2d[0,1] 和 arr2d[2,0])

4. ndarray 的形状操作

改变数组的形状而不改变其数据。

  • reshape(shape): 返回一个具有新形状的数组视图(可能不是副本)。
  • resize(shape): 直接修改数组的形状。
  • flatten(): 返回一个一维数组的副本。
  • ravel(): 返回一个一维数组的视图(或副本),通常优先于 flatten()
  • transpose()T: 数组转置。
  • np.newaxis: 用于增加数组的维度。
arr = np.arange(12)
print("\n原始数组:", arr)

# reshape: 从一维变二维
arr_reshaped = arr.reshape((3, 4))
print("reshape (3x4):\n", arr_reshaped)

# 转置
arr_T = arr_reshaped.T
print("转置:\n", arr_T)

# flatten vs ravel
arr_flat = arr_reshaped.flatten() # 副本
arr_ravel = arr_reshaped.ravel()   # 视图
print("flatten:", arr_flat)
print("ravel:", arr_ravel)

# 增加维度 (例如,将一维数组变为二维列向量)
arr_col = arr[:5, np.newaxis] # 或者 arr[:5].reshape(-1, 1)
print("增加维度 (列向量):\n", arr_col)
print("列向量形状:", arr_col.shape) # (5, 1)

# 增加维度 (行向量)
arr_row = arr[np.newaxis, :5] # 或者 arr[:5].reshape(1, -1)
print("增加维度 (行向量):\n", arr_row)
print("行向量形状:", arr_row.shape) # (1, 5)

5. ndarray 的基本运算

NumPy 的强大之处在于其向量化操作,可以直接对整个数组进行元素级的运算,无需显式循环。

5.1 元素级运算

对数组的每个元素执行相同的操作。

arr1 = np.array([1, 2, 3])
arr2 = np.array([4, 5, 6])

# 加法
print("arr1 + arr2:", arr1 + arr2) # [5 7 9]

# 减法
print("arr1 - arr2:", arr1 - arr2) # [-3 -3 -3]

# 乘法 (元素级乘法,不是矩阵乘法)
print("arr1 * arr2:", arr1 * arr2) # [4 10 18]

# 除法
print("arr1 / arr2:", arr1 / arr2) # [0.25 0.4  0.5 ]

# 标量运算
print("arr1 * 2:", arr1 * 2)     # [2 4 6]
print("arr1 + 10:", arr1 + 10)   # [11 12 13]

# 比较运算
print("arr1 > arr2:", arr1 > arr2) # [False False False]
print("arr1 == arr1:", arr1 == arr1) # [True True True]
5.2 矩阵运算

NumPy 提供了专门的函数进行矩阵运算。

  • np.dot(a, b)a @ b (Python 3.5+):矩阵乘法或点积。
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])

print("\n矩阵 A:\n", A)
print("矩阵 B:\n", B)

# 矩阵乘法
C_dot = np.dot(A, B)
C_at = A @ B
print("矩阵乘法 (A @ B):\n", C_at)

# 元素级乘法 (注意与矩阵乘法的区别)
C_elem_wise = A * B
print("元素级乘法 (A * B):\n", C_elem_wise)
5.3 广播 (Broadcasting)

广播是 NumPy 在执行不同形状数组之间的算术运算时的一种强大机制。当数组的形状不完全匹配时,NumPy 会尝试自动扩展较小的数组以适应较大的数组。

广播规则

  1. 如果两个数组的维度数不同,那么将维度较小的数组的形状在前面填充 1,直到它们的维度数相等。
  2. 在任何一个维度上,如果两个数组的长度相等,或者其中一个数组的长度为 1,那么在这个维度上就兼容。
  3. 如果两个数组在所有维度上都兼容,它们就能广播。
  4. 广播之后,每个维度上的大小将是两个输入数组在该维度上的大小的最大值。
matrix = np.array([[1, 2, 3], [4, 5, 6]]) # 形状 (2, 3)
vector = np.array([10, 20, 30])           # 形状 (3,)

# vector 会被广播成 [[10, 20, 30], [10, 20, 30]]
result = matrix + vector
print("\n广播示例 (矩阵 + 向量):\n", result)

scalar = 5
# scalar 会被广播成与 matrix 相同形状的数组
result2 = matrix * scalar
print("广播示例 (矩阵 * 标量):\n", result2)

# 另一个广播示例 (列向量与行向量)
col_vec = np.array([[10], [20]]) # 形状 (2, 1)
row_vec = np.array([1, 2, 3])     # 形状 (3,)

# 结果形状 (2, 3)
# col_vec 广播为 [[10, 10, 10], [20, 20, 20]]
# row_vec 广播为 [[1, 2, 3], [1, 2, 3]]
broadcast_sum = col_vec + row_vec
print("广播示例 (列向量 + 行向量):\n", broadcast_sum)

6. 聚合函数 (Aggregation Functions)

NumPy 提供了许多用于对数组进行统计计算的聚合函数。

  • sum(): 求和。
  • mean(): 求平均值。
  • median(): 求中位数。
  • min(): 求最小值。
  • max(): 求最大值。
  • std(): 求标准差。
  • var(): 求方差。
  • argmin(), argmax(): 求最小值/最大值的索引。
  • cumsum(): 求累积和。
  • cumprod(): 求累积积。

这些函数可以应用于整个数组,也可以通过指定 axis 参数在特定轴上进行操作。

  • axis=0: 沿着列进行操作(即对每一列进行操作,结果行数为1)。
  • axis=1: 沿着行进行操作(即对每一行进行操作,结果列数为1)。
data = np.array([[1, 2, 3], [4, 5, 6]])
print("\n原始数据:\n", data)

# 整个数组求和
print("总和:", data.sum()) # 21

# 沿着列求和 (axis=0)
print("列和:", data.sum(axis=0)) # [5 7 9]

# 沿着行求和 (axis=1)
print("行和:", data.sum(axis=1)) # [6 15]

# 平均值
print("平均值:", data.mean()) # 3.5
print("列平均值:", data.mean(axis=0)) # [2.5 3.5 4.5]

# 最大值和最小值
print("最大值:", data.max())
print("最小值:", data.min())

# 最大值的索引
print("最大值索引:", data.argmax()) # 5 (元素6的索引)
print("列最大值索引:", data.argmax(axis=0)) # [1 1 1] (每列最大值所在的行索引)

# 累积和
print("累积和:\n", data.cumsum()) # [ 1  3  6 10 15 21] (展平后)
print("列累积和:\n", data.cumsum(axis=0)) # [[1 2 3], [5 7 9]]

7. 深入理解 NumPy 的视图 (View) 与副本 (Copy)

这是 NumPy 中一个非常重要的概念,因为它会影响你对数据修改的行为。

  • 视图 (View):当你对数组进行切片操作时(例如 arr[1:5]),NumPy 通常会返回原始数组的一个视图。视图和原始数组共享底层数据。这意味着如果你修改了视图中的元素,原始数组中的对应元素也会被修改,反之亦然。这提高了效率,避免了不必要的内存复制。
  • 副本 (Copy):当你明确调用 copy() 方法,或者进行一些会强制创建新内存空间的操作时(例如花式索引、高级数学运算的结果),NumPy 会返回一个副本。副本与原始数组的数据是独立的,修改副本不会影响原始数组。
original_arr = np.arange(10)
print("\n原始数组:", original_arr) # [0 1 2 3 4 5 6 7 8 9]

# 创建一个视图
view_arr = original_arr[2:6]
print("视图数组:", view_arr) # [2 3 4 5]

# 修改视图中的元素
view_arr[0] = 99
print("修改视图后,视图数组:", view_arr) # [99 3 4 5]
print("修改视图后,原始数组:", original_arr) # [0 1 99 4 5 6 7 8 9] (原始数组受影响)

# 创建一个副本
copy_arr = original_arr[2:6].copy()
print("\n副本数组:", copy_arr) # [99 4 5 6] (注意这里是原始数组修改后的值)

# 修改副本中的元素
copy_arr[0] = 100
print("修改副本后,副本数组:", copy_arr) # [100 4 5 6]
print("修改副本后,原始数组:", original_arr) # [0 1 99 4 5 6 7 8 9] (原始数组不受影响)

# 通过 np.array() 从现有数组创建新数组通常也是副本
another_copy = np.array(original_arr)
another_copy[0] = -1
print("\n从数组创建的副本:", another_copy)
print("原始数组 (未变):", original_arr)

理解视图和副本对于避免程序中潜在的意外行为至关重要。


总结

NumPy 是 Python 数据科学生态系统中不可或缺的库。它的 ndarray 对象提供了高效的数据存储和强大的数值运算能力,通过向量化操作、广播机制以及灵活的索引切片,极大地简化了科学计算任务。掌握 NumPy 是你迈向高级数据分析和机器学习的第一步。


练习题

尝试独立完成以下练习题,并通过答案进行对照:

  1. 数组创建与属性:

    • 创建一个 3x3 的全零矩阵,数据类型为 float32
    • 创建一个 5 维的随机整数数组,每个维度大小为 2,整数范围在 10 到 50 之间。
    • 打印这两个数组的 ndim, shape, size, dtype 属性。
  2. 索引与切片:

    • 创建一个形状为 (4, 5) 的数组,包含从 1 到 20 的整数。
    • 获取第 2 行(索引为 1)的所有元素。
    • 获取所有行中第 3 列(索引为 2)的元素。
    • 获取前 2 行和后 3 列组成的子矩阵。
    • 使用布尔索引,选出数组中所有大于 15 的元素。
    • 使用花式索引,选出第 0, 2, 3 行。
  3. 形状操作:

    • 创建一个包含 1 到 25 的一维数组。
    • 将其重塑为 5x5 的二维矩阵。
    • 对这个 5x5 矩阵进行转置。
    • 将其展平回一维数组。
    • 创建一个新的包含 3 个元素的数组,通过 np.newaxis 将其转换为列向量和行向量,并打印其形状。
  4. 基本运算与广播:

    • 创建两个 3x3 的随机整数矩阵 A 和 B (范围 1-10)。
    • 计算 A 和 B 的元素级加法、减法、乘法、除法。
    • 计算 A 和 B 的矩阵乘法。
    • 创建一个 3x1 的矩阵 col_vector,值为 [[10], [20], [30]]
    • 计算 A 和 col_vector 的加法(观察广播效果)。
  5. 聚合函数:

    • 创建一个 4x4 的随机整数矩阵(范围 1-100)。
    • 计算整个矩阵的平均值、最大值和标准差。
    • 计算每列的和。
    • 计算每行的最小值。
    • 找出整个矩阵中最大值和最小值的索引。

练习题答案

import numpy as np

print("--- 练习题答案 ---")

# 1. 数组创建与属性
print("\n--- 练习 1: 数组创建与属性 ---")
zeros_matrix = np.zeros((3, 3), dtype=np.float32)
print("3x3 全零矩阵 (float32):\n", zeros_matrix)
print("ndim:", zeros_matrix.ndim)
print("shape:", zeros_matrix.shape)
print("size:", zeros_matrix.size)
print("dtype:", zeros_matrix.dtype)

random_5d_arr = np.random.randint(10, 51, size=(2, 2, 2, 2, 2))
print("\n5维随机整数数组 (10-50):\n", random_5d_arr)
print("ndim:", random_5d_arr.ndim)
print("shape:", random_5d_arr.shape)
print("size:", random_5d_arr.size)
print("dtype:", random_5d_arr.dtype)

# 2. 索引与切片
print("\n--- 练习 2: 索引与切片 ---")
arr_20 = np.arange(1, 21).reshape((4, 5))
print("原始数组 (4x5):\n", arr_20)

print("\n第 2 行 (索引为 1) 的所有元素:", arr_20[1, :]) # 或者 arr_20[1]
print("所有行中第 3 列 (索引为 2) 的元素:", arr_20[:, 2])
print("前 2 行和后 3 列的子矩阵:\n", arr_20[:2, 2:])

greater_than_15 = arr_20[arr_20 > 15]
print("大于 15 的元素:", greater_than_15)

selected_rows = arr_20[[0, 2, 3]]
print("选择第 0, 2, 3 行:\n", selected_rows)

# 3. 形状操作
print("\n--- 练习 3: 形状操作 ---")
arr_1_to_25 = np.arange(1, 26)
print("原始一维数组:", arr_1_to_25)

matrix_5x5 = arr_1_to_25.reshape((5, 5))
print("重塑为 5x5 矩阵:\n", matrix_5x5)

transposed_matrix = matrix_5x5.T
print("转置后的矩阵:\n", transposed_matrix)

flattened_arr = transposed_matrix.flatten() # 或 .ravel()
print("展平回一维数组:", flattened_arr)

new_arr = np.array([1, 2, 3])
col_vec = new_arr[:, np.newaxis]
row_vec = new_arr[np.newaxis, :]
print("\n原始小数组:", new_arr)
print("列向量:\n", col_vec)
print("列向量形状:", col_vec.shape)
print("行向量:\n", row_vec)
print("行向量形状:", row_vec.shape)

# 4. 基本运算与广播
print("\n--- 练习 4: 基本运算与广播 ---")
A = np.random.randint(1, 11, size=(3, 3))
B = np.random.randint(1, 11, size=(3, 3))
print("矩阵 A:\n", A)
print("矩阵 B:\n", B)

print("\nA + B (元素级加法):\n", A + B)
print("A - B (元素级减法):\n", A - B)
print("A * B (元素级乘法):\n", A * B)
print("A / B (元素级除法):\n", A / B)

print("\nA @ B (矩阵乘法):\n", A @ B)

col_vector = np.array([[10], [20], [30]])
print("\n列向量:\n", col_vector)
print("A + col_vector (广播加法):\n", A + col_vector)

# 5. 聚合函数
print("\n--- 练习 5: 聚合函数 ---")
data_matrix = np.random.randint(1, 101, size=(4, 4))
print("原始数据矩阵:\n", data_matrix)

print("\n整个矩阵的平均值:", data_matrix.mean())
print("整个矩阵的最大值:", data_matrix.max())
print("整个矩阵的标准差:", data_matrix.std())

print("\n每列的和:\n", data_matrix.sum(axis=0))
print("每行的最小值:\n", data_matrix.min(axis=1))

max_index = data_matrix.argmax()
min_index = data_matrix.argmin()
print(f"\n整个矩阵中最大值的展平索引: {max_index} (值为 {data_matrix.flatten()[max_index]})")
print(f"整个矩阵中最小值的展平索引: {min_index} (值为 {data_matrix.flatten()[min_index]})")

# 如果想获取多维索引
max_multi_index = np.unravel_index(max_index, data_matrix.shape)
min_multi_index = np.unravel_index(min_index, data_matrix.shape)
print(f"整个矩阵中最大值的多维索引: {max_multi_index}")
print(f"整个矩阵中最小值的多维索引: {min_multi_index}")
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值