NumPy 是 Python 数值计算非常重要的一个包。很多科学计算包都是以 NumPy 的数组对象为基础开发的。

本文用代码快速过了一遍 NumPy 的基本操作,对 NumPy 整体有一个把握。希望能对读者有帮助。

NumPy ndarray:多维数组对象

ndarray 是 NumPy 很重要的特性,它是快速而灵活的数据集容器。ndarray 可以在整个数据集上进行类似于标量的运算,既快速又方便。

在 Jupyter Notebook 上,首先引入 NumPy 包:

1 import numpy as np

创建 ndarray,其中包含随机值(np.random.randn):

1 np.random.seed(666)  # 设置随机种子
2 data = np.random.randn(2, 3)
3 data
array([[ 0.82418808,  0.479966  ,  1.17346801],
       [ 0.90904807, -0.57172145, -0.10949727]])

进行一些运算(乘法和加法):

1 data * 10
2 data + data
array([[ 8.24188083,  4.79966003, 11.73468012],
       [ 9.09048069, -5.71721452, -1.09497268]])
array([[ 1.64837617,  0.95993201,  2.34693602],
       [ 1.81809614, -1.1434429 , -0.21899454]])

ndarray 所有的数据必须是相同类型,其属性 shape 表示形状大小,dtype 表示该数组的数据类型。

1 data.shape
2 data.dtype
(2, 3)
dtype('float64')

ndarray 的创建

最简单的创建方法是使用 array 函数。该函数根据接收的序列对象,生成新的 NumPy 数组。

1 data1 = [6, 66, 666, 6666, 66666]
2 arr1 = np.array(data1)
3 arr1
1 arr2.shape
2 arr2.ndim

 

array([    6,    66,   666,  6666, 66666])

嵌套的序列(比如说嵌套列表),可以生成多维的数组。

1 data2 = [[6, 66, 666], [6666, 66666, 666666]]
2 arr2 = np.array(data2)
3 arr2
array([[     6,     66,    666],
       [  6666,  66666, 666666]])

前面介绍了 ndarray 的属性 shape 表示形状大小,它还有一个属性 ndim 表示维度大小,我们可以对比两者的区别。

1 arr2.shape
2 arr2.ndim
(2, 3)
2

除了 np.array,还有一些函数可以创建新数组:zerosonesempty 等等。(注意到 empty 不一定返回零值,某些情况它可能返回未初始化的不确定值

1 np.zeros(6)
2 np.ones([2, 3])
3 np.empty((2, 3, 2))
array([0., 0., 0., 0., 0., 0.])
Out[26]:
array([[1., 1., 1.],
       [1., 1., 1.]])
Out[26]:
array([[[0.00000000e+000, 0.00000000e+000],
        [2.96439388e-323, 0.00000000e+000],
        [2.14321575e-312, 1.58817677e-052]],

       [[6.50119216e-091, 1.74483574e-076],
        [1.28392645e+160, 1.35027265e+161],
        [3.99910963e+252, 9.02421558e-309]]])

np.arange 类似于 Python 的 range 函数,只不过它生成的是 ndarray。

1 np.arange(6)
2 np.array(range(6))  # arange 等价于该效果
array([0, 1, 2, 3, 4, 5])
array([0, 1, 2, 3, 4, 5])

ndarray 的数据类型

前面介绍的 dtype 可以在创建时指定:

1 arr1 = np.array([6, 66, 666], dtype=np.float64)
2 arr2 = np.array([6, 66, 666], dtype=np.int32)
3 arr1.dtype
4 arr2.dtype
dtype('float64')
dtype('int32')

一些典型的数据类型有:uint8、int32、float64、float128、complex64、bool、object、string_、unicode_ 等。

使用 astype 可以对数据类型进行显式转换。

1 arr = np.array([6, 66, 666])
2 arr.dtype
3 float_arr = arr.astype(np.float64)
4 float_arr.dtype
dtype('int64')
dtype('float64')

浮点数数组转为整型数组:

1 arr = np.array([1.7, 2.7, 3.7])
2 arr
3 arr.astype(np.int32)
array([1.7, 2.7, 3.7])
array([1, 2, 3], dtype=int32)

字符串数组转为浮点数数组(float 是 Python 类型,NumPy 自动解析识别):

1 numeric_strings = np.array(['1.7', '-2.7', '3.7'], dtype=np.string_)
2 float_arr = numeric_strings.astype(float)
3 float_arr
4 float_arr.dtype
array([ 1.7, -2.7,  3.7])
dtype('float64')

使用其他数组的 dtype 属性来进行转换:

1 int_arr = np.arange(6)
2 int_arr
3 float_arr = np.array([6., 66., 666.], dtype=np.float64)
4 int_arr.astype(float_arr.dtype)
array([0, 1, 2, 3, 4, 5])
array([0., 1., 2., 3., 4., 5.])

ndarray 的算术运算

ndarray 的算术运算可以避免写 for 循环,这既方便,性能也好。

数组与相同大小的数组之间的运算:

1 arr = np.array([[1., 2., 3.], [4., 5., 6.]])
2 arr
3 arr * arr
4 arr - arr
array([[1., 2., 3.],
       [4., 5., 6.]])
array([[ 1.,  4.,  9.],
       [16., 25., 36.]])
array([[0., 0., 0.],
       [0., 0., 0.]])

数组与标量的运算,会按数组的元素一一进行计算。

1 1 / arr
2 arr ** 0.5
array([[1.        , 0.5       , 0.33333333],
       [0.25      , 0.2       , 0.16666667]])
array([[1.        , 1.41421356, 1.73205081],
       [2.        , 2.23606798, 2.44948974]])

相同大小的数组之间的比较运算,会返回布尔值的数组:

1 arr2 = np.array([[0., 4., 1.], [7., 2., 12.]])
2 arr2
3 arr
4 arr2 > arr
array([[ 0.,  4.,  1.],
       [ 7.,  2., 12.]])
array([[1., 2., 3.],
       [4., 5., 6.]])
array([[False,  True, False],
       [ True, False,  True]])

索引和切片

一维数组很简单,看起来类似于 Python 列表:

1 arr = np.arange(10)
2 arr
3 arr[5]
4 arr[5:8]
5 arr[5:8] = 12
6 arr
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])-
5
array([5, 6, 7])
array([ 0,  1,  2,  3,  4, 12, 12, 12,  8,  9])

