Skip to content

NumPy

数据类型

创建数组

整型数组

1
2
In [10]: np.array([1, 4, 2, 5, 3])
Out[10]: array([1, 4, 2, 5, 3])

NumPy 要求数组必须包含同一类型的数据。如果类型不匹配,NumPy 将会向上转换(如果可行)。这里整型被转换为浮点型:

1
2
In [11]: np.array([3.14, 4, 2, 3])
Out[11]: array([3.14, 4.  , 2.  , 3.  ])

如果希望明确设置数组的数据类型,可以用 dtype 关键字:

1
2
In [12]: np.array([1, 2, 3, 4], dtype='float32')  
Out[12]: array([1., 2., 3., 4.], dtype=float32)

NumPy 数组可以被指定为多维的
嵌套列表构成的多维数组:

1
2
3
4
5
In [13]: np.array([range(i, i + 3) for i in [2, 4, 6]])
Out[13]:
array([[2, 3, 4],
       [4, 5, 6],
       [6, 7, 8]])

内层的列表被当作二维数组的行

从头创建数组

创建一个长度为 10 的数组,数组的值都是 0

1
2
3
4
5
6
In [16]: np.zeros(10)
Out[16]: array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
In [15]: np.zeros(10, dtype=int)
Out[15]: array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
In [5]: np.zeros(10, dtype='int64')
Out[5]: array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])

创建一个 3*5 的浮点型数组,数组的值都是 1

1
2
3
4
5
In [16]: np.ones((3, 5), dtype=float)
Out[16]:
array([[1., 1., 1., 1., 1.],
       [1., 1., 1., 1., 1.],
       [1., 1., 1., 1., 1.]])

创建一个 3*5 的浮点型数组,数组的值都是 3.14

1
2
3
4
5
In [17]: np.full((3, 5), 3.14)
Out[17]:
array([[3.14, 3.14, 3.14, 3.14, 3.14],
       [3.14, 3.14, 3.14, 3.14, 3.14],
       [3.14, 3.14, 3.14, 3.14, 3.14]])

创建一个线性序列数组,从 0 开始,到 20 结束,步长为 2

1
2
In [19]: np.arange(0, 20, 2)
Out[19]: array([ 0,  2,  4,  6,  8, 10, 12, 14, 16, 18])

创建一个 5 个元素的数组,这 5 个数均匀地分配到 0~1

1
2
In [26]: np.linspace(0, 1, 5)
Out[26]: array([0.  , 0.25, 0.5 , 0.75, 1.  ])

创建一个 3*3 的,在 0~1 均匀分布的随机数组成的数组

1
2
3
4
5
In [28]: np.random.random((3, 3))
Out[28]:
array([[0.31526358, 0.53433492, 0.57230016],
       [0.83118833, 0.03009349, 0.60761907],
       [0.14419613, 0.51515578, 0.28067978]])

创建一个 3*3 的,[0, 10) 区间的随机整型数组

1
2
3
4
5
In [30]: np.random.randint(0, 10, (3, 3))
Out[30]:
array([[9, 9, 7],
       [2, 2, 2],
       [2, 6, 5]])

创建一个 3*3 的单位矩阵

1
2
3
4
5
In [31]: np.eye(3)
Out[31]:
array([[1., 0., 0.],
       [0., 1., 0.],
       [0., 0., 1.]])

创建一个由 3 个整型数组成的未初始化的数组,数组的值是内存空间中的任意值

1
2
3
4
5
6
7
8
9
In [32]: np.empty(3)
Out[32]: array([1., 1., 1.])

创建一个 3 * 3 均值为 0标准差为  1 的正态分布的随机数数组
In [2]: np.random.normal(0, 1, (3, 3))
Out[2]:
array([[-0.26484118,  0.14764052, -1.77075177],
       [-1.42962484, -0.31617229, -0.00801558],
       [-0.12810741, -0.23212502,  0.44922251]])

NumPy 数组基础

NumPy 数组的属性

定义三个随机的数组:一个一维数组、一个二维数组和一个三维数组。 用 NumPy 的随机数生成器设置一组种子值,以确保每次程序执行时都可以生成同样的随机数组

1
2
3
4
In [33]: np.random.seed(0)
In [34]: x1 = np.random.randint(10, size=6)
In [35]: x2 = np.random.randint(10, size=(3, 4))
In [36]: x3 = np.random.randint(10, size=(3, 4, 5))

每个数组有 ndim(数组的维度),shape(数组每个维度的大小)和 size(数组的总大小)属性:

