Python之NumPy基础:数组与向量化计算

本博客为《利用Python进行数据分析》的读书笔记,请勿转载用于其他商业用途。


NumPy是Numerical Python的简称。

1. NumPy ndarray:多维数组对象

Numpy的核心特征之一就是N-维数组对象——ndarray。ndarray是Python中一个快速、灵活的大型数据集容器。数组允许你使用类似于标量的操作语法在整块数据上进行科学计算。
为了感受下Numpy如何使用类似于内建对象的标量计算语法进行批量计算,我们首先导入Numpy,再生成一个小的随机数组:

data = np.random.randn(2, 3)
data
#
array([[-0.26900244, -0.66725718,  0.73856274],
       [ 0.79269933, -1.56455072,  1.64151292]])

然后再给data加上一个数学操作:

data * 10
#
array([[ -2.69002443,  -6.6725718 ,   7.38562744],
       [  7.92699329, -15.6455072 ,  16.41512923]])
data + data
#
array([[-0.53800489, -1.33451436,  1.47712549],
       [ 1.58539866, -3.12910144,  3.28302585]])

一个ndarray是一个通用的多维同类数据容器,也就是说,它包含的每一个元素均为相同类型。每一个数组都有一个shape属性,用来表征数组每一维度的数量;每一个数组都有一个dtype属性,用来描述数组的数据类型:

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

1.1 生成ndarray

生成数组最简单的方式就是使用array函数。array函数接收任意的序列型对象,生成一个新的包含传递数组的Numpy数组。例如,列表的转换就是一个好例子:

data1 = [6, 7.5, 8, 0, 1]
arr1 = np.array(data1)
arr1
#
array([6. , 7.5, 8. , 0. , 1. ])

嵌套序列,例如同等长度的列表,将会自动转换成多维数组:

data2 = [[1, 2, 3, 4], [5, 6, 7, 8]]
arr2 = np.array(data2)
arr2
#
array([[1, 2, 3, 4],
       [5, 6, 7, 8]])

因为data2是一个包含列表的列表,所以Numpy数组arr2形成了二维数组。我们可以通过检查ndimshape属性来确认这一点:

arr2.ndim
#
2
arr2.shape
#
(2, 4)

除非显式地指定,否则np.array会自动推断生成数组的数据类型。数据类型被存储在一个特殊的元数据dtype中。例如,之前的两个例子:

arr1.dtype
#
dtype('float64')
arr2.dtype
#
dtype('int32')

除了np.array,还有很多其他函数可以创建新函数。例如,给定长度及形状后,zeros可以一次性创造全0数组,ones可以一次性创造全1数组。empty可以创建一个没有初始化数值的数组。想要创建高维数组,则需要为shape传递一个元组:

np.zeros(10)
#
array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
np.zeros((3, 6))
#
array([[0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0.]])
np.empty((2, 3, 2))
#
array([[[5.67027464e-312, 2.47032823e-322],
        [0.00000000e+000, 0.00000000e+000],
        [0.00000000e+000, 8.60952352e-072]],

       [[5.33502485e-091, 4.08370620e-033],
        [2.82953568e-033, 3.34488015e-061],
        [3.99910963e+252, 4.93432906e+257]]])

注意:想要用np.empty来生成一个全0数组并不安全,有些时候它可能会返回未初始化的垃圾数值。如上述代码块。

arange是Python内建函数range的数组版:

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

由于Numpy专注于数值计算,如果没有特别指明的话,默认的数据类型是float64(浮点型)。

表:数组生成函数

函数名 描述
array 将输入数据(可以是列表、元组、数组以及其他序列)转换为ndarray,如不显式指明数据类型,将自动推断;默认所有的输入数据
asarray 将输入转换为ndarray,但如果输入已经是ndarray则不再赋值
arange Python内建函数range的数组版,返回一个数组
ones 根据给定形状和数据类型生成全1数组
ones_like 根据所给的数组生成一个形状一样的全1数组
zeros 根据给定形状和数据类型生成全1数组
zeros_like 根据所给的数组生成一个形状一样的全0数组
empty 根据给定形状生成一个没有初始化数值的空数组
emtpy_like 根据所给数组生成一个形状一样但是没有初始化数值的空数组
full 根据给定的形状和数据类型生成指定数值的数组
full_like 根据所给的数组生成一个形状一样但是内容都是指定数值的数组
eye, identity 生成一个N*N特征矩阵(对角线位置都是1,其余位置是0)

1.2 ndarray的数据类型

dtype(数据类型)是一个特殊的对象,它含有 ndarray将一块内存解释为特定数据类型所需的信息:

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 对象即可。当你需要控制数据在内存和磁盘中的存储方式时(尤其是对大数据集),那就得了解如何控制存储类型。

你可以使用astype方法显示地转换数组的数据类型:

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')

