第4章 NumPy基础:数组和矢量计算

                                                   目     录

4.1 NumPy的ndarray:一种多维数组对象

4.1.1 创建ndarray

4.1.2 ndarray的数据类型

4.1.3 NumPy数组的运算

4.1.4 数组的广播

4.1.5 基本的索引和切片

4.1.6 切片索引

4.1.7 布尔型索引

4.1.8 花式索引

4.1.9 数组转置和轴对换

4.2 通用函数:快速的元素级数组函数

4.3 利用数组进行数据处理

4.3.1 将条件逻辑表述为数组运算

4.3.2 数学和统计方法

4.3.3 用于布尔型数组的方法

4.3.4 排序

4.3.5 唯一化以及其它的集合逻辑

4.4 用于数组的文件输入输出

4.5 线性代数

4.6 伪随机数生成

4.7 随机漫步

4.7.1 单个随机漫步

4.7.2 一次模拟多个随机漫步


NumPy(Numerical Python的简称)是Python数值计算最重要的基础包。大多数提供科学计算的包都是用NumPy的数组作为构建基础。

NumPy的部分功能如下:

  • ndarray,一个具有矢量算术运算和复杂广播能力的快速且节省空间的多维数组。
  • 用于对整组数据进行快速运算的标准数学函数(无需编写循环)。
  • 用于读写磁盘数据的工具以及用于操作内存映射文件的工具。
  • 线性代数、随机数生成以及傅里叶变换功能。
  • 用于集成由C、C++、Fortran等语言编写的代码的A C API。

由于NumPy提供了一个简单易用的C API,因此很容易将数据传递给由低级语言编写的外部库,外部库也能以NumPy数组的形式将数据返回给Python。NumPy本身并没有提供多么高级的数据分析功能,理解NumPy数组以及面向数组的计算将有助于你更加高效地使用诸如pandas之类的工具。

对于大部分数据分析应用而言,大家应该关注的功能主要为以下几点:

  • 用于数据整理和清理、子集构造和过滤、转换等快速的矢量化数组运算。
  • 常用的数组算法,如排序、唯一化、集合运算等。
  • 高效的描述统计和数据聚合/摘要运算。
  • 用于异构数据集的合并/连接运算的数据对齐和关系型数据运算。
  • 将条件逻辑表述为数组表达式(而不是带有if-elif-else分支的循环)。
  • 数据的分组运算(聚合、转换、函数应用等)。。

处理表格数据时,通常将pandas作为统计和分析工作的基础。

NumPy之于数值计算特别重要的原因之一,是因为它可以高效处理大数组的数据。

  • NumPy是在一个连续的内存块中存储数据,独立于其他Python内置对象。NumPy的C语言编写的算法库可以操作内存,而不必进行类型检查或其它前期工作。比起Python的内置序列,NumPy数组使用的内存更少。
  • NumPy可以在整个数组上执行复杂的计算,而不需要Python的for循环。

正如下面的例子:

import numpy as np

hust_arr = np.arange(1000000)
hust_list = list(range(1000000))
%time for _ in range(10): hust_arr2 = hust_arr * 2
out put:
Wall time: 19 ms
%time for _ in range(10): hust_list2 = [x * 2 for x in hust_list]

out put:
Wall time: 750 ms

基于NumPy的算法要比纯Python快10到100倍(甚至更快),并且使用的内存更少。

4.1 NumPy的ndarray:一种多维数组对象

NumPy最重要的一个特点就是其N维数组对象(即ndarray),该对象是一个快速而灵活的大数据集容器。利用数组对整块数据执行数学运算,其语法跟标量元素之间的运算一样。

 要明白Python是如何利用与标量值类似的语法进行批次计算,首先引入NumPy,然后生成一个包含随机数据的小数组:

import numpy as np
# Generate some random data

data = np.random.randn(2, 4)

data

array([[-0.10339226,  1.00605839, -1.86735608, -0.40424423],
       [ 0.06615611, -1.51357686, -0.32021485, -0.25574128]])

 然后进行数学运算:

data * 10

array([[ -1.03392263,  10.06058393, -18.6735608 ,  -4.04244231],
       [  0.66156112, -15.1357686 ,  -3.20214852,  -2.55741283]])

data + data

array([[-0.20678453,  2.01211679, -3.73471216, -0.80848846],
       [ 0.13231222, -3.02715372, -0.6404297 , -0.51148257]])


​

 第一个例子中,所有的元素都乘以10。第二个例子中,每个元素都与自身相加。

ndarray是一个通用的同构数据多维容器,其中的所有元素必须是相同类型的。每个数组都有一个shape(一个表示各维度大小的元组)和一个dtype(一个用于说明数组数据类型的对象):


In [7]: data.shape
Out[7]: (2, 4)


In [8]: data.dtype
Out[8]: dtype('float64')

 本章将会介绍NumPy数组的基本用法,这对于本书后面各章的理解基本够用。虽然大多数数据分析工作不需要深入理解NumPy,但是精通面向数组的编程和思维方式是成为Python科学计算牛人的一大关键步骤。

4.1.1 创建ndarray

创建数组最简单的办法就是使用array函数。它接受一切序列型的对象(包括其他数组),然后产生一个新的含有传入数据的NumPy数组。下面以一个列表的转换为例:


In [13]: data1 = [5, 3.4, 6, 2, 7]
         arr1 = np.array(data1)
         arr1
Out[13]: array([5. , 3.4, 6. , 2. , 7. ])

 嵌套序列(比如由一组等长列表组成的列表)将会被转换为一个多维数组:


In [14]: data2 = [[4, 3, 2, 1], [10, 9, 8, 7]]
In [15]: arr2 = np.array(data2)
In [16]: arr2
Out[16]: array([[ 4,  3,  2,  1],
                [10,  9,  8,  7]])

 因为data2是列表的列表,NumPy数组arr2的两个维度的shape是从data2引入的。可以用属性ndim和shape验证:


In [17]: arr2.ndim
Out[17]: 2
In [18]: arr2.shape
Out[18]: (2, 4)

除非特别说明(稍后将会详细介绍),np.array会尝试为新建的这个数组推断出一个较为合适的数据类型。数据类型保存在一个特殊的dtype对象中。比如说,在上面的两个例子中,我们有:


In [19]: arr1.dtype
Out[19]: dtype('float64')
In [20]: arr2.dtype
Out[20]: dtype('int32')