1
2
3
4
5
6
In [37]: print("x3 ndim", x3.ndim)
x3 ndim 3
In [38]: print("x3 shape", x3.shape)
x3 shape (3, 4, 5)
In [39]: print("x3 size", x3.size)
x3 size 60

另外一个有用的属性是 dtype,它是数组的数据类型

1
2
In [41]: print("dtype:", x3.dtype)
dtype: int64

其他的属性包括表示每个数组元素字节大小的 itemsize,以及表示数组总字节大小的属性 nbytes:

1
2
3
4
In [43]: print("itemsize:", x3.itemsize, "bytes")
itemsize: 8 bytes
In [44]: print("nbytes:", x3.nbytes, "bytes")
nbytes: 480 bytes

一般来说,可以认为 nbytes 跟 itemsize 和 size 和乘积大小相等

数组索引:获取单个元素

 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
In [21]: x1
Out[21]: array([5, 0, 3, 3, 7, 9])

In [22]: x1[0]
Out[22]: 5

In [24]: x1[-1]
Out[24]: 9

In [27]: x2
Out[27]:
array([[3, 5, 2, 4],
       [7, 6, 8, 8],
       [1, 6, 7, 7]])

In [28]: x2[0, 0]
Out[28]: 3

In [29]: x2[2, -1]
Out[29]: 7

In [30]: x2[0, 0] = 12
In [31]: x2
Out[31]:
array([[12,  5,  2,  4],
       [ 7,  6,  8,  8],
       [ 1,  6,  7,  7]])

注意:NumPy 数组是固定类型的。这意味着当你试图将一个浮点值插入一个整数数组时,浮点值会被截短成整型。并且这种截短是自动完成的,不会给你提示或警告,所以要特别注意这一点!

1
2
3
4
In [32]: x1[0] = 3.14159 # 这将被截短

In [33]: x1
Out[33]: array([3, 0, 3, 3, 7, 9])

数组切片:获取子数组

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
In [36]: x
Out[36]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

In [37]: x[:5]
Out[37]: array([0, 1, 2, 3, 4])

In [38]: x[4:7]
Out[38]: array([4, 5, 6])

In [39]: x[::2]
Out[39]: array([0, 2, 4, 6, 8])

In [40]: x[1::2]
Out[40]: array([1, 3, 5, 7, 9])

In [41]: x[::-1]
Out[41]: array([9, 8, 7, 6, 5, 4, 3, 2, 1, 0])

In [42]: x[5::-2]
Out[42]: array([5, 3, 1])
 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 [43]: x2
Out[43]:
array([[12,  5,  2,  4],
       [ 7,  6,  8,  8],
       [ 1,  6,  7,  7]])

In [44]: x2[:2, :3] # 两行,三列
Out[44]:
array([[12,  5,  2],
       [ 7,  6,  8]])

In [45]: x2[:3, ::2] # 所有行,每隔一列
Out[45]:
array([[12,  2],
       [ 7,  8],
       [ 1,  7]])

In [46]: x2[::-1, ::-1]
Out[46]:
array([[ 7,  7,  6,  1],
       [ 8,  8,  6,  7],
       [ 4,  2,  5, 12]])

In [48]: print(x2[:, 0]) # x2 的第一列
[12  7  1]

In [49]: print(x2[0, :]) # x2 的第一行
[12  5  2  4]

In [50]: print(x2[0]) # 等于 x2[0, :]
[12  5  2  4]

关于数组切片有一点很重要也非常有用,那就是数组切片返回的是数组数据的视图,而不是数值数据的副本。这一点也是 NumPy 数组切片和 Python 列表切片的不同之处

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
In [52]: print(x2)
[[12  5  2  4]
 [ 7  6  8  8]
 [ 1  6  7  7]]

In [53]: x2_sub = x2[:2, :2]
In [55]: x2_sub[0, 0] = 99

In [56]: print(x2)
[[99  5  2  4]
 [ 7  6  8  8]
 [ 1  6  7  7]]

这种默认的处理方式实际上非常有用:它意味着在处理非常大的数据集时,可以获取或处理这些数据的片段,而不用复制底层的数据缓存

尽管数组视图有一些非常好的特性,但是在有些时候明确地复制数组里的数据或子数组也是非常有用的。可以简单的通过 copy() 方法实现

1
In [57]: x2_sub_copy = x2[:2, :2].copy()

数组的变形

1
2
3
4
5
6
In [59]: grid = np.arange(1, 10).reshape(3, 3)

In [60]: print(grid)
[[1 2 3]
 [4 5 6]
 [7 8 9]]

原始数组的大小必须和变形后数组的大小一致。如果满足这个条件,reshape 方法将会用到原始数组的一个非副本视图。但实际情况是,在非连续的数据缓存的情况下,返回非副本视图往往不可能实现