在上面例子中,整数被转换成了浮点数。如果将浮点数转换成整数,则小数部分将会被截取删除:

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 将其转换为数值形式:

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中,当使用numpy.string_类型作字符串数据要小心,因为NumPy会修正它的大小或删除输入且不发出警告。pandas在处理非数值数据时有更直观的开箱型操作。

你也可以使用另一个数组的的type属性:

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.])

你还可以使用类型代码来传入数据类型:

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与之前一样。

1.3 NumPy数组算术

数组是所以重要是因为它允许你进行批量操作而无需任何for循环。NumPy用户称这种特性为向量化。任何在两个等尺寸数组之间的算数操作都应用了逐元素操作的方式:

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.]])

带有标量计算的算术操作,会把计算参数传递给数组的每一个元素:

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]])

同尺寸数组之间的比较,会产生一个布尔值数组:

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)

不同尺寸的数组间的操作,将会用到广播特性。

1.4 基础索引与切片

一位数组比较简单,看起来的和Python的列表很类似:

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 的切片:

In [66]: arr_slice = arr[5:8]
 
In [67]: arr_slice
Out[67]: array([12, 12, 12])

现在,当我修改 arr_slice 中的值,变动也会体现在原始数组上:

In [68]: arr_slice[1] = 12345
 
In [69]: arr
Out[69]: array([    0,     1,     2,     3,     4,    12, 12345,    12,     8,   
  9])

不写切片值的[:]将会引用数组的所有值:

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()。

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

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

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

In [74]: arr2d[0][2]
Out[74]: 3
 
In [75]: arr2d[0, 2]
Out[75]: 3

在多维数组中,如果省略了后面的索引,则返回对象会是一个维度低一点的 ndarray(它含有高一级维度上的所有数据)。因此,在 2×2×3 数组 arr3d 中:

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 数组:

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

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

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]返回的是一个一维数组:

In [84]: arr3d[1, 0]
Out[84]: array([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])

1.4.1 数组的切片索引

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

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,对数组进行切片略有不同:

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 的前两行”。

你可以一次传入多个切片,就像传入多个索引那样:

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

当你像上面的这个例子中那样切片时,你需要按照原数组的维度进行切片。如果将索引和切片混合,就可以得到低纬度的切片。
例如,我可以选择第二行但是只选择前两列:

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

类似的,我也可以选择第三列,但是只选择前两行:

In [94]: arr2d[:2, 2]
Out[94]: array([3, 6])

需要注意的是,单独一个冒号表示选取整个轴上的数据,因此你可以像下面这样在更高维度进行切片:

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

当然对切片表达式赋值时,整个切片都会重新赋值:

In [96]: arr2d[:2, 1:] = 0
 
In [97]: arr2d
Out[97]: 
array([[1, 0, 0],
       [4, 0, 0],
       [7, 8, 9]])

1.5 布尔索引

让我们考虑以下例子,假设我们的数据都在数组中,并且数组中的数据是一些存在重复的人名。我会使用numpy.random中的randn函数来生成一些随机正态分布的数据:

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’会产生一个布尔值数组:

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

在索引数组时可以传入布尔值数组:

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数组中,第0个和第3个为‘Bob’,所以为True,传入data后,则选择了第0行和第3行的数据。
布尔值数组的长度必须和数组轴索引长度一致。你甚至还可以用切片或整数值(或整数值的序列)对布尔值数组进行混合和匹配。
这里解释一下,上述例子中,布尔值数组长度为7,数组轴索引长度也为7。
注意:当布尔值数组的长度不正确时,布尔值选择数据的方法并不会报错,因此在使用该特性的时候要小心。
下面的例子,我选取了names == 'Bob'的行,并索引了列:

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’以外的其他数据,你可以使用!=或在条件表达式前使用~对条件取反:

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]])

~符号可以在你想要对一个通用条件取反时使用:

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]])

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

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对布尔值数组并没有作用,请使用&(and)和 |(or)来代替。

基于常识来设置布尔值数组的值也是可以的。将data中所有的负值设置成0,我们需要做:

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.    ]])

利用一维布尔值数组对每一行设置数值也是非常简单的:

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 方便的来做。

1.6 神奇的索引

神奇索引是NumPy中的术语,用于描述使用整数数组进行数据索引。
假设我们有一个 8×4 数组:

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 即可:

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.]])

如果使用负的索引,将从尾部进行选择:

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

传递多个索引数组时情况有些许不同,这样会根据每个索引元组对应的元素选出一个一维数组:

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])

最终选出的是元素 (1,0)、(5,3)、(7,1) 和(2,2)。如果不考虑数组的维数,神奇索引的结果总是一维的。
神奇索引的行为可能会跟某些用户的预期不一样(包括我在内),选取矩阵的行列子集应该是矩形区域的形式才对。下面是得到该结果的一个办法:

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]])

请牢记神奇索引与切片不同,它总是将数据复制到一个新的数组中。