除np.array之外,还有一些函数也可以新建数组。比如,zeros和ones分别可以创建指定长度或形状的全0或全1数组。empty可以创建一个没有任何具体值的数组。要用这些方法创建多维数组,只需传入一个表示形状的元组即可:


In [21]: np.zeros(10)
Out[21]: array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])

In [22]: # 定义4行6列的数组
         np.zeros((4, 6))
Out[22]:
array([[0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0.]])

In [23]: # 定义一个三维数组
         np.empty((2, 3, 2))
Out[23]:
array([[[9.39944447e-312, 2.86558075e-322],
        [0.00000000e+000, 0.00000000e+000],
        [0.00000000e+000, 3.76231868e+174]],

       [[4.96125191e-090, 1.57211215e-076],
        [3.61112012e+174, 1.31505911e-047],
        [6.48224660e+170, 5.82471487e+257]]])

​

 需要对空数组进行初始化,否则元素的值不一定全为0

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


In [26]: np.arange(10)
Out[26]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

表4-1列出了一些数组创建函数。由于NumPy关注的是数值计算,因此,如果没有特别指定,数据类型基本都是float64(浮点数)。

表4-1 数组创建函数

4.1.2 ndarray的数据类型

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


In [27]: arr1 = np.array([0, 1, 2], dtype = np.float64)
In [28]: arr2 = np.array([4, 5, 6], dtype = np.int32)

In [29]: arr1.dtype
Out[29]: dtype('float64')

In [30]: arr2.dtype
Out[30]: dtype('int32')

标准的双精度浮点值(即Python中的float对象)需要占用8字节(即64位)。因此,该类型在NumPy中就记作float64。表4-2列出了NumPy所支持的全部数据类型。

GitHub

GitHub

可以通过ndarray的astype方法明确地将一个数组从一个dtype转换成另一个dtype:


In [31]: arr = np.array([0, 1, 2, 3, 4, 5, 6])
In [32]: arr.dtype

Out[32]: dtype('int32')
In [33]: float_arr = arr.astype(np.float)

In [34]: float_arr.dtype
Out[34]: dtype('float64')

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


In [35]: arr = np.array([2.5, 3.9, 1.4])

In [36]: arr
Out[36]: array([2.5, 3.9, 1.4])

In [37]: arr.astype(np.int32)
Out[37]: array([2, 3, 1])

如果某字符串数组表示的全是数字,也可以用astype将其转换为数值形式:


In [39]: numeric_string = np.array(['3.12', '-8.23', '1932'], dtype = np.string_)
In [40]: numeric_string.astype(float)

Out[40]: array([   3.12,   -8.23, 1932.  ])

注意:使用numpy.string_类型时,一定要小心,因为NumPy的字符串数据是大小固定的,发生截取时,不会发出警告。pandas提供了更多非数值数据的便利的处理方法。

 数组的dtype还有另一个属性:


In [48]: int_array = np.arange(9)

In [49]: calibers = np.array([.23, .45, 89.34], dtype = np.float64)

In [50]: int_array.astype(calibers.dtype)
Out[50]: array([0., 1., 2., 3., 4., 5., 6., 7., 8.])

 可以用简洁的类型代码来表示dtype:


In [51]: empty_uint32 = np.empty(8, dtype = 'u4')
In [52]: empty_uint32

Out[52]:
array([4054629320,        442,          0,          0, 4072783887,
         32843595, 3782017648, 2147491258], dtype=uint32)

​

4.1.3 NumPy数组的运算


In [53]: arr = np.array([[1., 2., 3.], [6., 5., 4.]])

In [54]: arr
Out[54]:
array([[1., 2., 3.],
       [6., 5., 4.]])

In [55]: arr *arr
Out[55]:
array([[ 1.,  4.,  9.],
       [36., 25., 16.]])

In [56]: arr - arr
Out[56]:
array([[0., 0., 0.],
       [0., 0., 0.]])

 数组与标量的算术运算会将标量值传播到各个元素:


In [57]: 1 / arr
Out[57]:
array([[1.        , 0.5       , 0.33333333],
       [0.16666667, 0.2       , 0.25      ]])

In [58]: arr ** 2
Out[58]:
array([[ 1.,  4.,  9.],
       [36., 25., 16.]])

大小相同的数组之间的比较会生成布尔值数组:


In [59]: arr2 = np.array([[0., 4., 1.], [7., 2., 10.]])

In [60]: arr2
Out[60]:
array([[ 0.,  4.,  1.],
       [ 7.,  2., 10.]])

In [61]: arr2 > arr
Out[61]:
array([[False,  True, False],
       [ True, False,  True]])

不同大小的数组之间的运算叫做广播(broadcasting),下面对广播进行简单介绍:

4.1.4 数组的广播

如果两个数组的维数不相同,则元素到元素的操作是不可能的。 然而,在 NumPy 中仍然可以对形状不相似的数组进行操作,因为它拥有广播功能。 较小的数组会广播到较大数组的大小,以便使它们的形状可兼容。

如果满足以下规则,可以进行广播:

  • ndim较小的数组会在前面追加一个长度为 1 的维度。
  • 输出数组的每个维度的大小是输入数组该维度大小的最大值。
  • 如果输入在每个维度中的大小与输出大小匹配,或其值正好为 1,则可以在计算中使用该输入。
  • 如果输入的某个维度大小为 1,则该维度中的第一个数据元素将用于该维度的所有计算。

如果上述规则产生有效结果,并且满足以下条件之一,那么数组被称为可广播的。

  • 数组拥有相同形状。
  • 数组拥有相同的维数,每个维度拥有相同长度,或者长度为 1。
  • 数组拥有极少的维度,可以在其前面追加长度为 1 的维度,使上述条件成立。

请看下面实例:

import numpy as np 
a = np.array([[0.0,0.0,0.0],[10.0,10.0,10.0],[20.0,20.0,20.0],[30.0,30.0,30.0]]) 
b = np.array([1.0,2.0,3.0])  
print ('第一个数组:')  
print (a) 
print ('\n第二个数组:')  
print (b) 
print ('\n第一个数组加第二个数组:')  
print (a + b)

output
第一个数组:
[[  0.   0.   0.]
 [ 10.  10.  10.]
 [ 20.  20.  20.]
 [ 30.  30.  30.]]

第二个数组:
[ 1.  2.  3.]

第一个数组加第二个数组:
[[  1.   2.   3.]
 [ 11.  12.  13.]
 [ 21.  22.  23.]
 [ 31.  32.  33.]]

下面的图片展示了数组b如何通过广播来与数组a兼容。