另外一种常见的变形模式是将一个一维数组转变为二维的行或列的矩阵。可以通过 reshape 方法来实现,或者更简单地在一个切片操作中利用 newaxis 关键字:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
In [19]: x = np.array([1, 2, 3])
In [20]: x.reshape((1, 3)) # 通过变形获得的行向量
Out[20]: array([[1, 2, 3]])
In [21]: x[np.newaxis, :] # 通过 newaxis 获得的行向量
Out[21]: array([[1, 2, 3]])
In [22]: x.reshape((3, 1)) # 通过变形获得的列向量
Out[22]:
array([[1],
       [2],
       [3]])
In [23]: x[:, np.newaxis] # 通过 newaxis 获得的列向量
Out[23]:
array([[1],
       [2],
       [3]])

数组拼接和分裂

  • 数组的拼接

np.concatenate 将数组元组或数组列表作为第一个参数

1
2
3
4
5
6
7
8
9
In [24]: x = np.array([1, 2, 3])
In [26]: y = np.array([3, 2, 1])

In [27]: np.concatenate([x, y])
Out[27]: array([1, 2, 3, 3, 2, 1])

In [28]: z = [99, 99, 99]
In [29]: print(np.concatenate([x, y, z]))
[ 1  2  3  3  2  1 99 99 99]

二维数组的拼接

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
In [32]: grid = np.array([[1, 2, 3],
                          [4, 5, 6]])
In [33]: np.concatenate([grid, grid]) # 沿着第一个轴拼接
Out[33]:
array([[1, 2, 3],
       [4, 5, 6],
       [1, 2, 3],
       [4, 5, 6]])
In [34]: np.concatenate([grid, grid], axis=1) # 沿着第二个轴拼接
Out[34]:
array([[1, 2, 3, 1, 2, 3],
       [4, 5, 6, 4, 5, 6]])

沿着固定维度处理数组时,使用 np.vstack(垂直栈)和 np.hstack(水平栈) 函数会更简洁

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
In [35]: x = np.array([1, 2, 3])
In [36]: grid = np.array([[9, 8, 7],
                          [6, 5, 4]])
In [37]: np.vstack([x, grid]) # 垂直栈数组
Out[37]:
array([[1, 2, 3],
       [9, 8, 7],
       [6, 5, 4]])
In [38]: y = np.array([[99],
                       [99]])
In [39]: np.hstack([grid, y]) # 水平栈数组
Out[39]:
array([[ 9,  8,  7, 99],
       [ 6,  5,  4, 99]])
  • 数组的分裂

分裂可以通过 np.split、np.hsplit 和 np.vsplit 函数实现。可以向以上函数传递一个索引列表作为参数,索引列表记录的是分裂点位置

1
2
3
4
In [40]: x = [1, 2, 3, 99, 99, 3, 2, 1]
In [41]: x1, x2, x3 = np.split(x, [3, 5])
In [42]: print(x1, x2, x3)
[1 2 3] [99 99] [3 2 1]

值得注意的是,N 分裂点会得到 N + 1 个子数组。

 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
In [73]: grid = np.arange(16).reshape((4, 4))
In [75]: grid
Out[75]:
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11],
       [12, 13, 14, 15]])

In [76]: upper, lower = np.vsplit(grid, [2])
In [77]: print(upper)
[[0 1 2 3]
 [4 5 6 7]]
In [78]: print(lower)
[[ 8  9 10 11]
 [12 13 14 15]]

In [79]: left, right = np.hsplit(grid, [2])
In [80]: print(left)
[[ 0  1]
 [ 4  5]
 [ 8  9]
 [12 13]]
In [81]: print(right)
[[ 2  3]
 [ 6  7]
 [10 11]
 [14 15]]

NumPy 数组的计算:通用函数

NumPy 数组的计算有时非常快,有时也非常慢。使 Numpy 变快的关键是利用向量化操作,通常在 NumPy 的通用函数中实现。

通用函数

NumPy 为很多类型的操作提供了非常方便的、静态类型的、可编译程序的接口,也被称作向量操作。可以通过简单地对数组执行操作来实现,这里对数组的操作将会被用于数组中的每一个元素。这种向量方法被用于将循环推送至 NumPy 之下的编译层,这样会取得更快的执行效率。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
In [88]: np.random.seed(0)
       def compute_reciprocals(values):
           output = np.empty(len(values))
           for i in range(len(values)):
              output[i] = 1.0 / values[i]
           return output

In [89]: big_array = np.random.randint(1, 100, size=1000000)