1.7 数组转置和换轴

转置是一种特殊的数据重组形式,可以返回底层数据的视图而不需要复制任何内容。数组拥有transpose方法,也有特殊的T属性:

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

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方法可以接受包含轴编号的元组,用于置换轴:

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方法,该方法接受一对编号作为参数,并对轴进行调整用于重组数据:

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 也是返回源数据的视图(不会进行任何复制操作)。

2. 通用函数:快速的逐元素数组函数

通用函数,也可以成为ufunc,是一种ndarray数据中进行逐元素操作的函数。某些简单函数接收一个或多个标量数值,并产生一个或多个标量结果,而通用函数就是对这些简单函数的向量化封装。
有很多ufunc是简单的逐元素转换,比如sqrtexp函数:

In [137]: arr = np.arange(10)
 
In [138]: arr
Out[138]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
 
In [139]: np.sqrt(arr)
Out[139]: 
array([ 0.    ,  1.    ,  1.4142,  1.7321,  2.    ,  2.2361,  2.4495,
        2.6458,  2.8284,  3.    ])
 
In [140]: np.exp(arr)
Out[140]: 
array([    1.    ,     2.7183,     7.3891,    20.0855,    54.5982,
         148.4132,   403.4288,  1096.6332,  2980.958 ,  8103.0839])

这是所谓的一元通用函数。还有一些通用函数,比如addmaximum则会接收两个数组并返回一个数组作为结果,因此称为二元通用函数:

In [141]: x = np.random.randn(8)
 
In [142]: y = np.random.randn(8)
 
In [143]: x
Out[143]: 
array([-0.0119,  1.0048,  1.3272, -0.9193, -1.5491,  0.0222,  0.7584,
       -0.6605])
 
In [144]: y
Out[144]: 
array([ 0.8626, -0.01  ,  0.05  ,  0.6702,  0.853 , -0.9559, -0.0235,
       -2.3042])
 
In [145]: np.maximum(x, y)
Out[145]: 
array([ 0.8626,  1.0048,  1.3272,  0.6702,  0.853 ,  0.0222,  0.7584,   
       -0.6605])

这里,numpy.maximum逐个元素地将x和y中元素的最大值计算出来。
也有一些通用函数返回多个数组。比如modf,是Python内建函数divmod的向量化版本。它返回了一个浮点值数组的小数部分和整数部分:

In [146]: arr = np.random.randn(7) * 5
 
In [147]: arr
Out[147]: array([-3.2623, -6.0915, -6.663 ,  5.3731,  3.6182,  3.45  ,  5.0077])
 
In [148]: remainder, whole_part = np.modf(arr)
 
In [149]: remainder
Out[149]: array([-0.2623, -0.0915, -0.663 ,  0.3731,
0.6182,  0.45  ,  0.0077])
 
In [150]: whole_part
Out[150]: array([-3., -6., -6.,  5.,  3.,  3.,  5.])

通用函数接收一个可选参数out,允许对数组按位置操作:

In [151]: arr
Out[151]: array([-3.2623, -6.0915, -6.663 ,  5.3731,  3.6182,  3.45  ,  5.0077])
 
In [152]: np.sqrt(arr)
Out[152]: array([    nan,     nan,     nan,  2.318 ,  1.9022,  1.8574,  2.2378])
 
In [153]: np.sqrt(arr, arr)
Out[153]: array([    nan,     nan,     nan,  2.318 ,  1.9022,  1.8574,  2.2378])
 
In [154]: arr
Out[154]: array([    nan,     nan,     nan,  2.318 ,  1.9022,  1.8574,  2.2378])

讲真这部分我没太看懂,是说通过将arr传入两次,更改了原arr的数据内容么?

表: 一元通用函数

函数名 描述
abs、fabs 逐元素地计算整数、浮点数或复数的绝对值
sqrt 计算每个元素的平方根(与arr ** 0.5相等)
square 计算每个元素的平方(与arr**2相等)
exp 计算每个元素的自然指数值e**x
log、log10、log2、log1p 分别对应:自然对数(e为底)、对数10为底、对数2为底、log(1+x)
sign 计算每个元素的符号值:1(正数)、0(0)、-1(负数)
ceil 计算每个元素的最高整数值(即大于等于给定数值的最小整数)
floor 计算每个元素的最小整数值(即小于等于给定数值的最大整数)
modf 分别将数组中的小数部分和整数部分按数组形式返回
isnan 返回数组中的元素是否是一个NaN(不是一个数值),形式为布尔值数组
isfinite、isinf 分别返回数组中的元素是否有限(非inf、非NaN)、是否无限的,形式为布尔值数组
cos、cosh、sin、sinh、tan、tanh 常规的三角函数
arccos、arccosh、arcsin、arcsinh、arctan、arctanh 反三角函数
logical_not 对数组的元素按位取反(与~arr效果一样)

