11  Numpy 基础 II:ndarray对象的索引、切片与广播机制

11.1 理解多维数组

在开始学习NumPy数组的索引和切片之前,让我们先来理解什么是多维数组。

11.1.1 从生活中理解多维数组

想象一下我们熟悉的几个场景:

  1. 一维数组:就像是一条线上排列的数字
    • 例如:一个班级40名学生的数学成绩
    • 或者:一周七天的气温数据
  2. 二维数组:就像一个Excel表格
    • 例如:一个班级学生的多门课程成绩表
    • 行代表不同的学生,列代表不同的科目
  3. 三维数组:就像多个表格叠在一起
    • 例如:多个班级的学生成绩表
    • 每一层是一个班级的成绩表

让我们用代码来看看这些例子:

import numpy as np

# 一维数组:一个班级的数学成绩
math_scores = np.array([85, 92, 78, 95, 88])
print("数学成绩:")
print(math_scores)

# 二维数组:一个班级的多门课程成绩
class_scores = np.array([
    [85, 92, 78],  # 第一个学生的数学、语文、英语成绩
    [92, 88, 95],  # 第二个学生的成绩
    [78, 85, 89]   # 第三个学生的成绩
])
print("\n班级成绩表(二维数组):")
print(class_scores)

# 三维数组:两个班级的成绩
two_classes = np.array([
    # 第一个班
    [[85, 92, 78],
     [92, 88, 95],
     [78, 85, 89]],
    # 第二个班
    [[95, 89, 92],
     [88, 95, 85],
     [79, 88, 92]]
])
print("\n两个班级的成绩(三维数组):")
print(two_classes)
数学成绩:
[85 92 78 95 88]

班级成绩表(二维数组):
[[85 92 78]
 [92 88 95]
 [78 85 89]]

两个班级的成绩(三维数组):
[[[85 92 78]
  [92 88 95]
  [78 85 89]]

 [[95 89 92]
  [88 95 85]
  [79 88 92]]]
Note

多维数组就像是数据的容器,可以按照现实世界的结构来组织数据。一维是一条线,二维是一个平面(表格),三维是一个立体(多层表格)。

11.2 ndarray对象的索引与切片

11.2.1 基本索引操作

NumPy数组的索引与Python列表类似,但功能更加强大。索引从0开始,可以使用负数索引从末尾开始计数。

import numpy as np

# 创建一个一维数组
arr = np.array([1, 2, 3, 4, 5])

# 基本索引
print(f"第一个元素: {arr[0]}")
print(f"最后一个元素: {arr[-1]}")

# 修改元素
arr[2] = 30
print(f"修改后的数组: {arr}")
第一个元素: 1
最后一个元素: 5
修改后的数组: [ 1  2 30  4  5]

11.2.2 多维数组的索引

对于多维数组,我们使用逗号分隔的索引来访问元素。

# 创建一个2x3的二维数组
arr_2d = np.array([[1, 2, 3], 
                   [4, 5, 6]])

print("二维数组:")
print(arr_2d)

# 访问元素 [行, 列]
print(f"第1行第2列的元素: {arr_2d[0, 1]}")
print(f"第2行第3列的元素: {arr_2d[1, 2]}")

# 修改元素
arr_2d[1, 0] = 40
print("修改后的数组:")
print(arr_2d)
二维数组:
[[1 2 3]
 [4 5 6]]
第1行第2列的元素: 2
第2行第3列的元素: 6
修改后的数组:
[[ 1  2  3]
 [40  5  6]]
Note

在NumPy中,索引顺序是先行后列,这与数学中的矩阵表示法一致。

11.2.3 数组切片

切片操作允许我们获取数组的子集。语法为start:stop:step,其中start是起始索引,stop是结束索引(不包含),step是步长。

# 一维数组切片
arr = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

# 获取前5个元素
print(f"前5个元素: {arr[:5]}")

# 获取索引2到7的元素(不包含7)
print(f"索引2到7的元素: {arr[2:7]}")

# 获取所有偶数索引的元素
print(f"偶数索引的元素: {arr[::2]}")

# 反转数组
print(f"反转数组: {arr[::-1]}")
前5个元素: [0 1 2 3 4]
索引2到7的元素: [2 3 4 5 6]
偶数索引的元素: [0 2 4 6 8]
反转数组: [9 8 7 6 5 4 3 2 1 0]

11.2.4 多维数组的切片

对于多维数组,我们可以在每个维度上应用切片操作。

# 创建一个3x4的二维数组
arr_2d = np.arange(12).reshape(3, 4)
print("原始数组:")
print(arr_2d)

# 获取前2行
print("\n前2行:")
print(arr_2d[:2])