In [91]: %timeit compute_reciprocals(big_array)
2.2 s ± 3.98 ms per loop

In [93]: %timeit (1.0 / big_array)
2.33 ms ± 22.2 µs per loop

NumPy 中的向量操作是通过通用函数实现的。通用函数的主要目的是对 NumPy 数组中的值执行更快的重复操作。它非常灵活,可以进行标量和数组的运算,也可以对两个数组进行运算:

1
2
In [96]: np.arange(5) / np.arange(1, 6)
Out[96]: array([0.        , 0.5       , 0.66666667, 0.75      , 0.8       ])

通用函数也可以进行多维数组的运算:

1
2
3
4
5
6
7
In [97]: x = np.arange(9).reshape((3, 3))

In [98]: 2 ** x
Out[98]:
array([[  1,   2,   4],
       [  8,  16,  32],
       [ 64, 128, 256]])

数组的运算

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
In [100]: x = np.arange(4)
In [101]: print("x = ", x)
x =  [0 1 2 3]
In [102]: print("x + 5 = ", x + 5)
x + 5 =  [5 6 7 8]
In [103]: print("x - 5 = ", x - 5)
x - 5 =  [-5 -4 -3 -2]
In [104]: print("x * 2 = ", x * 2)
x * 2 =  [0 2 4 6]
In [105]: print("x / 2 = ", x / 2)
x / 2 =  [0.  0.5 1.  1.5]
In [106]: print("x // 2 = ", x // 2)
x // 2 =  [0 0 1 1]
In [107]: print("-x = ", -x)
-x =  [ 0 -1 -2 -3]
In [108]: print("x ** 2 = ", x ** 2)
x ** 2 =  [0 1 4 9]
In [109]: print("x % 2 = ", x % 2)
x % 2 =  [0 1 0 1]
In [110]: -(0.5 * x + 1) ** 2
Out[110]: array([-1.  , -2.25, -4.  , -6.25])

所有这些算数运算符都是 NumPy 内置函数的简单封装器

np.add: 加法, np.subtract: 减法, np.negative: 负数运算, np.multiply: 乘法, np.divide: 除法, np.floor_divide: 向下整除, np.power: 指数运算, np.mod 模/余数

绝对值

1
2
3
4
5
In [111]: x = np.array([-2, -1, 0, 1, 2])
In [112]: abs(x)
Out[112]: array([2, 1, 0, 1, 2])
In [113]: np.abs(x)
Out[113]: array([2, 1, 0, 1, 2])

指数

1
2
3
4
5
In [121]: x = [1, 2, 3]
In [122]: print("2^x = ", np.exp2(x))
2^x =  [2. 4. 8.]
In [123]: print("3^x = ", np.power(3, x))
3^x =  [ 3  9 27]

点乘

 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
35
36
37
38
39
40
41
42
43
44
45
In [22]: a = np.array([[1,2],
    ...:               [3,4]])
    ...: b = np.array([[5,6],
    ...:               [7,8]])

In [23]: a.dot(b)
Out[23]:
array([[19, 22],
       [43, 50]])

In [24]: np.dot(a, b)
Out[24]:
array([[19, 22],
       [43, 50]])

In [25]: b.dot(a)
Out[25]:
array([[23, 34],
       [31, 46]])

In [26]: a = np.array([1, 2, 3])

In [27]: b = np.array([3, 2, 1])

In [28]: a.dot(b)
Out[28]: 10

In [29]: b.dot(a)
Out[29]: 10

In [59]: a = a.reshape(1, 3)
In [60]: a
Out[60]: array([[1, 2, 3]])
In [61]: b = b.reshape(3, 1)
In [62]: b
Out[62]:
array([[3],
       [2],
       [1]])

In [63]: a.dot(b)
Out[63]: array([[10]])

In [64]: a.dot(b).shape
Out[64]: (1, 1)

高级的通用函数特性

  • 指定输出

在进行大量运算时,有时候指定一个用于存放运算结果的数组是非常有用的。不同于创建临时数组,你可以用这个特性将计算结果直接写入到你期望的存储位置。所有的通用函数都可以通过 out 参数来指定计算结果的存放位置:

1
2
3
4
5
6
In [2]: x = np.arange(5)
In [3]: y = np.empty(5)
In [4]: np.multiply(x, 10, out=y)
Out[4]: array([ 0., 10., 20., 30., 40.])
In [5]: print(y)
[ 0. 10. 20. 30. 40.]

这个特性也可以被用作数组视图,例如可以将计算结果写入指定数组的每隔一个元素的位置:

1
2
3
4
5
In [6]: y = np.zeros(10)
In [7]: np.power(2, x, out=y[::2])
Out[7]: array([ 1.,  2.,  4.,  8., 16.])
In [8]: print(y)
[ 1.  0.  2.  0.  4.  0.  8.  0. 16.  0.]

对于较大的数组,通过谨慎使用 out 参数将能有效节约内存

  • 聚合

对 add 通用函数调用 reduce 方法会返回数组中所有元素的和

1
2
3
In [9]: x = np.arange(1, 6)
In [11]: np.add.reduce(x)
Out[11]: 15

同样,对 multiply 通用函数调用 reduce 方法会返回数组中所有元素的乘积:

1
2
In [12]: np.multiply.reduce(x)
Out[12]: 120

如果需要存储每次计算的中间结果,可以使用 accumulate:

1
2
3
4
In [14]: np.add.accumulate(x)
Out[14]: array([ 1,  3,  6, 10, 15])
In [15]: np.multiply.accumulate(x)
Out[15]: array([  1,   2,   6,  24, 120])

注意,在一些特殊情况下,NumPy 提供了专用的函数(np.sum、np.prod、np.cumsum、np.cumprod),它们也可以实现以上 reduce 的功能。

  • 外积

任何通用函数都可以用 outer 方法获得两个不同输入数组所有元素对的函数运算结果,这意味着你可以用一行代码实现一个乘法表:

1
2
3
4
5
6
7
8
In [16]: x = np.arange(1, 6)
In [17]: np.multiply.outer(x, x)
Out[17]:
array([[ 1,  2,  3,  4,  5],
       [ 2,  4,  6,  8, 10],
       [ 3,  6,  9, 12, 15],
       [ 4,  8, 12, 16, 20],
       [ 5, 10, 15, 20, 25]])

聚合:最大值、最小值和其他值

  • 数组值求和
1
2
3
4
5
In [24]: x = np.random.randint(0, 10, 5)
In [25]: x
Out[25]: array([3, 6, 2, 6, 6])
In [26]: sum(x)
Out[26]: 23

它的语法和 NumPy 的 sum 函数非常相似,并且在这个简单的例子中的结果也是一样的:

1
2
In [27]: np.sum(x)
Out[27]: 23

但是,因为 NumPy 的 sum 函数在编译码中执行操作,所以 NumPy 的操作计算得更快一些:

1
2
3
4
5
6
7
In [29]: big_array = np.random.randint(1, 1000000, 1000000)

In [31]: %timeit sum(big_array)
83.1 ms ± 284 µs per loop

In [32]: %timeit np.sum(big_array)
443 µs ± 5.19 µs per loop
  • 最小值和最大值

同样,Python 也有内置的 min 函数和 max 函数,分别被用于给定数组的最小值和最大值:

1
2
In [35]: min(big_array), max(big_array)
Out[35]: (2, 999999)

NumPy 对应的函数也有类似的语法,并且也执行得更快:

1
2
In [36]: np.min(big_array), np.max(big_array)
Out[36]: (2, 999999)

当你操作 NumPy 数组时,确保你执行的是 NumPy 版本的聚合

  • 多维度聚合 一种常用的聚合操作是沿着一行或一列聚合。例如,假设你有一些数据存储在二维数组中:
1
2
3
4
5
6
In [37]: M = np.random.randint(1, 100, (3, 4))
In [38]: M
Out[38]:
array([[82, 71, 12, 18],
       [81, 96, 75, 23],
       [87, 13, 72, 30]])

默认情况下,每一个 NumPy 聚合函数将会返回对整个数组的聚合结果:

1
2
In [39]: M.sum()
Out[39]: 660

聚合函数还有一个参数,用于指定沿着哪个的方向进行聚合。例如,可以通过指定 axis=0 找到每一列的最小值:

1
2
In [40]: M.min(axis=0)
Out[40]: array([81, 13, 12, 18])

这个函数返回四个值,对应四列数字的计算值

同样,也可以找到每一行的最大值:

1
2
In [42]: M.max(axis=1)
Out[42]: array([82, 96, 87])

axis 关键字指定的是数组将会被折叠的维度,而不是将要返回的维度。因此指定 axis=0 意味着第一个轴将要被折叠--对于二维数组,这意味着每一列的值都将被聚合

NumPy 中其它可用的聚合函数

函数名称 描述
np.prod 计算元素的积
np.mean 计算元素的平均值
np.std 计算元素的标准差
np.var 计算元素的方差
np.argmin 找出最小值的索引
np.argmax 找出最大值的索引
np.median 计算元素的中位数
np.any 验证是否存在元素为真
np.all 验证所有元素是否为真

数组的计算:广播

NumPy 可以通过通用函数的向量化操作来减少缓慢的 Python 循环,另外一种向量化操作的方法是利用 NumPy 的广播功能。广播可以简单理解为用于不同大小数组的二元通用函数(加、减、乘等)的一组规则

广播的介绍

对于同样大小的数组,二元运算符是对相应元素逐个计算:

1
2
3
4
In [44]: a = np.array([0, 1, 2])
In [45]: b = np.array([5, 5, 5])
In [46]: a + b
Out[46]: array([5, 6, 7])

广播允许这些二元运算符可以用于不同大小的数组。例如,可以简单地将一个标量(可以认为是一个零维的数组)和一个数组相加:

1
2
In [47]: a + 5
Out[47]: array([5, 6, 7])

我们可以认为这个操作是将数值 5 扩展或重复至数组 [5, 5, 5],然后执行加法。NumPy 广播功能的好处是,这种对值的重复实际上并没有发生,但是这是一种很好用的理解广播的模型。

我们同样可以将这个原理扩展到更高维度的数组。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
In [48]: M = np.ones((3, 3))
In [49]: M
Out[49]:
array([[1., 1., 1.],
       [1., 1., 1.],
       [1., 1., 1.]])

In [50]: M + a
Out[50]:
array([[1., 2., 3.],
       [1., 2., 3.],
       [1., 2., 3.]])

这里这个一维数组就被扩展或者广播来。它沿着第二个维度扩展,扩展到匹配 M 数组的形状

更复杂的情况会涉及对两个数组的同时广播

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
In [51]: a = np.arange(3)
In [52]: b = np.arange(3)[:, np.newaxis]
In [53]: print(a)
[0 1 2]
In [54]: print(b)
[[0]
 [1]
 [2]]

In [55]: a + b
Out[55]:
array([[0, 1, 2],
       [1, 2, 3],
       [2, 3, 4]])

这里将 a 和 b 都进行来扩展来匹配一个公共的形状,最终的结果是一个二维数组

广播的规则

NumPy 的广播遵循一组严格的规则,设定这组规则是为了决定两个数组间的操作

  • 规则 1:如果两个数组的维度数不相同,那么小维度数组的形状将会在最左边补 1
  • 规则 2:如果两个数组的形状在任何一个维度上都不匹配,那么数组的形状会沿着维度为 1 的维度扩展以匹配到另外一个数组的形状
  • 规则 3:如果两个数组的形状在任何一个维度上都不匹配并且没有任何一个维度等于 1,那么会引发异常

比较、掩码和布尔逻辑

和通用函数类似的比较操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
In [56]: x = np.array([1, 2, 3, 4, 5])

In [57]: x < 3
Out[57]: array([ True,  True, False, False, False])

In [58]: x == 3
Out[58]: array([False, False,  True, False, False])

In [59]: x = np.random.randint(1, 10, (2, 2))
In [60]: x
Out[60]:
array([[4, 8],
       [5, 7]])

In [61]: x < 6
Out[61]:
array([[ True, False],
       [ True, False]])
运算符 对应的通用函数
== np.equal
!= np.not_equal
< np.less
<= np.less_equal
> np.greater
>= np.greater_equal

操作布尔数组

 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
In [65]: x
Out[65]:
array([[5, 0, 3, 3],
       [7, 9, 3, 5],
       [2, 4, 7, 6]])

In [66]: np.count_nonzero(x < 6) # 有多少值小于 6?
Out[66]: 8

In [68]: np.sum(x < 6)
Out[68]: 8

In [69]: np.sum(x < 6, axis=1) # 每行有多少值小于 6?
Out[69]: array([4, 2, 2])

In [70]: np.any(x > 8) # 有没有值大于 8?
Out[70]: True
In [71]: np.any(x < 0) # 有没有值小于 0?
Out[71]: False

In [72]: np.all(x < 10) # 是否所有值都小于 10?
Out[72]: True
In [73]: np.all(x == 6) # 是否所有值都等于 6?
Out[73]: False

In [74]: np.all(x < 8, axis=1) # 是否每行的所有值都小于 8?
Out[74]: array([ True, False,  True])

注意 Python 内置的 sum()、any() 和 all() 函数和 np.sum()、np.any() 和 np.all() 函数不要混用,如果在多维数组上混用这两个版本,会导致失败或产生不可预知的错误结果

逐位逻辑运算符

1
2
3
4
5
In [75]: np.sum((5 <= x) & (x <= 7))
Out[75]: 5

In [76]: np.sum(~((5 <= x) & (x <= 7)))
Out[76]: 7

逐位的布尔运算符和其对应的通用函数

运算符 对应的通用函数
& np.bitwise_and
| np.bitwise_or
^ np.bitwise_xor
~ np.bitwise_not

将布尔数组作为掩码

假设我们希望抽取数组中所有小于 5 的元素

1
2
3
4
5
In [77]: x < 5
Out[77]:
array([[False,  True,  True,  True],
       [False, False,  True, False],
       [ True,  True, False, False]])

为了将这些值从数组中选出,可以进行简单的索引,即掩码操作:

1
2
In [78]: x[x < 5]
Out[78]: array([0, 3, 3, 3, 2, 4])

现在返回了一个一维数组,它包含了所有满足条件的值。换句话说,所有的这些值是掩码数组对应位置为 True 的值

关键字 and/or 与使用逻辑操作运算符 &/| 的区别:and 和 or 判断整个对象是真或假,而 & 和 | 是指每个对象中的比特位

花哨的索引

探索花哨的索引

花哨的索引和前面那些简单的索引非常类似,但是传递的是索引数组,而不是单个标量。花哨的索引让我们能够获得并修改复杂的数组值的子数据集

1
2
3
4
In [79]: rand = np.random.RandomState(42) # 42 为随机数种子
In [4]: x = rand.randint(100, size=10)
In [5]: print(x)
[51 92 14 71 60 20 82 86 74 74]

假设我们希望获得三个不同的元素,可以用以下方式实现:

1
2
In [6]: [x[3], x[7], x[2]]
Out[6]: [71, 86, 14]

另外一种方法是通过传递索引的单个列表或数组来获得同样的结果:

1
2
3
In [7]: ind = [3, 7, 4]
In [8]: x[ind]
Out[8]: array([71, 86, 60])

利用花哨的索引,结果的形状与索引数组的形状一致,而不是与被索引数组的形状一致:

1
2
3
4
5
6
In [10]: ind = np.array([[3, 7], 
                         [4, 5]])
In [11]: x[ind]
Out[11]:
array([[71, 86],
       [60, 20]])

花哨的索引也对多个维度适用。假设我们有以下数组:

1
2
3
4
5
6
In [12]: x = np.arange(12).reshape(3, 4)
In [13]: x
Out[13]:
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])