GitHub

4.1.5 基本的索引和切片

NumPy数组的索引是一个内容丰富的主题,因为选取数据子集或单个元素的方式有很多。一维数组很简单。从表面上看,它们跟Python列表的功能差不多:


In [1]: import numpy as np
         arr = np.arange(10)
In [2]: arr
Out[2]:
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

In [3]: arr[5]
Out[3]:
5

In [4]: arr[5:8] #第6~8个元素,左闭右开
Out[4]:
array([5, 6, 7])

In [5]: arr[5:8] = 12
In [6]: arr

Out[6]:
array([ 0,  1,  2,  3,  4, 12, 12, 12,  8,  9])

将一个标量值赋值给一个切片时(如arr[5:8]=12),该值会自动传播到整个选区。跟列表最重要的区别在于,数组切片是原始数组的视图。这意味着数据不会被复制,视图上的任何修改都会直接反映到源数组上。

 作为例子,先创建一个arr的切片:


In [7]: arr_slice = arr[5:8]

In [8]: arr_slice

Out[8]: array([12, 12, 12])

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


In [9]: arr_slice[1] = 12345
In [10]: arr

Out[10]:
array([    0,     1,     2,     3,     4,    12, 12345,    12,     8,
           9])

 切片[ : ]会给数组中的所有值赋值:


In [11]: arr_slice[:] = 64
In [13]: arr

Out[13]:
array([ 0,  1,  2,  3,  4, 64, 64, 64,  8,  9])

注意:由于NumPy的设计目的是处理大数据,假如NumPy坚持要将数据复制来复制去的话会产生何等的性能和内存问题。所以ndarray就是直接在原始数据上进行操作,降低内存。

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


In [14]: arr2d = np.array([[1, 1, 1], [2, 3, 5], [2, 4, 5]])
In [15]: arr2d[2]

Out[15]: array([2, 4, 5])

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


In [16]: arr2d[0][2]
Out[16]: 1
In [17]: arr2d[0, 2]
Out[17]: 1

下图说明了二维数组的索引方式。轴0作为行,轴1作为列。

GitHub

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