第 5 行用到了 ndarray 的 broadcast 机制,这是和原生 Python 列表的第一个区别,我们可以看到列表会报错:

1 li = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
2 li
3 li[5]
4 li[5:8]
5 li[5:8] = 12
6 li
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
5
[5, 6, 7]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-43-b59603238145> in <module>
      3 li[5]
      4 li[5:8]
----> 5 li[5:8] = 12
      6 li

TypeError: can only assign an iterable

ndarray 的切片(或索引)是一个指针(NumPy 称之为视图 view),不会复制底层数据,修改切片会直接影响到数组本身。

1 arr_slice = arr[5:8]
2 arr_slice
3 arr_slice[1] = 666
4 arr
array([12, 12, 12])
array([  0,   1,   2,   3,   4,  12, 666,  12,   8,   9])

切片 [:] 的赋值会影响到所有元素:

1 arr_slice[:] = 777
2 arr
array([  0,   1,   2,   3,   4, 777, 777, 777,   8,   9])

由于 NumPy 经常和大型数据集打交道,因此切片是指针并不奇怪。如果希望不影响原数组,应该使用 copy 方法

1 arr_slice = arr[5:8].copy()
2 arr_slice[:] = 666
3 arr
array([  0,   1,   2,   3,   4, 777, 777, 777,   8,   9])

在二维数组中,每个索引代表的元素不再是标量。要找到每个元素有两种方法。

1 arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
2 arr2d[2]
3 # 法 1
4 arr2d[0][2]
5 # 法 2
6 arr2d[0, 2]
array([7, 8, 9])
3
3

在这样的二维数组中,axis 0 可以理解为,axis 1 可以理解为

三维数组的使用示例:

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

       [[ 7,  8,  9],
        [10, 11, 12]]])
array([[1, 2, 3],
       [4, 5, 6]])

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

1 old_values = arr3d[0].copy()
2 arr3d[0] = 666
3 arr3d
4 arr3d[0] = old_values
5 arr3d
array([[[666, 666, 666],
        [666, 666, 666]],

       [[  7,   8,   9],
        [ 10,  11,  12]]])
array([[[ 1,  2,  3],
        [ 4,  5,  6]],

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

用切片进行索引

1 arr
2 arr[1:6]
3 arr2d
4 arr2d[:2]
5 arr2d[:2, 1:]
array([  0,   1,   2,   3,   4, 777, 777, 777,   8,   9])
array([  1,   2,   3,   4, 777])
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])
array([[1, 2, 3],
       [4, 5, 6]])
array([[2, 3],
       [5, 6]])

注意,如果使用切片作为索引,得到的总是相同维度的视图,而如果使用索引,则必定得到的是低维的数组

1 arr2d
2 # 使用切片
3 arr2d[1:2, :2]
4 # 使用了索引
5 arr2d[1, :2]
6 arr2d[:2, 2]
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])
array([[4, 5]])
array([4, 5])
array([3, 6])