和标准的索引方式一样,第一个索引指的是行,第二个索引指的是列:

1
2
3
4
In [14]: row = np.array([0, 1, 2])
In [15]: col = np.array([2, 1, 3])
In [16]: x[row, col]
Out[16]: array([ 2,  5, 11])

这里需要注意,结果的第一个值是 x[0, 2],第二个值是 x[1, 1],第三个值是 x[2, 3]。在花哨的索引中,索引值的配对遵循广播的规则。因此当我们将一个列向量和一个行向量组合在一个索引中时,会得到一个二维的结果

1
2
3
4
5
In [17]: x[row[:, np.newaxis], col]
Out[17]:
array([[ 2,  1,  3],
       [ 6,  5,  7],
       [10,  9, 11]])

这里,每一行的值都与每一列的向量配对

花哨的索引返回的值反映的是广播后的索引数组的形状,而不是被索引数组的形状

组合索引

花哨的索引可以和其他索引方案结合起来形成更强大的索引操作:

可以将花哨的索引和简单的索引组合使用:

1
2
In [18]: x[2, [2, 0, 1]]
Out[18]: array([10,  8,  9])

也可以将花哨的索引和切片组合使用:

1
2
3
4
In [19]: x[1:, [2, 0, 1]]
Out[19]:
array([[ 6,  4,  5],
       [10,  8,  9]])

