Skip to content

数组索引

信息
概要

本文从选择对象入手, 根据选择对象内的元素的种类不同, 分为基础索引高级索引组合索引. 这几个索引又可以细分为:

需要重点关注的是:

  • 索引的类型
  • 返回数组的形状/维度
  • 索引的机理
  • 索引之间的联系, 如布尔数组索引可以被转化为整数数组索引

选择对象

x[obj], obj即为选择对象.

选择对象是用来索引的对象, 根据选择对象的不同, 可以判断属于基础索引还是高级索引.

  • 选择对象为元祖且仅含有数值, 切片对象: 属于基础索引
  • 选择对象为非元祖或一个含有至少一个序列(如列表, 元祖)或nd数组的元祖: 属于高级索引

基础索引

选择对象为元祖且仅含有数值或者切片对象时的索引属于基础索引.

注意

基础索引产生的新数组是视图.

Tip

x[([exp1], [exp2], ..., [expN])]x[[exp1], [exp2], ..., [expN]]是相等的, 后者是前者的语法糖.

维度

  • 降维操作: 当使用一个整数i索引的时候, 即单一元素索引. 返回数组的维度减少了1. 即a[0]a[0:1]在数值上虽然是相同的, 但是前者在维度上前者减1, 后者保持不变. 即切片索引不会对维度造成改变.

    例子
    >>> a = np.arange(12)
    >>> a
    array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])
    >>> a[1] # 返回的维度减少了1, 从一维变为零维
    1
    >>> a[0:1]
    array([0]) # 仍然为一维
    >>> arr = np.array([[1, 2, 3], [4, 5, 6]])
    >>> arr[0] # 返回的维度减少了1, 从二维变为一维
    array([1, 2, 3])
    >>> arr[0:1] # 仍然为二维
    array([[1, 2, 3]])
    
  • 升维操作: NumPy提供的newaxis对象或者也可以用None可以用来在返回的数组中新增一个维度.

    例子
    >>> arr = np.array([[1, 2, 3],
                        [4, 5, 6]])
    >>> arr.shape
    (2, 3)
    >>> expanded_arr1 = arr[np.newaxis, ...]
    >>> expanded_arr1
    array([[[1, 2, 3],
            [4, 5, 6]]])
    >>> expanded_arr1.shape
    (1, 2, 3)
    >>> expanded_arr2 = arr[:, np.newaxis, :]
    >>> expanded_arr2
    array([[[1, 2, 3]],
    
           [[4, 5, 6]]])
    >>> expanded_arr2.shape
    (2, 1, 3)
    >>> expanded_arr3 = arr[..., np.newaxis]
    >>> expanded_arr3
    array([[[1],
            [2],
            [3]],
    
           [[4],
            [5],
            [6]]])
    >>> expanded_arr3.shape
    (2, 3, 1)
    
    Tip

    可以利用np.newaxis方便的将一维行向量和列向量之间转换. (在NumPy中, 列向量本质是个二维数组, 形状为(n, 1); 行向量既可以用一维数组表示, 又可以用二维数组表示)

    例子
    >>> a = np.array([0.0, 10.0, 20.0, 30.0]) # 一维行向量
    >>> a
    array([ 0., 10., 20., 30.])
    >>> a[:, np.newaxis] # 行向量变列向量, 维度加1
    array([[ 0.],
           [10.],
           [20.],
           [30.]])
    

单一元素索引

在一维数组中的单一元素索引和在原生列表中的单一元素索引没有区别. 都是从0开始, 能够接受负数表示从末尾开始索引. 同样对于多维数组来说, 也和原生列表相似.

例子
>>> x = np.arange(10)
>>> x[2]
2
>>> x[-2]
8
>>> x.shape = (2, 5) # 不会创建新视图, 因为这是修改原数组的元数据
>>> x[1, 3]
8
>>> x[1, -1]
9
笔记

如果用比维度少的索引来索引多维数组, 会得到一个子维数组, 这个子维数组是一个视图. 所以常用单一元素索引来执行降维操作.

例子
>>> x = np.arange(10)
>>> x.shape = (2, 5)
>>> x[0]
array([0, 1, 2, 3, 4])
>>> x[0].base # 使用base属性判断是视图还是副本
array([[0, 1, 2, 3, 4],
       [5, 6, 7, 8, 9]]) # 为原数组所以为视图
>>> x[0][2]
Tip

x[u, v] == x[u][v]但是x[u][v]的效率更低, 因为x[u]会创建一个新视图, 随后在这个新视图上再进行索引.

切片索引

注意
  • NumPy切片创建的是视图, 而不像内置的Python序列如字符串, 元组和列表那样经过切片产生的是一个副本.
  • 从大数组中切片得到一个小的数组之后, 如果小数组不再发挥作用, 必须要小心, 因为小数组中的访问缓冲区和大数组中的数据缓冲区都是同一个, 当要删除大数组时, 必须删除全部由其派生的小数组, 数据缓冲区/内存才会被释放. 所以在这种情况下, 建议使用副本.
  • 切片索引不会改变维度

每一个维度使用[start]:[end]:[stride]即切片对象进行切片, 维度之间用,进行分割, 第一个切片对象表示最高维, 第二个切片对象表示次高维, ...

笔记

假设n是某一个维度的待切片的元素的数量.

  • 含义:

    • [start]: 表示想要的第一项的下标; 若为负数, 表示的下标为n+[start]
    • [end]: 表示第一个不想要的项的下标; 若为负数, 表示的下标为n+[end]
    • [stride]: 定义了向前(负数)还是向后(正数)以及步长(绝对值)
  • 默认值:

    • [start]: 若[stride]小于0, 默认为n-1; 若[stride]大于0, 默认为0.
    • [end]: 若[stride]小于0, 默认为-n-1; 若[stride]大于0, 默认为n
    • [stride]: 默认为1
