1.前言
Numpy是Numerical Python的简称,它是目前Python数值计算中最为重要的基础包。大多数计算包都提供了基于Numpy的科学函数功能,将Numpy的数组对象作为数据交换的通用语。
Numpy之所以如此重要,其中一个原因就是它的设计对于含有大量数组的数据非常有效。此外,还因为Numpy可以针对全量数组进行复杂计算而不需要写Python循环。并且,Numpy在内部将数据存储在连续的内存块上,这与其他的Python内建数据结构是不同的。Numpy的算法库是用C语言写的,所以在操作数据内存时,不需要任何类型检查或者其他管理操作。Numpy数组使用的内存量也小于其他Python内建序列。
此文,仅是笔者对于Numpy学习的一个笔记。如有错误,还请指出。
此外,本文代码在ipython中编写。
2.Numpy ndarray:多维数组对象
2.1简单认识
一个ndarray是一个通用的多维同类数据容器,也就是说,它包含的每一个元素均为相同类型。每一个数组都有一个shape属性,用来表征数组每一维度的数量;每一个数组都有一个dtype属性,用来描述数组的数据类型。
这里强调,数组允许你使用类似于标量的操作语法在整块数据上进行数学计算。先来感受一下。
In [1]: import numpy as np
# 生成随机数组
In [2]: data = np.random.randn(2, 3)
In [3]: data
Out[3]:
array([[-0.54037056, -0.08875289, 0.4357259 ],
[-0.92202963, 1.10186978, 0.29164677]])
In [4]: data * 10
Out[4]:
array([[-5.40370558, -0.88752891, 4.35725902],
[-9.2202963 , 11.01869779, 2.91646771]])
In [5]: data + data
Out[5]:
array([[-1.08074112, -0.17750578, 0.8714518 ],
[-1.84405926, 2.20373956, 0.58329354]])
In [6]: data.shape
Out[6]: (2, 3)
In [7]: data.dtype
Out[7]: dtype('float64')
可以看到,这些数学操作是对所有的元素同时进行操作。
2.2生成 ndarray
生成数组最简单的方式就是使用array函数。array函数接收任意的序列对象(当然也包括其他的数组),生成一个新的包含传递数据的Numpy数组。例如:
In [8]: data1 = [6, 7.5, 8, 0, 1]
In [9]: arr1 = np.array(data1)
In [10]: arr1
Out[10]: array([6. , 7.5, 8. , 0. , 1. ])
对于嵌套的序列,像等长度的列表,将会自动转换成多维数组:
In [11]: data2 = [[1, 2, 3, 4], [5, 6, 7, 8]]
In [12]: arr2 = np.array(data2)
In [13]: arr2
Out[13]:
array([[1, 2, 3, 4],
[5, 6, 7, 8]])
我们可以通过ndim和shape属性来确认:
In [14]: arr2.ndim
Out[14]: 2
In [15]: arr2.shape
Out[15]: (2, 4)
除非显式地指定,否则np.array会自动推断生成数组的数据类型。数据类型被存储在一个特殊的元数据dtype中。如之前所示。
除了np.array,还有很多其他函数可以创建新数组。例如,给定长度及形状后,zeros可以一次性创造全0数组,ones可以一次性创造全1数组。empty则可以创建一个没有初始化数值的数组。想要创建高维数组,则需要为shape传递一个元组:
In [16]: np.zeros(10)
Out[16]: array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
In [17]: np.zeros((6, 9))
Out[17]:
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., 0., 0., 0.],
[0., 0., 0., 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 [18]: np.empty((2, 2, 3))
Out[18]:
array([[[0., 0., 0.],
[0., 0., 0.]],
[[0., 0., 0.],
[0., 0., 0.]]])
注意:想要使用np.empty来生成一个全0数组,并不安全,有些时候它可能会返回未初始化的垃圾数值。
arange是Python内建函数range的数组版:
In [19]: np.arange(10)
Out[19]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
由于Numpy专注于数值计算,如果没有特别指明的情况的话,默认的数据类型是float64(浮点型)。
标准数组的生成函数表
2.3ndarray的数据类型
数据类型,即dtype,是一个特殊的对象,它包含了ndarray需要为某一种类型数据所申明的内存信息(也称元数据,即表示数据的数据)。
dtype是Numpy能够与其它系统数据都灵活交互的原因。通常,其他系统提供一个硬盘或内存与数据的对应关系,使得利用C或Fortran等底层语言读写数据变得十分方便。数组的dtype通常都是按照一个方式命名:类型名,比如float和int,后面再接上表明每个元素位数的数字。一个标准的双精度浮点值(Python中数据类型为float),将使用8字节或64位。因此,这个类型在Numpy中称为float64。
注意:不要担心如何记住Numpy的数据类型,尤其当你还是新手的时候。通常你只需要关心数据的大类,比如是否是浮点型、整数、布尔值、字符串或某个Python对象。当你需要在内存或硬盘上做更深入的存取操作时,尤其是大数据集时,你才真正需要了解存储的数据类型。
Numpy数据类型表
你可以使用astype方法显式地转换数组的数据类型:
In [20]: arr = np.array([1, 2, 3, 4, 5])
In [21]: arr.dtype
Out[21]: dtype('int32')
In [22]: float_arr = arr.astype(np.float64)
In [23]: float_arr.dtype
Out[23]: dtype('float64')
上面整数被转换成了浮点数,如果把浮点数转换成整数,则小数点后的部分将被消除:
In [24]: arr = np.array([3.7, -1.2, -2.6, 0.6, 12.8])
In [25]: arr
Out[25]: array([ 3.7, -1.2, -2.6, 0.6, 12.8])
In [26]: arr.astype(np.int32)
Out[26]: array([ 3, -1, -2, 0, 12])
如果有一个数组,里面的元素都是表达数字含义的字符串,也可以通过astype将字符串转换成数字:
In [27]: strings = np.array(['1.23', '2.3', '6.8'], dtype=np.string_)
In [28]: strings.astype(float)
Out[28]: array([1.23, 2.3 , 6.8 ])
注意:在Numpy中,当使用numpy.string_类型作字符串数据要小心,因为Numpy会修正它的大小或删除输入且不发出警告。pandas在处理非数值数据时有更直观的开箱型操作。
如果因为某些原因导致转换类型失败(比如字符串无法转换为float64位时),将会抛出一个ValueError,这里可以偷懒使用float来代替np.float64,是因为Numpy可以使用相同别名来表征与Python精度相同的Python数据类型。
2.4Numpy数组算术
数组之所以重要是因为它允许进行批量操作而无需任何for循环。这被称为向量化。任何两个等尺寸数组之间的算术操作都运用了逐元素操作的方式:
In [30]: arr
Out[30]:
array([[1., 2., 3.],
[4., 5., 6.]])
In [31]: arr * arr
Out[31]:
array([[ 1., 4., 9.],
[16., 25., 36.]])
In [32]: arr - arr
Out[32]:
array([[0., 0., 0.],
[0., 0., 0.]])
带有标量计算的算术操作,会把计算参数传递给数组的每一个元素:
In [33]: 1 / arr
Out[33]:
array([[1. , 0.5 , 0.33333333],
[0.25 , 0.2 , 0.16666667]])
In [34]: arr ** 0.5
Out[34]:
array([[1. , 1.41421356, 1.73205081],
[2. , 2.23606798, 2.44948974]])
同尺寸数组之间的比较,会产生一个布尔值数组:
In [35]: arr2 = np.array([[0., 4., 1.], [7., 2., 12.]])
In [36]: arr2
Out[36]:
array([[ 0., 4., 1.],
[ 7., 2., 12.]])
In [37]: arr2 > arr
Out[37]:
array([[False, True, False],
[ True, False, True]])
不同尺寸的数组间的操作将会用到广播特性,对于本文的内容并不需要深入理解广播特性。
2.5基础索引与切片
Numpy的数组索引是一个大话题,有很多种方式可以让你选中数据的子集或某个单个元素。一维数组比较简单,看起来和Python的列表很类似:
In [2]: arr = np.arange(10)
In [3]: arr
Out[3]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [4]: arr[5]
Out[4]: 5
In [5]: arr[5: 8]
Out[5]: array([5, 6, 7])
In [6]: arr[5: 8] = 12
In [7]: arr
Out[7]: array([ 0, 1, 2, 3, 4, 12, 12, 12, 8, 9])
如上面所见,如果传入了一个数值给数组的切片,例如arr[5: 8] = 12,数值被传递给了整个切片。区别于Python的内建列表,数组的切片是原数组的视图。这意味着数据并不是被复制了,任何对于视图的修改都会反映到原数组上。例子:
In [8]: arr_slice = arr[5: 8]
In [9]: arr_slice
Out[9]: array([12, 12, 12])
In [10]: arr_slice[1] = 123
In [11]: arr
Out[11]: array([ 0, 1, 2, 3, 4, 12, 123, 12, 8, 9])
不写切片值的[:]将会引用数组所有的值:
In [12]: arr_slice[:] = 64
In [13]: arr
Out[13]: array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9])
注意:如果想要一份数组切片的拷贝而不是一份视图的话,就必须显式地复制这个数组,例如arr[5: 8].copy()
对于更高维度的数组,会有更多的选择。在一个二维数组中,每个索引值对应的元素不再是一个值,而是一个一维数组:
In [14]: arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
In [15]: arr2d[2]
Out[15]: array([7, 8, 9])
In [16]: arr2d[0][2]
Out[16]: 3
In [17]: arr2d[0, 2]
Out[17]: 3
在多维数组中,可以省略后续索引值,返回的对象将是降低一个维度的数组:
In [18]: arr3d = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])
In [19]: arr3d
Out[19]:
array([[[ 1, 2, 3],
[ 4, 5, 6]],
[[ 7, 8, 9],
[10, 11, 12]]])
In [20]: arr3d[0]
Out[20]:
array([[1, 2, 3],
[4, 5, 6]])
标量和数组都可以传递给arr3d[0]:
In [21]: old_value = arr3d[0].copy()
In [22]: arr3d[0] = 42
In [23]: arr3d
Out[23]:
array([[[42, 42, 42],
[42, 42, 42]],
[[ 7, 8, 9],
[10, 11, 12]]])
In [24]: arr3d[0] = old_value
In [25]: arr3d
Out[25]:
array([[[ 1, 2, 3],
[ 4, 5, 6]],
[[ 7, 8, 9],
[10, 11, 12]]])
类似地,可以进行一些一维数组的操作,这里就不再演示。
注意:以上数组子集选择中,返回的数组都是视图。
2.6数组的切片索引
与Python列表的一维对象类似,数组可以通过类似的语法进行切片:
In [26]: arr
Out[26]: array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9])
In [27]: arr[1: 6]
Out[27]: array([ 1, 2, 3, 4, 64])
然而二维数组,arr2d,对数组进行切片略有不同:
In [28]: arr2d
Out[28]:
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
In [29]: arr2d[: 2]
Out[29]:
array([[1, 2, 3],
[4, 5, 6]])
还可以进行多组切片,和多组索引类似:
In [30]: arr2d[: 2, 1: ]
Out[30]:
array([[2, 3],
[5, 6]])
当像上面这个例子中那样切片时,你需要按照原数组的维度进行切片。如果将索引和切片混合,就可以得到低维度的切片:
In [31]: arr2d[1, : 2]
Out[31]: array([4, 5])
In [32]: arr2d[: 2, 2]
Out[32]: array([3, 6])
注意:单独一个冒号表示选择整个轴上的数组,因此可以按照下面的方式在更高维度进行切片:
In [33]: arr2d[:, : 1]
Out[33]:
array([[1],
[4],
[7]])
当然对切片表达式赋值时,整个切片都会重新赋值:
In [34]: arr2d[: 2, 1:] = 0
In [35]: arr2d
Out[35]:
array([[1, 0, 0],
[4, 0, 0],
[7, 8, 9]])
2.7布尔索引
思考一个问题:假设我们的数据都在数组中,并且数组中的数据是一些存在重复的人名。
让我们来操作。先使用numpy.random中的randn函数来生成一些随机正态分布的数据:
In [36]: names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
In [37]: data = np.random.randn(7, 4)
In [38]: names
Out[38]: array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'], dtype='<U4')
In [39]: data
Out[39]:
array([[-0.71240676, 0.32169452, 1.80395026, 1.32888448],
[-0.17922888, 0.18697225, 0.01124356, 0.54152674],
[-0.32664964, 0.31609398, -0.05846878, 0.72476623],
[-0.68702057, -0.59254829, 0.9455404 , -0.0726805 ],
[ 2.11357297, 1.14044095, 0.45549622, 0.04487061],
[-0.10284808, 2.63295542, -1.36069843, -0.81805588],
[-0.98528985, -0.14586161, 0.78240191, -0.9484678 ]])
假设每个人名都和data数组中的一行相对应,并且我们想要选中所有的'Bob'对应的行。与数学操作类似,数组的比较操作(比如==)也是可以向量化的。因此,比较names数组和字符串'Bob'会产生一个布尔值数组:
In [40]: names == 'Bob'
Out[40]: array([ True, False, False, True, False, False, False])
在索引数组时可以传入布尔值数组:
In [41]: data[names == 'Bob']
Out[41]:
array([[-0.71240676, 0.32169452, 1.80395026, 1.32888448],
[-0.68702057, -0.59254829, 0.9455404 , -0.0726805 ]])
注意:当布尔值数组的长度不正确时,布尔值选择数据的方法并不会报错,因此在使用该特性的时候要小心。
为了选择除了'Bob'以外的其他数据,你可以使用!=或在条件表达式前使用~对条件取反:
In [42]: names != 'Bob'
Out[42]: array([False, True, True, False, True, True, True])
In [43]: data[~(names == 'Bob')]
Out[43]:
array([[-0.17922888, 0.18697225, 0.01124356, 0.54152674],
[-0.32664964, 0.31609398, -0.05846878, 0.72476623],
[ 2.11357297, 1.14044095, 0.45549622, 0.04487061],
[-0.10284808, 2.63295542, -1.36069843, -0.81805588],
[-0.98528985, -0.14586161, 0.78240191, -0.9484678 ]])
~符号可以在你想要对一个通用条件进行取反时使用:
In [44]: cond = names == 'Bob'
In [45]: data[~cond]
Out[45]:
array([[-0.17922888, 0.18697225, 0.01124356, 0.54152674],
[-0.32664964, 0.31609398, -0.05846878, 0.72476623],
[ 2.11357297, 1.14044095, 0.45549622, 0.04487061],
[-0.10284808, 2.63295542, -1.36069843, -0.81805588],
[-0.98528985, -0.14586161, 0.78240191, -0.9484678 ]])
当要选择三个名字中的两个来组合多个布尔值条件时,需要使用布尔算术运算符,如&(and)和 | (or):
In [47]: mask = (names == 'Bob') | (names == 'Will')
In [48]: mask
Out[48]: array([ True, False, True, True, True, False, False])
In [49]: data[mask]
Out[49]:
array([[-0.71240676, 0.32169452, 1.80395026, 1.32888448],
[-0.32664964, 0.31609398, -0.05846878, 0.72476623],
[-0.68702057, -0.59254829, 0.9455404 , -0.0726805 ],
[ 2.11357297, 1.14044095, 0.45549622, 0.04487061]])
注意:使用布尔值索引选择数据时,总是生成数据的拷贝,即使返回的数组并没有任何变化。
基于常识来设置布尔值数组的值也是可行的。将data中所有的负值设置为0:
In [50]: data[data < 0] = 0
In [51]: data
Out[51]:
array([[0. , 0.32169452, 1.80395026, 1.32888448],
[0. , 0.18697225, 0.01124356, 0.54152674],
[0. , 0.31609398, 0. , 0.72476623],
[0. , 0. , 0.9455404 , 0. ],
[2.11357297, 1.14044095, 0.45549622, 0.04487061],
[0. , 2.63295542, 0. , 0. ],
[0. , 0. , 0.78240191, 0. ]])
利用一维布尔值数组对每一行或每一列设置数值也是非常简单的:
In [52]: data[names != 'Joe'] = 7
In [53]: data
Out[53]:
array([[7. , 7. , 7. , 7. ],
[0. , 0.18697225, 0.01124356, 0.54152674],
[7. , 7. , 7. , 7. ],
[7. , 7. , 7. , 7. ],
[7. , 7. , 7. , 7. ],
[0. , 2.63295542, 0. , 0. ],
[0. , 0. , 0.78240191, 0. ]])
2.8神奇索引
神奇索引是Numpy中的术语,用于描述使用整数数组进行数据索引。
假设有一个8×4的数组:
In [54]: arr = np.empty((8, 4))
In [55]: for i in range(8):
...: arr[i] = i
...:
In [56]: arr
Out[56]:
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.]])
为了选出一个符合特定顺序的子集,你可以简单地通过传递一个包含指明所需顺序的列表或数组来完成:
In [57]: arr[[4, 3, 0, 6]]
Out[57]:
array([[4., 4., 4., 4.],
[3., 3., 3., 3.],
[0., 0., 0., 0.],
[6., 6., 6., 6.]])
如果使用负的索引,将从尾部进行选择:
In [58]: arr[[-3, -5, -7]]
Out[58]:
array([[5., 5., 5., 5.],
[3., 3., 3., 3.],
[1., 1., 1., 1.]])
传递多个索引数组时情况有些许不同,这样会根据每个索引元组对应的元素选出一个一维数组:
In [60]: arr = np.arange(32).reshape((8, 4))
In [61]: arr
Out[61]:
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 [62]: arr[[1, 5, 7, 2], [0, 3, 1, 2]]
Out[62]: array([ 4, 23, 29, 10])
在上述例子中,元素(1, 0)、(5, 3)、(7, 1)、(2, 2)被选中。
在本例中,神奇索引的行为和我们一些人设想的并不一样。通常情况下,我们所设想的结果是通过选择矩阵中行列的子集所形成的矩阵区域。下面是实现这种想法的一种方式:
In [63]: arr[[1, 5, 7, 2]][:, [0, 3, 1, 2]]
Out[63]:
array([[ 4, 7, 5, 6],
[20, 23, 21, 22],
[28, 31, 29, 30],
[ 8, 11, 9, 10]])
注意:神奇索引与切片方式不同,它总是将数据复制到一个新的数组中。
2.9数组转置和换轴
转置是一种特殊的数据重组形式,可以返回底层数据的视图而不需要复制任何内容。数组拥有transpose方法,也有特殊的T属性:
In [64]: arr = np.arange(15).reshape((3, 5))
In [65]: arr
Out[65]:
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
In [66]: arr.T
Out[66]:
array([[ 0, 5, 10],
[ 1, 6, 11],
[ 2, 7, 12],
[ 3, 8, 13],
[ 4, 9, 14]])
当进行矩阵计算时,可能会经常进行一些特定操作,比如:当计算矩阵内积会使用np.dot:
In [67]: arr = np.random.randn(6, 3)
In [68]: arr
Out[68]:
array([[-0.48983232, -0.18950062, 0.26523396],
[-0.29958914, 0.31341065, 1.7422327 ],
[ 0.97803831, 1.06123225, -0.00539432],
[ 1.10822516, -1.84389661, -0.76180669],
[-0.80293105, -1.73839501, -0.59307407],
[ 0.83736668, -0.05644699, 2.2457714 ]])
In [69]: np.dot(arr.T, arr)
Out[69]:
array([[3.86029251, 0.3419468 , 0.85532836],
[0.3419468 , 7.68550881, 2.79897038],
[0.85532836, 2.79897038, 9.0813284 ]])
对于更高维度的数组,transpose方法可以接收包含轴编号的元组,用于置换轴:
In [70]: arr = np.arange(16).reshape((2, 2, 4))
In [71]: arr
Out[71]:
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7]],
[[ 8, 9, 10, 11],
[12, 13, 14, 15]]])
In [72]: arr.transpose((1, 0, 2))
Out[72]:
array([[[ 0, 1, 2, 3],
[ 8, 9, 10, 11]],
[[ 4, 5, 6, 7],
[12, 13, 14, 15]]])
这里轴已经被重新排序,使得原先的第二个轴变为第一个,原先的第一个轴变成了第二个,最后一个轴没有改变。这里有点考验思维,大家可以画图来理解。
使用.T进行置换是换轴的一个特殊案例。ndarray有一个swapaxes方法,该方法接收一对轴编号作为参数,并对轴进行调整用于重组数据:
In [73]: arr
Out[73]:
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7]],
[[ 8, 9, 10, 11],
[12, 13, 14, 15]]])
In [74]: arr.swapaxes(1, 2)
Out[74]:
array([[[ 0, 4],
[ 1, 5],
[ 2, 6],
[ 3, 7]],
[[ 8, 12],
[ 9, 13],
[10, 14],
[11, 15]]])
注意:swapaxes返回的是数据的视图,而没有对数据进行复制。
3.通用函数:快速的逐元素数组函数
通用函数,也可以称为ufunc,是一种在ndarray数据中进行逐元素操作的函数。某些简单函数接收一个或多个标量数值,并产生一个或多个标量结果,而通用函数就是对这些简单函数的向量化封装。
有很多ufunc是简单的逐元素转换,比如sqrt或exp函数:
In [3]: arr = np.arange(10)
In [4]: arr
Out[4]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [5]: np.sqrt(arr)
Out[5]:
array([0. , 1. , 1.41421356, 1.73205081, 2. ,
2.23606798, 2.44948974, 2.64575131, 2.82842712, 3. ])
In [6]: np.exp(arr)
Out[6]:
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])
这些是所谓的一元通用函数。还有一些通用函数,比如add或maximum则会接收两个数组并返回一个数组作为结果,因此称为二元通用函数:
In [7]: x = np.random.randn(8)
In [8]: y = np.random.randn(8)
In [9]: x
Out[9]:
array([-2.55537045, 0.65459761, -0.140486 , 1.02534316, -0.97565112,
0.7349102 , 0.79196332, -0.30165551])
In [10]: y
Out[10]:
array([ 1.03247062, -1.65878221, 0.15714057, -1.78566908, -0.24691178,
-0.82705192, -0.42482947, -1.27565654])
In [11]: np.maximum(x, y)
Out[11]:
array([ 1.03247062, 0.65459761, 0.15714057, 1.02534316, -0.24691178,
0.7349102 , 0.79196332, -0.30165551])
这里,numpy.maximum逐个元素地将x和y中元素的最大值计算出来。
也有一些通用函数返回多个数组。比如modf,是Python内建函数divmod的向量化版本。它返回了一个浮点值数组的小数部分和整数部分:
In [12]: arr = np.random.randn(7) * 5
In [13]: arr
Out[13]:
array([-1.06553477, 8.56364472, 3.17749937, -6.69018089, 4.38664393,
-5.80158689, 0.27913163])
In [14]: remainder, whole_part = np.modf(arr)
In [15]: remainder
Out[15]:
array([-0.06553477, 0.56364472, 0.17749937, -0.69018089, 0.38664393,
-0.80158689, 0.27913163])
In [16]: whole_part
Out[16]: array([-1., 8., 3., -6., 4., -5., 0.])
一元通用函数表
二元通用函数表
4.使用数组进行面向数组编程
4.1简单认识
使用Numpy数组可以使我们利用简单的数组表达式完成多种数据操作任务,而无须写些大量循环。这种利用数组表达式来替代显式循环的方法,称为向量化。通常,向量化的数组操作会比纯Python的等价实现在速度上快一到两个数量级(甚至更多),这对所有种类的数值计算产生了最大的影响。
4.2将条件逻辑作为数组操作
numpy.where函数是三元表达式 x if condition else y 的向量化版本。假设我们有一个布尔值数组和两个数值数组:
In [21]: xarr = np.array([1.1, 1.2, 1.3, 1.4, 1.5])
In [22]: yarr = np.array([2.1, 2.2, 2.3, 2.4, 2.5])
In [23]: cond = np.array([True, False, True, True, False])
假设cond中的元素为True时,我们取xarr中对应的元素,否则取yarr中的元素。我们可以通过列表推导式来完成:
In [25]: result = [(x if c else y) for x, y, c in zip(xarr, yarr, cond)]
In [26]: result
Out[26]: [1.1, 2.2, 1.3, 1.4, 2.5]
这样会产生多个问题。首先,如果数组很大的话,速度会很慢(因为所有的工作都是通过解释器解释Python代码完成)。其次,当数组是多维时,就无法奏效了。而使用np.where时,就可以非常简单地完成:
In [27]: result = np.where(cond, xarr, yarr)
In [28]: result
Out[28]: array([1.1, 2.2, 1.3, 1.4, 2.5])
np.where的第二个和第三个参数并不需要是数组,它们可以使标量。where在数据分析中的一个典型用法是根据一个数组来生成一个新的数组。假设你有一个随机生成的矩阵数据,并且你想将其中的正值都替换为2,将所有的负值替换为-2,使用np.where会很容易实现:
In [29]: arr = np.random.randn(4, 4)
In [30]: arr
Out[30]:
array([[ 0.7378883 , -1.24443928, -0.77474445, -0.30522405],
[-1.05066705, 1.26418168, -0.23136372, 0.05909658],
[-0.22787421, 0.13012829, -0.74296826, 2.16167171],
[ 0.02882691, 0.60330151, 0.3547923 , 0.57271937]])
In [31]: arr > 0
Out[31]:
array([[ True, False, False, False],
[False, True, False, True],
[False, True, False, True],
[ True, True, True, True]])
In [32]: np.where(arr > 0, 2, -2)
Out[32]:
array([[ 2, -2, -2, -2],
[-2, 2, -2, 2],
[-2, 2, -2, 2],
[ 2, 2, 2, 2]])
我们还可以使用np.where将标量和数组联合,例如,我们可以像下面代码那样将arr中的所有正值替换为常数2:
In [33]: np.where(arr > 0, 2, arr)
Out[33]:
array([[ 2. , -1.24443928, -0.77474445, -0.30522405],
[-1.05066705, 2. , -0.23136372, 2. ],
[-0.22787421, 2. , -0.74296826, 2. ],
[ 2. , 2. , 2. , 2. ]])
传递给np.where的数组既可以是同等大小的数组,也可以是标量。
4.3数学和统计方法
许多关于计算整个数组统计值或关于轴向数据的数学函数,可以作为数组类型的方法被调用。你可以使用聚合函数(通常也叫缩减函数),比如sum、mean和std(标准差),既可以直接调用数组实例的方法,也可以使用顶层的Numpy函数。
此处生成了一些正态分布的随机数,并计算了部分聚合统计数据:
In [34]: arr = np.random.randn(5, 4)
In [35]: arr
Out[35]:
array([[ 0.68718295, 0.12013561, -1.21921671, 0.21488688],
[-0.82431793, -0.11863703, -0.11304199, 0.02413128],
[-1.35576898, 1.27869779, 1.73524307, -0.30399552],
[ 0.45151912, -0.20782996, -0.23915002, 0.12596575],
[-0.86212048, -0.62354833, -0.49598071, -0.62957567]])
In [36]: arr.mean()
Out[36]: -0.11777104345334052
In [37]: np.mean(arr)
Out[37]: -0.11777104345334052
In [38]: arr.sum()
Out[38]: -2.3554208690668106
像mean、sum等函数可以接收一个可选参数axis,这个参数可以用于计算给定轴向上(沿着指定轴)的统计值,形成一个下降一维度的数组:
In [39]: arr.mean(axis=1)
Out[39]: array([-0.04925282, -0.25796642, 0.33854409, 0.03262622, -0.6528063 ])
In [40]: arr.sum(axis=0)
Out[40]: array([-1.90350532, 0.44881808, -0.33214636, -0.56858727])
其他的方法,例如cumsum和cumprod并不会聚合,它们会产生一个中间结果:
In [43]: arr = np.arange(8)
In [44]: arr
Out[44]: array([0, 1, 2, 3, 4, 5, 6, 7])
In [45]: arr.cumsum()
Out[45]: array([ 0, 1, 3, 6, 10, 15, 21, 28], dtype=int32)
在多维数组中,像cumsum这样的累计函数返回相同长度的数组,但是可以在指定轴向上根据较低维度的切片进行部分聚合:
In [46]: arr = np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
In [47]: arr
Out[47]:
array([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
In [48]: arr.cumsum(axis=0)
Out[48]:
array([[ 0, 1, 2],
[ 3, 5, 7],
[ 9, 12, 15]], dtype=int32)
In [49]: arr.cumprod(axis=1)
Out[49]:
array([[ 0, 0, 0],
[ 3, 12, 60],
[ 6, 42, 336]], dtype=int32)
基础数组统计方法表
4.4布尔值数组的方法
在前面介绍的方法,布尔值会被强制为1(True)和0(False)。因此,sum通常可以用于计算布尔值数组中True的个数:
In [50]: arr = np.random.randn(100)
In [51]: (arr > 0).sum()
Out[51]: 48
对于布尔值数组,有两个非常有用的方法any和all。any检查数组中是否至少有一个True,而all检查是否每个值都是True:
In [52]: bools = np.array([False, False, True, False])
In [53]: bools.any()
Out[53]: True
In [54]: bools.all()
Out[54]: False
这些方法也可适用于非布尔值数组,所有的非0元素都会按True处理。
4.5排序
和Python的内建列表类型相似,Numpy数组可以使用sort方法按位置排序:
In [55]: arr = np.random.randn(6)
In [56]: arr
Out[56]:
array([ 2.26149253, 1.61586282, -1.20309762, 0.07530536, 0.54645183,
0.07987414])
In [57]: arr.sort()
In [58]: arr
Out[58]:
array([-1.20309762, 0.07530536, 0.07987414, 0.54645183, 1.61586282,
2.26149253])
还可以在多维数组中根据传递的axis值,沿着轴向对每一个一维数据段进行排序:
In [59]: arr = np.random.randn(5, 3)
In [60]: arr
Out[60]:
array([[ 1.43903224, -0.44510533, -0.94407253],
[ 0.85377056, -1.20000504, -0.63774096],
[ 0.53064478, 1.34052818, 0.94231329],
[ 0.28688527, -0.55378042, -0.36100019],
[ 1.37593446, 0.14549879, 0.02041026]])
In [61]: arr.sort(1)
In [62]: arr
Out[62]:
array([[-0.94407253, -0.44510533, 1.43903224],
[-1.20000504, -0.63774096, 0.85377056],
[ 0.53064478, 0.94231329, 1.34052818],
[-0.55378042, -0.36100019, 0.28688527],
[ 0.02041026, 0.14549879, 1.37593446]])
顶层的np.sort方法返回的是已经排序好的数组拷贝,而不是对原数组按位置排序。下面的例子计算的是一个数组的分位数,并选出分位数所对应的值,这是一种应急的方式:
In [63]: large_arr = np.random.randn(1000)
In [65]: large_arr.sort()
In [66]: large_arr[int(0.05 * len(large_arr))]
Out[66]: -1.5776102968578045
4.6唯一值与其他集合逻辑
Numpy包含一些针对一维ndarray的基础集合操作。常用的一个方法是np.unique,返回的是数组中唯一值排序后形成的数组:
In [67]: names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
In [68]: np.unique(names)
Out[68]: array(['Bob', 'Joe', 'Will'], dtype='<U4')
In [69]: ints = np.array([3, 3, 3, 2, 2, 1, 1, 4, 4])
In [70]: np.unique(ints)
Out[70]: array([1, 2, 3, 4])
将np.unique和纯Python实现相比较:
In [71]: sorted(set(names))
Out[71]: ['Bob', 'Joe', 'Will']
另一个函数,np.in1d,可以检查一个数组中的值是否在另外一个数组中,并返回一个布尔值数组:
In [72]: values = np.array([6, 0, 0, 3, 2, 5, 6])
In [73]: np.in1d(values, [2, 3, 6])
Out[73]: array([ True, False, False, True, True, False, True])
数组的集合操作表
5.使用数组进行文件输入和输出
Numpy可以在硬盘中将数据以文本或二进制文件的形式进行存入硬盘或由硬盘载入。在这里,将只讨论Numpy的内建二进制格式,因为大部分人更倾向于使用pandas或其它工具来载入文本或表格型数据。
np.save和np.load是高效存取硬盘数据的两大工具函数。数组在默认情况下是以未压缩的格式进行存储的,后缀名是:.npy:
In [2]: arr = np.arange(10)
In [3]: np.save('some_array', arr)
如果文件存放路径中没写.npy时,后缀名会被自动加上。硬盘上的数组可以使用np.load进行载入:
In [4]: np.load('some_array.npy')
Out[4]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
还可以使用np.savez并将数组作为参数传递给该函数,用于在未压缩文件中保存多个数组:
In [5]: np.savez('array_archive.npz', a=arr, b=arr)
当载入一个.npz文件的时候,会获得一个字典型对象,并通过该对象很方便地载入单个数组:
In [6]: arch = np.load('array_archive.npz')
In [7]: arch['b']
Out[7]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
如果数据已经压缩好了,可以使用numpy.savez_compressed将数据存入已经压缩的文件:
In [8]: np.savez_compressed('array_compressed.npz', a=arr, b=arr)
6.线性代数
线性代数,比如矩阵乘法、分解、行列式等方阵数学,是所有数组类库的重要组成部分。和Matlab等其他语言相比,Numpy的线性代数中所不足的是 * 是矩阵的逐元素乘积,而不是矩阵的点乘积。因此,Numpy的数组方法和numpy命名空间中都有一个函数dot,用于矩阵的操作:
In [9]: x = np.array([[1., 2., 3.], [4., 5., 6.]])
In [10]: y = np.array([[6., 23.], [-1, 7], [8, 9]])
In [11]: x
Out[11]:
array([[1., 2., 3.],
[4., 5., 6.]])
In [12]: y
Out[12]:
array([[ 6., 23.],
[-1., 7.],
[ 8., 9.]])
In [13]: x.dot(y)
Out[13]:
array([[ 28., 64.],
[ 67., 181.]])
x.dot(y) 等价于 np.dot(x, y):
In [14]: np.dot(x, y)
Out[14]:
array([[ 28., 64.],
[ 67., 181.]])
一个二维数组和一个长度合适的一维数组之间的矩阵乘积,其结果是一个一维数组:
In [15]: np.dot(x, np.ones(3))
Out[15]: array([ 6., 15.])
特殊符号@也作为中缀操作符,用于点乘积操作:
In [16]: x @ np.ones(3)
Out[16]: array([ 6., 15.])
numpy.linalg拥有一个矩阵分解的标准函数集,以及其他常用函数,例如求逆和行列式求解:
In [17]: from numpy.linalg import inv, qr
In [18]: X = np.random.randn(5, 5)
In [19]: mat = X.T.dot(X)
In [20]: inv(mat)
Out[20]:
array([[ 0.40711443, -1.72465784, 0.60351129, 1.77765115,
0.77383146],
[ -1.72465784, 20.12243828, -6.42725971, -18.45385957,
-10.2804738 ],
[ 0.60351129, -6.42725971, 2.21578522, 5.96516283,
3.34519911],
[ 1.77765115, -18.45385957, 5.96516283, 18.40304648,
9.02884849],
[ 0.77383146, -10.2804738 , 3.34519911, 9.02884849,
5.67214899]])
In [21]: mat.dot(inv(mat))
Out[21]:
array([[ 1.00000000e+00, -8.07969017e-16, -2.34518979e-16,
7.85920178e-16, -3.21418751e-17],
[ 5.07667549e-16, 1.00000000e+00, -3.11970204e-15,
-1.79035537e-15, 6.37104773e-17],
[ 2.58774896e-16, 2.67544986e-16, 1.00000000e+00,
-4.45204493e-15, -6.84794810e-16],
[ 1.41960176e-16, 1.14899457e-16, -1.36478093e-17,
1.00000000e+00, 6.55546185e-16],
[-3.94442852e-16, -2.25196702e-15, -2.42196166e-15,
5.43962612e-15, 1.00000000e+00]])
In [22]: q, r = qr(mat)
In [23]: r
Out[23]:
array([[-5.59752945, -0.81784493, 5.38226473, -0.41366999, -3.27863982],
[ 0. , -3.61316226, -0.47682848, -1.64457561, -3.7552637 ],
[ 0. , 0. , -6.84884981, 1.04861652, 2.40550318],
[ 0. , 0. , 0. , -0.42560762, 0.78875528],
[ 0. , 0. , 0. , 0. , 0.06577087]])
常用的numpy.linalg函数表
7.伪随机数生成
numpy.random模块填补了Python内建的random模块的不足,可以高效地生成多种概率分布下的完整样本值数组。例如,可以使用normal来获取一个4×4的正态分布样本数据:
In [24]: samples = np.random.normal(size=(4, 4))
In [25]: samples
Out[25]:
array([[ 0.07812583, -0.32400965, -0.91518261, 0.90531419],
[-0.16558066, -2.06012003, 0.19985203, 0.197033 ],
[ 0.27698147, -0.83593625, -0.91022343, 0.20010458],
[ 1.06967544, -0.02807165, -0.07649251, 1.03392695]])
然而Python内建的random模块一次只能生成一个值。可以从下面的实例看到,numpy.random在生成大型样本时比纯Python的方式快了一个数量级:
In [27]: from random import normalvariate
In [28]: N = 1000000
In [29]: %timeit samples = [normalvariate(0, 1) for _ in range(N)]
395 ms ± 10.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [30]: %timeit np.random.normal(size=N)
16.1 ms ± 442 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
这些被称为伪随机数,因为它们是由具有确定性行为的算法根据随机数生成器中的随机数种子生成的。可以通过np.random.seed更改Numpy的随机数种子:
np.random.seed(1234)
numpy.random中的数据生成函数使用一个全局随机数种子。为了避免全局状态,可以使用numpy.random.RandomState创建一个随机数生成器,是数据独立于其他的随机数状态:
In [32]: rng = np.random.RandomState(1234)
In [33]: rng.randn(10)
Out[33]:
array([ 0.47143516, -1.19097569, 1.43270697, -0.3126519 , -0.72058873,
0.88716294, 0.85958841, -0.6365235 , 0.01569637, -2.24268495])
numpy.random中部分函数列表