布尔索引

首先生成一些数据:

1 names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
2 np.random.seed(666)
3 data = np.random.randn(7, 4)
4 names
5 data
array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'], dtype='<U4')
array([[ 0.82418808,  0.479966  ,  1.17346801,  0.90904807],
       [-0.57172145, -0.10949727,  0.01902826, -0.94376106],
       [ 0.64057315, -0.78644317,  0.60886999, -0.93101185],
       [ 0.97822225, -0.73691806, -0.29873262, -0.46058737],
       [-1.08879299, -0.57577075, -1.68290077,  0.22918525],
       [-1.75662522,  0.84463262,  0.27721986,  0.85290153],
       [ 0.1945996 ,  1.31063772,  1.5438436 , -0.52904802]])

假设每个 name 对应着 data 中的每一行,我们希望选择 Bob 有关的数据。

1 names == 'Bob'
2 data[names == 'Bob']
array([ True, False, False,  True, False, False, False])
array([[ 0.82418808,  0.479966  ,  1.17346801,  0.90904807],
       [ 0.97822225, -0.73691806, -0.29873262, -0.46058737]])

可以看到,选择了第 0 行和第 3 行的数据。需要注意的是布尔数组的长度必须和该维度的长度相同,否则会报错。

1 np.array([ True, False, False,  True, False, False, False])[:-1]
2 data[np.array([ True, False, False,  True, False, False, False])[:-1]]
array([ True, False, False,  True, False, False])
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-72-07628751d568> in <module>
      1 np.array([ True, False, False,  True, False, False, False])[:-1]
----> 2 data[np.array([ True, False, False,  True, False, False, False])[:-1]]

IndexError: boolean index did not match indexed array along dimension 0; dimension is 7 but corresponding boolean dimension is 6

布尔索引结合切片或索引的情况:

1 data[names == 'Bob', 2:]
2 data[names == 'Bob', 3]
array([[ 1.17346801,  0.90904807],
       [-0.29873262, -0.46058737]])
array([ 0.90904807, -0.46058737])

要取反,可以使用 != 或者 ~:

1 data [names != 'Bob']
2 data[~(names == 'Bob')]
array([[-0.57172145, -0.10949727,  0.01902826, -0.94376106],
       [ 0.64057315, -0.78644317,  0.60886999, -0.93101185],
       [-1.08879299, -0.57577075, -1.68290077,  0.22918525],
       [-1.75662522,  0.84463262,  0.27721986,  0.85290153],
       [ 0.1945996 ,  1.31063772,  1.5438436 , -0.52904802]])
array([[-0.57172145, -0.10949727,  0.01902826, -0.94376106],
       [ 0.64057315, -0.78644317,  0.60886999, -0.93101185],
       [-1.08879299, -0.57577075, -1.68290077,  0.22918525],
       [-1.75662522,  0.84463262,  0.27721986,  0.85290153],
       [ 0.1945996 ,  1.31063772,  1.5438436 , -0.52904802]])

如果要结合多个布尔条件,使用布尔运算符 &(且) 和 |(或):

1 names
2 mask = (names == 'Bob') | (names == 'Will')
3 mask
4 data[mask]
array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'], dtype='<U4')
array([ True, False,  True,  True,  True, False, False])
array([[ 0.82418808,  0.479966  ,  1.17346801,  0.90904807],
       [ 0.64057315, -0.78644317,  0.60886999, -0.93101185],
       [ 0.97822225, -0.73691806, -0.29873262, -0.46058737],
       [-1.08879299, -0.57577075, -1.68290077,  0.22918525]])

注意:使用布尔进行索引后返回的数组总是是原数组的拷贝,而非指针,也没有 broadcast 机制。这是与索引或者切片所不同的地方。

1 data
2 data_copy = data[names == 'Bob']
3 data_copy
4 data_copy[:, :] = 666
5 data_copy
6 data
array([[ 0.82418808,  0.479966  ,  1.17346801,  0.90904807],
       [-0.57172145, -0.10949727,  0.01902826, -0.94376106],
       [ 0.64057315, -0.78644317,  0.60886999, -0.93101185],
       [ 0.97822225, -0.73691806, -0.29873262, -0.46058737],
       [-1.08879299, -0.57577075, -1.68290077,  0.22918525],
       [-1.75662522,  0.84463262,  0.27721986,  0.85290153],
       [ 0.1945996 ,  1.31063772,  1.5438436 , -0.52904802]])
array([[ 0.82418808,  0.479966  ,  1.17346801,  0.90904807],
       [ 0.97822225, -0.73691806, -0.29873262, -0.46058737]])