例子
>>> a = np.arange(12)
>>> a
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])
>>> a[:4]
array([0, 1, 2, 3])
>>> a[1:]
array([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])
>>> a[1:3]
array([1, 2])
>>> a[1:10:2]
array([1, 3, 5, 7, 9])
>>> a[-3:3]
array([], dtype=int64) # 步长默认为1, 所以没有选中任何元素
>>> a[-3:3:-1]
array([9, 8, 7, 6, 5, 4])
>>> a[::-1] # 步长为1, 是一个负数, 所以[start]默认为11, [end]默认为-13
array([11, 10,  9,  8,  7,  6,  5,  4,  3,  2,  1,  0])
>>> a = np.array([[1, 2, 3], [4, 5, 6]])
>>> a
array([[1, 2, 3],
       [4, 5, 6]])
>>> a[:, ::2]
array([[1, 3],
       [4, 6]])
>>> a[0, ::2]
array([1, 3])
>>> a[1::, 1:3]
array([[5, 6]])
>>> a[1]  # 使用单一元素索引, 维度降为一维
array([4, 5, 6])
>>> a[::-1, 1:2]
array([[5],
       [2]])
>>> a[::, ::-1]
array([[3, 2, 1],
       [6, 5, 4]])
笔记
  • [start]:[end]:[stride]与由slice类创建的切片对象含义相同.

    例子
    >>> arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    >>> s = slice(2, 8, 2)
    >>> arr[s]
    [2, 4, 6]
    >>> s1 = slice(None, None, 2)
    >>> arr[s1]
    [0, 2, 4, 6, 8]
    
  • 可以对切片之后的数组进行赋值操作, 但是不能添加元素. 在x[obj] = [value]中设置的值必须和x[obj]有相同的形状.

  • 当切片对象的数量小于数组的维度N的时候, 剩余的维度会默认创建:切片对象(即选择整个维度)

    例子
    >>> arr = np.arange(24).reshape((4, 3, 2))
    >>> arr
    array([[[ 0,  1],
            [ 2,  3],
            [ 4,  5]],
    
           [[ 6,  7],
            [ 8,  9],
            [10, 11]],
    
           [[12, 13],
            [14, 15],
            [16, 17]],
    
           [[18, 19],
            [20, 21],
            [22, 23]]])
    >>> arr[0:1] # 保持3维
    array([[[0, 1],
            [2, 3],
            [4, 5]]])
    >>> arr[0] # 使用单一元素索引, 降为2维
    array([[0, 1],
           [2, 3],
           [4, 5]])
    

    这里切片对象只有一个, 即0:1, 剩余的2维会默认创建一个:切片对象, 即最终的形式为arr[0:1, :, :], 即选择第一个维度的第一个元素, 同时选择第二个和第三个维度的所有元素.

    >>>  x = np.array([[[1],[2],[3]], [[4],[5],[6]]])
    >>> x.shape
    (2, 3, 1)
    >>> x[1:2]
    array([[[4],
            [5],
            [6]]])
    
  • 若除了第p个切片对象外, 其他的切片对象都是:. 则返回的数组是通过通过p轴由第p个切片对象选定的元素堆叠形成的.

    例子
    >>> arr = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
    >>> arr
    array([[1, 2, 3],
           [4, 5, 6],
           [7, 8, 9]])
    >>> arr.shape
    (3, 3)
    >>> arr[:, 0:3:2]
    array([[1, 3],
           [4, 6],
           [7, 9]])
    

    返回的数组仍然是二维, 第二个切片对象0:3:2对每一个一维数组进行切片, 然后通过堆叠形成最终的返回数组.

  • 若存在多个非:的切片对象, 则其效果和分批切片是一样的. 因此x[[ind1], ..., [ind2], :]x[[ind1][..., [ind2], :]是等价的.

    例子
    >>> arr = np.arange(24).reshape((4, 3, 2))
    >>> arr
    array([[[ 0,  1],
            [ 2,  3],
            [ 4,  5]],
    
            [[ 6,  7],
            [ 8,  9],
            [10, 11]],
    
            [[12, 13],
            [14, 15],
            [16, 17]],
    
            [[18, 19],
            [20, 21],
            [22, 23]]])
    >>> arr[0:1][:, 1:2] #  arr[0:1]返回的仍然是一个三维的数组, 所以要在二维中切片, 保持三维不变, 所以是arr[0:1][:, 1:2]
    array([[[2, 3]]])
    >>> arr[0:1, 1:2]
    array([[[2, 3]]])
    >>> arr[0:1][1:2]  # arr[0:1]返回的仍然是一个三维的数组, 仍然在三维中切片, 由于经过第一轮切片之后三维中只剩下第一个元素了, 所以第二轮切片中无法切出三维中的第二个元素, 所以返回一个空的三维数组
    array([], shape=(0, 3, 2), dtype=int64) 
    >>> arr[0][1:2] # 先使用了单一元素索引. arr[0]返回的是一个降维之后的二维数组, 这个二维数组在数值上和三维数组的第一个元素是相同的, [1:2]表示对这个二维数组进行切片
    array([[2, 3]])
    

    所以arr[0:1][:, 1:2]arr[0:1, 1:2]是等价的.

  • 一些工具如Ellipsis可以代替多个:切片对象.

    例子
    >>> arr = np.arange(24).reshape((4, 3, 2))
    >>> arr
    array([[[ 0,  1],
            [ 2,  3],
            [ 4,  5]],
    
            [[ 6,  7],
            [ 8,  9],
            [10, 11]],
    
            [[12, 13],
            [14, 15],
            [16, 17]],
    
            [[18, 19],
            [20, 21],
            [22, 23]]])
    >>> arr[..., 0:1] # 保持三维
    array([[[ 0],
            [ 2],
            [ 4]],
    
           [[ 6],
            [ 8],
            [10]],
    
           [[12],
            [14],
            [16]],
    
           [[18],
            [20],
            [22]]])
    >>> arr[..., 0] # 使用单一元素索引, 降维至二维
    array([[ 0,  2,  4],
        [ 6,  8, 10],
        [12, 14, 16],
        [18, 20, 22]])
    >>> arr[:, :, 0:1] # 保持三维
    array([[[ 0],
            [ 2],
            [ 4]],
    
           [[ 6],
            [ 8],
            [10]],
    
           [[12],
            [14],
            [16]],
    
           [[18],
            [20],
            [22]]])
    >>> arr[:, :, 0] # 使用单一元素索引, 降维至二维
    array([[ 0,  2,  4],
        [ 6,  8, 10],
        [12, 14, 16],
        [18, 20, 22]])
    

    可以看出, arr[:, :, 0]arr[..., 0]的效果完全一样, arr[:, :, 0:1]和arr[..., 0]`的效果完全一样.

高级索引

选择对象为非元祖, 一个含有至少一个序列(如列表, 元祖)或nd数组的元祖时的索引属于高级索引.

注意
  • 高级索引产生的新数组是副本.
  • 在学习这一小节之前, 强烈建议复习一遍广播.
例子

x[(1, 2, 3),]x[(1, 2, 3)]有显著不同, 第一个是语法糖写法, 原始写法是: x[((1, 2, 3), )]; 第二个就是原始写法. 可以看到第一个是元祖, 但是里面含有一个元祖序列, 所以属于高级索引. 第二个是元祖, 里面仅含有数值, 所以属于基础索引.

整数数组索引

选择对象为一个或多个整数索引数组的索引称为整数数组索引. 每个索引数组表示该维度中的多个索引.

笔记
  • 索引数组, 被索引数组和选择对象

    例子
    >>> x = np.array([[1, 2], [3, 4], [5, 6]])
    >>> index_arr1 = np.array([0, 2])
    >>> index_arr2 = np.array([0, 1])
    >>> x[index_arr1, index_arr2]
    array([1, 6])
    
    • x[obj]中的obj作为一个整体被称为索引对象
    • index_arr1index_arr2为索引数组
    • x为被索引数组
  • 索引数组中允许出现负值

    例子
    >>> x = np.arange(10, 1, -1)
    >>> index_arr = np.array([3, 3, -1, 8])
    >>> x[index_arr]
    array([7, 7, 2, 2])
    
例子
>>> x = np.arange(10, 1, -1)
>>> x
array([10,  9,  8,  7,  6,  5,  4,  3,  2])
>>> x[np.array([3, 3, 1, 8])]
array([7, 7, 9, 2])
>>> x[np.array([3, 3, -3, 8])]
array([7, 7, 4, 2])

很好理解, 如x[np.array([3, 3, 1, 8])]的意思是选中了索引值为3, 3, 1, 8上的元素, 分别对应7, 7, 9, 2.

>>> x = np.array([[1, 2], [3, 4], [5, 6]])
>>> x
array([[1, 2],
        [3, 4],
        [5, 6]])
>>> x[np.array([1, -1])]
array([[3, 4],
        [5, 6]]) 
>>> x[np.array([0, 2]), np.array([0, 1])]
array([1, 6])
>>> x[np.array([[0, 2], [0, 1]]), np.array([[1, 1], [0, 1]])]
array([[2, 6],
        [1, 4]])
>>> x[np.array([[0, 2], [1, 1]])]
array([[[1, 2],
        [5, 6]],

        [[3, 4],
        [3, 4]]])
  • x[np.array([0, 2]), np.array([0, 1])]可以理解为最高纬上从0, 2选中一个索引值作为行索引值, 同时从最低纬0, 1选中一个索引值作为列索引值. 所以分别对应0, 02, 1处的元素
  • x[np.array([[0, 2], [0, 1]]), np.array([[1, 1], [0, 1]])]可以理解为最高纬从矩阵[[0, 2], [0, 1]]中选一个索引值作为行索引值, 同时最低维从矩阵[[1, 1], [0, 1]]选一个索引值作为列索引值. 所以分别对应0, 1, 2, 1, 0, 0, 1, 1处的元素
  • x[np.array([[0, 2], [1, 1]])]可以理解未最高维从矩阵[[0, 2], [0, 1]]中选一个索引值作为行索引值, 列索引值默认为全部. 所以行索引值0, 2的为一个矩阵, 行索引值0, 1的为一个矩阵, 这两个矩阵合在一起得到了一个三维数组

机理

整数索引的机理如下:

  • 索引数组的个数小于被索引数组的维度

    仅仅只会索引部分维度, 剩余的维度用:代替, 即用切片代替剩余的维度. 这组合了高级索引和基础索引.

    例子
    >>> x = np.array([[0, 1, 2],
                      [3, 4, 5],
                      [6, 7, 8]])
    >>> index_arr = np.array([0, 2]) # 被索引数组为二维, 索引数组只有1个
    >>> x[index_arr]
    array([[0, 1, 2],
           [6, 7, 8]])
    

    只选中了最高维度的索引值为02的元素, 即[0, 1, 2][6, 7, 8]. 剩余的维度用:代替, 相当于x[np.array([0, 2]), :]

    Tip

    对于三维以上的高维被索引对象, 如果有多于1个索引对象的情况下, 这些索引对象也会被广播至形状相同

  • 索引数组的个数大于被索引数组的维度

    会报错, 无法进行索引操作.

    例子
    >>> x = np.array([[0, 1, 2],
                      [3, 4, 5],
                      [6, 7, 8]])
    >>> index_arr1 = np.array([0, 1])
    >>> index_arr2 = np.array([0, 1])
    >>> index_arr3 = np.array([0, 1])
    >>> x[index_arr1, index_arr2, index_arr3]
    Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
    IndexError: too many indices for array: array is 2-dimensional, but 3 were indexed
    

    因为根本不存在index_arr3所要索引的维度.

  • 索引数组的个数等于被索引数组的维度

    会索引全部的维度.

    例子
    >>> x = np.array([[1, 2], [3, 4], [5, 6]])
    >>> x
    array([[1, 2],
            [3, 4],
            [5, 6]])
    >>> x[np.array([0, 2]), np.array([0, 1])]
    array([1, 6])
    >>> x[np.array([[0, 2], [0, 1]]), np.array([[1, 1], [0, 1]])]
    array([[2, 6],
            [1, 4]])
    

形状

结果数组的形状将是所有索引数组广播后的形状与被索引数组中任何未使用维度的形状的拼接.

Tip

若无法广播至形状相同, 报错: IndexError: shape mismatch: indexing arrays could not be broadcast together with shapes....

例子
>>> x = np.array([[1, 2], [3, 4], [5, 6]])
>>> index_arr1 = np.array([0, 1])
>>> index_arr2 = np.int8(0)
>>> x[index_arr1, index_arr2]
array([1, 3])
>>> x.shape
(3, 2)
>>> index_arr1.shape
(2, )
>>> index_arr2.shape
()
>>> x[index_arr1, index_arr2].shape
(2, )

解释:

  1. 索引数组为index_arr1index_arr2, 它们的形状不一致, 尝试广播

    定义:

    index_arr1的形状: 2
    index_arr2的形状: 
    
    1. 缺失的维度大小假设为1

      index_arr1的形状: 2
      index_arr2的形状: 1
      
    2. 从右到左比较每一个维度

      21, 其中一个维度是1, 兼容. 这表示可以广播.

    3. 广播结果数组的维度和输入的两个数组中最大的维度相等, 广播结果数组中每个维度的大小和输入的两个数组中对应维度的最大大小相等. 执行广播返回广播结果数组.

      index_arr1的形状: 2
      index_arr2的形状: 1
      广播结果数组的形状: 2
      

    index_arr2会被广播至index_arr2_broadcast:

    array([0, 0]) 
    
  2. 获得广播结果之后, 上述操作相当于x[index_arr1, index_arr2_broadcast], 由于索引数组的个数和被索引数组的维度相同, 所以被索引数组中没有未使用的维度, 不需要拼接, 所以返回数组的形状为(2, ).

>>> x = np.array([[1, 2], [3, 4], [5, 6]])
>>> index_arr1 = np.array([[0, 2], [0, 1]])
>>> index_arr2 = np.array([1, 1])
>>> x[index_arr1, index_arr2]
array([[2, 6],
       [2, 4]]) 
>>> x.shape
(3, 2)
>>> index_arr1.shape
(2, 2)
>>> index_arr2.shape
(2, )
>>> x[index_arr1, index_arr2].shape
(2, 2)

解释:

  1. 索引数组为index_arr1index_arr2, 它们的形状不一致, 尝试广播

    定义:

    index_arr1的形状: 2 * 2
    index_arr2的形状:     2
    
    1. 缺失的维度大小假设为1

      index_arr1的形状: 2 * 2
      index_arr2的形状: 1 * 2
      
    2. 从右到左比较每一个维度

      • 最右边的维度: 22, 相等, 兼容
      • 最左边的维度: 21, 其中有一个为1, 兼容

      这表示可以广播.

    3. 广播结果数组的维度和输入的两个数组中最大的维度相等, 广播结果数组中每个维度的大小和输入的两个数组中对应维度的最大大小相等. 执行广播返回广播结果数组.

      index_arr1的形状: 2 * 2
      index_arr2的形状: 2 * 2
      广播结果数组的形状: 2 * 2
      

    index_arr2会被广播至index_arr2_broadcast:

    array([[1, 1],
        [1, 1]])
    
  2. 获得广播结果之后, 上述操作相当于x[index_arr1, index_arr2_broadcast], 由于索引数组的个数和被索引数组的维度相同, 所以被索引数组中没有未使用的维度, 不需要拼接, 所以返回数组的形状为(2, 2).

>>> x = np.array([[1, 2], [3, 4], [5, 6]])
>>> index_arr = np.array([[0, 2], [1, 1]]) 
>>> x[index_arr]
array([[[1, 2],
        [5, 6]],

       [[3, 4],
        [3, 4]]])
>>> x.shape
(2, 2)
>>> index_arr.shape
(2, 2)
>>> x[index_arr].shape
(2, 2, 2)

解释:

  1. 只有一个索引数组, 无需广播
  2. 由于索引数组的个数和被索引数组的维度不同, 被索引数组中有一个未使用的维度, 所以返回数组的形状是索引数组的形状和被索引数组中未使用维度的形状的拼接, 被索引数组形状为(2, 2), 未使用的维度形状为最后一个2, 即(2, 2)(, 2)拼接. 所以返回数组的形状为(2, 2, 2).
>>> x = np.random.rand(3, 4, 5)
>>> x
array([[[0.55466894, 0.51799315, 0.78576553, 0.01404337, 0.91133221],
        [0.92907975, 0.34859234, 0.43999193, 0.78651397, 0.01118377],
        [0.11216395, 0.49555847, 0.05999374, 0.48128311, 0.59141431],
        [0.28872995, 0.41625684, 0.38342567, 0.30393309, 0.1339127 ]],

       [[0.54648285, 0.39631359, 0.55229484, 0.92354217, 0.82128316],
        [0.36401197, 0.35216312, 0.48898517, 0.70356991, 0.84831573],
        [0.98796396, 0.13612022, 0.80908123, 0.72921041, 0.10010538],
        [0.97922128, 0.63756104, 0.34690585, 0.17157988, 0.97766046]],

       [[0.23605187, 0.45610698, 0.47592261, 0.26819004, 0.9662657 ],
        [0.54926935, 0.88694126, 0.31731081, 0.53162318, 0.2381443 ],
        [0.48462102, 0.67643836, 0.2836442 , 0.45926631, 0.9567899 ],
        [0.55863473, 0.86290501, 0.58643116, 0.05157535, 0.68392416]]])
>>> index_arr1 = np.array([[0, 2], [1, 1]]) 
>>> index_arr2 = np.array([0, 1])
>>> x[index_arr1, index_arr2]
array([[[0.55466894, 0.51799315, 0.78576553, 0.01404337, 0.91133221],
        [0.54926935, 0.88694126, 0.31731081, 0.53162318, 0.2381443 ]],

       [[0.54648285, 0.39631359, 0.55229484, 0.92354217, 0.82128316],
        [0.36401197, 0.35216312, 0.48898517, 0.70356991, 0.84831573]]])
>>> x.shape
(3, 4, 5)
>>> index_arr1.shape
(2, 2)
>>> index_arr2.shape
(2, )
>>> x[index_arr1, index_arr2].shape
(2, 2, 5)

解释:

  1. 索引数组为index_arr1index_arr2, 它们的形状不一致, 尝试广播

    定义:

    index_arr1的形状: 2 * 2
    index_arr2的形状:     2
    
    1. 缺失的维度大小假设为1

      index_arr1的形状: 2 * 2
      index_arr2的形状: 1 * 2
      
    2. 从右到左比较每一个维度

      • 最右边的维度: 22, 相等, 兼容
      • 最左边的维度: 21, 其中有一个为1, 兼容

      这表示可以广播.

    3. 广播结果数组的维度和输入的两个数组中最大的维度相等, 广播结果数组中每个维度的大小和输入的两个数组中对应维度的最大大小相等. 执行广播返回广播结果数组.

      index_arr1的形状: 2 * 2
      index_arr2的形状: 2 * 2
      广播结果数组的形状: 2 * 2
      

    index_arr2会被广播至index_arr2_broadcast:

    array([[0, 1],
           [0, 1]])
    
  2. 获得广播结果之后, 上述操作相当于x[index_arr1, index_arr2_broadcast], 由于索引数组的个数和被索引数组的维度不同, 被索引数组中未使用的维度的形状为最后一个5, 需要拼接, 即(2, 2)(, 5)拼接, 所以返回数组的形状为(2, 2, 5).

布尔数组索引

选择对象为一个或者多个布尔索引数组的索引称为布尔数组索引.

笔记

索引数组, 被索引数组和选择对象:

例子
>>> x = np.array([[1, 2, 3], 
                  [4, 5, 6], 
                  [7, 8, 9]])
>>> index_arr = np.array([[True, False, True], 
                          [False, True, False], 
                          [True, False, True]])
>>> x[index_arr]
array([1, 3, 5, 7, 9])
  • x[obj]中的obj作为一个整体被称为索引对象
  • index_arr为索引数组
  • x为被索引数组

机理

可以将布尔索引数组转换为整数索引数组, 然后用整数数组索引的思想做布尔数组索引.

[布尔索引数组].nonzero()会返回一个包含整数索引数组的元组(整数索引数组的个数为[布尔索引数组].ndim). 这个整数索引数组会显示布尔索引数组中为True的元素的索引.

Tip
  • [arr].ndim返回[arr]的维度.

    例子
    >>> x = np.array([1, 2, 3])
    >>> x.ndim
    1
    >>> y = np.zeros((2, 3, 4))
    >>> y.ndim
    3
    
例子
>>> x = np.array([1, 2, 3, 4, 5])
>>> index_arr = np.array([True, False, True, False, True])
>>> x[index_arr]
array([1, 3, 5])
>>> index_arr = index_arr.nonzero() # 布尔索引数组的维度为一维, 所以返回的元组中包含一个整数索引数组
>>> index_arr
(array([0, 2, 4]),)
>>> x[index_arr]
array([1, 3, 5])
>>> x = np.array([[1, 2, 3], 
                  [4, 5, 6], 
                  [7, 8, 9]])
>>> index_arr1 = np.array([True, False, True])
>>> index_arr2 = np.array([False, True, False])
>>> x[index_arr1, index_arr2]
array([2, 8])
>>> index_arr1 = index_arr1.nonzero() # 布尔索引数组的维度为一维, 所以返回的元组中包含一个整数索引数组
>>> index_arr2 = index_arr2.nonzero() # 布尔索引数组的维度为一维, 所以返回的元组中包含一个整数索引数组
>>> index_arr1
(array([0, 2]),)
>>> index_arr2
(array([1]),)
>>> x[index_arr1, index_arr2]
array([[2, 8]])
>>> x = np.array([[1, 2, 3], 
                  [4, 5, 6], 
                  [7, 8, 9]])
>>> index_arr = np.array([[True, False, True], 
                          [False, True, False], 
                          [True, False, True]])
>>> x[index_arr]
array([1, 3, 5, 7, 9])
>>> index_arr = index_arr.nonzero()
>>> index_arr
(array([0, 0, 1, 2, 2]), array([0, 2, 1, 0, 2])) # 布尔索引数组的维度为二维, 所以返回的元组中包含两个整数索引数组
>>> x[index_arr]
array([1, 3, 5, 7, 9])

可以看到, 使用[布尔索引数组].nonzero()转化为整数索引数组之后, 效果一摸一样.

布尔数组索引和整数数组索引机理不同之处:

  • 若索引数组的个数小于被索引数组的维度, 则NumPy会尝试将索引数组转化为对各个维度的索引, 直至索引数组的维数降为一维; 若索引数组的个数等于被索引数组的个数, 不会发生转化.

    例子

    定义:

    >>> x = np.array([[1, 2, 3], 
                    [4, 5, 6], 
                    [7, 8, 9]])
    >>> index_arr = np.array([[True, False, True], 
                            [False, True, False], 
                            [True, False, True]])
    >>> x[index_arr]
    array([1, 3, 5, 7, 9])
    >>> index_arr = index_arr.nonzero()
    >>> index_arr
    (array([0, 0, 1, 2, 2]), array([0, 2, 1, 0, 2])) # 布尔索引数组的维度为二维, 所以返回的元组中包含两个整数索引数组
    >>> x[index_arr]
    array([1, 3, 5, 7, 9])
    

    可以看到索引数组只有一个index_arr, 但是被索引数组的维度是二维, 通过[布尔索引数组].nonzero()的返回值我们可以看出, index_arr这个索引数组被转化为了两个整数索引数组代表对不同维度(行和列)的索引, 降维之后的两个整索引数数组维度都是一维.

    定义:

    >>> x = np.random.rand(3, 4, 5)
    >>> x
    array([[[0.23673831, 0.940436  , 0.39577829, 0.62040794, 0.22681905],
            [0.14211138, 0.11274975, 0.46591164, 0.46971519, 0.29042329],
            [0.8070686 , 0.94403658, 0.34894486, 0.93585197, 0.02829665],
            [0.36130014, 0.29910215, 0.47659743, 0.58873084, 0.67748054]],
    
           [[0.93853782, 0.9929817 , 0.07529918, 0.91509346, 0.01952679],
            [0.27805297, 0.84250372, 0.55447263, 0.38749964, 0.50540984],
            [0.28453704, 0.8649879 , 0.72778931, 0.01349646, 0.93421428],
            [0.98410699, 0.90143817, 0.68410446, 0.53849741, 0.90207178]],
    
           [[0.62090463, 0.36651723, 0.42687352, 0.35570636, 0.57066032],
            [0.72869677, 0.99781255, 0.50897335, 0.89871309, 0.70440234],
            [0.73481014, 0.20775587, 0.43352938, 0.62857611, 0.82711123],
            [0.62713943, 0.71702863, 0.27292808, 0.60370543, 0.24383279]]])
    >>> index_arr = np.random.choice([True, False], size=(3, 4))
    >>> index_arr
    array([[False, False, False,  True],
           [ True, False,  True, False],
           [ True, False, False,  True]])
    >>> x[index_arr]
    array([[0.36130014, 0.29910215, 0.47659743, 0.58873084, 0.67748054],
           [0.93853782, 0.9929817 , 0.07529918, 0.91509346, 0.01952679],
           [0.28453704, 0.8649879 , 0.72778931, 0.01349646, 0.93421428],
           [0.62090463, 0.36651723, 0.42687352, 0.35570636, 0.57066032],
           [0.62713943, 0.71702863, 0.27292808, 0.60370543, 0.24383279]])
    >>> index_arr = index_arr.nonzero()
    >>> index_arr
    (array([0, 1, 1, 2, 2]), array([3, 0, 2, 0, 3]))
    >>> x[index_arr]
    array([[0.36130014, 0.29910215, 0.47659743, 0.58873084, 0.67748054],
           [0.93853782, 0.9929817 , 0.07529918, 0.91509346, 0.01952679],
           [0.28453704, 0.8649879 , 0.72778931, 0.01349646, 0.93421428],
           [0.62090463, 0.36651723, 0.42687352, 0.35570636, 0.57066032],
           [0.62713943, 0.71702863, 0.27292808, 0.60370543, 0.24383279]])
    

    可以看到索引数组只有一个index_arr, 但是被索引数组的维度是三维, 通过[布尔索引数组].nonzero()的返回值我们可以看出, index_arr这个索引数组被转化为了两个整数索引数组代表对不同维度(行和列)的索引, 降维之后的两个整数索引数组维度都是一维. 转化之后索引数组的数量仍然小于被索引数组的维度, 参考整数数组索引机理, 仅会索引前面的两个维度, 剩余的维度相当于补充:. 组合了高级索引和基础索引.

  • 若选择对象的形状和被索引数组的相应维度不匹配, 会报错.

    例子

    定义:

    >>> x = np.array([[1, 2, 3], 
                      [4, 5, 6], 
                      [7, 8, 9]])
    >>> index_arr = np.array([[True, False], 
                              [False, True], 
                              [True, False]])
    >>> x[index_arr]
    ---------------------------------------------------------------------------
    IndexError                                Traceback (most recent call last)
    Cell In[88], line 1
    ----> 1 x[index_arr]
    
    IndexError: boolean index did not match indexed array along dimension 1; dimension is 3 but corresponding boolean dimension is 2
    

    这是因为:

    • 索引数组index_arr的高维的大小为3, 相应被索引数组x中高维的大小为3
    • 索引数组index_arr的低维的大小为2, 相应被索引数组x中高维的大小为3

    无法对应, 所以报错.

    定义:

    >>> x = np.array([[1, 2, 3], 
                      [4, 5, 6], 
                      [7, 8, 9]])
    >>> index_arr1 = np.array([False, True])
    >>> index_arr2 = np.array([False, True, False])
    >>> x[index_arr1, index_arr2]
    ---------------------------------------------------------------------------
    IndexError                                Traceback (most recent call last)
    Cell In[94], line 1
    ----> 1 x[index_arr1, index_arr2]
    
    IndexError: boolean index did not match indexed array along dimension 0; dimension is 3 but corresponding boolean dimension is 2
    

    这是因为:

    • 索引数组index_arr1的大小为2, 相应被索引数组x中高维的大小为3
    • 索引数组index_arr2的大小为3, 相应被索引数组x中低维的大小为3

    无法对应, 所以报错.

应用

  • 筛选所有不是NaN的条目

    例子
    >>> x = np.array([[1., 2.], [np.nan, 3.], [np.nan, np.nan]])
    >>> np.isnan(x)
    array([[False, False],
           [ True, False],
           [ True,  True]])
    >>> ~np.isnan(x)
    array([[ True,  True],
           [False,  True],
           [False, False]])
    >>> x[~np.isnan(x)]
    
    Tip

    波浪号是按位取反运算符, 用于对每个元素进行取反操作.

  • 对特定的元素添加一个常量

    例子
    >>> x = np.array([1., -1., -2., 3])
    >>> x < 0
    array([False,  True,  True, False])
    >>> x[x < 0] += 20
    >>> x
    array([ 1., 19., 18.,  3.])
    
  • 选择所有和小于或等于2的行

    例子
    >>> x = np.array([[0, 1], [1, 1], [2, 2]])
    >>> x.sum(-1)
    array([1, 2, 4])
    >>> rowsum = x.sum(-1)
    >>> rowsum <= 2
    array([ True,  True, False])
    >>> x[rowsum <= 2]
    array([[0, 1],
           [1, 1]])
    
  • 选择所有和为偶数的行, 同时使用整数属组索引选择列

    例子

    定义:

    >>> x = np.array([[ 0,  1,  2],
                      [ 3,  4,  5],
                      [ 6,  7,  8],
                      [ 9, 10, 11]])
    >>> rows = (x.sum(-1) % 2) == 0
    >>> rows
    array([False,  True, False,  True])
    >>> columns = [0, 2]
    >>> x[np.ix_(rows, columns)]
    array([[ 3,  5],
           [ 9, 11]])
    

    解释:

    rows可以理解为rows.nonzero(), 即(array([1, 3]),), 所以np.ix_(rows, columns)可以理解为np.ix_(np.array([1, 3]), np.array([0, 2])).

    Tip3

    np.ix_()函数用于创建开放网格.

    例子

    定义:

    >>> a = np.arange(5*5).reshape(5,5)
    >>> a
    array([[ 0,  1,  2,  3,  4],
           [ 5,  6,  7,  8,  9],
           [10, 11, 12, 13, 14],
           [15, 16, 17, 18, 19],
           [20, 21, 22, 23, 24]])
    >>> sub_index = np.ix_([1, 3], [0, 3])
    >>> sub_index
    (array([[1], [3]]), array([[0, 3]]))
    >>> a[sub_index]
    array([[ 5,  8],
           [15, 18]])
    

    可以看到np.ix_()将两个一维数组分别转为了两个二维整数索引数组(一个列向量和一个行向量), 这两个数组会被广播:

    • array([[1], [3]]) -> array([[1, 1], [3, 3]])
    • array([[0, 3]]) -> array([[0, 3], [0, 3]])

    所以最终的结果相当于是:

    >>> a[np.array([[1, 1], [3, 3]]), np.array([[0, 3], [0, 3]])]
    array([[ 5,  8],
           [15, 18]])
    

    上述过程可以解释为:

     col 0    col 3
        |        |
        v        v
     [[ 0  1  2  3  4]   
      [ 5  6  7  8  9]   <- row 1
      [10 11 12 13 14]
      [15 16 17 18 19]   <- row 3
      [20 21 22 23 24]]
    

    如果没有np.ix_(), 只会选择对角线元素:

    >>> x[rows, columns]
    array([ 3, 11])
    

组合索引

注意

组合高级索引和基础索引返回数组是副本还是视图(/numpy/view-copy/#视图)要看最后一个方括号中的第一个元素是高级索引数组还是基础索引(切片或单一数值).

例子
>>> x = np.arange(35).reshape(5, 7)
>>> x
array([[ 0,  1,  2,  3,  4,  5,  6],
       [ 7,  8,  9, 10, 11, 12, 13],
       [14, 15, 16, 17, 18, 19, 20],
       [21, 22, 23, 24, 25, 26, 27],
       [28, 29, 30, 31, 32, 33, 34]])
>>> x[:, np.array([0, 2, 4])].base # 返回视图
array([[ 0,  7, 14, 21, 28],
       [ 2,  9, 16, 23, 30],
       [ 4, 11, 18, 25, 32]])
>>> x[np.array([0, 2, 4]), :].base # 返回副本
>>> x[np.array([0, 2, 4]), :][:, 1:3] # 返回视图
array([[ 1,  2],
       [15, 16],
       [29, 30]])
>>> x[1:3, np.array([0, 2, 4])].base # 返回视图
array([[ 7, 14],
       [ 9, 16],
       [11, 18]])

当至少有一个切片, 省略号或者np.newaxis在包含高级索引的选择对象中, 或者被索引数组的维度高于高级索引数组的个数(会自动填充:, 即自动添加切片)时, NumPy会组合高级索引和基础索引.

实际上, 基础索引和高级索引的操作是独立的, 可以将它们分开进行.

例子
>>> y = np.arange(35).reshape(5, 7)
>>> y
array([[ 0,  1,  2,  3,  4,  5,  6],
       [ 7,  8,  9, 10, 11, 12, 13],
       [14, 15, 16, 17, 18, 19, 20],
       [21, 22, 23, 24, 25, 26, 27],
       [28, 29, 30, 31, 32, 33, 34]])
>>> y[np.array([0, 2, 4]), 1:3]
array([[ 1,  2],
       [15, 16],
       [29, 30]])
>>> y[:, 1:3][np.array([0, 2, 4]), :]
array([[ 1,  2],
       [15, 16],
       [29, 30]])
>>> y[np.array([0, 2, 4]), :][:, 1:3]
array([[ 1,  2],
       [15, 16],
       [29, 30]])
>>> y[1:3, np.array([0, 2, 4])]
array([[ 7,  9, 11],
       [14, 16, 18]])
>>> y[1:3, :][:, np.array([0, 2, 4])]
array([[ 7,  9, 11],
       [14, 16, 18]])
>>> y[:, np.array([0, 2, 4])][1:3, :]
array([[ 7,  9, 11],
       [14, 16, 18]])

注意:

  • y[np.array([0, 2, 4]), 1:3]相当于y[np.array([0, 2, 4]), :][:, 1:3]或者y[:, 1:3][np.array([0, 2, 4]), :]
  • y[1:3, np.array([0, 2, 4])]相当于y[1:3, :][:, np.array([0, 2, 4])]或者y[:, np.array([0, 2, 4])][1:3, :]

要注意到两者的不同.

多个高级索引组合

理解多个高维索引组合的最简单方法是根据结果的形状进行思考. 根据上面的结果, 我们理解到, 组合索引操作有两个部分, 由基础索引部分定义的子空间(不包括单一元素索引, 可能会导致降维, 为了简便先不考虑)和由高级索引部分定义的子空间. 需要区分两种情况:

  1. 高级索引由切片, 冒号或者np.newaxis分隔, 例如x[arr1, :, arr2]
  2. 高级索引全部彼此相邻, 例如x[..., arr1, arr2, :]

在第一种情况下, 高级索引操作产生的维度首先出现在结果数组中, 然后是子空间维度; 在第二种情况下, 高级索引操作的维度按照其在初始数组中的位置插入到结果数组中.

例子

假设被索引数组x的形状为(10, 20, 30). ind是一个形状为(2, 3, 4)的整数索引数组, 则result = [..., ind, :]的形状为(10, 2, 3, 4, 30). 因为(20, )形状的子空间已经被(2, 3, 4)形状的子空间替换.

假设被索引数组x的形状为(10, 20, 30, 40, 50), 假设ind_1ind_2可以广播到形状(2, 3, 4). 则result_1 = [:, ind_1, ind_2]的形状为(10, 2, 3, 4, 40, 50), 因为(20, 30)形状的子空间已经被(2, 3, 4)形状的子空间替换. 但是result_2 = [:, ind_1, :, ind_2]的形状为(2, 3, 4, 10, 30, 50)`, 因为没有明确的位置可以放置子空间, 因此将其附加到开头.

字段索引

结构化数组可以通过类似于字典的字符串对数组进行索引来访问数组的字段对应的数据.

笔记

什么是字段? 官方说明见这里.

例子
x = np.array([('Rex', 9, 81.0), ('Fido', 3, 27.0)], dtype=[('name', 'U10'), ('age', 'i4'), ('weight', 'f4')])

其中('name', 'U10')为字段, ('name', 'U10'), ('age', 'i4'), ('weight', 'f4')为字段序列, 由于它们都有名称, 所以被称为"命名字段"/"命名字段序列".

x是一个长度为2的一维数组, 其数据类型是一个包含三个字段的结构: 一个长度不大于10的字符串, 名字为name; 一个32位整数, 名字为age; 一个32位浮点数, 名字为weight.

索引x['[field-name]']返回的是一个新的视图. 形状与x相同(除了字段的类型本身就是一个子数组, 这种情况下, 子数组的维度将附加到结果的形状), 但是类型是x.dtype['[field-name]'], 并且仅仅含有相应字段的数据.

还可以通过字段列表进行索引. 这将仅仅返回包含这些字段数据的视图.

例子
>>> a = np.zeros((2, 2), dtype=np.int32)
>>> a.shape
(2, 2)
>>> a
array([[0, 0],
       [0, 0]], dtype=int32)
>>> x = np.zeros((2, 2), dtype=[('a', np.int32), ('b', np.float64, (3, 3))])
>>> x.shape
(2, 2)
>>> x
array([[(0, [[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]]),
        (0, [[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]])],
       [(0, [[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]]),
        (0, [[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]])]],
      dtype=[('a', '<i4'), ('b', '<f8', (3, 3))])
>>> x['a'].shape
(2, 2)
>>> x['a'].dtype
dtype('int32')
>>> x['b'].shape
(2, 2, 3, 3)
>>> x['b'].dtype
dtype('float64')

x这个数组中, 每一个元素都是复合数据类型, 包含两个字段, 一个整数和一个3*3的浮点数矩阵. 可以看到ax的区别, x中的每一个元素都是复合的, 每一个元素都用元祖表示. 由于输入的元素是单一的, 所以会被转换为复合数据类型, 所有的值都用原先的值填充. 如单一元素0被转为复合数据类型后变成了(0, [[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]]).


  1. Indexing on ndarrays—NumPy v2.0 Manual. (n.d.). Retrieved June 21, 2024, from https://numpy.org/doc/stable/user/basics.indexing.html 

  2. 理解python索引和切片 · Issue #15 · qiwihui/blog. (n.d.). GitHub. Retrieved June 21, 2024, from https://github.com/qiwihui/blog/issues/15 

  3. Ehsan. (2020, June 21). Answer to “What does numpy.ix_() function do and what is the output used for?” Stack Overflow. https://stackoverflow.com/a/62505277