表: 二元通用函数

函数名 描述
add 将数组的对应元素增加
subtract 在第二个数组中,将第一个数组中包含的元组去除
multiply 将数组的对应元素相乘
divide, floor_divide 除或整除(放弃余数)
power 将第二个数组的元素作为第一个数组对应元素的幂次方
maximum, fmax 逐个元素计算最大值,fmax忽略NaN
minimum, fmin 逐个元素计算最小值,fmax忽略NaN
mod 按元素的求模计算(即求除法的余数)
copysign 将第一个数组的符号值改为第二个数组的符号值
greater, greater_equal, less, less_equal, equal, not_equal, logical_and, logical_or, logical_xor 进行逐个元素的比较,返回布尔值数组(与数学操作符>、>=、<、<=、==、!= 效果一致)进行逐个元素的逻辑操作(与逻辑操作符&、|、^ 效果一致)

3. 使用数组进行面向数组编程

利用数组表达式来替代显示循环的方法,称为向量化。通常,向量化的数组操作会比纯Python的等价实现在速度上快一到两个数量级,这对所有种类的数值计算产生了最大的影响。
作为简单的例子,假设我们想要在一组值(网格型)上计算函数sqrt(x^2 + y^2)。np.meshgrid函数接受两个一维数组,并产生两个二维矩阵(对应于两个数组中所有的 (x,y) 对):

In [155]: points = np.arange(-5, 5, 0.01) # 1000 equally spaced points
 
In [156]: xs, ys = np.meshgrid(points, points)
In [157]: ys
Out[157]: 
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]])

现在,你可以利用和两个座标值同样的表达式来使用函数:

In [158]: z = np.sqrt(xs ** 2 + ys ** 2)
 
In [159]: z
Out[159]: 
array([[ 7.0711,  7.064 ,  7.0569, ...,  7.0499,  7.0569,  7.064 ],
       [ 7.064 ,  7.0569,  7.0499, ...,  7.0428,  7.0499,  7.0569],
       [ 7.0569,  7.0499,  7.0428, ...,  7.0357,  7.0428, 7.0499],
       ..., 
       [ 7.0499,  7.0428,  7.0357, ...,  7.0286,  7.0357,  7.0428],
       [ 7.0569,  7.0499,  7.0428, ...,  7.0357,  7.0428,  7.0499],
       [ 7.064 ,  7.0569,  7.0499, ...,  7.0428,  7.0499,  7.0569]])

3.1 将条件逻辑作为数组操作

numpy.where函数式三元表达式x if condition else y的向量化版本。假设我们有一个布尔值数组和两个数值数组:

In [165]: xarr = np.array([1.1, 1.2, 1.3, 1.4, 1.5])
 
In [166]: yarr = np.array([2.1, 2.2, 2.3, 2.4, 2.5])
 
In [167]: cond = np.array([True, False, True, True, False])

假设cond中的元素为True时,我们取axrr中的对应元素值,否则取yarr中的元素值,可以通过以上代码完成。但如果数组很大,速度会很慢,而使用np.where时,就可以非常简单地完成:

In [168]: result = [(x if c else y)
   .....:           for x, y, c in zip(xarr, yarr, cond)]
 
In [169]: result
Out[169]: [1.1000000000000001, 2.2000000000000002, 1.3, 1.3999999999999999, 2.5]

这样会产生很多问题。首先,如果数组很大的话,速度会很慢(因为所有的工作都是通过解释器解释Python代码完成)。其次,当数组是多维时,就无法奏效了。而使用np.where时,就可以非常简单地完成:

In [170]: result = np.where(cond, xarr, yarr)
 
In [171]: result
Out[171]: array([ 1.1,  2.2,  1.3,  1.4,  2.5])

np.where的第二个和第三个参数并不需要是数组,它们可以是标量。where在数据分析中的一个典型用法是根据一个数组来生成一个新的数组。假设你有一个随机生成的矩阵数据,并且你想将其中的正值都替换成2,将所有的负值都替换成-2,使用np.where会很容易实现:

In [172]: arr = np.random.randn(4, 4)
 
In [173]: arr
Out[173]: 
array([[-0.5031, -0.6223, -0.9212, -0.7262],
       [ 0.2229,  0.0513, -1.1577,  0.8167],
       [ 0.4336,  1.0107,  1.8249, -0.9975],
       [ 0.8506, -0.1316,  0.9124,  0.1882]])
 
In [174]: arr > 0
Out[174]: 
array([[False, False, False, False],
       [ True,  True, False,  True],
       [ True,  True,  True, False],
       [ True, False,  True,  True]], dtype=bool)
 
In [175]: np.where(arr > 0, 2, -2)
Out[175]: 
array([[-2, -2, -2, -2],
       [ 2,  2, -2,  2],
       [ 2,  2,  2, -2],
       [ 2, -2,  2,  2]])

