当前位置: 首页 > news >正文

费曼学习法7 - NumPy 数组的 “变形术”:形状变换与索引切片 (基础篇)

第二篇:NumPy 数组的 “变形术”:形状变换与索引切片 (基础篇)

开篇提问:

你有没有整理过书架? 有时候,我们会把书按照 高度 从矮到高排列,有时候又会按照 颜色 分类摆放,甚至会按照 阅读进度 来组织。 虽然书还是那些书,但是 摆放方式 (形状) 改变了,查找和取用就更方便了。

在 NumPy 数组的世界里,我们也可以对数组进行 “变形” 操作,改变数组的 形状,让数据以更适合我们分析的角度呈现。 同时,当我们面对一堆 “积木” (数据) 时,如何 快速找到我们需要的 “那一块” 呢? 这就需要用到 NumPy 数组的 索引 (indexing) 和切片 (slicing) 技术,它们就像是 数据的 “导航仪” 和 “手术刀”,可以让我们精准地定位和提取数组中的数据。 今天,就让我们一起学习 NumPy 数组的 “变形术” 和 “索引切片” 魔法,让数据在我们的指尖 “翩翩起舞”!

核心概念讲解 (费曼式解释):

  1. 数组形状变换 (Shape Transformation): “给 “积木” 重新塑形”

    数组形状变换就像 给 “积木” 重新塑形 一样,可以 改变数组的维度和形状,但 不改变数组中数据的数量和内容。 形状变换在数据分析中非常重要,因为不同的数据分析方法和算法,可能需要 不同形状 的数据输入。 NumPy 提供了多种形状变换的方法,就像一个 “橡皮泥模具” 工具箱,可以让我们随意塑造数组的形状。

    • reshape(): “重新塑造,任意变形”

      reshape(newshape) 方法是 最常用 的形状变换方法,它可以 将数组变成指定的新形状 newshapenewshape 是一个 元组,表示新形状的维度和大小。 注意: 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() vs ravel()

        特性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 属性 只能用于二维数组 的转置,更简洁常用。
  2. 数组索引 (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 相同,元素值为 TrueFalse,表示对应位置的元素是否满足条件。
      • array1[bool_array]: 使用布尔数组 bool_array 作为索引,可以 筛选出 array1 中对应 bool_arrayTrue 位置的元素。 这就是布尔索引的基本原理。
  3. 案例应用: 处理学生成绩单 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 数组形状变换和索引切片的知识,包括:

  • 为什么要进行数组形状变换? 我们学习了哪些形状变换的方法? reshape(), flatten(), ravel(), transpose() 分别有什么作用和特点? 它们返回的是视图还是副本?
  • 什么是数组索引和切片? 我们学习了哪些索引类型? 整数索引和切片索引分别有什么作用? 如何使用整数索引和切片索引访问一维数组和多维数组的元素?
  • 在学生成绩单案例中,我们是如何运用形状变换和索引切片来处理数据的?

像给你的朋友讲解一样,用最简单的语言解释这些概念,并结合生活中的例子和代码示例,帮助他们理解 NumPy 数组的 “变形术” 和 “索引切片” 魔法。

课后思考 (拓展延伸):

  1. 尝试修改学生成绩单案例的代码,例如:
    • 使用 reshape() 将成绩单变成不同的形状,例如 (15,) 的一维数组,或者 (5, 1, 3) 的三维数组,看看如何使用索引和切片访问不同形状的数组元素?
    • 尝试使用更复杂的切片操作,例如步长不为 1 的切片,或者多维度混合切片,提取更精细的数据子集?
    • 尝试将形状变换和索引切片与其他 NumPy 功能结合起来,例如使用 np.mean() 计算特定学生、特定科目、或特定区域的平均分?
  2. 思考一下,除了学生成绩单,数组形状变换和索引切片还可以应用在哪些数据处理场景中? 例如,图像处理、音频处理、文本处理等等。 你有什么有趣的创意想法吗?
  3. 尝试查阅 NumPy 官方文档或其他 NumPy 教程,了解更多关于数组形状变换和索引切片的技巧,例如:
    • 高级索引 (整数数组索引、布尔数组索引,我们会在后续文章中详细讲解布尔索引)
    • np.newaxisnp.expand_dims() 增加数组维度
    • np.squeeze() 压缩数组维度

恭喜你!完成了 NumPy 费曼学习法的第二篇文章学习! 你已经掌握了 NumPy 数组的 “变形术” 和 “索引切片” 魔法,可以开始灵活地 “玩转” 数组的形状和数据了! 下一篇文章,我们将继续深入探索 NumPy 数组的 “算术魔法”,学习如何对数组进行各种高效的数学运算,以及神秘的 “广播机制”,让你的数据分析能力更上一层楼! 敬请期待!

相关文章:

  • 当PHP遇上区块链:一场奇妙的技术之旅
  • 基于SSA-KELM-Adaboost(麻雀搜索优化的极限学习机自适应提升算法)的多输入单输出回归预测【MATLAB】
  • 如何用python将pdf转为text并提取其中的图片
  • js基础语法
  • 前端监控与埋点
  • Three.js 入门(辅助、位移、父子关系、缩放旋转、响应式布局)
  • VC++零基础入门之系列教程 【附录E MFC快速参考指南】
  • 20250212:ZLKMedia 推流
  • Visual Studio Code 远程开发方法
  • C#从入门到精通(35)—如何防止winform程序因为误操作被关闭
  • 為什麼使用不限量動態住宅IP採集數據?
  • 2024-2025 学年广东省职业院校技能大赛 “信息安全管理与评估”赛项 技能测试试卷(四)
  • Redis详解
  • 为AI聊天工具添加一个知识系统 之122 详细设计之63 实体范畴论和神经元元模型:命名法函子
  • MySQL 入门“鸡”础
  • 如何查看PostgreSQL的版本
  • Java常见设计模式(中):结构型模式
  • 375_C++_cloud手机推送,添加人脸告警信息到任务队列中,UploadAlarmPush是典型的工厂模式应用,为什么使用工厂模式完成这部分代码
  • Python入门12:面向对象的三大特征与高级特性详解
  • 视频字幕识别和翻译
  • 老旧小区加装电梯后续维护谁负责?上海:各区属房管集团托底保障
  • 网络直播间销售玩具盲盒被指侵权,法院以侵犯著作权罪追责
  • 国新办10时将举行新闻发布会,介绍4月份国民经济运行情况
  • “80后”北大硕士罗婕履新甘肃宁县县委常委、组织部部长
  • 发射后失联,印度地球观测卫星发射任务宣告失败
  • 推开“房间”的门:一部“生命存在的舞台” 史