费曼学习法7 - NumPy 数组的 “变形术”:形状变换与索引切片 (基础篇)
第二篇:NumPy 数组的 “变形术”:形状变换与索引切片 (基础篇)
开篇提问:
你有没有整理过书架? 有时候,我们会把书按照 高度 从矮到高排列,有时候又会按照 颜色 分类摆放,甚至会按照 阅读进度 来组织。 虽然书还是那些书,但是 摆放方式 (形状) 改变了,查找和取用就更方便了。
在 NumPy 数组的世界里,我们也可以对数组进行 “变形” 操作,改变数组的 形状,让数据以更适合我们分析的角度呈现。 同时,当我们面对一堆 “积木” (数据) 时,如何 快速找到我们需要的 “那一块” 呢? 这就需要用到 NumPy 数组的 索引 (indexing) 和切片 (slicing) 技术,它们就像是 数据的 “导航仪” 和 “手术刀”,可以让我们精准地定位和提取数组中的数据。 今天,就让我们一起学习 NumPy 数组的 “变形术” 和 “索引切片” 魔法,让数据在我们的指尖 “翩翩起舞”!
核心概念讲解 (费曼式解释):
-
数组形状变换 (Shape Transformation): “给 “积木” 重新塑形”
数组形状变换就像 给 “积木” 重新塑形 一样,可以 改变数组的维度和形状,但 不改变数组中数据的数量和内容。 形状变换在数据分析中非常重要,因为不同的数据分析方法和算法,可能需要 不同形状 的数据输入。 NumPy 提供了多种形状变换的方法,就像一个 “橡皮泥模具” 工具箱,可以让我们随意塑造数组的形状。
-
reshape()
: “重新塑造,任意变形”reshape(newshape)
方法是 最常用 的形状变换方法,它可以 将数组变成指定的新形状newshape
。newshape
是一个 元组,表示新形状的维度和大小。 注意:reshape()
变换后的数组和原始数组共享数据,只是形状不同。 如果修改变换后的数组,原始数组也会被修改! 这就像用橡皮泥模具塑形,橡皮泥本身没有变,只是形状变了。 另外,reshape()
变换前后,数组元素的总数必须保持一致,否则会报错! 就像橡皮泥总量不变,才能塑造成不同的形状。import numpy as np # 创建一个一维数组 array1 = np.arange(12) # [ 0 1 2 3 4 5 6 7 8 9 10 11] print("原始一维数组:\n", array1) print("原始数组的形状:", array1.shape) # (12,) # 使用 reshape() 变换成 (3, 4) 的二维数组 array2 = array1.reshape((3, 4)) # 或者 array1.reshape(3, 4) 形状参数可以是元组或直接写多个整数 print("\nreshape 成 (3, 4) 的二维数组:\n", array2) print("变换后数组的形状:", array2.shape) # (3, 4) # 使用 reshape() 变换成 (2, 6) 的二维数组 array3 = array1.reshape(2, 6) # 另一种写法,形状参数直接写多个整数 print("\nreshape 成 (2, 6) 的二维数组:\n", array3) print("变换后数组的形状:", array3.shape) # (2, 6) # 使用 reshape() 变换成 (3, 2, 2) 的三维数组 array4 = array1.reshape((3, 2, 2)) print("\nreshape 成 (3, 2, 2) 的三维数组:\n", array4) print("变换后数组的形状:", array4.shape) # (3, 2, 2) # 使用 reshape(-1, new_dimension) 自动计算维度大小 array5 = array1.reshape(-1, 6) # -1 表示自动计算行数,列数为 6 print("\nreshape 成 (-1, 6) 的二维数组 (自动计算行数):\n", array5) print("变换后数组的形状:", array5.shape) # (2, 6) NumPy 自动计算出 12/6 = 2 行 array6 = array1.reshape(4, -1) # -1 表示自动计算列数,行数为 4 print("\nreshape 成 (4, -1) 的二维数组 (自动计算列数):\n", array6) print("变换后数组的形状:", array6.shape) # (4, 3) NumPy 自动计算出 12/4 = 3 列 # 尝试 reshape 成形状不兼容的 (5, 3) 会报错,因为 12 个元素无法 reshape 成 5x3=15 个元素的数组 # array7 = array1.reshape((5, 3)) # ValueError: cannot reshape array of size 12 into shape (5,3)
代码解释:
array.reshape(newshape)
:reshape()
方法将数组array
变换成newshape
指定的新形状。newshape
可以是 元组 或 多个整数,表示新形状的维度和大小。reshape(-1, new_dimension)
自动计算维度大小: 当新形状的 某个维度大小不确定,但 总元素数量确定 时,可以使用-1
作为该维度的大小,NumPy 会 自动计算 出该维度的大小。 例如reshape(-1, 6)
,表示列数为 6,行数自动计算 (总元素数量 / 列数)。 注意:-1
只能在一个维度上使用!
-
flatten()
和ravel()
: " “积木” 铺平" (降维成一维数组)flatten()
和ravel()
方法都可以 将多维数组 “铺平” 成一维数组,也就是 降维 操作。 它们就像把多层 “积木” 拆开,然后平铺在地面上,变成一堆 “散落的积木”。-
flatten()
: “完全展开,生成副本”flatten()
方法 返回一个将原始数组完全展开成一维数组的 “副本 (copy)”。 修改flatten()
返回的数组,不会影响原始数组。 这就像把橡皮泥压扁成一张薄片,但原来的橡皮泥还是在那里,没有改变。import numpy as np # 创建一个 (2, 3) 的二维数组 array1 = np.array([[1, 2, 3], [4, 5, 6]]) print("原始二维数组:\n", array1) print("原始数组的形状:", array1.shape) # (2, 3) # 使用 flatten() 展开成一维数组 array2 = array1.flatten() print("\nflatten() 展开成一维数组:\n", array2) print("展开后数组的形状:", array2.shape) # (6,) # 修改 flatten() 返回的数组,不会影响原始数组 array2[0] = 99 print("\n修改 flatten() 后的数组:\n", array2) # 修改了第一个元素 print("原始数组仍然不变:\n", array1) # 原始数组保持不变
-
ravel()
: “尽量 “共享” 数据,返回视图”ravel()
方法也 返回一个将原始数组展开成一维数组的 “视图 (view)”。 如果可能,ravel()
会返回原始数组的 “视图”,也就是和原始数组共享数据,修改ravel()
返回的数组,会影响原始数组! 但如果原始数组的内存布局不连续,ravel()
也可能会返回副本。ravel()
比flatten()
更高效,因为它尽量避免数据复制。 这就像把书架上的书拿下来,排成一排,书还是那些书,只是摆放方式变了,书架上的书也跟着变动 (如果ravel()
返回视图)。import numpy as np # 创建一个 (2, 3) 的二维数组 array1 = np.array([[1, 2, 3], [4, 5, 6]]) print("原始二维数组:\n", array1) print("原始数组的形状:", array1.shape) # (2, 3) # 使用 ravel() 展开成一维数组 array2 = array1.ravel() print("\nravel() 展开成一维数组:\n", array2) print("展开后数组的形状:", array2.shape) # (6,) # 修改 ravel() 返回的数组,会影响原始数组 (因为返回的是视图) array2[0] = 99 print("\n修改 ravel() 后的数组:\n", array2) # 修改了第一个元素 print("原始数组也被修改了:\n", array1) # 原始数组的第一个元素也被修改了!
flatten()
vsravel()
:特性 flatten()
ravel()
返回值 原始数组的 副本 (copy) 原始数组的 视图 (view) (尽量) 修改返回值是否影响原始数组 不影响 可能影响 效率 相对较低 (因为复制数据) 相对较高 (尽量避免复制) 使用场景 需要 独立副本 时 追求效率,可以接受修改原始数组时
-
-
transpose()
或.T
: “矩阵转置,行列互换”transpose()
方法或.T
属性可以 对二维数组 (矩阵) 进行转置,也就是 交换数组的行和列。 这就像把一个表格的行和列互换,或者把一个矩阵沿着对角线翻转。transpose()
和.T
都返回原始数组的 “视图”,修改转置后的数组,会影响原始数组。import numpy as np # 创建一个 (2, 3) 的二维数组 array1 = np.array([[1, 2, 3], [4, 5, 6]]) print("原始二维数组:\n", array1) print("原始数组的形状:", array1.shape) # (2, 3) # 使用 transpose() 进行转置 array2 = array1.transpose() # 或者 array1.T print("\ntranspose() 转置后的数组:\n", array2) print("转置后数组的形状:", array2.shape) # (3, 2) 行和列互换了 # 使用 .T 属性进行转置 (更简洁) array3 = array1.T print("\n.T 转置后的数组:\n", array3) # 修改转置后的数组,会影响原始数组 (因为返回的是视图) array3[0, 0] = 99 # 修改转置后数组的第一个元素 (原数组的 [0, 0] 变成了转置后数组的 [0, 0]) print("\n修改转置后数组后:\n", array3) print("原始数组也被修改了:\n", array1) # 原始数组的 [0, 0] 也被修改了!
代码解释:
array.transpose(*axes)
或array.T
:transpose()
方法和.T
属性都用于 数组转置。 对于二维数组,就是 行列互换。 对于更高维度的数组,可以指定axes
参数来控制轴的交换顺序,不常用,默认是反转轴的顺序。.T
属性 只能用于二维数组 的转置,更简洁常用。
-
-
数组索引 (Indexing) 与切片 (Slicing): “精准定位,数据 “手术刀””
数组索引和切片就像是 数据的 “导航仪” 和 “手术刀”,可以让我们 精准地访问和提取数组中的数据。 通过索引和切片,我们可以 获取数组中特定位置的元素,或者 提取数组的子区域,就像从书架上取出特定的书,或者从地图上截取特定的区域一样。
-
整数索引 (Integer Indexing): “按 “坐标” 查找”
整数索引就像是 按 “坐标” 查找,使用 整数 来指定要访问的数组元素的 位置。 NumPy 数组的索引 从 0 开始,就像 Python 列表的索引一样。
-
一维数组的索引: 直接使用 一个整数 索引,表示 元素的位置。
import numpy as np # 创建一个一维数组 array1 = np.array([10, 20, 30, 40, 50]) print("一维数组:\n", array1) # 访问第一个元素 (索引 0) print("\n第一个元素 (索引 0):", array1[0]) # 10 # 访问第三个元素 (索引 2) print("第三个元素 (索引 2):", array1[2]) # 30 # 访问最后一个元素 (索引 -1 或 len(array1)-1) print("最后一个元素 (索引 -1):", array1[-1]) # 50 print("最后一个元素 (索引 len(array1)-1):", array1[len(array1)-1]) # 50 # 尝试访问超出索引范围的元素会报错 IndexError: index out of bounds # print(array1[5]) # IndexError: index out of bounds
-
多维数组的索引: 使用 多个整数 索引,用 逗号分隔,表示 每个维度上的位置。 例如,对于二维数组
array[row_index, column_index]
,表示访问 第row_index
行,第column_index
列 的元素。import numpy as np # 创建一个 (3, 4) 的二维数组 array1 = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]) print("二维数组:\n", array1) # 访问第一行第一列的元素 (索引 [0, 0]) print("\n第一行第一列的元素 (索引 [0, 0]):", array1[0, 0]) # 1 # 访问第二行第三列的元素 (索引 [1, 2]) print("第二行第三列的元素 (索引 [1, 2]):", array1[1, 2]) # 7 # 访问最后一行最后一列的元素 (索引 [-1, -1]) print("最后一行最后一列的元素 (索引 [-1, -1]):", array1[-1, -1]) # 12 # 也可以使用多个方括号 [] 连续索引,效果相同 print("第一行第一列的元素 (连续索引 array1[0][0]):", array1[0][0]) # 1 print("第二行第三列的元素 (连续索引 array1[1][2]):", array1[1][2]) # 7
-
-
切片 (Slicing): “截取数据片段”
切片就像是 “截取数据片段”,使用 冒号
:
来指定要访问的数组元素的 范围。 切片操作可以 一次性访问多个连续的元素,非常灵活高效。 切片语法类似于 Python 列表的切片,但 NumPy 数组的切片功能更强大。-
一维数组的切片: 使用
array[start:stop:step]
语法,表示 从索引start
开始,到索引stop
结束 (不包含stop
),步长为step
的元素切片。start
,stop
,step
都可以省略,默认值分别是0
, 数组长度,1
。import numpy as np # 创建一个一维数组 array1 = np.arange(10) # [0 1 2 3 4 5 6 7 8 9] print("一维数组:\n", array1) # 切片前 5 个元素 (索引 0 到 4) print("\n前 5 个元素 (array1[0:5]):", array1[0:5]) # [0 1 2 3 4] 相当于 array1[:5] # 切片索引 5 之后的所有元素 (索引 5 到 结束) print("索引 5 之后的所有元素 (array1[5:]):", array1[5:]) # [5 6 7 8 9] # 切片索引 2 到 7 的元素 (不包含 8) print("索引 2 到 7 的元素 (array1[2:8]):", array1[2:8]) # [2 3 4 5 6 7] # 切片所有元素,步长为 2 (隔一个取一个) print("所有元素,步长为 2 (array1[::2]):", array1[::2]) # [0 2 4 6 8] # 切片索引 1 到 8 的元素,步长为 3 print("索引 1 到 8 的元素,步长为 3 (array1[1:8:3]):", array1[1:8:3]) # [1 4 7] # 切片所有元素,倒序 (步长为 -1) print("所有元素,倒序 (array1[::-1]):", array1[::-1]) # [9 8 7 6 5 4 3 2 1 0]
-
多维数组的切片: 每个维度都可以进行切片,使用 逗号分隔 不同维度的切片,语法与一维数组切片类似。 例如,对于二维数组
array[row_slice, column_slice]
,表示对 行维度进行row_slice
切片,对列维度进行column_slice
切片。import numpy as np # 创建一个 (4, 5) 的二维数组 array1 = np.array([[ 0, 1, 2, 3, 4], [ 5, 6, 7, 8, 9], [10, 11, 12, 13, 14], [15, 16, 17, 18, 19]]) print("二维数组:\n", array1) # 切片前两行 (索引 0 和 1),所有列 (冒号 : 表示所有列) print("\n前两行,所有列 (array1[0:2, :]):\n", array1[0:2, :]) # 相当于 array1[:2, :] # 切片所有行,前三列 (索引 0, 1, 2) print("\n所有行,前三列 (array1[:, 0:3]):\n", array1[:, 0:3]) # 相当于 array1[:, :3] # 切片第 2 行到最后一行,第 2 列到最后一列 (索引都从 1 开始) print("\n第 2 行到最后一行,第 2 列到最后一列 (array1[1:, 1:]):\n", array1[1:, 1:]) # 切片第 1, 3 行 (索引 0 和 2),所有列 (使用列表索引,后面会讲到) # print("\n第 1, 3 行,所有列 (array1[[0, 2], :]):\n", array1[[0, 2], :]) # 这种高级索引方式,这里先不展开讲 # 切片第 1, 3 行 (索引 0 和 2),第 2, 4 列 (索引 1 和 3) (使用列表索引) # print("\n第 1, 3 行,第 2, 4 列 (array1[[0, 2], [1, 3]]):\n", array1[[0, 2], [1, 3]]) # 高级索引,这里先不展开讲
-
-
布尔索引 (Boolean Indexing): “按条件筛选” (后续文章详细讲解)
布尔索引是一种 更高级、更强大的索引方式,它可以 根据条件表达式,筛选出满足条件的数组元素。 布尔索引在数据分析中非常常用,可以用来 过滤数据、提取特定子集 等。 我们会在 后续文章中详细讲解布尔索引,这里先简单了解一下概念。
import numpy as np # 创建一个一维数组 array1 = np.array([10, 25, 5, 30, 15]) print("一维数组:\n", array1) # 创建一个布尔数组,判断哪些元素大于 20 bool_array = array1 > 20 # [False True False True False] print("\n布尔数组 (array1 > 20):\n", bool_array) # 使用布尔数组作为索引,筛选出大于 20 的元素 filtered_array = array1[bool_array] # 或者 array1[array1 > 20] 更简洁的写法 print("\n筛选出大于 20 的元素 (布尔索引):\n", filtered_array) # [25 30]
代码解释:
array1 > 20
: 这是一个 条件表达式,它会对array1
中的 每个元素都进行判断,返回一个 布尔数组,形状与array1
相同,元素值为True
或False
,表示对应位置的元素是否满足条件。array1[bool_array]
: 使用布尔数组bool_array
作为索引,可以 筛选出array1
中对应bool_array
为True
位置的元素。 这就是布尔索引的基本原理。
-
-
案例应用: 处理学生成绩单 NumPy 数组 (形状变换与索引切片)
我们继续使用上一篇文章的 学生成绩单 NumPy 数组,来演示 形状变换和索引切片 的应用。
import numpy as np # 学生成绩单 (二维数组) (与上一篇文章相同) grades_array = np.array([ [85, 92, 78], # 学生 1 的成绩 (语文, 数学, 英语) [90, 88, 95], # 学生 2 的成绩 [75, 80, 82], # 学生 3 的成绩 [95, 98, 92], # 学生 4 的成绩 [80, 85, 88] # 学生 5 的成绩 ]) print("学生成绩单 (原始):\n", grades_array) print("原始成绩单的形状:", grades_array.shape) # (5, 3) # 1. 获取学生 3 的所有科目成绩 (第三行,索引 2) student3_grades = grades_array[2, :] # 或者 grades_array[2] 省略列索引表示所有列 print("\n学生 3 的所有科目成绩:\n", student3_grades) # 2. 获取所有学生的数学成绩 (第二列,索引 1) math_grades = grades_array[:, 1] # 冒号 : 表示所有行,索引 1 表示第二列 print("\n所有学生的数学成绩:\n", math_grades) # 3. 获取学生 2, 3, 4 的 语文 和 英语 成绩 (行索引 1, 2, 3,列索引 0 和 2) subset_grades = grades_array[1:4, [0, 2]] # 行切片 1:4 (不包含 4),列索引列表 [0, 2] print("\n学生 2, 3, 4 的 语文和英语成绩:\n", subset_grades) # 4. 将成绩单转置,变成科目为行,学生为列的形状 (方便按科目分析) transposed_grades = grades_array.T # 或者 grades_array.transpose() print("\n转置后的成绩单 (科目为行,学生为列):\n", transposed_grades) print("转置后成绩单的形状:", transposed_grades.shape) # (3, 5) # 5. 从转置后的成绩单中,获取数学科目的所有学生成绩 (第一行,索引 1,因为转置后数学变成第一行) transposed_math_grades = transposed_grades[1, :] # 或者 transposed_grades[1] print("\n转置后成绩单中,数学科目的所有学生成绩:\n", transposed_math_grades) # 和之前的 math_grades 结果相同,只是顺序可能不同
代码解释:
- 案例应用展示了如何使用索引和切片操作,从学生成绩单 NumPy 数组中,灵活地提取各种我们需要的数据子集,并使用
transpose()
进行形状变换,以适应不同的分析需求。 例如,转置后的成绩单,更方便我们按科目进行分析,计算每门科目的平均分、最高分等 (按行计算)。
- 案例应用展示了如何使用索引和切片操作,从学生成绩单 NumPy 数组中,灵活地提取各种我们需要的数据子集,并使用
费曼回顾 (知识巩固):
现在,请你用自己的话,总结一下今天我们学习的 NumPy 数组形状变换和索引切片的知识,包括:
- 为什么要进行数组形状变换? 我们学习了哪些形状变换的方法?
reshape()
,flatten()
,ravel()
,transpose()
分别有什么作用和特点? 它们返回的是视图还是副本? - 什么是数组索引和切片? 我们学习了哪些索引类型? 整数索引和切片索引分别有什么作用? 如何使用整数索引和切片索引访问一维数组和多维数组的元素?
- 在学生成绩单案例中,我们是如何运用形状变换和索引切片来处理数据的?
像给你的朋友讲解一样,用最简单的语言解释这些概念,并结合生活中的例子和代码示例,帮助他们理解 NumPy 数组的 “变形术” 和 “索引切片” 魔法。
课后思考 (拓展延伸):
- 尝试修改学生成绩单案例的代码,例如:
- 使用
reshape()
将成绩单变成不同的形状,例如 (15,) 的一维数组,或者 (5, 1, 3) 的三维数组,看看如何使用索引和切片访问不同形状的数组元素? - 尝试使用更复杂的切片操作,例如步长不为 1 的切片,或者多维度混合切片,提取更精细的数据子集?
- 尝试将形状变换和索引切片与其他 NumPy 功能结合起来,例如使用
np.mean()
计算特定学生、特定科目、或特定区域的平均分?
- 使用
- 思考一下,除了学生成绩单,数组形状变换和索引切片还可以应用在哪些数据处理场景中? 例如,图像处理、音频处理、文本处理等等。 你有什么有趣的创意想法吗?
- 尝试查阅 NumPy 官方文档或其他 NumPy 教程,了解更多关于数组形状变换和索引切片的技巧,例如:
- 高级索引 (整数数组索引、布尔数组索引,我们会在后续文章中详细讲解布尔索引)
np.newaxis
和np.expand_dims()
增加数组维度np.squeeze()
压缩数组维度
恭喜你!完成了 NumPy 费曼学习法的第二篇文章学习! 你已经掌握了 NumPy 数组的 “变形术” 和 “索引切片” 魔法,可以开始灵活地 “玩转” 数组的形状和数据了! 下一篇文章,我们将继续深入探索 NumPy 数组的 “算术魔法”,学习如何对数组进行各种高效的数学运算,以及神秘的 “广播机制”,让你的数据分析能力更上一层楼! 敬请期待!