Numpy基础之 索引和切片

Numpy数组的索引是一个内容丰富的主题,因为选取数据子集和单个元素的方式有很多。一维数组很简单。从表面上看,它们跟Python列表的功能差不多:

In [109]: arr=np.arange(10)

In [110]: arr
Out[110]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

In [111]: arr[5]
Out[111]: 5

In [112]: arr[5:8]
Out[112]: array([5, 6, 7])

In [113]: arr[5:8]=12

In [114]: arr
Out[114]: array([ 0,  1,  2,  3,  4, 12, 12, 12,  8,  9])

如上所示,当你将一个标量赋值给一个切片时(如arr[5;8]=12),该值会自动广播到整个选区。跟列表最重要的区别在于,数组切片是原始数组的视图。这意味着数据不会被复制,视图上的任何修改都会直接反映到原数组上:

In [117]: arr_slice=arr[5:8]

In [118]: arr_slice[1]=12345

In [119]: arr
Out[119]: array([    0,     1,     2,     3,     4,    12, 12345,    12,     8,
    9])

In [120]: arr_slice[:]=64

In [121]: arr
Out[121]: array([ 0,  1,  2,  3,  4, 64, 64, 64,  8,  9])

如果你刚开始接触Python,可能会对此感到惊讶(尤其是当你曾经用过其他热衷于复制数组数据的语言)。由于Numpy的设计目的是处理大数据,所以你可以想象一下,假如Numpy坚持要将数据复制来复制去的话会产生何等性能和内存问题。

当然,如果你想要得到的是ndarray切片的一份副本而非视图,就需要显示地进行复制操作,例如arr[5:8].copy()。

对于高维数组,能做的事情更多。在一个二维数组中,各索引位置上的元素不再是标量而是一维数组:

In [122]: arr2d=np.array([[1,2,3],[4,5,6],[7,8,9]])

In [123]: arr2d[2]
Out[123]: array([7, 8, 9])

因此,可以对各个元素进行递归访问,但这样需要做的事情有点多。你可以传入一个以逗号隔开的索引列表来选取单个元素。也就是说,下面两种方式是等价的:

In [124]: arr2d[0][2]
Out[124]: 3

In [125]: arr2d[0,2]
Out[125]: 3

在多维数组中,如果省略了后面的索引,则返回对象会是一个维度低一点的ndarray。因此,在2*2*3数组arr3d中:

In [126]: arr3d=np.array([[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]])

In [127]: arr3d
Out[127]:
array([[[ 1,  2,  3],
        [ 4,  5,  6]],

       [[ 7,  8,  9],
        [10, 11, 12]]])

arr3d[0]是一个2*3数组:

In [128]: arr3d[0]
Out[128]:
array([[1, 2, 3],
       [4, 5, 6]])

标量值和数组都可以被赋值给arr3d[0]:

In [129]: old_values=arr3d[0].copy()

In [130]: arr3d[0]=42

In [131]: arr3d
Out[131]:
array([[[42, 42, 42],
        [42, 42, 42]],

       [[ 7,  8,  9],
        [10, 11, 12]]])

In [132]: arr3d[0]=old_values

In [133]: arr3d
Out[133]:
array([[[ 1,  2,  3],
        [ 4,  5,  6]],

       [[ 7,  8,  9],
        [10, 11, 12]]])

以此类推,arr3d[1,0]可以访问索引以(1,0)开头的那些值(以一维数组的形式返回):

In [134]: arr3d[1,0]
Out[134]: array([7, 8, 9])

切片索引

ndarray的切片语法跟Python列表这样的一维对象差不多:

In [136]: arr[1:6]
Out[136]: array([ 1,  2,  3,  4, 64])

高维度对象的花样更多,你可以在一个或多个轴上进行切片,也可以跟整数索引混合使用。对于上面那个二维数组arr2d,其切片方式稍显不同:

In [137]: arr2d
Out[137]:
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

In [138]: arr2d[:2]
Out[138]:
array([[1, 2, 3],
       [4, 5, 6]])

可以看书,它是沿着第0轴(即第一个轴)切片的。也就是说,切片是沿着一个轴向选取元素的。你可以一次传入多个切片,就像传入多个索引那样:

In [139]: arr2d[:2,:1]
Out[139]:
array([[1],
       [4]])

这样选取切片时,只能得到相同维数的数组视图。通过将整数索引和切片混合,可以得到低维度的切片:

In [140]: arr2d[1,:2]
Out[140]: array([4, 5])

注意:“只有冒号”表示选取整个轴,因此你可以像下面这样只对高维数组进行切片:

In [141]: arr2d[:,:1]
Out[141]:
array([[1],
       [4],
       [7]])

自然,对切片表达式的赋值操作也会被扩散到整个选区:

In [142]: arr2d[:2,1:]=0

In [143]: arr2d
Out[143]:
array([[1, 0, 0],
       [4, 0, 0],
       [7, 8, 9]])