# 获取所有行的前3列
print("\n所有行的前3列:")
print(arr_2d[:, :3])

# 获取特定区域(第1-2行,第1-3列)
print("\n第1-2行,第1-3列:")
print(arr_2d[0:2, 0:3])

# 获取第1行和第3行
print("\n第1行和第3行:")
print(arr_2d[[0, 2]])
原始数组:
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]

前2行:
[[0 1 2 3]
 [4 5 6 7]]

所有行的前3列:
[[ 0  1  2]
 [ 4  5  6]
 [ 8  9 10]]

第1-2行,第1-3列:
[[0 1 2]
 [4 5 6]]

第1行和第3行:
[[ 0  1  2  3]
 [ 8  9 10 11]]
Tip

切片操作返回的是原始数组的视图,而不是副本。这意味着修改切片会影响原始数组。如果需要副本,可以使用copy()方法。

# 演示视图与副本的区别
arr = np.array([1, 2, 3, 4, 5])

# 创建视图
view = arr[1:4]
print(f"原始数组: {arr}")
print(f"视图: {view}")

# 修改视图
view[0] = 20
print(f"修改视图后的原始数组: {arr}")

# 创建副本
copy = arr[1:4].copy()

# 修改副本
copy[0] = 30
print(f"修改副本后的原始数组: {arr}")
原始数组: [1 2 3 4 5]
视图: [2 3 4]
修改视图后的原始数组: [ 1 20  3  4  5]
修改副本后的原始数组: [ 1 20  3  4  5]

11.2.5 高级索引

11.2.5.1 布尔索引

布尔索引允许我们基于条件选择元素。

# 创建一个数组
arr = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])

# 创建布尔掩码
mask = arr > 5
print(f"布尔掩码: {mask}")

# 使用布尔掩码选择元素
print(f"大于5的元素: {arr[mask]}")

# 简化写法
print(f"小于5的元素: {arr[arr < 5]}")

# 复合条件
print(f"大于3且小于8的元素: {arr[(arr > 3) & (arr < 8)]}")
布尔掩码: [False False False False False  True  True  True  True]
大于5的元素: [6 7 8 9]
小于5的元素: [1 2 3 4]
大于3且小于8的元素: [4 5 6 7]

11.2.5.2 花式索引

花式索引允许我们使用整数数组作为索引。

# 创建一个数组
arr = np.arange(10)
print(f"原始数组: {arr}")

# 使用整数数组作为索引
indices = [1, 3, 5, 7]
print(f"选定的元素: {arr[indices]}")

# 二维数组的花式索引
arr_2d = np.arange(16).reshape(4, 4)
print("\n二维数组:")
print(arr_2d)

# 选择特定行和列
rows = np.array([0, 2, 3])
cols = np.array([1, 2, 0])
print("\n选择的元素 (rows, cols):")
print(arr_2d[rows, cols])  # 选择 (0,1), (2,2), (3,0)

# 选择特定行的所有列
print("\n选择的行:")
print(arr_2d[rows])  # 选择第0, 2, 3行
原始数组: [0 1 2 3 4 5 6 7 8 9]
选定的元素: [1 3 5 7]

二维数组:
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]
 [12 13 14 15]]

选择的元素 (rows, cols):
[ 1 10 12]

选择的行:
[[ 0  1  2  3]
 [ 8  9 10 11]
 [12 13 14 15]]

11.3 NumPy广播机制

11.3.1 什么是广播?

广播(Broadcasting)是NumPy的一种强大功能,它允许不同形状的数组在算术运算中一起使用。广播机制自动扩展较小的数组,使其与较大的数组兼容,而无需实际复制数据。

Note

广播遵循一定的规则,不是所有形状的数组都可以广播。

11.3.2 广播规则

  1. 如果两个数组的维度数不同,形状较小的数组会在前面补1
  2. 如果两个数组的形状在任何维度上不匹配,但其中一个维度的大小为1,则在该维度上进行广播
  3. 如果两个数组的形状在任何维度上不匹配,且没有一个维度的大小为1,则会引发错误
# 标量与数组的运算(最简单的广播)
arr = np.array([1, 2, 3, 4])
print(f"数组: {arr}")
print(f"数组 + 10: {arr + 10}")

# 一维数组与二维数组的广播
arr_1d = np.array([1, 2, 3])
arr_2d = np.array([[10], [20], [30]])

print("\n一维数组:")
print(arr_1d)
print("\n二维数组:")
print(arr_2d)

# 广播结果
result = arr_2d + arr_1d
print("\n广播结果:")
print(result)
数组: [1 2 3 4]
数组 + 10: [11 12 13 14]

一维数组:
[1 2 3]

二维数组:
[[10]
 [20]
 [30]]