你可以使用np.where将标量和数组联合,例如,我可以像下面的代码那样将arr中所有正值都替换为常数2:

In [176]: np.where(arr > 0, 2, arr) # set only positive values to 2
Out[176]: 
array([[-0.5031, -0.6223, -0.9212, -0.7262],
       [ 2.    ,  2.    , -1.1577,  2.    ],
       [ 2.    ,  2.    ,  2.    , -0.9975],
       [ 2.    , -0.1316,  2.    ,  2.    ]])

3.2 数学和统计方法

可以通过数组上的一组数学函数对整个数组或某个轴向的数据进行统计计算。sum、mean 以及标准差 std 等聚合计算(aggregation,通常也叫做缩减函数(reduction))既可以当做数组的实例方法调用,也可以当做顶级 NumPy 函数使用。

这里,我生成了一些正态分布随机数据,然后做了聚类统计:

In [177]: arr = np.random.randn(5, 4)
 
In [178]: arr
Out[178]: 
array([[ 2.1695, -0.1149,  2.0037,  0.0296],
       [ 0.7953,  0.1181, -0.7485,  0.585 ],
       [ 0.1527, -1.5657, -0.5625, -0.0327],
       [-0.929 , -0.4826, -0.0363,  1.0954],
       [ 0.9809, -0.5895,  1.5817, -0.5287]])
 
In [179]: arr.mean()
Out[179]: 0.19607051119998253
 
In [180]: np.mean(arr)
Out[180]: 0.19607051119998253
 
In [181]: arr.sum()
Out[181]: 3.9214102239996507

meansum等函数可以接收一个可选参数axis,这个参数可以用于计算给定轴向上的统计值,形成一个下降一维度的数组:

In [182]: arr.mean(axis=1)
Out[182]: array([ 1.022 ,  0.1875, -0.502 , -0.0881,  0.3611])
 
In [183]: arr.sum(axis=0)
Out[183]: array([ 3.1693, -2.6345,  2.2381,  1.1486])

这里,arr.mean(1)是 “计算行的平均值”,arr.sum(0) 是“计算每列的和”。

其他如 cumsumcumprod 之类的方法则不聚合,而是产生一个由中间结果组成的数组:

In [184]: arr = np.array([0, 1, 2, 3, 4, 5, 6, 7])
 
In [185]: arr.cumsum()
Out[185]: array([ 0,  1,  3,  6, 10, 15, 21, 28])

在多维数组中,像cumsum这样的累积函数返回相同长度的数组,但是可以在指定轴向上根据维度的切片进行部分聚合:

In [186]: arr = np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
 
In [187]: arr
Out[187]: 
array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])
 
In [188]: arr.cumsum(axis=0)
Out[188]: 
array([[ 0,  1,  2],
       [ 3,  5,  7],
       [ 9, 12, 15]])
 
In [189]: arr.cumprod(axis=1)
Out[189]: 
array([[  0,   0,   0],
       [  3,  12,  60],
       [  6,  42, 336]])

表: 基础数组统计方法

方法 描述
sum 沿着轴向计算所有元素的累和,0长度的数组,累和为0
mean 数学平均,0长度的数组平均值为NaN
std, var 标准差和方差,可以选择自由度调整(默认分母是n)
min, max 最大值和最小值
argmin, argmax 最大值和最小值的位置
cumsum 从0开始元素累积和
cumprod 从1开始元素累积积

3.3 布尔值数组的方法

前面介绍的方法,布尔值会被强制为1(True)和0(False)。因此,sum通常可以用于计算布尔值数组中的True的个数:

In [190]: arr = np.random.randn(100)
 
In [191]: (arr > 0).sum() # Number of positive values
Out[191]: 42

注意!此处的sum是统计大于0的数字的个数,而不是大于0的数字的和。

对于布尔值数组,有两个非常有用的方法anyallany检查数组中是否至少有一个True,而all检查是否每个值都是True:

In [192]: bools = np.array([False, False, True, False])
 
In [193]: bools.any()
Out[193]: True
 
In [194]: bools.all()
Out[194]: False

这些方法也可以适用于非布尔值数组,所有的非0元素都会按True处理。

3.4 排序

和Python的内建列表类型相似,Numpy数组可以使用sort方法按位置排序。默认为由小向大排列:

In [195]: arr = np.random.randn(6)
 
In [196]: arr
Out[196]: array([ 0.6095, -0.4938,  1.24  , -0.1357,  1.43  , -0.8469])
 
In [197]: arr.sort()
 
In [198]: arr
Out[198]: array([-0.8469, -0.4938, -0.1357,  0.6095,  1.24  ,  1.43  ])

我们可以在多维数组中根据传递的axis值,沿着轴向对每个一维数组据段进行排序:

In [199]: arr = np.random.randn(5, 3)
 