array([[666., 666., 666., 666.],
       [666., 666., 666., 666.]])
array([[ 0.82418808,  0.479966  ,  1.17346801,  0.90904807],
       [-0.57172145, -0.10949727,  0.01902826, -0.94376106],
       [ 0.64057315, -0.78644317,  0.60886999, -0.93101185],
       [ 0.97822225, -0.73691806, -0.29873262, -0.46058737],
       [-1.08879299, -0.57577075, -1.68290077,  0.22918525],
       [-1.75662522,  0.84463262,  0.27721986,  0.85290153],
       [ 0.1945996 ,  1.31063772,  1.5438436 , -0.52904802]])

但是注意是返回后的数组才是拷贝,可以用布尔索引来直接进行赋值,也可以使用 broadcast 机制。

1 data[names == 'Bob'] = 666
2 data
array([[ 6.66000000e+02,  6.66000000e+02,  6.66000000e+02,
         6.66000000e+02],
       [-5.71721452e-01, -1.09497268e-01,  1.90282649e-02,
        -9.43761065e-01],
       [ 6.40573153e-01, -7.86443172e-01,  6.08869993e-01,
        -9.31011849e-01],
       [ 6.66000000e+02,  6.66000000e+02,  6.66000000e+02,
         6.66000000e+02],
       [-1.08879299e+00, -5.75770746e-01, -1.68290077e+00,
         2.29185248e-01],
       [-1.75662522e+00,  8.44632619e-01,  2.77219863e-01,
         8.52901533e-01],
       [ 1.94599603e-01,  1.31063772e+00,  1.54384360e+00,
        -5.29048024e-01]])

fancy 索引

fancy 索引在 NumPy 中指的是使用整型数组来进行索引。

假设有以下一个 ndarray:

1 arr = np.empty((8, 4))
2 for i in range(8):
3     arr[i] = i
4 arr
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.]])

使用 fancy 索引可以选择行的顺序:

1 arr[[7, 6, 5, 4, 3, 2, 1, 0]]
2 arr[[-1, 0]]
array([[7., 7., 7., 7.],
       [6., 6., 6., 6.],
       [5., 5., 5., 5.],
       [4., 4., 4., 4.],
       [3., 3., 3., 3.],
       [2., 2., 2., 2.],
       [1., 1., 1., 1.],
       [0., 0., 0., 0.]])
array([[7., 7., 7., 7.],
       [0., 0., 0., 0.]])

如果传入多个索引数组的话,会针对每个既定顺序的行选择既定顺序的列,最终得到几个元素的数组。

1 arr = np.arange(32).reshape((8, 4))
2 arr
3 arr[[3, 2, 1, 0], [3, 2, 1, 0]]
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]])
array([15, 10,  5,  0])

下面示例的效果是变换数组所选行和列的顺序,而不会降维。

1 arr[[3, 2, 1, 0]][:, [3, 2, 1, 0]]
array([[15, 14, 13, 12],
       [11, 10,  9,  8],
       [ 7,  6,  5,  4],
       [ 3,  2,  1,  0]])

注意:fancy 索引同样返回的是数组的拷贝,而非指针。

数组的转置和轴的交换

转置(.T)返回的是底层数组的视图,而非数组的拷贝。

1 arr = np.arange(15). reshape((3, 5))
2 arr
3 arr.transpose()  # 转置
4 arr.T  # 更简便的写法
array([[ 0,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14]])
array([[ 0,  5, 10],
       [ 1,  6, 11],
       [ 2,  7, 12],
       [ 3,  8, 13],
       [ 4,  9, 14]])
array([[ 0,  5, 10],
       [ 1,  6, 11],
       [ 2,  7, 12],
       [ 3,  8, 13],
       [ 4,  9, 14]])

矩阵的内积计算使用 np.dot

1 arr1 = np.array([[3, -1], [1, 2]])
2 arr1
3 arr2 = np.array([[2, 1]])
4 arr2
5 np.dot(arr1, arr2.T)
array([[ 3, -1],
       [ 1,  2]])
array([[2, 1]])
array([[5],
       [4]])

transpose 可以接收元组参数来交换轴:

 1 arr = np.arange(6).reshape((2, 3))
 2 arr
 3 arr.transpose((0, 1))
 4 arr.transpose((1, 0))
 5 arr = np.arange(16).reshape((2, 2, 4))
 6 arr
 7 # 置换 axis=1 和 axis=0, axis=2 不变
 8 # 由于 axis=2 不变,那么
 9 # 设 a = [0, 1, 2, 3] b = [4, 5, 6, 7] c = [8, 9, 10, 11] d = [12, 13, 14, 15]
