在数值计算领域,说Numpy
是python
最重要的包也不为过。在numpy
中有下面这些东西:
ndarray
, 一个有效的多维数组,能提供以数组为导向的快速数值计算和灵活的广播功能(broadcasting
)
便利的数学函数
用于读取/写入(reading/writing
)数据到磁盘的便利工具
线性代数,随机数生成,傅里叶变换能力
可以用C API
来写C,C++
,或FORTRAN
通过学习理解numpy
中数组和数组导向计算,能帮我们理解pandas
之类的工具。
N-dimensional array object
(n维数组对象), or ndarray
,这是numpy
的关键特征。先来尝试一下,生成一个随机数组:
import numpy as np
e:\python3.7\lib\site-packages\numpy\_distributor_init.py:32: UserWarning: loaded more than 1 DLL from .libs:
e:\python3.7\lib\site-packages\numpy\.libs\libopenblas.TXA6YQSD3GCQQC22GEQ54J2UDCXDXHWN.gfortran-win_amd64.dll
e:\python3.7\lib\site-packages\numpy\.libs\libopenblas.XWYDX2IKJW2NMTWSFYNGFUWKQU3LYTCZ.gfortran-win_amd64.dll
stacklevel=1)
# Generate some random data
data = np.random.randn(2, 3)
data
array([[ 0.66415323, -1.4920515 , 0.35804571],
[ 0.59032967, -0.28798428, 0.52392625]])
进行一些数学运算:
data * 10
array([[-3.55123655, -6.37795453, 1.41379333],
[ 3.66420556, 3.0898139 , -8.70402916]])
data + data
array([[-0.71024731, -1.27559091, 0.28275867],
[ 0.73284111, 0.61796278, -1.74080583]])
每一个数组都有一个shape
,来表示维度大小。而dtype
,用来表示data type
:
data.shape
(2, 3)
data.dtype
dtype('float64')
最简单的方法使用array
函数,输入一个序列即可,比如list
:
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
是一个list of lists
, 所以arr2
维度为2。我们能用ndim
和shape
属性来确认一下:
arr2.ndim
2
arr2.shape
(2, 4)
除非主动声明,否则np.array
会自动给data
搭配适合的类型,并保存在dtype
里:
arr1.dtype
dtype('float64')
arr2.dtype
dtype('int64')
除了np.array
,还有一些其他函数能创建数组。比如zeros,ones,
另外还可以在一个tuple
里指定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([[[ 0.00000000e+000, 0.00000000e+000],
[ 2.16538378e-314, 2.16514681e-314],
[ 2.16511832e-314, 2.16072529e-314]],
[[ 0.00000000e+000, 0.00000000e+000],
[ 2.14037397e-314, 6.36598737e-311],
[ 0.00000000e+000, 0.00000000e+000]]])
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])
dtype
保存数据的类型:
arr1 = np.array([1, 2, 3], dtype=np.float64)
arr2 = np.array([1, 2, 3], dtype=np.int32)
arr1.dtype
dtype('float64')
arr2.dtype
dtype('int32')
dtype
才是numpy
能灵活处理其他外界数据的原因。
可以用astype
来转换类型:
arr = np.array([1, 2, 3, 4, 5])
arr.dtype
dtype('int64')
float_arr = arr.astype(np.float64)
float_arr.dtype
dtype('float64')
上面是把int
变为float
。如果是把float
变为int
,小数点后的部分会被丢弃:
arr = np.array([3.7, -1.2, -2.6, 0.5, 12.9, 10.1])
arr
array([ 3.7, -1.2, -2.6, 0.5, 12.9, 10.1])
arr.astype(np.int32)
array([ 3, -1, -2, 0, 12, 10], dtype=int32)
还可以用astype
把string
里的数字变为实际的数字:
numeric_strings = np.array(['1.25', '-9.6', '42'], dtype=np.string_)
numeric_strings
array([b'1.25', b'-9.6', b'42'],
dtype='|S4')
numeric_strings.astype(float)
array([ 1.25, -9.6 , 42. ])
要十分注意numpy.string_
类型,这种类型的长度是固定的,所以可能会直接截取部分输入而不给警告。
如果转换(casting
)失败的话,会给出一个ValueError
提示。
可以用其他数组的dtype
直接来制定类型:
int_array = np.arange(10)
calibers = np.array([.22, .270, .357, .380, .44, .50], dtype=np.float64)
int_array.astype(calibers.dtype)
array([ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])
还可以利用类型的缩写,比如u4
就代表unit32
:
empty_unit32 = np.empty(8, dtype='u4')
empty_unit32
array([0, 0, 0, 0, 0, 0, 0, 0], dtype=uint32)
记住,astype
总是会返回一个新的数组
数组之所以重要,是因为不用写for
循环就能表达很多操作,这种特性叫做vectorization
(向量化)。任何两个大小相等的数组之间的运算,都是element-wise
(点对点):
arr = np.array([[1., 2., 3.], [4., 5., 6.]])
arr
array([[ 1., 2., 3.],
[ 4., 5., 6.]])
arr * arr
array([[ 1., 4., 9.],
[ 16., 25., 36.]])
arr - arr
array([[ 0., 0., 0.],
[ 0., 0., 0.]])
element-wise
我翻译为点对点,就是指两个数组的运算,在同一位置的元素间才会进行运算。
这种算数操作如果涉及标量(scalar
)的话,会涉及到数组的每一个元素:
1 / arr
array([[ 1. , 0.5 , 0.33333333],
[ 0.25 , 0.2 , 0.16666667]])
arr ** 0.5
array([[ 1. , 1.41421356, 1.73205081],
[ 2. , 2.23606798, 2.44948974]])
两个数组的比较会产生布尔数组:
arr2 = np.array([[0., 4., 1.], [7., 2., 12.]])
arr2
array([[ 0., 4., 1.],
[ 7., 2., 12.]])
arr2 > arr
array([[False, True, False],
[ True, False, True]], dtype=bool)
一维的我们之前已经在list
部分用过了,没什么不同:
arr = np.arange(10)
arr
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
arr[5]
5
arr[5:8]
array([5, 6, 7])
arr[5:8] = 12
arr
array([ 0, 1, 2, 3, 4, 12, 12, 12, 8, 9])
这里把12赋给arr[5:8]
,其实用到了broadcasted
(我觉得应该翻译为广式转变)。这里有一个比较重要的概念需要区分,python
内建的list
与numpy
的array
有个明显的区别,这里array
的切片后的结果只是一个views
(视图),用来代表原有array
对应的元素,而不是创建了一个新的array
。但list
里的切片是产生了一个新的list
:
arr_slice = arr[5:8]
arr_slice
array([12, 12, 12])
如果我们改变arr_slice
的值,会反映在原始的数组arr
上:
arr_slice[1] = 12345
arr
array([ 0, 1, 2, 3, 4, 12, 12345, 12, 8, 9])
[:]
这个赋值给所有元素:
arr_slice[:] = 64
arr
array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9])
之所以这样设计是出于性能和内存的考虑,毕竟如果总是复制数据的话,会很影响运算时间。当然如果想要复制,可以使用copy()
方法,比如arr[5:8].copy()
在一个二维数组里,单一的索引指代的是一维的数组:
arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
arr2d[2]
array([7, 8, 9])
有两种方式可以访问单一元素:
arr2d[0][2]
3
arr2d[0, 2]
3
对于多维数组,如果省略后面的索引,返回的将是一个低纬度的多维数组。比如下面一个2 x 2 x 3
数组:
arr3d = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])
arr3d
array([[[ 1, 2, 3],
[ 4, 5, 6]],
[[ 7, 8, 9],
[10, 11, 12]]])
arr3d[0]
是一个2x3
数组:
arr3d[0]
array([[1, 2, 3],
[4, 5, 6]])
标量和数组都能赋给arr3d[0]
:
old_values = arr3d[0].copy()
arr3d[0] = 42
arr3d
array([[[42, 42, 42],
[42, 42, 42]],
[[ 7, 8, 9],
[10, 11, 12]]])
arr3d[0] = old_values
arr3d
array([[[ 1, 2, 3],
[ 4, 5, 6]],
[[ 7, 8, 9],
[10, 11, 12]]])
arr3d[1, 0]
会给你一个(1, 0)的一维数组:
arr3d[1, 0]
array([7, 8, 9])
上面的一步等于下面的两步:
x = arr3d[1]
x
array([[ 7, 8, 9],
[10, 11, 12]])
x[0]
array([7, 8, 9])
一定要牢记这些切片后返回的数组都是views
一维的话和python
里的list
没什么差别:
arr
array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9])
arr[1:6]
array([ 1, 2, 3, 4, 64])
二维的话,数组的切片有点不同:
arr2d[:2]
array([[1, 2, 3],
[4, 5, 6]])
可以看到,切片是沿着axis 0
(行)来处理的。所以,数组中的切片,是要沿着设置的axis
来处理的。我们可以把arr2d[:2]
理解为“选中arr2d
的前两行”。
当然,给定多个索引后,也可以使用复数切片:
arr2d
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
arr2d[:2, 1:] # 前两行,第二列之后
array([[2, 3],
[5, 6]])
记住,选中的是array view
。通过混合整数和切片,能做低维切片。比如,我们选中第二行的前两列:
arr2d[1, :2]
array([4, 5])
选中第三列的前两行:
arr2d[:2, 2]
array([3, 6])
冒号表示提取整个axis
(轴):
arr2d[:, :1]
array([[1],
[4],
[7]])
赋值也很方便:
arr2d[:2, 1:] = 0
arr2d
array([[1, 0, 0],
[4, 0, 0],
[7, 8, 9]])
假设我们的数组数据里有一些重复。这里我们用numpy.random
里的randn
函数来随机生成一些离散数据:
names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
names
array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'],
dtype='
data = np.random.randn(7, 4)
data
array([[ 0.02584271, -1.53529621, 0.73143988, -0.34086189],
[ 0.40864782, 0.53476799, 1.09620596, 0.4846564 ],
[ 1.95024076, -0.37291038, -0.40424703, 0.30297059],
[-0.48632936, 0.63817756, -0.40792716, -1.48037389],
[-0.81976335, -1.10162466, -0.59823212, -0.10926744],
[-0.5212113 , 0.29449179, 2.0568032 , 2.00515735],
[-2.36066876, -0.3294302 , -0.24464646, -0.81432884]])
假设每一个name
对应data
数组中的一行,我们想要选中name
为’Bob
’的所有行。就像四则运算,用比较运算符(==):
names == 'Bob'
array([ True, False, False, True, False, False, False], dtype=bool)
然后用这个布尔数组当做索引:
data[names == 'Bob']
array([[ 0.02584271, -1.53529621, 0.73143988, -0.34086189],
[-0.48632936, 0.63817756, -0.40792716, -1.48037389]])
注意:布尔数组和data
数组的长度要一样。
我们可以选中names=='Bob'
的行,然后索引列:
data[names == 'Bob', 2:]
array([[ 0.73143988, -0.34086189],
[-0.40792716, -1.48037389]])
data[names == 'Bob', 3]
array([-0.34086189, -1.48037389])
选中除了’Bob
’外的所有行,可以用!=
或者~
:
names != 'Bob'
array([False, True, True, False, True, True, True], dtype=bool)
data[~(names == 'Bob')]
array([[ 0.40864782, 0.53476799, 1.09620596, 0.4846564 ],
[ 1.95024076, -0.37291038, -0.40424703, 0.30297059],
[-0.81976335, -1.10162466, -0.59823212, -0.10926744],
[-0.5212113 , 0.29449179, 2.0568032 , 2.00515735],
[-2.36066876, -0.3294302 , -0.24464646, -0.81432884]])
当想要反转一个条件时,用~
操作符很方便:
cond = names == 'Bob'
data[~cond]
array([[ 0.40864782, 0.53476799, 1.09620596, 0.4846564 ],
[ 1.95024076, -0.37291038, -0.40424703, 0.30297059],
[-0.81976335, -1.10162466, -0.59823212, -0.10926744],
[-0.5212113 , 0.29449179, 2.0568032 , 2.00515735],
[-2.36066876, -0.3294302 , -0.24464646, -0.81432884]])
选中2个或3个名字,组合多个布尔条件,用布尔运算符&,|,另外python
中的关键词and
和or
不管用:
names
array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'],
dtype='
mask = (names == 'Bob') | (names == 'Will')
mask
array([ True, False, True, True, True, False, False], dtype=bool)
data[mask]
array([[ 0.02584271, -1.53529621, 0.73143988, -0.34086189],
[ 1.95024076, -0.37291038, -0.40424703, 0.30297059],
[-0.48632936, 0.63817756, -0.40792716, -1.48037389],
[-0.81976335, -1.10162466, -0.59823212, -0.10926744]])
用布尔索引总是会返回一份新创建的数据,原本的数据不会被改变。
更改值的方式也很直觉。比如我们想让所有负数变为0:
data[data < 0] = 0
data
array([[ 0.02584271, 0. , 0.73143988, 0. ],
[ 0.40864782, 0.53476799, 1.09620596, 0.4846564 ],
[ 1.95024076, 0. , 0. , 0.30297059],
[ 0. , 0.63817756, 0. , 0. ],
[ 0. , 0. , 0. , 0. ],
[ 0. , 0.29449179, 2.0568032 , 2.00515735],
[ 0. , 0. , 0. , 0. ]])
用一维的布尔数组也能更改所有行或列:
names
array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'],
dtype='
data[names != 'Joe'] = 7
data
array([[ 7. , 7. , 7. , 7. ],
[ 0.40864782, 0.53476799, 1.09620596, 0.4846564 ],
[ 7. , 7. , 7. , 7. ],
[ 7. , 7. , 7. , 7. ],
[ 7. , 7. , 7. , 7. ],
[ 0. , 0.29449179, 2.0568032 , 2.00515735],
[ 0. , 0. , 0. , 0. ]])
通过整数数组来索引。假设我们有一个8 x 4的数组:
arr = np.empty((8, 4))
for i in range(8):
arr[i] = i
arr
array([[ 0., 0., 0., 0.],
[ 1., 1., 1., 1.],
[ 2., 2., 2., 2.],
[ 3., 3., 3., 3.],
[ 4., 4., 4., 4.],
[ 5., 5., 5., 5.],
[ 6., 6., 6., 6.],
[ 7., 7., 7., 7.]])
想要按一定顺序选出几行,可以用一个整数list
或整数ndarray
来指定顺序:
arr[[4, 3, 0, 6]]
array([[ 4., 4., 4., 4.],
[ 3., 3., 3., 3.],
[ 0., 0., 0., 0.],
[ 6., 6., 6., 6.]])
用符号来从后选择row
:
arr[[-3, -5, -7]]
array([[ 5., 5., 5., 5.],
[ 3., 3., 3., 3.],
[ 1., 1., 1., 1.]])
用多维索引数组,能选出由一维数组中的元素,通过在每个tuple
中指定索引:
arr = np.arange(32).reshape((8, 4))
arr
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]])
arr[[1, 5, 7, 2], [0, 3, 1, 2]]
array([ 4, 23, 29, 10])
可以看到[ 4, 23, 29, 10]
分别对应(1, 0), (5, 3), (7, 1), (2, 2)
。不论数组有多少维,fancy indexing
的结果总是一维。
对于长方形区域,有下面的方法来截取:
arr[[1, 5, 7, 2]][:, [0, 3, 1, 2]]
array([[ 4, 7, 5, 6],
[20, 23, 21, 22],
[28, 31, 29, 30],
[ 8, 11, 9, 10]])
上面的意思是,先从arr
中选出[1, 5, 7, 2]
这四行:
array([[ 4, 5, 6, 7],
[20, 21, 22, 23],
[28, 29, 30, 31],
[ 8, 9, 10, 11]])
然后[:, [0, 3, 1, 2]]
表示选中所有行,但是列的顺序要按0,3,1,2
来排。于是得到:
array([[ 4, 7, 5, 6],
[20, 23, 21, 22],
[28, 31, 29, 30],
[ 8, 11, 9, 10]])
要记住,fancy indexing
和切片不同,得到的是一个新的array
。
转置也是返回一个view
,而不是新建一个数组。有两种方式,一个是transpose
方法,一个是T属性:
arr = np.arange(15).reshape((3, 5))
arr
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
arr.T
array([[ 0, 5, 10],
[ 1, 6, 11],
[ 2, 7, 12],
[ 3, 8, 13],
[ 4, 9, 14]])
做矩阵计算的时候,这个功能很常用,计算矩阵乘法的时候,用np.dot
:
arr = np.arange(8).reshape((4, 2))
print(arr.T)
print(arr)
[[0 2 4 6]
[1 3 5 7]]
[[0 1]
[2 3]
[4 5]
[6 7]]
np.dot(arr.T, arr)
array([[56, 68],
[68, 84]])
上面的例子是 (2x4) x (4x2) = (2x2)
。得到的结果是2x2维,就是普通的矩阵乘法。
对于多维数组,transpose
会接受由轴数字组成的tuple
,来交换轴:
arr = np.arange(16).reshape((2, 2, 4))
arr
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7]],
[[ 8, 9, 10, 11],
[12, 13, 14, 15]]])
arr.transpose((1, 0, 2))
array([[[ 0, 1, 2, 3],
[ 8, 9, 10, 11]],
[[ 4, 5, 6, 7],
[12, 13, 14, 15]]])
这里,secode axis(1)
被设为第一个,first axis(0)
第二个,最后的axis
没边。
使用.T
来转置swapping axes
(交换轴)的一个特殊情况。ndarray
有方法叫做swapaxes
, 这个方法取两个axis
值,并交换这两个轴:
arr
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7]],
[[ 8, 9, 10, 11],
[12, 13, 14, 15]]])
arr.swapaxes(1, 2) # 直交换second axis和last axis
array([[[ 0, 4],
[ 1, 5],
[ 2, 6],
[ 3, 7]],
[[ 8, 12],
[ 9, 13],
[10, 14],
[11, 15]]])
swapaxes
也是返回view
,不生成新的data
。