布尔型索引

来看这样一个例子,假设我们有一个用于存储数据的数组以及一个存储姓名的数组(含有重复项)。在这里,我将使用numpy.random中的randn函数生成一些正态分布的随机数据:

In [144]: names=np.array(['Bob','Joe','Will','Bob','Will','Joe','Joe'])

In [145]: data=randn(7,4)

In [146]: names
Out[146]:
array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'],
      dtype='|S4')

In [147]: data
Out[147]:
array([[-1.04671318, -1.61005019,  0.7341528 ,  0.50755629],
       [-0.61720538, -1.05656481,  0.51147741, -0.11091961],
       [ 0.13172863, -1.82681829,  0.25647457,  0.75193541],
       [ 1.77991999, -1.07012481,  0.40510516,  1.23841188],
       [-1.01946802,  0.99361739, -0.54690575,  0.63130108],
       [ 1.18127863, -0.20092513, -0.3435272 , -0.01927443],
       [ 0.35822422, -0.3245485 , -1.41210765, -0.96058923]])

假设每个名字都对应data数组的一行,而我们想要选出对应于名字“Bob”的所有行。跟算术运算一样,数组的比较操作(如==)也是矢量化的。因此,对names和字符串“Bob”的比较运算将会产生一个布尔型数组:

In [148]: names=='Bob'
Out[148]: array([ True, False, False,  True, False, False, False], dtype=bool)

这个布尔型数组可用于数组索引:

In [149]: data[names=='Bob']
Out[149]:
array([[-1.04671318, -1.61005019,  0.7341528 ,  0.50755629],
       [ 1.77991999, -1.07012481,  0.40510516,  1.23841188]])

布尔型数组的长度必须跟被索引数组的长度一直。此外,还可以将布尔型数组跟切片、整数(或整数序列,稍后将对此进行详细讲解)混合使用:

In [150]: data[names=='Bob',:2]
Out[150]:
array([[-1.04671318, -1.61005019],
       [ 1.77991999, -1.07012481]])

In [151]: data[names=='Bob',3]
Out[151]: array([ 0.50755629,  1.23841188])

要选择除‘Bob’以为的其他值,既可以使用不等于符号(!=),也可以通过负号(~)对条件进行否定:

In [156]: names!='Bob'
Out[156]: array([False,  True,  True, False,  True,  True,  True], dtype=bool)

In [157]: data[~(names=='Bob')]
Out[157]:
array([[-0.61720538, -1.05656481,  0.51147741, -0.11091961],
       [ 0.13172863, -1.82681829,  0.25647457,  0.75193541],
       [-1.01946802,  0.99361739, -0.54690575,  0.63130108],
       [ 1.18127863, -0.20092513, -0.3435272 , -0.01927443],
       [ 0.35822422, -0.3245485 , -1.41210765, -0.96058923]])

选取三个名字中的两个需要组合应用多个布尔条件,使用&(和)、|(或)之类的布尔算术运算符即可:

In [158]: mask=(names=='Bob')|(names=='Will')

In [159]: mask
Out[159]: array([ True, False,  True,  True,  True, False, False], dtype=bool)

In [160]: data[mask]
Out[160]:
array([[-1.04671318, -1.61005019,  0.7341528 ,  0.50755629],
       [ 0.13172863, -1.82681829,  0.25647457,  0.75193541],
       [ 1.77991999, -1.07012481,  0.40510516,  1.23841188],
       [-1.01946802,  0.99361739, -0.54690575,  0.63130108]])

通过布尔型索引选取数组中的数据,将总是创建数据的副本,即使返回一模一样的数组也是如此。举个例子,如下:

In [197]: data=randn(4,4)

In [198]: data
Out[198]:
array([[-0.39535105,  0.53360207,  1.2005151 ,  0.32416725],
       [-0.24823878,  0.7545432 ,  1.56430848, -0.1740108 ],
       [ 1.01638718, -0.56379031,  0.49237574,  0.61092716],
       [ 0.95138555,  0.61950592, -0.03049269,  1.71516366]])

In [199]: data_bool=data[data>0]

In [200]: data_bool
Out[200]:
array([ 0.53360207,  1.2005151 ,  0.32416725,  0.7545432 ,  1.56430848,
        1.01638718,  0.49237574,  0.61092716,  0.95138555,  0.61950592,
        1.71516366])

In [201]: data_bool[:]=0

In [202]: data_bool
Out[202]: array([ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.])

In [203]: data
Out[203]:
array([[-0.39535105,  0.53360207,  1.2005151 ,  0.32416725],
       [-0.24823878,  0.7545432 ,  1.56430848, -0.1740108 ],
       [ 1.01638718, -0.56379031,  0.49237574,  0.61092716],
       [ 0.95138555,  0.61950592, -0.03049269,  1.71516366]])