10 # arr = [[a, b], [c, d]]
11 # arr.transpose(1, 0) = arr.T = [[a, c], [b, d]] = [[[0, 1, 2, 3], [8, 9, 10, 11]], [[4, 5, 6, 7], [12, 13, 14, 15]]]
12 arr.transpose((1, 0, 2))  
array([[0, 1, 2],
       [3, 4, 5]])
array([[0, 1, 2],
       [3, 4, 5]])
array([[0, 3],
       [1, 4],
       [2, 5]])
array([[[ 0,  1,  2,  3],
        [ 4,  5,  6,  7]],

       [[ 8,  9, 10, 11],
        [12, 13, 14, 15]]])
array([[[ 0,  1,  2,  3],
        [ 8,  9, 10, 11]],

       [[ 4,  5,  6,  7],
        [12, 13, 14, 15]]])

swapaxes 同样可以交换轴:

1 arr
2 arr.swapaxes(1, 2)
3 arr.transpose(0, 2, 1)  # 类似于 arr.swapaxes(1, 2)
array([[[ 0,  1,  2,  3],
        [ 4,  5,  6,  7]],

       [[ 8,  9, 10, 11],
        [12, 13, 14, 15]]])
array([[[ 0,  4],
        [ 1,  5],
        [ 2,  6],
        [ 3,  7]],

       [[ 8, 12],
        [ 9, 13],
        [10, 14],
        [11, 15]]])
array([[[ 0,  4],
        [ 1,  5],
        [ 2,  6],
        [ 3,  7]],

       [[ 8, 12],
        [ 9, 13],
        [10, 14],
        [11, 15]]])

注意 swapaxes 同样返回的是数组的视图,而不是数组的拷贝。

通用函数

通用函数执行的是按元素的运算。

sqrtexp

1 arr = np.arange(10)
2 arr
3 np.sqrt(arr)
4 np.exp(arr)
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
array([0.        , 1.        , 1.41421356, 1.73205081, 2.        ,
       2.23606798, 2.44948974, 2.64575131, 2.82842712, 3.        ])
array([1.00000000e+00, 2.71828183e+00, 7.38905610e+00, 2.00855369e+01,
       5.45981500e+01, 1.48413159e+02, 4.03428793e+02, 1.09663316e+03,
       2.98095799e+03, 8.10308393e+03])

maximumminimum

1 np.random.seed(666)
2 x = np.random.randn(8)
3 x
4 y = np.random.randn(8)
5 y
6 np.maximum(x, y)
7 np.minimum(x, y)
array([ 0.82418808,  0.479966  ,  1.17346801,  0.90904807, -0.57172145,
       -0.10949727,  0.01902826, -0.94376106])
array([ 0.64057315, -0.78644317,  0.60886999, -0.93101185,  0.97822225,
       -0.73691806, -0.29873262, -0.46058737])
array([ 0.82418808,  0.479966  ,  1.17346801,  0.90904807,  0.97822225,
       -0.10949727,  0.01902826, -0.46058737])
array([ 0.64057315, -0.78644317,  0.60886999, -0.93101185, -0.57172145,
       -0.73691806, -0.29873262, -0.94376106])

典型的一元通用函数有:

  • abs, fabs
  • sqrt
  • square
  • exp
  • log, log10, log2, log1p
  • sign
  • ceil, floor, rint
  • modf
  • isnan
  • isfinite, isinf
  • cos, cosh, sin, sinh, tan, tanh
  • arccos, arccosh, arcsin, arcsinh, arctan, arctanh
  • logical_not

典型的二元通用函数有:

  • add
  • subtract
  • multiply
  • divide, floor_divide
  • power
  • maximum, fmax
  • minimum, fmin
  • mod
  • copysign
  • greater, greater_equal, less, less_equal, equal, not_equal
  • logical_and, logical_or, logical_xor

面向数组编程

直接面向数组编程可以避免使用 for 循环,这又称为向量化(vectorization),一般来说性能会好上一两个量级以上。

1 points = np.arange(-5, 5, 0.01)
2 xs, ys = np.meshgrid(points, points)
3 xs
4 ys
array([[-5.  , -4.99, -4.98, ...,  4.97,  4.98,  4.99],
       [-5.  , -4.99, -4.98, ...,  4.97,  4.98,  4.99],
       [-5.  , -4.99, -4.98, ...,  4.97,  4.98,  4.99],
       ...,
       [-5.  , -4.99, -4.98, ...,  4.97,  4.98,  4.99],
       [-5.  , -4.99, -4.98, ...,  4.97,  4.98,  4.99],
       [-5.  , -4.99, -4.98, ...,  4.97,  4.98,  4.99]])