In [200]: arr
Out[200]: 
array([[ 0.6033,  1.2636, -0.2555],
       [-0.4457,  0.4684, -0.9616],
       [-1.8245,  0.6254,  1.0229],
       [ 1.1074,  0.0909, -0.3501],
       [ 0.218 , -0.8948, -1.7415]])
 
In [201]: arr.sort(1)
 
In [202]: arr
Out[202]: 
array([[-0.2555,  0.6033,  1.2636],
       [-0.9616, -0.4457,  0.4684],
       [-1.8245,  0.6254,  1.0229],
       [-0.3501,  0.0909,  1.1074],
       [-1.7415, -0.8948,  0.218 ]])

顶层的np.sort方法返回的是已经排序好的数组拷贝,而不是对原数组按位置排序。可以向sort()方法中传入参数0或1,传入0时按列排序,传入1时按行排序,都是由小到大进行排列。
下面的例子计算的是一个数组的分位数,并选出分位数所对应的数,这是一种应急的方式:

In [203]: large_arr = np.random.randn(1000)
 
In [204]: large_arr.sort()
 
In [205]: large_arr[int(0.05 * len(large_arr))] # 5% quantile
Out[205]: -1.5311513550102103

3.5 唯一值与其他集合逻辑

Numpy包含一些针对一维ndarray的基础集合操作。常用的一个方法是np.unique,返回的是数组中唯一值排序后形成的数组:

In [206]: names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
 
In [207]: np.unique(names)
Out[207]: 
array(['Bob', 'Joe', 'Will'],
      dtype='<U4')
 
In [208]: ints = np.array([3, 3, 3, 2, 2, 1, 1, 4, 4])
 
In [209]: np.unique(ints)
Out[209]: array([1, 2, 3, 4])

拿跟np.unique 等价的纯 Python 代码来对比一下:

In [210]: sorted(set(names))
Out[210]: ['Bob', 'Joe', 'Will']

另一个函数,np.in1d,可以检查一个数组中的值是否在另外一个数组中,并返回一个布尔值数组:

In [211]: values = np.array([6, 0, 0, 3, 2, 5, 6])
 
In [212]: np.in1d(values, [2, 3, 6])
Out[212]: array([ True, False, False,  True,  True, False,  True], dtype=bool)

表 数组集合操作

方法 描述
unique(x) 计算x的唯一值,并排序
intersect1d(x, y) 计算x和y的交集,并排序
union1d(x, y) 计算x和y的并集,并排序
in1d(x, y) 计算x中的元素是否包含在y中,返回一个布尔值数组
setdiff1d(x, y) 差集,在x中但不在y中的x的元素
setxor1d(x,y) 异或集,在x或y中,但不属于x、y交集的元素

4. 使用数组进行文件输入和输出

Numpy可以在硬盘中将数据以文本或二进制文件的形式进行存入硬盘或由硬盘载入。
np.savenp.load是高效存取硬盘数据的两大工具函数。数组在默认情况下是以未压缩的格式进行存储的,后缀名是 .npy:

In [213]: arr = np.arange(10)
In [214]: np.save('some_array', arr)

如果文件存放路径中没写 .npy时,后缀名会被自动加上。硬盘上的数组可以使用np.load进行载入:

In [215]: np.load('some_array.npy')
Out[215]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

我们可以使用np.savez并将数组作为参数传递给该函数,用于在未压缩文件中保存多个数组:

In [216]: np.savez('array_archive.npz', a=arr, b=arr)

当载入一个.npy文件的时候,我们会获得一个字典型的对象,并通过该对象很方便地载入单个数组:

In [217]: arch = np.load('array_archive.npz')
 
In [218]: arch['b']
Out[218]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

如果要将数据压缩,可以使用 numpy.savez_compressed

In [219]: np.savez_compressed('arrays_compressed.npz', a=arr, b=arr)

5. 线性代数

线性代数,比如矩阵乘法、分解、行列式等方阵数学,是所有数组类库的重要组成部分。和Matlab等其他语言相比,Numpy的线性代数中所不同的是 *是矩阵的逐元素乘积,而不是矩阵的点乘积。因此Numpy的数组方法和numpy命名空间中都有一个函数dot,用于矩阵的操作:

In [223]: x = np.array([[1., 2., 3.], [4., 5., 6.]])
 
In [224]: y = np.array([[6., 23.], [-1, 7], [8, 9]])
 
In [225]: x
Out[225]: 
array([[ 1.,  2.,  3.],
       [ 4.,  5.,  6.]])
 
In [226]: y
Out[226]: 
array([[  6.,  23.],
       [ -1.,   7.],
       [  8.,   9.]])
 
In [227]: x.dot(y)
Out[227]: 
array([[  28.,   64.],
       [  67.,  181.]])

x.dot(y)等价于np.dot(x, y):