In [204]: data[data>0]
Out[204]:
array([ 0.53360207,  1.2005151 ,  0.32416725,  0.7545432 ,  1.56430848,
        1.01638718,  0.49237574,  0.61092716,  0.95138555,  0.61950592,
        1.71516366])

In [205]: data[data>0]=0

In [206]: data
Out[206]:
array([[-0.39535105,  0.        ,  0.        ,  0.        ],
       [-0.24823878,  0.        ,  0.        , -0.1740108 ],
       [ 0.        , -0.56379031,  0.        ,  0.        ],
       [ 0.        ,  0.        , -0.03049269,  0.        ]])


注意:Python关键字 and 和 or 在布尔型数组中无效。

通过布尔型数组设置值是一种经常用到的手段。为了将data中的所有负值都设置为0,我们只需:

In [161]: data[data<0]=0

In [162]: data
Out[162]:
array([[ 0.        ,  0.        ,  0.7341528 ,  0.50755629],
       [ 0.        ,  0.        ,  0.51147741,  0.        ],
       [ 0.13172863,  0.        ,  0.25647457,  0.75193541],
       [ 1.77991999,  0.        ,  0.40510516,  1.23841188],
       [ 0.        ,  0.99361739,  0.        ,  0.63130108],
       [ 1.18127863,  0.        ,  0.        ,  0.        ],
       [ 0.35822422,  0.        ,  0.        ,  0.        ]])

通过一维布尔数组设置整行或整列的值也很简单:

In [163]: data[names!='Joe']=7

In [164]: data
Out[164]:
array([[ 7.        ,  7.        ,  7.        ,  7.        ],
       [ 0.        ,  0.        ,  0.51147741,  0.        ],
       [ 7.        ,  7.        ,  7.        ,  7.        ],
       [ 7.        ,  7.        ,  7.        ,  7.        ],
       [ 7.        ,  7.        ,  7.        ,  7.        ],
       [ 1.18127863,  0.        ,  0.        ,  0.        ],
       [ 0.35822422,  0.        ,  0.        ,  0.        ]])

花式索引

花式索引(Fancy indexing)是一个Numpy术语,它指的是利用整数数组进行索引。假设我们有一个8*4数组:

In [165]: arr=np.empty((8,4))

In [166]: for i in range(8):
   .....:     arr[i]=i
   .....:

In [167]: arr
Out[167]:
array([[ 0.,  0.,  0.,  0.],
       [ 1.,  1.,  1.,  1.],
       [ 2.,  2.,  2.,  2.],
       [ 3.,  3.,  3.,  3.],
       [ 4.,  4.,  4.,  4.],
       [ 5.,  5.,  5.,  5.],
       [ 6.,  6.,  6.,  6.],
       [ 7.,  7.,  7.,  7.]])

为了以特定顺序选取行子集,只需传入一个用于指定顺序的证书列表或ndarray即可:

In [168]: arr[[4,3,0,6]]
Out[168]:
array([[ 4.,  4.,  4.,  4.],
       [ 3.,  3.,  3.,  3.],
       [ 0.,  0.,  0.,  0.],
       [ 6.,  6.,  6.,  6.]])

这段代码确实打到我们的要求了!使用负数索引将会从末尾开始选取行:

In [169]: arr[[-3,-5,-7]]
Out[169]:
array([[ 5.,  5.,  5.,  5.],
       [ 3.,  3.,  3.,  3.],
       [ 1.,  1.,  1.,  1.]])

一次传入多个索引数组会有一点特别。它返回的是一个一维数组,其中的元素对应各个索引元组:

In [170]: arr=np.arange(32).reshape((8,4))

In [171]: arr
Out[171]:
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]])

In [172]: arr[[1,5,7,2],[0,3,1,2]]
Out[172]: array([ 4, 23, 29, 10])

我们来看看具体是怎么一回事。最终选出的是元素(1,0)、(5,3)、(7,1)和(2,2)。这个花式索引的行为可能会跟某些用户的预期不一样(包括我在内),选取矩阵的行列子集应该是矩形区域的形式才对。下面是得到该结果的一个办法:

In [173]: arr[[1,5,7,2]][:,[0,3,1,2]]
Out[173]:
array([[ 4,  7,  5,  6],
       [20, 23, 21, 22],
       [28, 31, 29, 30],
       [ 8, 11,  9, 10]])

另外一个办法是使用np.ix_函数,它可以将两个一维数组转换为一个用于选取方形区域的索引器:

In [175]: arr[np.ix_([1,5,7,2],[0,3,1,2])]
Out[175]:
array([[ 4,  7,  5,  6],
       [20, 23, 21, 22],
       [28, 31, 29, 30],
       [ 8, 11,  9, 10]])

记住:花式索引和切片不一样,它总是将数据复制到新数组中(和布尔型索引类似,不再举例说明)。

你可能感兴趣的:(numpy)