array([[-5.  , -5.  , -5.  , ..., -5.  , -5.  , -5.  ],
       [-4.99, -4.99, -4.99, ..., -4.99, -4.99, -4.99],
       [-4.98, -4.98, -4.98, ..., -4.98, -4.98, -4.98],
       ...,
       [ 4.97,  4.97,  4.97, ...,  4.97,  4.97,  4.97],
       [ 4.98,  4.98,  4.98, ...,  4.98,  4.98,  4.98],
       [ 4.99,  4.99,  4.99, ...,  4.99,  4.99,  4.99]])
1 z = np.sqrt(xs ** 2 + ys ** 2)
2 z
array([[7.07106781, 7.06400028, 7.05693985, ..., 7.04988652, 7.05693985,
        7.06400028],
       [7.06400028, 7.05692568, 7.04985815, ..., 7.04279774, 7.04985815,
        7.05692568],
       [7.05693985, 7.04985815, 7.04278354, ..., 7.03571603, 7.04278354,
        7.04985815],
       ...,
       [7.04988652, 7.04279774, 7.03571603, ..., 7.0286414 , 7.03571603,
        7.04279774],
       [7.05693985, 7.04985815, 7.04278354, ..., 7.03571603, 7.04278354,
        7.04985815],
       [7.06400028, 7.05692568, 7.04985815, ..., 7.04279774, 7.04985815,
        7.05692568]])

这里使用 matplotlib 建立这个二维数组的数据可视化。

1 import matplotlib.pyplot as plt
2 
3 plt.imshow(z, cmap=plt.cm.gray)
4 plt.colorbar()
5 
6 plt.title('Image plot of $\sqrt{x^2 + y^2}$ for a grid of values')

带条件逻辑的数组运算

numpy.where 函数是 x if condition else y 的向量化版本。 

1 x = np.array([6, 66, 666, 6666, 66666])
2 y = np.array([7, 77, 777, 7777, 77777])
3 cond = np.array([True, False, True, True, False])
4 result = np.where(cond, x, y)
5 result
array([    6,    77,   666,  6666, 77777])

典型的使用场景:

1 np.random.seed(666)
2 arr = np.random.randn(4, 4)
3 arr
4 arr > 0
5 np.where(arr > 0, 1, -1)  # 对两种情况都赋值
6 np.where(arr > 0, arr, 0)  # 只处理小于零的情况
array([[ 0.82418808,  0.479966  ,  1.17346801,  0.90904807],
       [-0.57172145, -0.10949727,  0.01902826, -0.94376106],
       [ 0.64057315, -0.78644317,  0.60886999, -0.93101185],
       [ 0.97822225, -0.73691806, -0.29873262, -0.46058737]])
array([[ True,  True,  True,  True],
       [False, False,  True, False],
       [ True, False,  True, False],
       [ True, False, False, False]])
array([[ 1,  1,  1,  1],
       [-1, -1,  1, -1],
       [ 1, -1,  1, -1],
       [ 1, -1, -1, -1]])
array([[0.82418808, 0.479966  , 1.17346801, 0.90904807],
       [0.        , 0.        , 0.01902826, 0.        ],
       [0.64057315, 0.        , 0.60886999, 0.        ],
       [0.97822225, 0.        , 0.        , 0.        ]])

数学和统计运算

典型的聚合函数有:sum,mean,std。

1 np.random.seed(666)
2 arr = np.random.randn(6, 6)
3 arr
4 arr.mean()
5 np.mean(arr)
6 arr.sum()
7 arr.std()
array([[ 0.82418808,  0.479966  ,  1.17346801,  0.90904807, -0.57172145,
        -0.10949727],
       [ 0.01902826, -0.94376106,  0.64057315, -0.78644317,  0.60886999,
        -0.93101185],
       [ 0.97822225, -0.73691806, -0.29873262, -0.46058737, -1.08879299,
        -0.57577075],
       [-1.68290077,  0.22918525, -1.75662522,  0.84463262,  0.27721986,
         0.85290153],
       [ 0.1945996 ,  1.31063772,  1.5438436 , -0.52904802, -0.6564723 ,
        -0.2015057 ],
       [-0.70061583,  0.68713795, -0.02607576, -0.82975832,  0.29655378,
        -0.3126795 ]])
-0.03691228536732661
-0.03691228536732661
-1.328842273223758
0.81811319613762

mean 和 sum 有一个 axis 参数用于指定针对哪一行:

1 arr.mean(axis=1)
2 arr.sum(axis=0)
array([ 0.45090857, -0.23212411, -0.36376326, -0.20593112,  0.27700915,
       -0.14757295])