In [228]: np.dot(x, y)
Out[228]: 
array([[  28.,   64.],
       [  67.,  181.]])

一个二维数组和一个长度合适的一维数组之间的矩阵乘积,其结果是一个一维数组:

In [229]: np.dot(x, np.ones(3))
Out[229]: array([  6.,  15.])

特殊符号@也作为中缀操作符,用于点乘矩阵操作:

In [230]: x @ np.ones(3)
Out[230]: array([  6.,  15.])

numpy.linalg拥有一个矩阵分解的标准函数集,以及其他常用函数,例如求逆和行列式求解。这些函数都是通过在MATLAB和R等其他语言使用的相同的行业标准线性代数库来实现的,例如BLAS、LAPACK或英特尔专用的MKL(数学核心库):

In [231]: from numpy.linalg import inv, qr
 
In [232]: X = np.random.randn(5, 5)
 
In [233]: mat = X.T.dot(X)
 
In [234]: inv(mat)
Out[234]: 
array([[  933.1189,   871.8258, -1417.6902, -1460.4005,  1782.1391],
       [  871.8258,   815.3929, -1325.9965, -1365.9242,  1666.9347],
       [-1417.6902, -1325.9965,  2158.4424,  2222.0191, -2711.6822],
       [-1460.4005, -1365.9242,  2222.0191,  2289.0575, -2793.422 ],
       [ 1782.1391,  1666.9347, -2711.6822, -2793.422 ,  3409.5128]])
 
In [235]: mat.dot(inv(mat))
Out[235]: 
array([[ 1.,  0., -0., -0., -0.],
       [-0.,  1.,  0.,  0.,  0.],
       [ 0.,  0.,  1.,  0.,  0.],
       [-0.,  0.,  0.,  1., -0.],
       [-0.,  0.,  0.,  0.,  1.]])
 
In [236]: q, r = qr(mat)
 
In [237]: r
Out[237]: 
array([[-1.6914,  4.38  ,  0.1757,  0.4075, -0.7838],
       [ 0.    , -2.6436,  0.1939, -3.072 , -1.0702],
       [ 0.    ,  0.    , -0.8138,  1.5414,  0.6155],
       [ 0.    ,  0.    ,  0.    , -2.6445, -2.1669],
       [ 0.    ,  0.    ,  0.    ,  0.    ,  0.0002]])

表达式 X.T.dot(X) 计算 X 和它的转置 X.T 的点积。

表:常用numpy.linalg函数

函数 描述
diag 将一个方阵的对角(或非对角)元素作为一维数组返回,或者将一维数组转换成一个方阵,并且在非对角线上有零点
dot 矩阵点乘
trace 计算对角元素和
det 计算矩阵的行列式
eig 计算方阵的特征值和特征向量
inv 计算方阵的逆矩阵
pinv 计算矩阵的Moore-Penrose伪逆
qr 计算QR分解
svd 计算奇异值分解(SVD)
solve 求解x的线性系统Ax=b,其中A是方阵
lstsq 计算Ax=b的最小二乘解

6. 伪随机数生成

numpy.random模块填补了Python内建的random模块的不足,可以高效地生成多种概率分布下的完整样本值数组。例如,可以使用normal来获得一个4X4的正态分布样本数组:

In [238]: samples = np.random.normal(size=(4, 4))
 
In [239]: samples
Out[239]: 
array([[ 0.5732,  0.1933,  0.4429,  1.2796],
       [ 0.575 ,  0.4339, -0.7658, -1.237 ],
       [-0.5367,  1.8545, -0.92  , -0.1082],
       [ 0.1525,  0.9435, -1.0953, -0.144 ]])

然而Python内建的random模块一次只能生成一个值。你可以从下面的示例中看到,numpy.random在生成大型样本时比纯Python的方式快了一个数量级:

In [240]: from random import normalvariate
 
In [241]: N = 1000000
 
In [242]: %timeit samples = [normalvariate(0, 1) for _ in range(N)]
1.77 s +- 126 ms per loop (mean +- std. dev. of 7 runs, 1 loop each)
 
In [243]: %timeit np.random.normal(size=N)
61.7 ms +- 1.32 ms per loop (mean +- std. dev. of 7 runs, 10 loops each)

我们说这些都是伪随机数,是因为它们都是通过算法基于随机数生成器种子,在确定性的条件下生成的。你可以用 NumPy 的 np.random.seed 更改随机数生成种子:

In [244]: np.random.seed(1234)

numpy.random 的数据生成函数使用了全局的随机种子。要避免全局状态,你可以使用 numpy.random.RandomState,创建一个与其它隔离的随机数生成器:

In [245]: rng = np.random.RandomState(1234)
 
In [246]: rng.randn(10)
Out[246]: 
array([ 0.4714, -1.191 ,  1.4327, -0.3127, -0.7206,  0.8872,  0.8596,
       -0.6365,  0.0157, -2.2427])