广播结果:
[[11 12 13]
 [21 22 23]
 [31 32 33]]

11.3.3 广播的实际应用

11.3.3.1 数据归一化

# 创建一个表示多个样本特征的2D数组
data = np.array([[1, 2, 3], 
                 [4, 5, 6], 
                 [7, 8, 9]])

# 计算每列的均值
means = data.mean(axis=0)
print("列均值:")
print(means)

# 使用广播进行归一化
normalized_data = data - means
print("\n归一化后的数据:")
print(normalized_data)
列均值:
[4. 5. 6.]

归一化后的数据:
[[-3. -3. -3.]
 [ 0.  0.  0.]
 [ 3.  3.  3.]]

11.3.3.2 添加偏置向量

# 创建一个表示多个样本的2D数组
samples = np.array([[1, 2, 3], 
                    [4, 5, 6]])

# 创建一个偏置向量
bias = np.array([10, 20, 30])

# 使用广播添加偏置
result = samples + bias
print("添加偏置后的结果:")
print(result)
添加偏置后的结果:
[[11 22 33]
 [14 25 36]]
Tip

广播机制使NumPy代码更加简洁高效,但也可能导致意外的行为。理解广播规则对于编写正确的NumPy代码至关重要。

11.4 结构化数组

11.4.1 什么是结构化数组?

结构化数组是NumPy中的一种特殊数组类型,它允许我们定义具有命名字段的复合数据类型,类似于C语言中的结构体或数据库中的表。

11.4.2 创建结构化数组

# 定义结构化数据类型
dt = np.dtype([
    ('name', 'U20'),     # Unicode字符串,最大长度20
    ('age', 'i4'),       # 32位整数
    ('weight', 'f4')     # 32位浮点数
])

# 创建结构化数组
people = np.array([
    ('张三', 25, 70.5),
    ('李四', 35, 80.0),
    ('王五', 30, 65.2)
], dtype=dt)

print("结构化数组:")
print(people)
结构化数组:
[('张三', 25, 70.5) ('李四', 35, 80. ) ('王五', 30, 65.2)]

11.4.3 访问结构化数组的字段

# 访问单个字段
print("\n所有人的名字:")
print(people['name'])

print("\n所有人的年龄:")
print(people['age'])

# 访问单个记录
print("\n第一个人的信息:")
print(people[0])

# 访问单个记录的特定字段
print("\n第二个人的体重:")
print(people[1]['weight'])

所有人的名字:
['张三' '李四' '王五']

所有人的年龄:
[25 35 30]

第一个人的信息:
('张三', 25, 70.5)

第二个人的体重:
80.0

11.4.4 结构化数组的操作

# 根据字段排序
sorted_by_age = np.sort(people, order='age')
print("按年龄排序:")
print(sorted_by_age)

# 过滤数据
old_people = people[people['age'] > 30]
print("\n年龄大于30的人:")
print(old_people)

# 修改数据
people[0]['weight'] = 72.0
print("\n修改后的数组:")
print(people)
按年龄排序:
[('张三', 25, 70.5) ('王五', 30, 65.2) ('李四', 35, 80. )]

年龄大于30的人:
[('李四', 35, 80.)]

修改后的数组:
[('张三', 25, 72. ) ('李四', 35, 80. ) ('王五', 30, 65.2)]

11.4.5 嵌套结构化数据类型

# 定义嵌套的结构化数据类型
address_dt = np.dtype([
    ('city', 'U20'),
    ('street', 'U30')
])

person_dt = np.dtype([
    ('name', 'U20'),
    ('age', 'i4'),
    ('address', address_dt)  # 嵌套类型
])

# 创建嵌套结构化数组
persons = np.array([
    ('张三', 25, ('北京', '朝阳路')),
    ('李四', 35, ('上海', '南京路')),
    ('王五', 30, ('广州', '天河路'))
], dtype=person_dt)

print("嵌套结构化数组:")
print(persons)

# 访问嵌套字段
print("\n所有人的城市:")
print(persons['address']['city'])
嵌套结构化数组:
[('张三', 25, ('北京', '朝阳路')) ('李四', 35, ('上海', '南京路'))
 ('王五', 30, ('广州', '天河路'))]

所有人的城市:
['北京' '上海' '广州']

11.5 小结

  • NumPy的索引和切片操作提供了灵活的方式来访问和修改数组元素
  • 广播机制使不同形状的数组能够进行算术运算,提高了代码的简洁性和效率
  • 结构化数组允许我们在NumPy中处理复合数据类型,类似于数据库表
Note

这些功能使NumPy成为数据处理和科学计算的强大工具。在实际应用中,理解这些概念对于高效处理大型数据集至关重要。