In [21]: arr3d = np.array([[[1, 2, 3],[4, 5, 6]],[[7, 8, 9],[10, 11, 12]]])
In [22]: arr3d
Out[22]:
array([[[ 1,  2,  3],
        [ 4,  5,  6]],

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

arr3d[0]是一个2×3数组


In [23]: arr3d[0]

Out[23]:
array([[1, 2, 3],
       [4, 5, 6]])

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


In [24]: old_values = arr3d[0].copy()
In [25]: arr3d[0] = 42

In [26]: arr3d
Out[26]:
array([[[42, 42, 42],
        [42, 42, 42]],

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

In [27]: arr3d[0] = old_values
In [28]: arr3d

Out[28]:
array([[[ 1,  2,  3],
        [ 4,  5,  6]],

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

相似的,arr3d[1,0]可以访问索引以(1,0)开头的那些值(以一维数组的形式返回):


In [29]: arr3d[1,0]
Out[29]:
array([7, 8, 9])

可以分解成两步进行,但结果是一致的。


In [30]: x = arr3d[1]
In [31]: x

Out[31]: array([[ 7,  8,  9], [10, 11, 12]])

In [32]: x[0]
Out[32]: array([7, 8, 9])

注意,在上面所有这些选取数组子集的例子中,返回的数组都是视图。

4.1.6 切片索引

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


In [33]: arr

Out[33]: array([ 0,  1,  2,  3,  4, 64, 64, 64,  8,  9])
In [34]: arr[1:6]

Out[34]: array([ 1,  2,  3,  4, 64])

对于之前的二维数组arr2d,其切片方式稍显不同:


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

可以看出,它是沿着第0轴(即第一个轴)切片的。也就是说,切片是沿着一个轴向选取元素的。表达式arr2d[:2]可以被认为是“选取arr2d的前两行”。

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


In [37]: arr2d[:2, 1:]

Out[37]:
array([[1, 1],
       [3, 5]])

像这样进行切片时,只能得到相同维数的数组视图。通过将整数索引和切片混合,可以得到低维度的切片。

例如,我可以选取第三行的前两列:


In [38]: arr2d[2, :2]

Out[38]: array([2, 4])

下图对此进行了说明。注意,“只有冒号”表示选取整个轴,因此可以像下面这样只对高维轴进行切片:


In [39]: arr2d[:, :1]

Out[39]:
array([[1],
       [2],
       [2]])
二维数组切片

自然,对切片表达式的赋值操作也会被扩散到整个选区:


In [40]: arr2d[:2,1:] = 0
In [41]: arr2d

Out[41]:
array([[1, 0, 0],
       [2, 0, 0],
       [2, 4, 5]])

4.1.7 布尔型索引

假设我们有一个用于存储数据的数组以及一个存储姓名的数组(含有重复项)。在这里,我将使用numpy.random中的randn函数生成一些正态分布的随机数据:


In [42]: names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
         data = np.random.randn(7,4)
In [43]: names

Out[43]: array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'], dtype='<U4')
In [44]: data

Out[44]:
array([[ 0.73603062, -0.12614702, -0.54407036,  1.21298923],
       [-0.59730696,  1.2702293 ,  0.71270184, -0.74228686],
       [-0.12988137,  0.23263983, -0.08371573,  0.42319046],
       [ 1.78970064,  0.20477398, -0.12707496,  1.96050673],
       [ 0.71197402,  1.13861239,  1.58047673,  1.48136366],
       [-0.70338155, -1.15144029, -0.50996925,  0.79713611],
       [ 1.41090564,  0.40505494, -0.04298217,  0.51167567]])

假设每个名字都对应data数组中的一行,而我们想要选出对应于名字"Bob"的所有行。跟算术运算一样,数组的比较运算(如==)也是矢量化的。因此,对names和字符串"Bob"的比较运算将会产生一个布尔型数组:


In [45]: names == 'Bob'

Out[45]: array([ True, False, False,  True, False, False, False])

这个布尔型数组可用于数组索引:


In [46]: data[names == 'Bob']

Out[46]:
array([[ 0.73603062, -0.12614702, -0.54407036,  1.21298923],
       [ 1.78970064,  0.20477398, -0.12707496,  1.96050673]])

布尔型数组的长度必须跟被索引的轴长度一致。此外,还可以将布尔型数组跟切片、整数(或整数序列,稍后将对此进行详细讲解)混合使用:

下面的例子,我选取了names == 'Bob'的行,并索引了列:


In [47]: data[names == 'Bob', 2:]
Out[47]:
array([[-0.54407036,  1.21298923],
       [-0.12707496,  1.96050673]])

In [48]: data[names == 'Bob', 3]
Out[48]: array([1.21298923, 1.96050673])

要选择除"Bob"以外的其他值,既可以使用不等于符号(!=),也可以通过~对条件进行否定:


In [49]: names != 'Bob'
Out[49]: array([False,  True,  True, False,  True,  True,  True])
In [50]: data[~(names == 'Bob')]
Out[50]:
array([[-0.59730696,  1.2702293 ,  0.71270184, -0.74228686],
       [-0.12988137,  0.23263983, -0.08371573,  0.42319046],
       [ 0.71197402,  1.13861239,  1.58047673,  1.48136366],
       [-0.70338155, -1.15144029, -0.50996925,  0.79713611],
       [ 1.41090564,  0.40505494, -0.04298217,  0.51167567]])
​

~操作符用来反转条件很好用:


In [51]: cond = names == 'Bob'
In [52]: data[~cond]
Out[52]:
array([[-0.59730696,  1.2702293 ,  0.71270184, -0.74228686],
       [-0.12988137,  0.23263983, -0.08371573,  0.42319046],
       [ 0.71197402,  1.13861239,  1.58047673,  1.48136366],
       [-0.70338155, -1.15144029, -0.50996925,  0.79713611],
       [ 1.41090564,  0.40505494, -0.04298217,  0.51167567]])

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


In [56]: mask = (names == 'Bob') | (names == 'Will')
In [57]: mask

Out[57]: array([ True, False,  True,  True,  True, False, False])

In [58]: data[mask]

Out[58]:
array([[ 0.73603062, -0.12614702, -0.54407036,  1.21298923],
       [-0.12988137,  0.23263983, -0.08371573,  0.42319046],
       [ 1.78970064,  0.20477398, -0.12707496,  1.96050673],
       [ 0.71197402,  1.13861239,  1.58047673,  1.48136366]])

通过布尔型索引选取数组中的数据,将总是创建数据的副本,即使返回一模一样的数组也是如此。

注意:Python关键字and和or在布尔型数组中无效。要使用&与|。

通过布尔型数组设置值是一种经常用到的手段。为了将data中的所有负值都设置为0,我们只需:


In [59]: data[data < 0] = 0
In [60]: data

Out[60]:
array([[0.73603062, 0.        , 0.        , 1.21298923],
       [0.        , 1.2702293 , 0.71270184, 0.        ],
       [0.        , 0.23263983, 0.        , 0.42319046],
       [1.78970064, 0.20477398, 0.        , 1.96050673],
       [0.71197402, 1.13861239, 1.58047673, 1.48136366],
       [0.        , 0.        , 0.        , 0.79713611],
       [1.41090564, 0.40505494, 0.        , 0.51167567]])

通过一维布尔数组设置整行或列的值也很简单:


In [61]: data[names == 'Joe'] = 7
In [62]: data

Out[62]:
array([[0.73603062, 0.        , 0.        , 1.21298923],
       [7.        , 7.        , 7.        , 7.        ],
       [0.        , 0.23263983, 0.        , 0.42319046],
       [1.78970064, 0.20477398, 0.        , 1.96050673],
       [0.71197402, 1.13861239, 1.58047673, 1.48136366],
       [7.        , 7.        , 7.        , 7.        ],
       [7.        , 7.        , 7.        , 7.        ]])

当然,这些操作也可以用Pandas来做。

4.1.8 花式索引

花式索引(Fancy indexing)是一个NumPy术语,它指的是利用整数数组进行索引。假设我们有一个8×4数组:


In [2]: import numpy as np
         arr = np.empty((8,4))
In [3]: for i in range(8):
             arr[i] = i
In [4]: arr

Out[4]:
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 [5]: arr[[4, 3, 0, 6]]

Out[5]:
array([[4., 4., 4., 4.],
       [3., 3., 3., 3.],
       [0., 0., 0., 0.],
       [6., 6., 6., 6.]])

这段代码确实达到我们的要求了!使用负数索引将会从末尾开始选取行:


In [6]:barr[[-3, -5, -7]]

Out[6]:
array([[5., 5., 5., 5.],
       [3., 3., 3., 3.],
       [1., 1., 1., 1.]])

 一次传入多个索引数组会有一点特别。它返回的是一个一维数组,其中的元素对应各个索引元组:


In [7]: arr = np.arange(32).reshape((8,4))
In [8]: arr

Out[8]:
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 [9]: arr[[1, 5, 7, 2], [0, 3, 1, 2]]
Out[9]: array([ 4, 23, 29, 10])

 最终选出的是元素(1,0)、(5,3)、(7,1)和(2,2)。无论数组是多少维的,花式索引总是一维的。

记住,花式索引跟切片不一样,它总是将数据复制到新数组中。

4.1.9 数组转置和轴对换

转置是重塑的一种特殊形式,它返回的是源数据的视图(不会进行任何复制操作)。数组不仅有transpose方法,还有一个特殊的T属性:


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

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

 在进行矩阵计算时,经常需要用到该操作,比如利用np.dot计算矩阵内积:


In [13]: arr = np.random.randn(6, 3)
In [14]: arr
Out[14]:
array([[ 0.76416581, -0.59371434,  0.68181681],
       [ 0.30954994,  0.73782933,  1.17631282],
       [ 0.06555348,  0.33505602,  0.22756087],
       [-0.05010979,  2.00622115, -1.17668862],
       [ 0.66066926,  1.20410624, -0.27191449],
       [ 1.48285532,  0.1020095 , -0.74993385]])

In [15]: np.dot(arr.T, arr)
Out[15]:
array([[ 3.32192256,  0.64291292, -0.33265926],
       [ 0.64291292,  6.49435244, -2.22525257],
       [-0.33265926, -2.22525257,  3.92130431]])

 对于高维数组,transpose需要得到一个由轴编号组成的元组才能对这些轴进行转置(比较费脑子):


In [16]: arr = np.arange(16).reshape((2, 2, 4))
In [17]: arr

Out[17]:
array([[[ 0,  1,  2,  3],
        [ 4,  5,  6,  7]],

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

In [18]: arr.transpose((1, 0, 2))

Out[18]:
array([[[ 0,  1,  2,  3],
        [ 8,  9, 10, 11]],

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

这里,第一个轴被换成了第二个,第二个轴被换成了第一个,最后一个轴不变。

简单的转置可以使用.T,它其实就是进行轴对换而已。ndarray还有一个swapaxes方法,它需要接受一对轴编号:


In [19]: arr
Out[19]:
array([[[ 0,  1,  2,  3],
        [ 4,  5,  6,  7]],

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

In [20]: arr.swapaxes(1, 2)
Out[20]:
array([[[ 0,  4],
        [ 1,  5],
        [ 2,  6],
        [ 3,  7]],

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

swapaxes也是返回源数据的视图(不会进行任何复制操作)。

4.2 通用函数:快速的元素级数组函数

通用函数(即ufunc)是一种对ndarray中的数据执行元素级运算的函数。你可以将其看做简单函数(接受一个或多个标量值,并产生一个或多个标量值)的矢量化包装器。

许多ufunc都是简单的元素级变体,如sqrt和exp:


In [21]: arr = np.arange(10)
In [22]: arr

Out[22]:
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

In [23]: np.sqrt(arr)
Out[23]:
array([0.        , 1.        , 1.41421356, 1.73205081, 2.        ,
       2.23606798, 2.44948974, 2.64575131, 2.82842712, 3.        ])

In [24]: np.exp(arr)
Out[24]:
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])

这些都是一元(unary)ufunc。另外一些(如add或maximum)接受2个数组(因此也叫二元(binary)ufunc),并返回一个结果数组:


In [25]: x = np.random.randn(8)
In [26]: y = np.random.randn(8)
In [27]: x
Out[27]:
array([-0.95361393, -1.61004574,  0.15676379, -0.21776008,  0.69062465,
       -1.52815122, -0.04371557,  2.01002837])

In [28]: y
Out[28]:
array([ 0.41073896, -0.67278923, -0.15690505, -0.71940875, -1.68803873,
        1.20356913,  0.13458085, -1.6300652 ])

In [30]: np.maximum(x, y)
Out[30]:
array([ 0.41073896, -0.67278923,  0.15676379, -0.21776008,  0.69062465,
        1.20356913,  0.13458085,  2.01002837])

这里,numpy.maximum计算了x和y中元素级别最大的元素。

虽然并不常见,但有些ufunc的确可以返回多个数组。modf就是一个例子,它是Python内置函数divmod的矢量化版本,它会返回浮点数数组的小数和整数部分:


In [31]: arr = np.random.randn(7) * 5
In [32]: arr
Out[32]:
array([-4.4759812 , -0.69628583, -9.28954211,  0.97787069, -9.80958373,
        3.50748262,  1.95993912])

In [33]: remainder, whole_part = np.modf(arr)
In [34]: remainder
Out[34]:
array([-0.4759812 , -0.69628583, -0.28954211,  0.97787069, -0.80958373,
        0.50748262,  0.95993912])

In [35]: whole_part
Out[35]:
array([-4., -0., -9.,  0., -9.,  3.,  1.])

Ufuncs可以接受一个out可选参数,这样就能在数组原地进行操作:

print(arr)
print(np.sqrt(arr))
print(np.sqrt(arr, arr))
print(arr)

output
[ -2.88452855  -1.14582772   3.00420715   1.22281642  -0.38185437
 -11.36241841   6.52291062]
[        nan         nan  1.73326488  1.1058103          nan         nan
  2.55399895]
[        nan         nan  1.73326488  1.1058103          nan         nan
  2.55399895]
[        nan         nan  1.73326488  1.1058103          nan         nan
  2.55399895]

表4-3和表4-4分别列出了一些一元和二元ufunc。

4.3 利用数组进行数据处理

NumPy数组使你可以将许多种数据处理任务表述为简洁的数组表达式(否则需要编写循环)。用数组表达式代替循环的做法,通常被称为矢量化。

作为简单的例子,假设我们想要在一组值(网格型)上计算函数sqrt(x^2+y^2)。np.meshgrid函数接受两个一维数组,并产生两个二维矩阵(对应于两个数组中所有的(x,y)对):


In [40]: points = np.arange(-5, 5, 0.01) #步长0.01 ,总计1000个离散点
In [41]: xs, ys = np.meshgrid(points, points)
In [42]: ys

Out[42]:
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 [45]: z = np.sqrt(xs ** 2 + ys ** 2)
In [46]: z
Out[46]:
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创建了这个二维数组的可视化:


In [47]: import matplotlib.pyplot as plt
In [48]: plt.imshow(z, cmap = plt.cm.gray); plt.colorbar()

Out[48]: <matplotlib.colorbar.Colorbar at 0x16e18e7dc88>

In [49]: plt.title('Image plot of $\sqrt{x^2 + y^2}$ for a grid of values')
Out[49]: Text(0.5,1,'Image plot of $\\sqrt{x^2 + y^2}$ for a grid of values')

这张图是用matplotlib的imshow函数创建的。

4.3.1 将条件逻辑表述为数组运算

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


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

假设我们想要根据cond中的值选取xarr和yarr的值:当cond中的值为True时,选取xarr的值,否则从yarr中选取。列表推导式的写法应该如下所示:


In [63]: result = [(x if c else y)
         for x, y, c in zip(xarr, yarr, cond)]
In [64]: result

Out[64]:
[1.1, 2.2, 1.3, 1.4, 2.5]

这有几个问题。第一,它对大数组的处理速度不是很快(因为所有工作都是由纯Python完成的)。第二,无法用于多维数组。若使用np.where,则可以将该功能写得非常简洁:


In [65]: result = np.where(cond, xarr, yarr)
In [66]: result

Out[66]:
array([1.1, 2.2, 1.3, 1.4, 2.5])

np.where的第二个和第三个参数不必是数组,它们都可以是标量值。在数据分析工作中,where通常用于根据另一个数组而产生一个新的数组。假设有一个由随机数据组成的矩阵,你希望将所有正值替换为2,将所有负值替换为-2。若利用np.where,则会非常简单:


In [67]: arr = np.random.randn(4, 4)
In [68]: arr

Out[68]:
array([[-0.64496779,  0.52414139,  1.18606729,  1.24215752],
       [ 0.14011134,  0.68157075, -1.17944087,  0.77633086],
       [-1.52888488,  0.74012192,  0.67633297,  1.07999609],
       [-1.85749656, -0.98081351,  0.56145716, -0.19420933]])

In [69]: arr > 0
Out[69]:
array([[False,  True,  True,  True],
       [ True,  True, False,  True],
       [False,  True,  True,  True],
       [False, False,  True, False]])

In [70]: np.where(arr > 0, 2, -2 )
Out[70]:
array([[-2,  2,  2,  2],
       [ 2,  2, -2,  2],
       [-2,  2,  2,  2],
       [-2, -2,  2, -2]])

使用np.where,可以将标量和数组结合起来。例如,我可用常数2替换arr中所有正的值:


In [71]: np.where(arr > 0, 2, arr)
Out[71]:
array([[-0.64496779,  2.        ,  2.        ,  2.        ],
       [ 2.        ,  2.        , -1.17944087,  2.        ],
       [-1.52888488,  2.        ,  2.        ,  2.        ],
       [-1.85749656, -0.98081351,  2.        , -0.19420933]])

传递给where的数组大小可以不相等,甚至可以是标量值。

4.3.2 数学和统计方法

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

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


In [72]: arr = np.random.randn(5, 4)
In [73]: arr
Out[73]:
array([[-0.18969458,  2.42217634, -0.11111327, -0.57700033],
       [-1.01329696,  0.00691129, -1.50832842,  0.24767229],
       [-1.41086114, -1.17894482, -0.71458589,  0.18638283],
       [-0.02377788,  0.83628221, -0.98033713,  1.26823669],
       [ 1.18835246,  0.37427623,  0.57923965, -1.60212904]])

In [74]: arr.mean()
Out[74]: -0.11002697365467654

In [75]: np.mean(arr)
Out[75]: -0.11002697365467654

In [76]: arr.sum()
Out[76]: -2.2005394730935306

mean和sum这类的函数可以接受一个axis选项参数,用于计算该轴向上的统计值,最终结果是一个少一维的数组:


In [77]: arr.mean(axis = 1)
Out[77]: array([ 0.38609204, -0.56676045, -0.77950226,  0.27510097,  0.13493483])
In [78]: arr.sum(axis = 0)
Out[78]: array([-1.4492781 ,  2.46070125, -2.73512506, -0.47683756])

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

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


In [79]: arr = np.array([1, 2, 3, 4, 5, 6, 7, 8])
In [80]: arr.cumsum()
Out[80]: array([ 1,  3,  6, 10, 15, 21, 28, 36], dtype=int32)

在多维数组中,累加函数(如cumsum)返回的是同样大小的数组,但是会根据每个低维的切片沿着标记轴计算部分聚类:


In [81]: arr = np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
In [82]: arr
Out[82]:
array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])

In [83]: arr.cumsum(axis = 0)
Out[83]:
array([[ 0,  1,  2],
       [ 3,  5,  7],
       [ 9, 12, 15]], dtype=int32)

In [84]: arr.cumprod(axis = 1)
Out[84]:
array([[  0,   0,   0],
       [  3,  12,  60],
       [  6,  42, 336]], dtype=int32)

​

表4-5列出了全部的基本数组统计方法。后续章节中有很多例子都会用到这些方法。

4.3.3 用于布尔型数组的方法

在上面这些方法中,布尔值会被强制转换为1(True)和0(False)。因此,sum经常被用来对布尔型数组中的True值计数:


In [85]: arr = np.random.randn(100)
In [86]: (arr > 0).sum()
Out[86]: 40
​

另外还有两个方法any和all,它们对布尔型数组非常有用。any用于测试数组中是否存在一个或多个True,而all则检查数组中所有值是否都是True:


In [87]: bools = np.array([False, False, True, False])
In [88]: bools.any()
Out[88]: True
In [89]: bools.all()
Out[89]: False

这两个方法也能用于非布尔型数组,所有非0元素将会被当做True。

4.3.4 排序

跟Python内置的列表类型一样,NumPy数组也可以通过sort方法就地排序:


In [99]: arr = np.random.randn(6)
In [100]: arr

Out[100]:
array([ 0.89934768,  0.49889372, -1.80138268,  1.35137542, -0.29677817,
       -1.04741995])

In [101]: arr.sort()
In [102]: arr

Out[102]:
array([-1.80138268, -1.04741995, -0.29677817,  0.49889372,  0.89934768,
        1.35137542])

多维数组可以在任何一个轴向上进行排序,只需将轴编号传给sort即可:


In [95]: arr = np.random.randn(5, 3)
In [96]: arr

Out[96]:
array([[-0.44073589, -0.84379604, -1.74775132],
       [-1.17503284,  0.54021733, -0.18219635],
       [-0.22771804,  0.98648339, -0.71349615],
       [ 0.5721064 ,  0.38012503,  0.67521682],
       [ 0.49641397, -0.50484124, -2.19343809]])

In [97]: arr.sort(1)
In [98]: print(arr)
[[-1.74775132 -0.84379604 -0.44073589]
 [-1.17503284 -0.18219635  0.54021733]
 [-0.71349615 -0.22771804  0.98648339]
 [ 0.38012503  0.5721064   0.67521682]
 [-2.19343809 -0.50484124  0.49641397]]

顶级方法np.sort返回的是数组的已排序副本,而就地排序则会修改数组本身。计算数组分位数最简单的办法是对其进行排序,然后选取特定位置的值:


In [103]: large_arr = np.random.randn(1000)
In [104]: large_arr.sort()
In [105]: large_arr[int(0.05 * len(large_arr))]
Out[105]: -1.6334705078901595

​

NumPy中提供了各种排序相关功能。 这些排序函数实现不同的排序算法,每个排序算法的特征在于执行速度,最坏情况性能,所需的工作空间和算法的稳定性。 下表显示了三种排序算法的比较。

种类 速度 最坏情况 工作空间 稳定性
'quicksort'(快速排序) 1 O(n^2) 0
'mergesort'(归并排序) 2 O(n*log(n)) ~n/2
'heapsort'(堆排序) 3 O(n*log(n)) 0

numpy.sort()

sort()函数返回输入数组的排序副本。 它有以下参数:

numpy.sort(a, axis, kind, order)

其中:

序号 参数及描述
1 a 要排序的数组
2 axis 沿着它排序数组的轴,如果没有数组会被展开,沿着最后的轴排序
3 kind 默认为'quicksort'(快速排序)
4 order 如果数组包含字段,则是要排序的字段

示例:

In [90]: import numpy as np
​
          a = np.array([[3, 7], [9, 2]])
In [94]:

print('我们的数组:')
print(a)
print('\n')
print('调用sort()函数:')
print(np.sort(a))
print('\n')
print('沿轴 0 排序:')
print(np.sort(a, axis = 0))
print('\n')
# 对字段排序
dt = np.dtype([('name', 'S10'), ('age', int)])
a = np.array([('raju',21),('anil', 25),('ravi', 17), ('amar', 27)], dtype = dt)
print('我们的数组:')
print(a)
print('\n')
print('按 name 排序')
print(np.sort(a, order = 'name'))

output:
我们的数组:
[[3 7]
 [9 2]]


调用sort()函数:
[[3 7]
 [2 9]]


沿轴 0 排序:
[[3 2]
 [9 7]]


我们的数组:
[(b'raju', 21) (b'anil', 25) (b'ravi', 17) (b'amar', 27)]


按 name 排序
[(b'amar', 27) (b'anil', 25) (b'raju', 21) (b'ravi', 17)]

numpy.argsort()

numpy.argsort()函数对输入数组沿给定轴执行间接排序,并使用指定排序类型返回数据的索引数组。 这个索引数组用于构造排序后的数组。


In [6]: import numpy as np
In [7]: x = np.array([3, 1, 2])
In [9]:

print('我们的数组是:')
print(x)
print('\n')
print('对 x 调用 argsort()函数:')
y = np.argsort(x)
print(y)
print('\n')
print('以排序后的顺序重构原数组')
print(x[y])
print('\n')
print('使用循环重构原数组')
for i in y:
    print (x[i])

putput:
我们的数组是:
[3 1 2]

对 x 调用 argsort()函数:
[1 2 0]

以排序后的顺序重构原数组
[1 2 3]

使用循环重构原数组
1
2
3

​

numpy.lexsort()

函数使用键序列执行间接排序。 键可以看作是电子表格中的一列。 该函数返回一个索引数组,使用它可以获得排序数据。 注意,最后一个键恰好是 sort 的主键。

import numpy as np 
 
nm =  ('raju','anil','ravi','amar') 
dv =  ('f.y.',  's.y.',  's.y.',  'f.y.') 
ind = np.lexsort((dv,nm))  
print ('调用 lexsort() 函数:' ) 
print (ind) 
print ('\n')  
print ('使用这个索引来获取排序后的数据:')  
print ([nm[i]  +  ", "  + dv[i]  for i in ind])
 
output
调用 lexsort() 函数:
[3 1 0 2]


使用这个索引来获取排序后的数据:
['amar, f.y.', 'anil, s.y.', 'raju, f.y.', 'ravi, s.y.']

4.3.5 唯一化以及其它的集合逻辑

NumPy提供了一些针对一维ndarray的基本集合运算。最常用的可能要数np.unique了,它用于找出数组中的唯一值并返回已排序的结果:


In [11]: names = np.array(['Bob', 'Joe', 'Will', 'Bob', "Will", 'Joe', "Joe"])
In [12]: np.unique(names)

Out[12]: array(['Bob', 'Joe', 'Will'], dtype='<U4')

In [13]: ints = np.array([3, 3, 3, 2, 2, 1, 1, 4, 4])
In [14]: np.unique(ints)

Out[14]: array([1, 2, 3, 4])

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


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

​

另一个函数np.in1d用于测试一个数组中的值在另一个数组中的成员资格,返回一个布尔型数组:


In [16]: values = np.array([6, 0, 0, 3, 2, 5, 6])
In [18]: np.in1d(values, [2, 3, 6])
Out[18]: array([ True, False, False,  True,  True, False,  True])

NumPy中的集合函数如下表所示:

GitHub

4.4 用于数组的文件输入输出

NumPy能够读写磁盘上的文本数据或二进制数据。

np.save和np.load是读写磁盘数组数据的两个主要函数。默认情况下,数组是以未压缩的原始二进制格式保存在扩展名为.npy的文件中的:


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

In [21]: print(np.load('some_array.npy'))

out[22]: [0 1 2 3 4 5 6 7 8 9]
​

如果文件路径末尾没有扩展名.npy,则该扩展名会被自动加上。然后就可以通过np.load读取磁盘上的数组。

通过np.savez可以将多个数组保存到一个未压缩文件中,将数组以关键字参数的形式传入即可:


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

加载.npz文件时,你会得到一个类似字典的对象,该对象会对各个数组进行延迟加载:


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

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


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

4.5 线性代数

线性代数(如矩阵乘法、矩阵分解、行列式以及其他方阵数学等)是任何数组库的重要组成部分。不像某些语言(如MATLAB),通过*对两个二维数组相乘得到的是一个元素级的积,而不是一个矩阵点积。因此,NumPy提供了一个用于矩阵乘法的dot函数(既是一个数组方法也是numpy命名空间中的一个函数):


In [27]: x = np.array([[1., 2., 3.],[4., 5., 6]])
In [28]: y = np.array([[6., 23.],[-1, 7],[8, 9]])
In [29]: x

Out[29]:
array([[1., 2., 3.],
       [4., 5., 6.]])

In [30]: y

Out[30]:
array([[ 6., 23.],
       [-1.,  7.],
       [ 8.,  9.]])

In [31]: x.dot(y)

Out[31]:
array([[ 28.,  64.],
       [ 67., 181.]])

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


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

一个二维数组跟一个大小合适的一维数组的矩阵点积运算之后将会得到一个一维数组:


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

@符(类似Python 3.5)也可以用作中缀运算符,进行矩阵乘法:


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

numpy.linalg中有一组标准的矩阵分解运算以及诸如求逆和行列式之类的东西。它们跟MATLAB和R等语言所使用的是相同的行业标准线性代数库,如BLAS、LAPACK、Intel MKL(Math Kernel Library,可能有,取决于你的NumPy版本)等:


In [36]: from numpy.linalg import inv, qr
In [37]: x = np.random.rand(5, 5)
In [38]: mat = x.T.dot(x)
In [39]: inv(mat)

Out[39]:
array([[ 24.25773408,   3.09904598, -10.49682692, -23.85575513,
          7.05938574],
       [  3.09904598,   2.63122057,  -2.15738514,  -2.84277908,
         -0.4158934 ],
       [-10.49682692,  -2.15738514,   6.91840105,   9.17259942,
         -4.10985733],
       [-23.85575513,  -2.84277908,   9.17259942,  25.96915031,
         -7.82584247],
       [  7.05938574,  -0.4158934 ,  -4.10985733,  -7.82584247,
          6.09047827]])

In [40]: mat.dot(inv(mat))
Out[40]:
array([[ 1.00000000e+00,  1.08490052e-15,  8.63057225e-17,
         1.50049454e-16, -8.01708725e-16],
       [ 4.76736930e-15,  1.00000000e+00, -7.01519917e-17,
        -5.44503590e-15,  3.71342593e-16],
       [ 5.85014959e-15,  1.23737453e-15,  1.00000000e+00,
        -3.13033967e-15,  9.67269400e-16],
       [-3.18522853e-15,  6.30828711e-16,  3.05358331e-15,
         1.00000000e+00, -4.94376604e-16],
       [ 2.91234983e-15,  1.16323349e-15, -6.18529051e-16,
        -2.43961692e-15,  1.00000000e+00]])

In [41]: q, r = qr(mat)
In [42]: r

Out[42]:
array([[-3.1693019 , -2.59359093, -3.57833789, -2.67421668, -2.40708289],
       [ 0.        , -0.88414288, -0.52716641, -0.06335676, -0.54377552],
       [ 0.        ,  0.        , -0.28652111,  0.08813383, -0.10099342],
       [ 0.        ,  0.        ,  0.        , -0.08465008, -0.23351305],
       [ 0.        ,  0.        ,  0.        ,  0.        ,  0.07779415]])

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

下表列出了一些最常用的线性代数函数:

GitHub

4.6 伪随机数生成

numpy.random模块对Python内置的random进行了补充,增加了一些用于高效生成多种概率分布的样本值的函数。例如,你可以用normal来得到一个标准正态分布的4×4样本数组:


In [43]: samples = np.random.normal(size =(4, 4))
In [44]: samples
Out[44]:
array([[ 0.26031823,  0.98180027, -0.12338395, -1.66632337],
       [ 0.71714765,  0.3356353 ,  0.40195087,  0.24449305],
       [ 1.55741521,  0.73821615, -0.635539  ,  1.01435757],
       [ 0.97447286,  0.32439164,  0.12732344, -0.36458816]])

而Python内置的random模块则只能一次生成一个样本值。从下面的测试结果中可以看出,如果需要产生大量样本值,numpy.random快了不止一个数量级:


In [45]: from random import normalvariate
In [46]: N = 1000000
In [47]: %timeit smples = [normalvariate(0, 1) for _ in range(N)]

617 ms ± 1.57 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [49]: %timeit np.random.normal(size = N)

25 ms ± 209 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

​

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

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

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


In [51]: rng = np.random.RandomState(1234)
In [52]: rng.randn(10)

Out[52]:
array([ 0.47143516, -1.19097569,  1.43270697, -0.3126519 , -0.72058873,
        0.88716294,  0.85958841, -0.6365235 ,  0.01569637, -2.24268495])

下表列出了numpy.random中的部分函数。在下一节中,我将给出一些利用这些函数一次性生成大量样本值的范例。

4.7 随机漫步

4.7.1 单个随机漫步

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

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


In [56]:

import random
import matplotlib.pyplot as plt
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)
​
plt .plot(walk[:100])
简单的随机漫步

不难看出,这其实就是随机漫步中各步的累计和,可以用一个数组运算来实现。因此,我用np.random模块一次性随机产生1000个“掷硬币”结果(即两个数中任选一个),将其分别设置为1或-1,然后计算累计和:


In [57]: nsteps = 1000
In [58]: draws = np.random.randint(0, 2, size = nsteps)
In [59]: steps = np.where(draws > 0, 1, -1)
In [60]: walk = steps.cumsum()

有了这些数据之后,我们就可以沿着漫步路径做一些统计工作了,比如求取最大值和最小值:


In [61]: walk.min()

Out[61]: -9

In [62]: walk.max()

Out[62]: 60

现在来看一个复杂点的统计任务——首次穿越时间,即随机漫步过程中第一次到达某个特定值的时间。假设我们想要知道本次随机漫步需要多久才能距离初始0点至少10步远(任一方向均可)。np.abs(walk)>=10可以得到一个布尔型数组,它表示的是距离是否达到或超过10,而我们想要知道的是第一个10或-10的索引。可以用argmax来解决这个问题,它返回的是该布尔型数组第一个最大值的索引(True就是最大值):


In [63]: (np.abs(walk) >= 10).argmax()
Out[63]: 297

注意,这里使用argmax并不是很高效,因为它无论如何都会对数组进行完全扫描。在本例中,只要发现了一个True,那我们就知道它是个最大值了。

4.7.2 一次模拟多个随机漫步

如果你希望模拟多个随机漫步过程(比如5000个),只需对上面的代码做一点点修改即可生成所有的随机漫步过程。只要给numpy.random的函数传入一个二元元组就可以产生一个二维数组,然后我们就可以一次性计算5000个随机漫步过程(一行一个)的累计和了:


In [64]: nwalks = 5000
In [65]: nsteps = 1000
In [66]: draws = np.random.randint(0, 2, size = (nwalks, nsteps)) # 0 或者 1
In [68]: steps = np.where(draws > 0, 1, -1)
In [69]: walks = steps.cumsum(1)
In [70]: walks

Out[70]:
array([[  1,   2,   3, ...,  46,  47,  46],
       [  1,   0,   1, ...,  40,  41,  42],
       [  1,   2,   3, ..., -26, -27, -28],
       ...,
       [  1,   0,   1, ...,  64,  65,  66],
       [  1,   2,   1, ...,   2,   1,   0],
       [ -1,  -2,  -3, ...,  32,  33,  34]], dtype=int32)

现在,我们来计算所有随机漫步过程的最大值和最小值:


In [71]: walks.max()

Out[71]: 122

In [72]: walks.min()

Out[72]: -128

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


In [73]: hits30 = (np.abs(walks) >= 30).any(1)
In [74]: hits30

Out[74]: array([ True,  True,  True, ...,  True, False,  True])

In [75]: hits30.sum()

Out[75]: 3368

然后我们利用这个布尔型数组选出那些穿越了30(绝对值)的随机漫步(行),并调用argmax在轴1上获取穿越时间:


In [76]: crossing_times = (np.abs(walks[hits30]) >= 30).argmax(1)
In [78]: crossing_times.mean()
Out[78]: 509.99762470308787

请尝试用其他分布方式得到漫步数据。只需使用不同的随机数生成函数即可,如normal用于生成指定均值和标准差的正态分布数据:


In [79]: steps = np.random.normal(loc = 0, scale = 0.25, size =(nwalks,nsteps))

4.8 参考

作者:SeanCheney
链接:https://www.jianshu.com/p/a380222a3292
來源:简书

猜你喜欢

转载自blog.csdn.net/xyk_hust/article/details/84067476