array([-0.3674784 ,  1.0262478 ,  1.27645117, -0.8521562 , -1.13434309,
       -1.27756353])

一些典型的统计性方法:

  • sum
  • mean
  • std, var
  • min, max
  • argmin, argmax
  • cumsum
  • cumprod

布尔数组的方法 

any 检查某数组是否有一个以上的 True,all 检查某数组是否所有值都为 True。

1 bools = np.array([False, False, True, False])
2 bools.any()
3 bools.all()
True
False

排序

1 np.random.seed(666)
2 arr = np.random.randn(6)
3 arr
4 arr.sort()
5 arr
array([ 0.82418808,  0.479966  ,  1.17346801,  0.90904807, -0.57172145,
       -0.10949727])
array([-0.57172145, -0.10949727,  0.479966  ,  0.82418808,  0.90904807,
        1.17346801])

针对某一维度进行排序:

1 np.random.seed(666)
2 arr = np.random.randn(5, 3)
3 arr
4 arr.sort(1)
5 arr
array([[ 0.82418808,  0.479966  ,  1.17346801],
       [ 0.90904807, -0.57172145, -0.10949727],
       [ 0.01902826, -0.94376106,  0.64057315],
       [-0.78644317,  0.60886999, -0.93101185],
       [ 0.97822225, -0.73691806, -0.29873262]])
array([[ 0.479966  ,  0.82418808,  1.17346801],
       [-0.57172145, -0.10949727,  0.90904807],
       [-0.94376106,  0.01902826,  0.64057315],
       [-0.93101185, -0.78644317,  0.60886999],
       [-0.73691806, -0.29873262,  0.97822225]])

查看四分位数:

1 np.random.seed(666)
2 arr = np.random.randn(1000)
3 arr.sort()
4 arr[int(0.25 * len(arr))]
5 arr[int(0.75 * len(arr))]
-0.6786323778244538
0.6879940536373379

唯一值和集合逻辑

NumPy 有一些一维数组的基本的集合运算。其中 np.unique 是非常常用的。

1 names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
2 np.unique(names)
3 ints = np.array([3, 3, 3, 2, 2, 1, 1, 4, 4])
4 np.unique(ints)
5 # 纯 Python 实现
6 sorted(set(names))
array(['Bob', 'Joe', 'Will'], dtype='<U4')
array([1, 2, 3, 4])
['Bob', 'Joe', 'Will']

np.in1d 检查两个数组值的关系。

1 values = np.array([6, 0, 0, 3, 2, 5, 6])
2 np.in1d(values, [2, 3, 6])
array([ True, False, False,  True,  True, False,  True])

数组常用的集合运算有:

  • unique(x)
  • intersect1d(x, y)
  • union1d(x, y)
  • in1d(x, y)
  • setdiff1d(x, y)
  • setxor1d(x, y)

数组的文件输入和输出

NumPy 可以用文本或者二进制的方式把数据存储到硬盘,我们一般都选用二进制的方式。

np.savenp.load 分别是以二进制存储和加载的函数。

1 arr = np.arange(10)
2 # 持久化
3 np.save('arr_data.npy', arr)
4 # 加载
5 np.load('arr_data.npy')
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

还可以以非压缩或者压缩的格式保存多个 NumPy 数据:

1 # 非压缩
2 np.savez('arr_archive.npz', a=arr, b=arr)
3 # 压缩
4 np.savez_compressed('arr_compressed.npz', a=arr, b=arr)
5 # 加载
6 arch = np.load('arr_archive.npz')
7 arch['b']
8 cmp = np.load('arr_compressed.npz')
9 cmp['a']
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

线性代数

矩阵运算使用的是 dot 函数。

 

1 x = np.array([[1., 2., 3.], [4., 5., 6.]])
2 y = np.array([[1., 2.], [-1, 0], [0, 1]])
3 x
4 y
5 x.dot(y)
6 np.dot(x, y)  # 等价于 x.dot(y)
7 x @ y  # Python3.5,等价于 x.dot(y)
array([[1., 2., 3.],
       [4., 5., 6.]])
array([[ 1.,  2.],
       [-1.,  0.],
       [ 0.,  1.]])
array([[-1.,  5.],
       [-1., 14.]])
array([[-1.,  5.],
       [-1., 14.]])
array([[-1.,  5.],
       [-1., 14.]])

numpy.linalg 有很多关于矩阵分解(如求逆、行列式等)的函数。

1 from numpy.linalg import inv, qr
2 
3 np.random.seed(666)
4 X = np.random.randn(3, 3)
5 mat = X.T.dot(X)
6 inv(mat)
7 mat.dot(inv(mat))  # 等于单位矩阵
8 q, r = qr(mat)
9 r
array([[ 0.94360937,  0.09940334, -0.46256198],
       [ 0.09940334,  0.70123855, -0.05689685],
       [-0.46256198, -0.05689685,  0.78260277]])
