NumPy之于数值计算特别重要的原因之一, 是因为它可以高效处理大数组的数据。 这是因为:
- NumPy是在一个连续的内存块中存储数据, 独立于其他Python内置对象。 NumPy的C语言编写的算法库可以操作内存, 而不必进行类型检查或其它前期工作。 比起Python的内置序列,NumPy数组使用的内存少。
- NumPy可以在整个数组上执行复杂的计算, 而不需要Python的for循环。
可见,基于NumPy的算法要比纯Python块10到100倍(甚至更快),并且使用内存更少>>> import numpy as np
>>> %time for _ in range(10): my_arr2 = my_arr * 2
Wall time: 36.9 ms
>>> %time for _ in range(10): my_list2 = [x * 2 for x in my_list]
Wall time: 794 ms
NumPy的ndarray:一种多维数组对象
```python In [1]: import numpy as np
In [2]: data = np.random.randn(2, 3)
In [3]: data Out[3]: array([[-0.45730858, 0.53635669, -0.78764046], [-0.27321996, 1.40008167, 0.54588833]])
然后进行数学运算:
```python
In [4]: data * 10
Out[4]:
array([[-4.57308583, 5.36356689, -7.87640465],
[-2.73219957, 14.00081674, 5.45888334]])
In [5]: data + data
Out[5]:
array([[-0.91461717, 1.07271338, -1.57528093],
[-0.54643991, 2.80016335, 1.09177667]])
ndarray是一个通用的同构数据多维容器, 也就是说, 其中的所有元素必须是相同类型的。 每个数组都有一个shape(一个表示各维度大小的元组) 和一个dtype(一个用于说明数组数据类型的对象)
In [6]: data.shape
Out[6]: (2, 3)
In [7]: data.dtype
Out[7]: dtype('float64')
创建ndarray
创建数组最简单的办法就是使用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. ])
嵌套序列(比如由一组等长列表组成的列表) 将会被转换为一个多维数组,可以用ndim和shape验证
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]])
In [14]: arr2.ndim
Out[14]: 2
In [14]: arr2.shape
Out[14]: (2, 4)
In [15]: arr2.dtype
Out[15]: dtype('int64')
除np.array之外,还有一些函数也可以新建数组。比如,zeros和ones分别可以创建指定长度或形状的全0或全1数组。empty可以创建一个没有任何具体值的数组。 要用这些方法创建多维数组, 只需传入一个表示形状的元组即可
In [1]: import numpy as np
In [2]: np.zeros(10)
Out[2]: array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
In [3]: np.zeros((3,6))
Out[3]:
array([[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.]])
In [4]: np.empty((2,3,2))
Out[4]:
array([[[0., 0.],
[0., 0.],
[0., 0.]],
[[0., 0.],
[0., 0.],
[0., 0.]]])
注意: 认为np.empty会返回全0数组的想法是不安全的。 很多情况下(如前所示) , 它返回的都是一些未初始化的垃圾值
arange是Python内置函数range的数组版
In [3]: np.arange(15)
Out[3]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
- 一些数组创建函数。由于NumPy关注的是数值计算, 因此, 如果没有特别指定, 数据类型基本都是float64(浮点数)
ndarray数据类型
dtype(数据类型)是一个特殊的对象, 它含有ndarray将一块内存解释为特定数据类型所需的信息
In [5]: arr1 = np.array([1,2,3], dtype=np.float64)
In [6]: arr2 = np.array([1,2,3], dtype=np.int32)
In [7]: arr1.dtype
Out[7]: dtype('float64')
In [8]: arr2.dtype
Out[8]: dtype('int32')
可以通过ndarray的astype方法明确地将一个数组从一个dtype转换成另一个dtype
In [9]: arr = np.array([1,2,3,4,5])
In [10]: arr.dtype
Out[10]: dtype('int32')
In [11]: float_arr = arr.astype(np.float64)
如果某字符串数组表示的全是数字, 也可以用astype将其转换为数值形式
In [12]: numeric_strings = np.array(['1.25', '-9.6', '42'], dtype=np.string_)
In [13]: numeric_strings.astype(float)
Out[13]: array([ 1.25, -9.6 , 42. ])
注意: 使用numpy.string_类型时, 一定要小心, 因为NumPy的字符串数据是大小固定的, 发生截取时, 不会发出警告。 pandas提供了更多非数值数据的便利的处理方法。
如果转换过程因为某种原因而失败了(比如某个不能被转换为float64的字符串) , 就会引发一个ValueError
在上面的代码中,即使将float作为目标转换类型,NumPy也能将其映射到对应的dtype上
还可以用简洁的类型代码来表示dtype
In [11]: empty_uint32 = np.empty(8, dtype='u4')
In [12]: empty_uint32
Out[12]:
array([ 0, 1072693248, 0, 1073741824, 0,
1074266112, 0, 1074790400], dtype=uint32)
astype总会创建一个新的数组(一个数据的备份),即使新的dtype与旧的dtype相同
NumPy数组的运算
In [14]: arr = np.array([[1,2,3], [4,5,6]])
In [15]: arr * arr
Out[15]:
array([[ 1, 4, 9],
[16, 25, 36]])
In [16]: arr - arr
Out[16]:
array([[0, 0, 0],
[0, 0, 0]])
数组与标量的算术运算会将标量值传播到各个元素
In [17]: 1 / arr
Out[17]:
array([[1. , 0.5 , 0.33333333],
[0.25 , 0.2 , 0.16666667]])
In [18]: arr ** 0.5
Out[18]:
array([[1. , 1.41421356, 1.73205081],
[2. , 2.23606798, 2.44948974]])
大小相同的数组之间的比较会生成布尔值数组
In [19]: arr2 = np.array([[0., 4., 1.], [7., 2., 12.]])
In [20]: arr2
Out[20]:
array([[ 0., 4., 1.],
[ 7., 2., 12.]])
In [21]: arr2 > arr
Out[21]:
array([[False, True, False],
[ True, False, True]])
基本的索引和切片
In [1]: import numpy as np
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:8] = 12
In [5]: arr
Out[5]: array([ 0, 1, 2, 3, 4, 12, 12, 12, 8, 9])
如上所示, 当你将一个标量值赋值给一个切片时(如arr[5:8]=12) , 该值会自动传播(也就说后面将会讲到的“广播”) 到整个选区。 跟列表最重要的区别在于, 数组切片是原始数组的视图。 这意味着数据不会被复制, 视图上的任何修改都会直接反映到源数组上
In [6]: arr_slice = arr[5:8]
In [7]: arr_slice[:] = 64
In [8]: arr
Out[8]: array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9])
之所以这么做,是因为NumPy的设计目的是处理大数据,所以就放弃了很多语言的复制来复制过去的做法
注意: 如果你想要得到的是ndarray切片的一份副本而非视图, 就需要明确地进行复制操作,
例如 arr[5:8].copy()
对于高维度数组, 能做的事情更多。 在一个二维数组中, 各索引位置上的元素不再是标量而是一维数组
In [9]: arr2d = np.array([[1,2,3],[4,5,6],[7,8,9]])
In [10]: arr2d[2]
Out[10]: array([7, 8, 9])
下面两种访问单个元素的方式是等价的
In [11]: arr2d[0][2]
Out[11]: 3
In [12]: arr2d[0, 2]
Out[12]: 3
下图说明了二维数组的索引方式。 轴0作为行, 轴1作为列
在多维数组中, 如果省略了后面的索引, 则返回对象会是一个维度低一点的ndarray
In [13]: arr3d = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])
In [14]: arr3d
Out[14]:
array([[[ 1, 2, 3],
[ 4, 5, 6]],
[[ 7, 8, 9],
[10, 11, 12]]])
arr3d[0]
是一个数组
In [15]: arr3d[0]
Out[15]:
array([[1, 2, 3],
[4, 5, 6]])
标量值和数组都可以被赋值给 arr3d[0]
In [16]: old_values = arr3d[0].copy()
In [17]: arr3d[0] = 42
In [18]: arr3d
Out[18]:
array([[[42, 42, 42],
[42, 42, 42]],
[[ 7, 8, 9],
[10, 11, 12]]])
In [19]: arr3d[0] = old_values
In [20]: arr3d
Out[20]:
array([[[ 1, 2, 3],
[ 4, 5, 6]],
[[ 7, 8, 9],
[10, 11, 12]]])
相似的, arr3d[1,0]
可以访问索引以(1,0)开头的那些值(以一维数组的形式返回)
In [21]: arr3d[1,0]
Out[21]: array([7, 8, 9])
切片索引
对于以为数组,跟Python的列表切片基本一样。对于之前的二维数组arr2d, 其切片方式稍显不同
In [22]: arr2d
Out[22]:
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
In [23]: arr2d[:2]
Out[23]:
array([[1, 2, 3],
[4, 5, 6]])
可以看出, 它是沿着第0轴(即第一个轴) 切片的。 也就是说, 切片是沿着一个轴向选取元素的。
表达式 arr2d[:2]
可以被认为是“选取arr2d的前两行”。
可以一次传入多个切片, 就像传入多个索引那样
In [24]: arr2d[:2, 1:]
Out[24]:
array([[2, 3],
[5, 6]])
像这样进行切片时, 只能得到相同维数的数组视图。 通过将整数索引和切片混合, 可以得到低维度
In [25]: arr2d[1, :2]
Out[25]: array([4, 5])
布尔类型索引
In [26]: names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
In [27]: data = np.random.randn(7, 4)
In [29]: names
Out[29]: array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'], dtype='<U4')
In [30]: data
Out[30]:
array([[ 0.70507199, -0.63721573, -1.86149772, -1.80573623],
[ 0.09655486, 0.24640234, -0.70079906, -0.12647153],
[-0.11690175, -1.19385087, 3.31880226, 0.55853635],
[-0.63570942, 0.32473218, -0.2127324 , 1.01473567],
[-0.92873195, 0.00682564, -0.08354234, 0.50574418],
[-0.76971669, -0.5399367 , -1.05318819, -1.25870414],
[ 0.3762395 , -0.55172785, -0.47706216, -1.22961565]])
假设每个名字都对应data数组中的一行, 而我们想要选出对应于名字”Bob”的所有行。 跟算术运算一样, 数组的比较运算(如==) 也是矢量化的。 因此, 对 names
和字符串”Bob”的比较运算将会产生一个布尔型数组
In [31]: names == 'Bob'
Out[31]: array([ True, False, False, True, False, False, False])
这个布尔型数组可用于数组索引
In [33]: data[names == 'Bob']
Out[33]:
array([[ 0.70507199, -0.63721573, -1.86149772, -1.80573623],
[-0.63570942, 0.32473218, -0.2127324 , 1.01473567]])
布尔型数组的长度必须跟被索引的轴长度一致。此外,还可以将布尔型数组跟切片、整数(或整数序列, 稍后将对此进行详细讲解)混合使用
In [34]: data[names=='Bob', 2:]
Out[34]:
array([[-1.86149772, -1.80573623],
[-0.2127324 , 1.01473567]])
要选择除”Bob”以外的其他值,既可以使用不等于符号(!=),也可以通过~对条件进行否定
In [35]: names != 'Bob'
Out[35]: array([False, True, True, False, True, True, True])
In [36]: data[~(names == 'Bob')]
Out[36]:
array([[ 0.09655486, 0.24640234, -0.70079906, -0.12647153],
[-0.11690175, -1.19385087, 3.31880226, 0.55853635],
[-0.92873195, 0.00682564, -0.08354234, 0.50574418],
[-0.76971669, -0.5399367 , -1.05318819, -1.25870414],
[ 0.3762395 , -0.55172785, -0.47706216, -1.22961565]])
还可以这么用
In [37]: cond = names == 'Bob'
In [38]: data[~cond]
Out[38]:
array([[ 0.09655486, 0.24640234, -0.70079906, -0.12647153],
[-0.11690175, -1.19385087, 3.31880226, 0.55853635],
[-0.92873195, 0.00682564, -0.08354234, 0.50574418],
[-0.76971669, -0.5399367 , -1.05318819, -1.25870414],
[ 0.3762395 , -0.55172785, -0.47706216, -1.22961565]])
选取这三个名字中的两个需要组合应用多个布尔条件,使用&(和)、|(或)之类的布尔算术运算符即可通过布尔型索引选取数组中的数据, 将总是创建数据的副本, 即使返回一模一样的数组也是如此
In [39]: mask = (names == 'Bob') | (names == 'Will')
In [42]: mask
Out[42]: array([ True, False, True, True, True, False, False])
In [43]: data[mask]
Out[43]:
array([[ 0.70507199, -0.63721573, -1.86149772, -1.80573623],
[-0.11690175, -1.19385087, 3.31880226, 0.55853635],
[-0.63570942, 0.32473218, -0.2127324 , 1.01473567],
[-0.92873195, 0.00682564, -0.08354234, 0.50574418]])
通过布尔型索引选取数组中的数据, 将总是创建数据的副本, 即使返回一模一样的数组也是如此
注意: Python关键字and和or在布尔型数组中无效。 要使用&与|
通过布尔型数组设置值是一种经常用到的手段。 为了将data中的所有负值都设置为0, 我们只需
In [44]: data[data < 0] = 0
In [45]: data
Out[45]:
array([[0.70507199, 0. , 0. , 0. ],
[0.09655486, 0.24640234, 0. , 0. ],
[0. , 0. , 3.31880226, 0.55853635],
[0. , 0.32473218, 0. , 1.01473567],
[0. , 0.00682564, 0. , 0.50574418],
[0. , 0. , 0. , 0. ],
[0.3762395 , 0. , 0. , 0. ]])
通过一维布尔数组设置整行或列的值也很简单
In [46]: data[names != 'Joe'] = 7
In [47]: data
Out[47]:
array([[7. , 7. , 7. , 7. ],
[0.09655486, 0.24640234, 0. , 0. ],
[7. , 7. , 7. , 7. ],
[7. , 7. , 7. , 7. ],
[7. , 7. , 7. , 7. ],
[0. , 0. , 0. , 0. ],
[0.3762395 , 0. , 0. , 0. ]])
花式索引
花式索引(Fancy indexing) 是一个NumPy术语,它指的是利用整数数组进行索引
In [48]: arr = np.empty((8,4))
In [49]: for i in range(8):
...: arr[i] = i
...:
In [50]: arr
Out[50]:
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即可
array([[4., 4., 4., 4.],
[3., 3., 3., 3.],
[0., 0., 0., 0.],
[6., 6., 6., 6.]])
使用负数索引将会从末尾开始选取行
In [52]: arr[[-3, -5, -7]]
Out[52]:
array([[5., 5., 5., 5.],
[3., 3., 3., 3.],
[1., 1., 1., 1.]])
一次传入多个索引数组会有一点特别。 它返回的是一个一维数组, 其中的元素对应各个索引元组
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 [55]: arr[[1,5,7,2], [0,3,1,2]]
Out[55]: array([ 4, 23, 29, 10])
最终选出的是元素(1,0)、(5,3)、(7,1)和(2,2)。无论数组是多少维的,花式索引总是一维的