更可以将花哨的索引和掩码组合使用

1
2
3
4
5
6
7
In [20]: mask = np.array([1, 0, 1, 0], dtype=bool)

In [21]: x[row[:, np.newaxis], mask]
Out[21]:
array([[ 0,  2],
       [ 4,  6],
       [ 8, 10]])

用花哨的索引修改值

1
2
3
4
5
6
In [22]: x = np.arange(10)
In [23]: i = np.array([2, 1, 8, 4])

In [24]: x[i] = 99
In [25]: print(x)
[ 0 99 99  3 99  5  6  7 99  9]

可以用任何的赋值操作来实现,例如:

1
2
3
In [26]: x[i] -= 10
In [27]: print(x)
[ 0 89 89  3 89  5  6  7 89  9]

不过需要注意,操作中重复的索引会导致一些出乎意料的结果产生,如下例子所示:

1
2
3
4
In [28]: x = np.zeros(10)
In [29]: x[[0, 0]] = [4, 6]
In [30]: print(x)
[6. 0. 0. 0. 0. 0. 0. 0. 0. 0.]

4 去哪里了呢?这个操作首先赋值 x[0]=4,然后赋值 x[0] = 6,因此当然 x[0] 的值为 6.以上还算合理,但是设想以下操作

1
2
3
4
5
In [31]: i = [2, 3, 3, 4, 4, 4]
In [32]: x[i] += 1