表:numpy.random中的部分函数列表

函数 描述
seed 向随机数生成器传递随机状态种子
permutation 返回一个序列的随机排列,或者返回一个乱序的整数范围序列
shuffle 随机排列一个序列 rand 从均匀分布中抽取样本
randint 根据给定的由低到高的范围抽取随机整数
randn 从均值0方差1的正态分布中抽取样本(MATLAB型接口)
binomial 从二项分布中抽取样本
normal 从正态(高斯)分布中抽取样本
beta 从beta分布中抽取样本
chisquare 从卡方分布中抽取样本
gamma 从伽马分布中抽取样本
uniform 从均匀[0,1)分布中抽取样本

7. 随机漫步

我们通过模拟随机漫步来说明如何运用数组运算。先来看一个简单的随机漫步的例子:从 0 开始,步长 1 和-1 出现的概率相等。

下面是一个通过内置的 random 模块以纯 Python 的方式实现 1000 步的随机漫步:

In [247]: import random
   .....: position = 0
   .....: walk = [position]
   .....: steps = 1000
   .....: for i in range(steps):
   .....:     step = 1 if random.randint(0, 1) else -1
   .....:     position += step
   .....:     walk.append(position)
   .....:

下图是对上面随机漫步的前100步的可视化:
在这里插入图片描述
你可能会观察到walk只是对随机步进的累积,并且可以通过一个数组表达式实现。因为,我使用np.random模块一次性抽取1,000次掷硬币的结果,每次投掷的结果为1或-1,然后计算累积值:

In [251]: nsteps = 1000
 
In [252]: draws = np.random.randint(0, 2, size=nsteps)
 
In [253]: steps = np.where(draws > 0, 1, -1)
 
In [254]: walk = steps.cumsum()

由此我们开始从漫步轨道上提取一些统计数据,比如最大值、最小值等:

In [255]: walk.min()
Out[255]: -3
 
In [256]: walk.max()
Out[256]: 31

更复杂的统计是第一次穿越时间,即随机漫步的某一步达到了某个特定值。这里假设我们想要知道漫步中是合适连续朝某个方向连续走了10步。np.abs(walk)>=10给我们一个布尔值数组,用于表明漫步是否连续在同一方向走了十步,但是我们想要的是第一次走了10步或 -10步的位置。我们可以使用argmax来计算,该函数可以返回布尔值数组中最大值的第一个位置(True就是最大值):

In [257]: (np.abs(walk) >= 10).argmax()
Out[257]: 37

请注意,这里使用argmax的效率并不高,因为它总是完整地扫描整个数组。在这个特殊的示例中,一旦True被发现,我们就知道最大值了。

7.1 一次性模拟多次随机漫步

如果你的目标是模拟多次随机漫步,比如说5000步。你可以稍微修改下之前的代码来生成所有的随机步。如果传入一个2个元素的元组,numpy.random中的函数可以生成一个二维的抽取数组,并且我们可以一次性地跨行算出全部5000个随机步的累积和:

In [258]: nwalks = 5000
 
In [259]: nsteps = 1000
 
In [260]: draws = np.random.randint(0, 2, size=(nwalks, nsteps)) # 0 or 1
 
In [261]: steps = np.where(draws > 0, 1, -1)
 
In [262]: walks = steps.cumsum(1)
 
In [263]: walks
Out[263]: 
array([[  1,   0,   1, ...,   8,   7,   8],
       [  1,   0,  -1, ...,  34,  33,  32],
       [  1,   0,  -1, ...,   4,   5,   4],
       ..., 
       [  1,   2,   1, ...,  24,  25,  26],
       [  1,   2,   3, ...,  14,  13,  14],
       [ -1,  -2,  -3, ..., -24, -23, -22]])

得到这些数据之后,我们来计算 30 或-30 的最小穿越时间。这里稍微复杂些,因为不是 5000 个过程都到达了 30。我们可以用 any 方法来对此进行检查:

In [266]: hits30 = (np.abs(walks) >= 30).any(1)
 
In [267]: hits30
Out[267]: array([False,  True, False, ..., False,  True, False], dtype=bool)
 
In [268]: hits30.sum() # Number that hit 30 or -30
Out[268]: 3410

我们可以使用布尔值数组来选出绝对步数超过30的步所在的行,并使用argmax从轴向1上获取穿越时间:

In [269]: crossing_times = (np.abs(walks[hits30]) >= 30).argmax(1)
 
In [270]: crossing_times.mean()
Out[270]: 498.88973607038122

利用其它分布而不是等概率的掷硬币实验来随机漫步也是很容易的。你只需要使用一个不同的随机数生成函数,比如normal,在根据特定的均值和标准差即可生成正态分布下的随机步:

In [271]: steps = np.random.normal(loc=0, scale=0.25,
   .....:                          size=(nwalks, nsteps))
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章