1、NumPy 的ndarray:一种多维数组对象
NumPy 最重要的一个特点就是其 N 维数组对象(即ndarray),该对象是一个快速而灵活的大数据集容器。你可以利用这种数组对整块数据执行一些数学运算,其语法跟标量元素之间的运算一样。
要明白 Python 是如何利用与标量值类似的语法进行批次计算,我先引入 NumPy,然后生成一个包含随机数据的小数组:
1 2 3 4 5 6 7 8 9
| In [12]: import numpy as np
In [13]: data = np.random.randn(2, 3)
In [14]: data Out[14]: array([[-0.2047, 0.4789, -0.5194], [-0.5557, 1.9658, 1.3934]])
|
然后进行数学运算:
1 2 3 4 5 6 7 8 9
| In [15]: data * 10 Out[15]: array([[ -2.0471, 4.7894, -5.1944], [ -5.5573, 19.6578, 13.9341]])
In [16]: data + data Out[16]: array([[-0.4094, 0.9579, -1.0389], [-1.1115, 3.9316, 2.7868]])
|
第一个例子中,所有的元素都乘以 10。第二个例子中,每个元素都与自身相加。
笔记:在本章及全书中,我会使用标准的 NumPy 惯用法import numpy as np。你当然也可以在代码中使用from numpy import *,但不建议这么做。numpy的命名空间很大,包含许多函数,其中一些的名字与 Python 的内置函数重名(比如min和max)。
ndarray是一个通用的同构数据多维容器,也就是说,其中的所有元素必须是相同类型的。每个数组都有一个shape(一个表示各维度大小的元组)和一个dtype(一个用于说明数组数据类型的对象):
1 2 3 4 5
| In [17]: data.shape Out[17]: (2, 3)
In [18]: data.dtype Out[18]: dtype('float64')
|
本章将会介绍 NumPy 数组的基本用法,这对于本书后面各章的理解基本够用。虽然大多数数据分析工作不需要深入理解 NumPy,但是精通面向数组的编程和思维方式是成为 Python 科学计算牛人的一大关键步骤。
笔记:当你在本书中看到“数组”、“NumPy 数组”、ndarray时,基本上都指的是同一样东西,即ndarray对象。
创建ndarray
创建数组最简单的办法就是使用array函数。它接受一切序列型的对象(包括其他数组),然后产生一个新的含有传入数据的 NumPy 数组。以一个列表的转换为例:
1 2 3 4 5 6
| In [19]: data1 = [6, 7.5, 8, 0, 1]
In [20]: arr1 = np.array(data1)
In [21]: arr1 Out[21]: array([ 6. , 7.5, 8. , 0. , 1. ])
|
嵌套序列(比如由一组等长列表组成的列表)将会被转换为一个多维数组:
1 2 3 4 5 6 7 8
| In [22]: data2 = [[1, 2, 3, 4], [5, 6, 7, 8]]
In [23]: arr2 = np.array(data2)
In [24]: arr2 Out[24]: array([[1, 2, 3, 4], [5, 6, 7, 8]])
|
因为data2是列表的列表,NumPy 数组arr2的两个维度的shape是从data2引入的。可以用属性ndim和shape验证:
1 2 3 4 5
| In [25]: arr2.ndim Out[25]: 2
In [26]: arr2.shape Out[26]: (2, 4)
|
除非特别说明(稍后将会详细介绍),np.array会尝试为新建的这个数组推断出一个较为合适的数据类型。数据类型保存在一个特殊的dtype对象中。比如说,在上面的两个例子中,我们有:
1 2 3 4
| In [27]: arr1.dtype Out[27]: dtype('float64') In [28]: arr2.dtype Out[28]: dtype('int64')
|
除np.array之外,还有一些函数也可以新建数组。比如,zeros和ones分别可以创建指定长度或形状的全 0 或全 1 数组。empty可以创建一个没有任何具体值的数组。要用这些方法创建多维数组,只需传入一个表示形状的元组即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| In [29]: np.zeros(10) Out[29]: array([ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
In [30]: np.zeros((3, 6)) Out[30]: array([[ 0., 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0., 0.]])
In [31]: np.empty((2, 3, 2)) Out[31]: array([[[ 0., 0.], [ 0., 0.], [ 0., 0.]], [[ 0., 0.], [ 0., 0.], [ 0., 0.]]])
|
注意:认为np.empty会返回全 0 数组的想法是不安全的。很多情况下(如前所示),它返回的都是一些未初始化的垃圾值。
arange是 Python 内置函数range的数组版:
1 2
| In [32]: np.arange(15) Out[32]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
|
表 4-1 列出了一些数组创建函数。由于 NumPy 关注的是数值计算,因此,如果没有特别指定,数据类型基本都是float64(浮点数)。

ndarray的数据类型
dtype(数据类型)是一个特殊的对象,它含有ndarray将一块内存解释为特定数据类型所需的信息:
1 2 3 4 5 6 7 8 9
| In [33]: arr1 = np.array([1, 2, 3], dtype=np.float64)
In [34]: arr2 = np.array([1, 2, 3], dtype=np.int32)
In [35]: arr1.dtype Out[35]: dtype('float64')
In [36]: arr2.dtype Out[36]: dtype('int32')
|
dtype是 NumPy 灵活交互其它系统的源泉之一。多数情况下,它们直接映射到相应的机器表示,这使得“读写磁盘上的二进制数据流”以及“集成低级语言代码(如 C、Fortran)”等工作变得更加简单。数值型dtype的命名方式相同:一个类型名(如float或int),后面跟一个用于表示各元素位长的数字。标准的双精度浮点值(即 Python 中的float对象)需要占用 8 字节(即 64 位)。因此,该类型在 NumPy 中就记作float64。表 4-2 列出了 NumPy 所支持的全部数据类型。
笔记:记不住这些 NumPy 的dtype也没关系,新手更是如此。通常只需要知道你所处理的数据的大致类型是浮点数、复数、整数、布尔值、字符串,还是普通的 Python 对象即可。当你需要控制数据在内存和磁盘中的存储方式时(尤其是对大数据集),那就得了解如何控制存储类型。
你可以通过ndarray的astype方法明确地将一个数组从一个dtype转换成另一个dtype:
1 2 3 4 5 6 7 8 9
| In [37]: arr = np.array([1, 2, 3, 4, 5])
In [38]: arr.dtype Out[38]: dtype('int64')
In [39]: float_arr = arr.astype(np.float64)
In [40]: float_arr.dtype Out[40]: dtype('float64')
|
在本例中,整数被转换成了浮点数。如果将浮点数转换成整数,则小数部分将会被截取删除:
1 2 3 4 5 6 7
| In [41]: arr = np.array([3.7, -1.2, -2.6, 0.5, 12.9, 10.1])
In [42]: arr Out[42]: array([ 3.7, -1.2, -2.6, 0.5, 12.9, 10.1])
In [43]: arr.astype(np.int32) Out[43]: array([ 3, -1, -2, 0, 12, 10], dtype=int32)
|
如果某字符串数组表示的全是数字,也可以用astype将其转换为数值形式:
1 2 3 4
| In [44]: numeric_strings = np.array(['1.25', '-9.6', '42'], dtype=np.string_)
In [45]: numeric_strings.astype(float) Out[45]: array([ 1.25, -9.6 , 42. ])
|
注意:使用numpy.string_类型时,一定要小心,因为 NumPy 的字符串数据是大小固定的,发生截取时,不会发出警告。pandas 提供了更多非数值数据的便利的处理方法。
如果转换过程因为某种原因而失败了(比如某个不能被转换为float64的字符串),就会引发一个ValueError。这里,我比较懒,写的是float而不是np.float64;NumPy 很聪明,它会将 Python 类型映射到等价的dtype上。
数组的dtype还有另一个属性:
1 2 3 4 5 6
| In [46]: int_array = np.arange(10)
In [47]: calibers = np.array([.22, .270, .357, .380, .44, .50], dtype=np.float64)
In [48]: int_array.astype(calibers.dtype) Out[48]: array([ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])
|
你还可以用简洁的类型代码来表示dtype:
1 2 3 4 5 6
| In [49]: empty_uint32 = np.empty(8, dtype='u4')
In [50]: empty_uint32 Out[50]: array([ 0, 1075314688, 0, 1075707904, 0, 1075838976, 0, 1072693248], dtype=uint32)
|
笔记:调用astype总会创建一个新的数组(一个数据的备份),即使新的dtype与旧的dtype相同。
数组很重要,因为它使你不用编写循环即可对数据执行批量运算。NumPy 用户称其为向量化(vectorization)。大小相等的数组之间的任何算术运算都会将运算应用到元素级:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| In [51]: arr = np.array([[1., 2., 3.], [4., 5., 6.]])
In [52]: arr Out[52]: array([[ 1., 2., 3.], [ 4., 5., 6.]])
In [53]: arr * arr Out[53]: array([[ 1., 4., 9.], [ 16., 25., 36.]])
In [54]: arr - arr Out[54]: array([[ 0., 0., 0.], [ 0., 0., 0.]])
|
数组与标量的算术运算会将标量值传播到各个元素:
1 2 3 4 5 6 7 8 9
| In [55]: 1 / arr Out[55]: array([[ 1. , 0.5 , 0.3333], [ 0.25 , 0.2 , 0.1667]])
In [56]: arr ** 0.5 Out[56]: array([[ 1. , 1.4142, 1.7321], [ 2. , 2.2361, 2.4495]])
|
大小相同的数组之间的比较会生成布尔值数组:
1 2 3 4 5 6 7 8 9 10 11
| In [57]: arr2 = np.array([[0., 4., 1.], [7., 2., 12.]])
In [58]: arr2 Out[58]: array([[ 0., 4., 1.], [ 7., 2., 12.]])
In [59]: arr2 > arr Out[59]: array([[False, True, False], [ True, False, True]], dtype=bool)
|
不同大小的数组之间的运算叫做广播(broadcasting),将在附录 A 中对其进行详细讨论。本书的内容不需要对广播机制有多深的理解。
基本的索引和切片
NumPy 数组的索引是一个内容丰富的主题,因为选取数据子集或单个元素的方式有很多。一维数组很简单。从表面上看,它们跟 Python 列表的功能差不多:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| In [60]: arr = np.arange(10)
In [61]: arr Out[61]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [62]: arr[5] Out[62]: 5
In [63]: arr[5:8] Out[63]: array([5, 6, 7])
In [64]: arr[5:8] = 12
In [65]: arr Out[65]: array([ 0, 1, 2, 3, 4, 12, 12, 12, 8, 9])
|
如上所示,当你将一个标量值赋值给一个切片时(如arr[5:8]=12),该值会自动传播(也就说后面将会讲到的“广播”)到整个选区。跟列表最重要的区别在于,数组切片是原始数组的视图。这意味着数据不会被复制,视图上的任何修改都会直接反映到源数组上。
作为例子,先创建一个arr的切片:
1 2 3 4
| In [66]: arr_slice = arr[5:8]
In [67]: arr_slice Out[67]: array([12, 12, 12])
|
现在,当我修改arr_slice中的值,变动也会体现在原始数组arr中:
1 2 3 4 5
| In [68]: arr_slice[1] = 12345
In [69]: arr Out[69]: array([ 0, 1, 2, 3, 4, 12, 12345, 12, 8, 9])
|
切片[:]会给数组中的所有值赋值:
1 2 3 4
| In [70]: arr_slice[:] = 64
In [71]: arr Out[71]: array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9])
|
如果你刚开始接触 NumPy,可能会对此感到惊讶(尤其是当你曾经用过其他热衷于复制数组数据的编程语言)。由于 NumPy 的设计目的是处理大数据,所以你可以想象一下,假如 NumPy 坚持要将数据复制来复制去的话会产生何等的性能和内存问题。
注意:如果你想要得到的是ndarray切片的一份副本而非视图,就需要明确地进行复制操作,例如arr[5:8].copy()。
对于高维度数组,能做的事情更多。在一个二维数组中,各索引位置上的元素不再是标量而是一维数组:
1 2 3 4
| In [72]: arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
In [73]: arr2d[2] Out[73]: array([7, 8, 9])
|
因此,可以对各个元素进行递归访问,但这样需要做的事情有点多。你可以传入一个以逗号隔开的索引列表来选取单个元素。也就是说,下面两种方式是等价的:
1 2 3 4 5
| In [74]: arr2d[0][2] Out[74]: 3
In [75]: arr2d[0, 2] Out[75]: 3
|
图 4-1 说明了二维数组的索引方式。轴 0 作为行,轴 1 作为列。
在多维数组中,如果省略了后面的索引,则返回对象会是一个维度低一点的ndarray(它含有高一级维度上的所有数据)。因此,在2×2×3数组arr3d中:
1 2 3 4 5 6 7 8
| In [76]: arr3d = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])
In [77]: arr3d Out[77]: array([[[ 1, 2, 3], [ 4, 5, 6]], [[ 7, 8, 9], [10, 11, 12]]])
|
arr3d[0]是一个2×3数组:
1 2 3 4
| In [78]: arr3d[0] Out[78]: array([[1, 2, 3], [4, 5, 6]])
|
标量值和数组都可以被赋值给arr3d[0]:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| In [79]: old_values = arr3d[0].copy()
In [80]: arr3d[0] = 42
In [81]: arr3d Out[81]: array([[[42, 42, 42], [42, 42, 42]], [[ 7, 8, 9], [10, 11, 12]]])
In [82]: arr3d[0] = old_values
In [83]: arr3d Out[83]: array([[[ 1, 2, 3], [ 4, 5, 6]], [[ 7, 8, 9], [10, 11, 12]]])
|
相似的,arr3d[1,0]可以访问索引以(1,0)开头的那些值(以一维数组的形式返回):
1 2
| In [84]: arr3d[1, 0] Out[84]: array([7, 8, 9])
|
虽然是用两步进行索引的,表达式是相同的:
1 2 3 4 5 6 7 8 9
| In [85]: x = arr3d[1]
In [86]: x Out[86]: array([[ 7, 8, 9], [10, 11, 12]])
In [87]: x[0] Out[87]: array([7, 8, 9])
|
注意,在上面所有这些选取数组子集的例子中,返回的数组都是视图。
切片索引
ndarray的切片语法跟 Python 列表这样的一维对象差不多:
1 2 3 4 5
| In [88]: arr Out[88]: array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9])
In [89]: arr[1:6] Out[89]: array([ 1, 2, 3, 4, 64])
|
对于之前的二维数组arr2d,其切片方式稍显不同:
1 2 3 4 5 6 7 8 9 10
| In [90]: arr2d Out[90]: array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
In [91]: arr2d[:2] Out[91]: array([[1, 2, 3], [4, 5, 6]])
|
可以看出,它是沿着第 0 轴(即第一个轴)切片的。也就是说,切片是沿着一个轴向选取元素的。表达式arr2d[:2]可以被认为是“选取arr2d的前两行”。
你可以一次传入多个切片,就像传入多个索引那样:
1 2 3 4
| In [92]: arr2d[:2, 1:] Out[92]: array([[2, 3], [5, 6]])
|
像这样进行切片时,只能得到相同维数的数组视图。通过将整数索引和切片混合,可以得到低维度的切片。
例如,我可以选取第二行的前两列:
1 2
| In [93]: arr2d[1, :2] Out[93]: array([4, 5])
|
相似的,还可以选择第三列的前两行:
1 2
| In [94]: arr2d[:2, 2] Out[94]: array([3, 6])
|
图 4-2 对此进行了说明。注意,“只有冒号”表示选取整个轴,因此你可以像下面这样只对高维轴进行切片:
1 2 3 4 5
| In [95]: arr2d[:, :1] Out[95]: array([[1], [4], [7]])
|
自然,对切片表达式的赋值操作也会被扩散到整个选区:
1 2 3 4 5 6 7
| In [96]: arr2d[:2, 1:] = 0
In [97]: arr2d Out[97]: array([[1, 0, 0], [4, 0, 0], [7, 8, 9]])
|
布尔型索引
来看这样一个例子,假设我们有一个用于存储数据的数组以及一个存储姓名的数组(含有重复项)。在这里,我将使用numpy.random中的randn函数生成一些正态分布的随机数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| In [98]: names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
In [99]: data = np.random.randn(7, 4)
In [100]: names Out[100]: array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'], dtype='<U4')
In [101]: data Out[101]: array([[ 0.0929, 0.2817, 0.769 , 1.2464], [ 1.0072, -1.2962, 0.275 , 0.2289], [ 1.3529, 0.8864, -2.0016, -0.3718], [ 1.669 , -0.4386, -0.5397, 0.477 ], [ 3.2489, -1.0212, -0.5771, 0.1241], [ 0.3026, 0.5238, 0.0009, 1.3438], [-0.7135, -0.8312, -2.3702, -1.8608]])
|
假设每个名字都对应data数组中的一行,而我们想要选出对应于名字"Bob"的所有行。跟算术运算一样,数组的比较运算(如==)也是向量化的。因此,对names和字符串"Bob"的比较运算将会产生一个布尔型数组:
1 2
| In [102]: names == 'Bob' Out[102]: array([ True, False, False, True, False, False, False], dtype=bool)
|
这个布尔型数组可用于数组索引:
1 2 3 4
| In [103]: data[names == 'Bob'] Out[103]: array([[ 0.0929, 0.2817, 0.769 , 1.2464], [ 1.669 , -0.4386, -0.5397, 0.477 ]])
|
布尔型数组的长度必须跟被索引的轴长度一致。此外,还可以将布尔型数组跟切片、整数(或整数序列,稍后将对此进行详细讲解)混合使用:
1 2 3 4
| In [103]: data[names == 'Bob'] Out[103]: array([[ 0.0929, 0.2817, 0.769 , 1.2464], [ 1.669 , -0.4386, -0.5397, 0.477 ]])
|
注意:如果布尔型数组的长度不对,布尔型选择就会出错,因此一定要小心。
下面的例子,我选取了names == 'Bob'的行,并索引了列:
1 2 3 4 5 6 7
| In [104]: data[names == 'Bob', 2:] Out[104]: array([[ 0.769 , 1.2464], [-0.5397, 0.477 ]])
In [105]: data[names == 'Bob', 3] Out[105]: array([ 1.2464, 0.477 ])
|
要选择除”Bob”以外的其他值,既可以使用不等于符号(!=),也可以通过~对条件进行否定:
1 2 3 4 5 6 7 8 9 10
| In [106]: names != 'Bob' Out[106]: array([False, True, True, False, True, True, True], dtype=bool)
In [107]: data[~(names == 'Bob')] Out[107]: array([[ 1.0072, -1.2962, 0.275 , 0.2289], [ 1.3529, 0.8864, -2.0016, -0.3718], [ 3.2489, -1.0212, -0.5771, 0.1241], [ 0.3026, 0.5238, 0.0009, 1.3438], [-0.7135, -0.8312, -2.3702, -1.8608]])
|
~操作符用来反转条件很好用:
1 2 3 4 5 6 7 8 9
| In [108]: cond = names == 'Bob'
In [109]: data[~cond] Out[109]: array([[ 1.0072, -1.2962, 0.275 , 0.2289], [ 1.3529, 0.8864, -2.0016, -0.3718], [ 3.2489, -1.0212, -0.5771, 0.1241], [ 0.3026, 0.5238, 0.0009, 1.3438], [-0.7135, -0.8312, -2.3702, -1.8608]])
|
选取这三个名字中的两个需要组合应用多个布尔条件,使用&(和)、|(或)之类的布尔算术运算符即可:
1 2 3 4 5 6 7 8 9 10 11
| In [110]: mask = (names == 'Bob') | (names == 'Will')
In [111]: mask Out[111]: array([ True, False, True, True, True, False, False], dtype=bool)
In [112]: data[mask] Out[112]: array([[ 0.0929, 0.2817, 0.769 , 1.2464], [ 1.3529, 0.8864, -2.0016, -0.3718], [ 1.669 , -0.4386, -0.5397, 0.477 ], [ 3.2489, -1.0212, -0.5771, 0.1241]])
|
通过布尔型索引选取数组中的数据,将总是创建数据的副本,即使返回一模一样的数组也是如此。
注意:Python 关键字and和or在布尔型数组中无效。要使用&与|。
通过布尔型数组设置值是一种经常用到的手段。为了将data中的所有负值都设置为 0,我们只需:
1 2 3 4 5 6 7 8 9 10 11
| In [113]: data[data < 0] = 0
In [114]: data Out[114]: array([[ 0.0929, 0.2817, 0.769 , 1.2464], [ 1.0072, 0. , 0.275 , 0.2289], [ 1.3529, 0.8864, 0. , 0. ], [ 1.669 , 0. , 0. , 0.477 ], [ 3.2489, 0. , 0. , 0.1241], [ 0.3026, 0.5238, 0.0009, 1.3438], [ 0. , 0. , 0. , 0. ]])
|
通过一维布尔数组设置整行或列的值也很简单:
1 2 3 4 5 6 7 8 9 10 11
| In [115]: data[names != 'Joe'] = 7
In [116]: data Out[116]: array([[ 7. , 7. , 7. , 7. ], [ 1.0072, 0. , 0.275 , 0.2289], [ 7. , 7. , 7. , 7. ], [ 7. , 7. , 7. , 7. ], [ 7. , 7. , 7. , 7. ], [ 0.3026, 0.5238, 0.0009, 1.3438], [ 0. , 0. , 0. , 0. ]])
|
后面会看到,这类二维数据的操作也可以用 pandas 方便的来做。
花式索引
花式索引(Fancy indexing)是一个 NumPy 术语,它指的是利用整数数组进行索引。假设我们有一个8×4数组:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| In [117]: arr = np.empty((8, 4))
In [118]: for i in range(8): .....: arr[i] = i
In [119]: arr Out[119]: 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即可:
1 2 3 4 5 6
| In [120]: arr[[4, 3, 0, 6]] Out[120]: array([[ 4., 4., 4., 4.], [ 3., 3., 3., 3.], [ 0., 0., 0., 0.], [ 6., 6., 6., 6.]])
|
这段代码确实达到我们的要求了!使用负数索引将会从末尾开始选取行:
1 2 3 4 5
| In [121]: arr[[-3, -5, -7]] Out[121]: array([[ 5., 5., 5., 5.], [ 3., 3., 3., 3.], [ 1., 1., 1., 1.]])
|
一次传入多个索引数组会有一点特别。它返回的是一个一维数组,其中的元素对应各个索引元组:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| In [122]: arr = np.arange(32).reshape((8, 4))
In [123]: arr Out[123]: 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 [124]: arr[[1, 5, 7, 2], [0, 3, 1, 2]] Out[124]: array([ 4, 23, 29, 10])
|
附录 A 中会详细介绍reshape方法。
最终选出的是元素(1,0)、(5,3)、(7,1)和(2,2)。无论数组是多少维的,花式索引总是一维的。
这个花式索引的行为可能会跟某些用户的预期不一样(包括我在内),选取矩阵的行列子集应该是矩形区域的形式才对。下面是得到该结果的一个办法:
1 2 3 4 5 6
| In [125]: arr[[1, 5, 7, 2]][:, [0, 3, 1, 2]] Out[125]: array([[ 4, 7, 5, 6], [20, 23, 21, 22], [28, 31, 29, 30], [ 8, 11, 9, 10]])
|
记住,花式索引跟切片不一样,它总是将数据复制到新数组中。
数组转置和轴对换
转置是重塑的一种特殊形式,它返回的是源数据的视图(不会进行任何复制操作)。数组不仅有transpose方法,还有一个特殊的T属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| In [126]: arr = np.arange(15).reshape((3, 5))
In [127]: arr Out[127]: array([[ 0, 1, 2, 3, 4], [ 5, 6, 7, 8, 9], [10, 11, 12, 13, 14]])
In [128]: arr.T Out[128]: array([[ 0, 5, 10], [ 1, 6, 11], [ 2, 7, 12], [ 3, 8, 13], [ 4, 9, 14]])
|
在进行矩阵计算时,经常需要用到该操作,比如利用np.dot计算矩阵内积:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| In [129]: arr = np.random.randn(6, 3)
In [130]: arr Out[130]: array([[-0.8608, 0.5601, -1.2659], [ 0.1198, -1.0635, 0.3329], [-2.3594, -0.1995, -1.542 ], [-0.9707, -1.307 , 0.2863], [ 0.378 , -0.7539, 0.3313], [ 1.3497, 0.0699, 0.2467]])
In [131]: np.dot(arr.T, arr) Out[131]: array([[ 9.2291, 0.9394, 4.948 ], [ 0.9394, 3.7662, -1.3622], [ 4.948 , -1.3622, 4.3437]])
|
对于高维数组,transpose需要得到一个由轴编号组成的元组才能对这些轴进行转置(比较费脑子):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| In [132]: arr = np.arange(16).reshape((2, 2, 4))
In [133]: arr Out[133]: array([[[ 0, 1, 2, 3], [ 4, 5, 6, 7]], [[ 8, 9, 10, 11], [12, 13, 14, 15]]])
In [134]: arr.transpose((1, 0, 2)) Out[134]: array([[[ 0, 1, 2, 3], [ 8, 9, 10, 11]], [[ 4, 5, 6, 7], [12, 13, 14, 15]]])
|
这里,第一个轴被换成了第二个,第二个轴被换成了第一个,最后一个轴不变。
简单的转置可以使用.T,它其实就是进行轴对换而已。ndarray还有一个swapaxes方法,它需要接受一对轴编号:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| In [135]: arr Out[135]: array([[[ 0, 1, 2, 3], [ 4, 5, 6, 7]], [[ 8, 9, 10, 11], [12, 13, 14, 15]]])
In [136]: arr.swapaxes(1, 2) Out[136]: array([[[ 0, 4], [ 1, 5], [ 2, 6], [ 3, 7]], [[ 8, 12], [ 9, 13], [10, 14], [11, 15]]])
|
swapaxes 也是返回源数据的视图(不会进行任何复制操作)。