In [33]: x
Out[33]: array([6., 0., 1., 1., 1., 0., 0., 0., 0., 0.])

你可能期望 x[3] 的值为 2,x[4] 的值为 3,因为这是这些索引值重复的次数。但是为什么结果不同于我们的预想呢?从概念的角度理解,这是因为 x[i] += 1 是 x[i] = x[i] + 1 的简写。x[i] + 1 计算后,这个结果被赋值给了 x 相应的索引值。记住这个原理后,我们却发现数组并没有发生多次累加,而是发生了赋值,显然这不是我们希望的结果

因此,如果你希望累加,该怎么做呢?可以借助通用函数中的 at() 方法来实现:

1
2
3
4
In [35]: x = np.zeros(10)
In [36]: np.add.at(x, i, 1)
In [37]: print(x)
[0. 0. 1. 2. 3. 0. 0. 0. 0. 0.]

at() 函数在这里对给定的操作、给定的索引(这里是 i)以及给定的值(这里是 1)执行的是就地操作。另一个可以实现该功能的类似方法是通用函数中的 reduceat() 函数

数组的排序

np.sort 和 np.argsort

如果想在不修改原始输入数组的基础上返回一个排好序的数组,可以使用 np.sort:

1
2
3
In [38]: x = np.array([2, 1, 4, 3, 5])
In [39]: np.sort(x)
Out[39]: array([1, 2, 3, 4, 5])

