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