array([[ 1.00000000e+00,  8.64663156e-18, -4.04784612e-17],
       [ 1.27360445e-17,  1.00000000e+00,  6.98841494e-18],
       [-7.26002573e-17, -9.92956304e-18,  1.00000000e+00]])
array([[-1.7499547 ,  0.22916446, -1.66008207],
       [ 0.        , -1.43686954, -0.22584526],
       [ 0.        ,  0.        ,  1.09786224]])

常用的 numpy.linalg 函数:

  • diag
  • dot
  • trace
  • det
  • eig
  • inv
  • pinv
  • qr
  • svd
  • solve
  • lstsq

生成伪随机数

numpy.random 有各种生成随机数样本的函数,比原生 Python 随机数生成模块速度快很多。

生成的随机数称为伪随机数,因为这些随机数实际上依赖于 np.random.seed 的值。

1 np.random.seed(666)
2 samples = np.random.normal(size=(4, 4))
3 samples
array([[ 0.82418808,  0.479966  ,  1.17346801,  0.90904807],
       [-0.57172145, -0.10949727,  0.01902826, -0.94376106],
       [ 0.64057315, -0.78644317,  0.60886999, -0.93101185],
       [ 0.97822225, -0.73691806, -0.29873262, -0.46058737]])

可以使用全局随机种子:

1 rng = np.random.RandomState(666)
2 rng.randn(10)
array([ 0.82418808,  0.479966  ,  1.17346801,  0.90904807, -0.57172145,
       -0.10949727,  0.01902826, -0.94376106,  0.64057315, -0.78644317])

常用的随机函数如下:

  • seed
  • permutation
  • shuffle
  • rand
  • randint
  • randn
  • binomial
  • normal
  • beta
  • chisquare
  • gamma
  • uniform

使用 NumPy 实现一个随机游走的例子

1 np.random.seed(666)
2 nsteps = 1000  # 随机游走的步数
3 draws = np.random.randint(0, 2, size=nsteps)  # 生成 0 或 1,一共 nsteps 个点
4 steps = np.where(draws == 0, 1, -1)  # 等于 0 则设为 1,不等于 0 则设为 -1
5 walk = steps.cumsum()  # 累积求和
6 walk.min()
7 walk.max()
8 (np.abs(walk) >= 10).argmax()  # 第一次绝对值超过 10 的索引
9 plt.plot(walk[:100])

同时模拟多个随机游走

1 np.random.seed(666)
2 nwalks = 5000
3 nsteps = 1000
4 draws = np.random.randint(0, 2, size=(nwalks, nsteps))
5 steps = np.where(draws > 0, 1, -1)
6 walks = steps.cumsum(1)  # 1 表示针对 axis=1
7 walks
8 walks.shape  # 行表示模拟的不同随机游走趋势,列表示不同的步数
array([[ -1,  -2,  -1, ..., -22, -21, -22],
       [  1,   2,   1, ...,  20,  19,  20],
       [  1,   0,  -1, ..., -14, -13, -12],
       ...,
       [ -1,   0,   1, ...,  24,  25,  26],
       [ -1,  -2,  -1, ...,  16,  15,  16],
       [ -1,   0,  -1, ..., -24, -23, -22]])
(5000, 1000)

总结

本文介绍了 NumPy 基本操作。

NumPy 最重要的数据结构是 ndarray。我们熟悉了:

  • 怎么创建 ndarray
  • ndarray 的数据类型
  • ndarray 的算术运算
  • 如何使用索引切片布尔索引fancy 索引
  • 数组的转置轴的变换

围绕着 ndarray,NumPy 还提供了很多函数和方法:

  • 通用函数:实现快速的按元素的运算
  • 面向数组的编程:条件逻辑、数学和统计方法、布尔数组的方法、排序方法、唯一值及集合逻辑
  • 数组的持久化和加载方法
  • 线性代数方面dotnumpy.linalg
  • 有关随机数生成的 np.random

最后是一个有关随机游走的简单示例。

通过本文我们主要知道了 NumPy 的核心数据结构 ndarray,以及有关它的各种函数出现的源头都是:

向量化的思想——数值计算尽量别用 for 循环,这样代码看起来简单,性能也好得多。

最后欢迎留言,有不足之处请指正。

 

参考

  • 《Python for Data Analysis, 2nd Edition》by Wes McKinney

 

 posted on 2019-08-11 21:12  何雨龙  阅读(1135)  评论(0编辑  收藏  举报