如果希望用排好序的数组替代原始数组,可以使用数组的 sort 方法:

1
2
3
In [40]: x.sort()
In [41]: print(x)
[1 2 3 4 5]

另外一个相关的函数是 argsort,该函数返回的是原始数组排好序的索引值:

1
2
3
4
In [42]: x = np.array([2, 1, 4, 3, 5])
In [43]: i = np.argsort(x)
In [44]: print(i)
[1 0 3 2 4]

以上结果的第一个元素是数组中最小元素的索引值,第二个值给出的是次小元素的索引值,以此类推。这些索引值可以被用于(通过花哨的索引)创建有序的数组:

1
2
In [45]: x[i]
Out[45]: array([1, 2, 3, 4, 5])

沿着行或列排序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
In [46]: rand = np.random.RandomState(42)
In [47]: x = rand.randint(0, 10, (4, 6))
In [48]: print(x)
[[6 3 7 4 6 9]
 [2 6 7 4 3 7]
 [7 2 5 4 1 7]
 [5 1 4 0 9 5]]
In [49]: np.sort(x, axis=0)
Out[49]:
array([[2, 1, 4, 0, 1, 5],
       [5, 2, 5, 4, 3, 7],
       [6, 3, 7, 4, 6, 7],
       [7, 6, 7, 4, 9, 9]])
In [50]: np.sort(x, axis=1)
Out[50]:
array([[3, 4, 6, 6, 7, 9],
       [2, 3, 4, 6, 7, 7],
       [1, 2, 4, 5, 7, 7],
       [0, 1, 4, 5, 5, 9]])

需要记住的是,这种处理方式是将行或列当作独立的数组,任何行或列的值之间的关系将会丢失!

部分排序:分隔

1
2
3
In [51]: x = np.array([7, 2, 3, 1, 6, 5, 4])
In [52]: np.partition(x, 3)
Out[52]: array([2, 1, 3, 4, 6, 5, 7])

请注意,结果数组中前三个值是数组中最小的三个值,剩下的位置是原始数组剩下的值。在这两个分隔区间中,元素都是任意排列的

与排序类似,也可以沿着多维数组任意的轴进行分隔:

1
2
3
4
5
6
In [58]: np.partition(x, 2, axis=1)
Out[58]:
array([[3, 4, 6, 7, 6, 9],
       [2, 3, 4, 7, 6, 7],
       [1, 2, 4, 5, 7, 7],
       [0, 1, 4, 5, 9, 5]])

输出结果是一个数组,该数组每一行的前两个元素是该行最小的两个值,每行的其他值分布在剩下的位置

最后,正如 np.argsort 函数计算的是排序的索引值,也有一个 np.argpartition 函数计算的是分隔的索引值

注意

如果使用了 numpy,要注意 numpy 的数据类型例如 int64, float64 是不支持序列化,还有就是在传递参数的时候也要注意数据类型,不然可能会导致很奇怪,很难发现的错误