1. NumPy介绍
NumPy是Python中科学计算的基础包,它是一个Python库,提供多维数组对象,各种派生对象(如掩码数组和矩阵),以及用于数组快速操作的各种API,有包括数学、逻辑、形状操作、排序、选择、输入输出、离散傅立叶变换、基本线性代数,基本统计运算和随机模拟等等。
NumPy包的核心是ndarray
对象。它封装了python原生的同数据类型的n维数组。由于NumPy底层采用C代码进行预编译,所以运行效率比Python原生的数组高很多。同时NumPy具有矢量化和广播的特性,所以代码也更简洁精简。
本文主要介绍NumPy的基础概念和用法,旨在可以快速上手NumPy。
2. 基础知识 - 数组
原生的Python列表支持同时包含不同类型的元素,而NumPy的主要对象是同类型的多维数组。在NumPy中维度称之为 轴。
NumPy的数组类是ndarray
,它相对Python原生的数组拥有更多的属性:
-
ndarray.ndim
数组的轴(维度)的数量,也成为rank。
-
ndarray.shape
数组的维度,是一个数组的元组,代表这数组每个维度的尺寸。如果矩阵是
n
行和m
列,shape
就是(n, m)
。ndim
就是shape
元组的长度。 -
ndarray.size
数组中元素的总数量,等于
shape
中各元素的乘积。 -
ndarray.dtype
用来描述数组中元素类型的对象。可以使用标准的Python数据类型。也可以使用NumPy提供的数据类型,比如
numpy.int32
,numpy.int64
和numpy.float64
等。 -
ndarray.itemsize
元素类型的字节长度。比如
float64
类型的itemsize
是8(=64/8),再比如complex32
类型的itemsize
是4(=32/8)。它和ndarray.dtype.itemsize
相等。 -
ndarray.data
包含元素中实际元素的缓冲区。正常来说,我们不需要直接使用这个属性,因为我们可以通过索引访问数组中的元素。
3. 创建数组
NumPy中数组是一个核心概念,所以创建数组是第一步。创建数组有5种常规机制:
- 从其他Python结构(例如,列表,元组)转换
- numpy原生数组的创建(例如,arange、ones、zeros等)
- 从磁盘读取数组,无论是标准格式还是自定义格式
- 通过使用字符串或缓冲区从原始字节创建数组
- 使用特殊库函数(例如,random)
下面汇总了一些常用的创建数组的方式(不仅是创建常规的数组,也包括了NumPy为创建各种特殊数组提供的API):
序号 | API | 描述 |
---|---|---|
1 | np.array() | 通过Python列表或元组创建数组 |
2 | np.arange() | 创建一个数列形式的数组 |
3 | np.zeros() | 创建一个全为0组成的数组 |
4 | np.zeros_like() | 创建一个和目标数组尺寸一样的全0数组 |
5 | np.ones() | 创建一个全为1组成的数组 |
6 | np.ones_like() | 创建一个和目标数组尺寸一样的全1数组 |
7 | np.empty() | 创建一个指定尺寸,但不给值初始化的数组 |
8 | np.empty_like() | 创建一个和目标数组尺寸一样,且不给值初始化的数组 |
9 | np.lispace() | 创建一个等差数组 |
10 | np.logspace() | 创建一个等比数组 |
11 | np.indices() | 创建一个指定shape序号的数组集合 |
12 | np.random.random | 创建一个随机数组 |
13 | np.fromfunction() | 通过指定的函数创建数组 |
14 | np.fromfile() | 通过指定的文件创建数组 |
15 | np.identity() | 创建一个行和列相等的单位矩阵 |
16 | np.eye() | 创建一个二维矩阵,对角线的元素全为1,行和列可以不等,可以指定对角线偏移 |
17 | np.mgrid() | 返回一个多维结构的数组 |
18 | np.ogird() | 返回一个稀疏的多维结构的数组 |
3.1 np.array()
创建一维数组:
>>> a = np.array([1,2,3])
>>> a
array([1, 2, 3])
创建二维数组:
>>> b = np.array([[1,2,3], [4,5,6]])
>>> b
array([[1, 2, 3],
[4, 5, 6]])
创建数组时,手动指定数组的类型:
# 指定为float32类型
>>> c = np.array([1, 2, 3], dtype=np.float32)
>>> c
array([1., 2., 3.], dtype=float32)
NumPy可以对于python的列表和元组都可以接受,所以传入
array()
的是元组或列表都可以。也包括有些函数需要传入比如shape
的参数,传入元组或列表都可以。
3.2 np.arange()
创建从0开始,某个数结束的数列,默认步长为1:
>>> a = np.arange(3)
>>> a
array([0, 1, 2])
创建从某个数开始,到某个数结束的数组,指定步长和类型:
>>> a = np.arange(start=1, stop=8, step=2, dtype=np.int32)
>>> a
array([1, 3, 5, 7])
arange()创建数组的方式是左闭右开 [start, stop)
3.3 np.zeros()
创建指定shape的全0数组:
>>> a = np.zeros((3,2))
>>> a
array([[0., 0.],
[0., 0.],
[0., 0.]])
>>> a.dtype
dtype('float64')
函数zeros
默认是float64
类型。可以在创建时,手动指定其他类型:
>>> a = np.zeros((3,2), dtype=np.int32)
>>> a
array([[0, 0],
[0, 0],
[0, 0]])
>>> a.dtype
dtype('int32')
3.4 np.zeros_like()
创建和目标数组一样尺寸的全0数组:
>>> a = np.array([[1,2,3], [4,5,6]])
>>> a
array([[1, 2, 3],
[4, 5, 6]])
>>> b = np.zeros_like(a)
>>> b
array([[0, 0, 0],
[0, 0, 0]])
3.5 np.ones()
创建指定shape的全1数组,和np.zeros()
用法一样,就不赘述了。默认类型也是float64
,可以通过dtype
指定类型。
3.6 np.ones_like()
创建和目标数组一样尺寸的全1数组,和np.zeros_like()
用法一样。
3.7 np.empty()
创建一个指定shape的数组,但数组里的值不初始化。empty由于不初始化,所以创建速度比较快。可用于创建需要一个占位数组的场景:
>>> a = np.empty((3, 4))
>>> a
array([[6.23042070e-307, 2.04719289e-306, 6.23057010e-307,
1.24611741e-306],
[1.78019082e-306, 6.23058028e-307, 1.06811422e-306,
3.56043054e-307],
[1.37961641e-306, 8.90071135e-308, 1.78021527e-306,
1.66889876e-307]])
3.8 np.empty_like()
创建和目标数组尺寸一样的数组,但不进行初始化。
3.9 np.linspace()
创建一个等差数组,从某个数开始,到某个数结束,并指定区间内的元素数量:
>>> a = np.linspace(2, 9, num=8)
>>> a
array([2., 3., 4., 5., 6., 7., 8., 9.])
函数linspace
创建数组的区间是默认左闭右闭 [start, stop],即包含stop,可以通过参数Endpoint
来指定是否包含stop:
>>> a = np.linspace(2, 9, num=7, endpoint=False)
>>> a
array([2., 3., 4., 5., 6., 7., 8.])
函数linspace
和arange
比较相似,函数arange
指定的是步长,只不过函数linspace
指定的总数,它的步长是动态计算出来的,我们可以通过参数retstep
得到计算出的步长:
>>> a = np.linspace(2, 9, num=7, endpoint=False, retstep=True)
>>> a
(array([2., 3., 4., 5., 6., 7., 8.]), 1.0)
>>> a = np.linspace(2, 9, num=7, endpoint=False, retstep=True)
# 指定retstep之后可以同时得到数组和步长
>>> a
(array([2., 3., 4., 5., 6., 7., 8.]), 1.0)
# 获取创建出来的数组
>>> a[0]
array([2., 3., 4., 5., 6., 7., 8.])
# 获取数组的步长
>>> a[1]
1.0
3.10 np.logspace()
创建一个等比数组,默认以10为底数,从
1
0
s
t
a
r
t
10^{start}
10start 开始,到
1
0
s
t
o
p
10^{stop}
10stop 结束,总数为num
(默认50):
>>> a = np.logspace(1, 5, num=5)
>>> a
array([1.e+01, 1.e+02, 1.e+03, 1.e+04, 1.e+05])
>>> b = np.logspace(1, 5, num=4, dtype=np.int32)
>>> b
array([ 10, 215, 4641, 100000])
可以通过参数base
指定底数,这样就是从
b
a
s
e
s
t
a
r
t
base^{start}
basestart 开始,到
b
a
s
e
s
t
o
p
base^{stop}
basestop 结束,当然也可以通过参数endpoint
指定是否包含stop
:
>>> a = np.logspace(1, 10, base=2, num=10, dtype=np.int32)
>>> a
array([ 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024])
>>> b = np.logspace(1, 5, base=3, num=5, dtype=np.int32)
>>> b
array([ 3, 9, 27, 81, 243])
3.11 np.indices()
创建一个指定shape序号的数组集合,集合中是每个维度的序号。说起来比较抽象,看个例子应该比较好理解:
>>> a = np.indices((3,2))
>>> a
array([[[0, 0],
[1, 1],
[2, 2]],
[[0, 1],
[0, 1],
[0, 1]]])
>>> a[0]
array([[0, 0],
[1, 1],
[2, 2]])
>>> a[1]
array([[0, 1],
[0, 1],
[0, 1]])
上面的例子中,我们指定的shape
是(3, 2)。试想一下,一个3x2的矩阵,坐标分布应该是这样的:
[
a
0
,
0
a
0
,
1
a
1
,
0
a
1
,
1
a
2
,
0
a
2
,
1
]
\begin{bmatrix} a_{0,0} & a_{0,1} \\ a_{1,0} & a_{1,1} \\ a_{2,0} & a_{2,1} \\ \end{bmatrix}
a0,0a1,0a2,0a0,1a1,1a2,1
高维的坐标矩阵是,
[
0
,
0
1
,
1
2
,
2
]
\begin{bmatrix} 0, 0 \\ 1, 1 \\ 2, 2 \\ \end{bmatrix}
0,01,12,2
低维的坐标矩阵是,
[
0
,
1
0
,
1
0
,
1
]
\begin{bmatrix} 0, 1 \\ 0, 1 \\ 0, 1 \\ \end{bmatrix}
0,10,10,1
正好对应了上面例子中的结果。我们再来看一个3维矩阵的例子:
>>> a = np.indices((2,3,2))
>>> a[0]
array([[[0, 0],
[0, 0],
[0, 0]],
[[1, 1],
[1, 1],
[1, 1]]])
>>> a[1]
array([[[0, 0],
[1, 1],
[2, 2]],
[[0, 0],
[1, 1],
[2, 2]]])
>>> a[2]
array([[[0, 1],
[0, 1],
[0, 1]],
[[0, 1],
[0, 1],
[0, 1]]])
同样,一个2x3x2的三维矩阵就是把下面两个二维矩阵前后重叠起来,可以脑补一下:
[
a
0
,
0
,
0
a
0
,
0
,
1
a
0
,
1
,
0
a
0
,
1
,
1
a
0
,
2
,
0
a
0
,
2
,
1
]
\begin{bmatrix} a_{0,0,0} & a_{0,0,1} \\ a_{0,1,0} & a_{0,1,1} \\ a_{0,2,0} & a_{0,2,1} \\ \end{bmatrix}
a0,0,0a0,1,0a0,2,0a0,0,1a0,1,1a0,2,1
[ a 1 , 0 , 0 a 1 , 0 , 1 a 1 , 1 , 0 a 1 , 1 , 1 a 1 , 2 , 0 a 1 , 2 , 1 ] \begin{bmatrix} a_{1,0,0} & a_{1,0,1} \\ a_{1,1,0} & a_{1,1,1} \\ a_{1,2,0} & a_{1,2,1} \\ \end{bmatrix} a1,0,0a1,1,0a1,2,0a1,0,1a1,1,1a1,2,1
最高维的坐标矩阵是,
[
0
,
0
0
,
0
0
,
0
]
\begin{bmatrix} 0, 0 \\ 0, 0 \\ 0, 0 \\ \end{bmatrix}
0,00,00,0
[ 1 , 1 1 , 1 1 , 1 ] \begin{bmatrix} 1, 1 \\ 1, 1 \\ 1, 1 \\ \end{bmatrix} 1,11,11,1
剩下两个维度的坐标矩阵同理,就不赘述了。函数indices
生成的序号数组的作用可用作计算一个数组的子数组,比如:
>>> a = np.arange(12).reshape(3,4)
>>> a
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
>>> b = np.indices((2,3))
>>> b
array([[[0, 0, 0],
[1, 1, 1]],
[[0, 1, 2],
[0, 1, 2]]])
>>> a_sub = a[b[0], b[1]]
>>> a_sub
array([[0, 1, 2],
[4, 5, 6]])
# 在数组切片层面,效果同
>>> a[:2, :3]
array([[0, 1, 2],
[4, 5, 6]])
函数indices
默认创建的数组是非稀疏的,可以通过参数sparse
指定成稀疏的:
>>> a = np.indices((3,2), sparse=True)
>>> a
(array([[0],
[1],
[2]]), array([[0, 1]]))
3.12 np.random
np.random.random()
生成一个指定shape
的随机数组,值得范围默认是 [0, 1) :
>>> a = np.random.random((2,3))
>>> a
array([[0.48152382, 0.58393539, 0.61701583],
[0.05104326, 0.23513154, 0.21062412]])
np.random.randint()
生成一个指定shape
的随机数组,值的默认范围通过参数传入:
# [3,9)之间的随机整数,数组尺寸3X4
>>> a = np.random.randint(3, 9, (3,4))
>>> a
array([[6, 3, 3, 4],
[5, 7, 7, 3],
[7, 6, 3, 7]])
函数np.random.shuffle()
的作用是将数组随机打乱,np.random
包里有不少其他的API用于生成随机数组,这里只介绍了最基本的两个函数。
3.13 np.fromfunction()
创建一个指定尺寸的数组,并通过传入一个lambda表达式进行值的初始化,lambda表达式的参数是值对应的坐标:
>>> a = np.fromfunction(lambda i,j: i, (2,2))
>>> a
array([[0., 0.],
[1., 1.]])
# 将坐标i+j作为数组元素的初始化值
>>> b = np.fromfunction(lambda i,j: i + j, (3,3), dtype=np.int32)
>>> b
array([[0, 1, 2],
[1, 2, 3],
[2, 3, 4]])
上面的例子是一个lambda,或者直接传入一个函数:
>>> def f(x,y):
... return 10*x+y
...
>>> a = np.fromfunction(f, (2,2), dtype=np.int32)
>>> a
array([[ 0, 1],
[10, 11]])
3.14 np.fromfile()
从文件中读取数据并生成数组:
>>> import tempfile
>>> fname = tempfile.mkstemp()[1]
>>> fname
'C:\\Users\\doudou\\AppData\\Local\\Temp\\tmpmyq7otph'
>>> a = np.arange(12)
>>> a
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
>>> a.tofile(fname)
>>> b = np.fromfile(fname, dtype=np.int32)
>>> b
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
需要注意的是,函数tofile
和fromfile
是利用二进制的方式来实现数据存储和读取的,一来不是平台独立的,二来这种方式不能保存数组的字节顺序和数据类型。比如如果存储一个二维数据,通过函数fromfile
读出来的原始数据是一维的,而且必须指定dtype
才能正确读出数据:
>>> a = np.arange(12).reshape(3,4)
>>> a
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
>>> a.tofile(fname)
# 这种方式不存储数组结构信息,所以读取出来变成了一维的
>>> b = np.fromfile(fname, dtype=np.int32)
>>> b
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
# 这种方式不存储数据类型,所以如果不指定dtype,是无法正确读出数据的
>>> c = np.fromfile(fname)
>>> c
array([2.12199579e-314, 6.36598737e-314, 1.06099790e-313, 1.48539705e-313,
1.90979621e-313, 2.33419537e-313])
综上tofile
和fromfile
方式的缺陷,官方建议使用save
和load
的方式进行数据存储和读取:
>>> fname = tempfile.mkstemp()[1]
>>> fname
'C:\\Users\\doudou\\AppData\\Local\\Temp\\tmpynt0tqt3'
>>> a = np.arange(12).reshape(3,4)
>>> a
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
>>> np.save(fname, a)
>>> b = np.load(fname + '.npy')
>>> b
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
可以看到,save
和load
的方式,可以正确还原数据的结构和数据类型,方便不少。
3.15 np.identity()
创建行和列均为N的单位矩阵
>>> a = np.identity(4)
>>> a
array([[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 1.]])
>>> b = np.identity(2)
>>> b
array([[1., 0.],
[0., 1.]])
3.16 np.eye()
创建一个指定行和列的二维矩阵,对角线的元素全为1,如不指定列数,默认列数和行数相等:
>>> a = np.eye(4)
>>> a
array([[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 1.]])
>>> b = np.eye(4, 5)
>>> b
array([[1., 0., 0., 0., 0.],
[0., 1., 0., 0., 0.],
[0., 0., 1., 0., 0.],
[0., 0., 0., 1., 0.]])
通过设置参数k
,可以指定全1对角线的偏移。k
为正则向上便宜,为负则向下偏移:
>>> a = np.eye(4, k=1)
>>> a
array([[0., 1., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 1.],
[0., 0., 0., 0.]])
>>> b = np.eye(4, k=2)
>>> b
array([[0., 0., 1., 0.],
[0., 0., 0., 1.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]])
>>> c = np.eye(4, k=-1)
>>> c
array([[0., 0., 0., 0.],
[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 1., 0.]])
3.17 np.mgrid()
返回一个多维结构的数组,可以指定每一个维度的起始和终点值以及步长。
np.mgrid[第1维, 第2维, 第3维, ...]
第n维的书写形式为:
a:b:c
c表示步长,为实数表示间隔;该为长度为[a,b),左开右闭
或:
a:b:cj
cj表示步长,为复数表示点数;该长度为[a,b],左闭右闭
>>> x, y = np.mgrid[1:3:1, 4:5:2j]
>>> x
array([[1., 1.],
[2., 2.]])
>>> y
array([[4., 5.],
[4., 5.]])
函数
np.mgrid()
和函数np.indices()
在返回的结果上类似,但np.indices()
返回的数组中的值是连续的坐标,而np.mgrid()
的值是通过参数指定的区间里数值。
3.18 np.ogrid()
返回一个稀疏的多维结构的数组,调用方式和参数和函数np.mgrid()
一致:
>>> x, y = np.ogrid[1:3:1, 4:5:2j]
>>> x
array([[1.],
[2.]])
>>> y
array([[4., 5.]])
4. 索引
数组索引是指用方括号 []
来索引数组的值。
4.1 一维数组的单元素索引
>>> a = np.arange(10)
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> a[2]
2
>>> a[-2]
8
4.2 多维数组的单元素索引
>>> a = np.arange(10).reshape(2, 5)
>>> a
array([[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9]])
>>> a[1, 3]
8
>>> a[1, -1]
9
>>> a[1][-1]
9
对于标准 Python 的列表和元组,不支持
[x, y]
这种索引方式,只支持[x][y]
的方式,这种方式在NumPy中也适用
4.3 一维数组切片和步长索引
针对一维数组,NumPy切片和步长的索引方式和列表和元组一样:
>>> a = np.arange(10)
>>> a[2:5]
array([2, 3, 4])
>>> a[:-7]
array([0, 1, 2])
>>> a[1:7:2]
array([1, 3, 5])
4.4 多维数组切片和步长索引
针对多维数组,NumPy支持切片和步长的索引方式,而列表和元组不支持这种方式:
>>> a = np.arange(35).reshape(5,7)
>>> a
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, 32, 33, 34]])
>>> a[1:5:2,::3]
array([[ 7, 10, 13],
[21, 24, 27]])
4.5 索引数组
NumPy支持通过一个数组或列表的方式(元组不行),来达到同时索引本数组中的多个元素的目的。
索引数组必须是整数类型,可以使用负数:
>>> a = np.arange(10)
>>> a[np.array([1, 1, 3, 5, -2])]
array([1, 1, 3, 5, 8])
>>> a[[1, 1, 3, 5, -2]]
array([1, 1, 3, 5, 8])
我们也可以使用多维索引数组(注意:不是对多维数组进行索引,而是多维索引数组 - 即本身就是多维度的索引数组):
>>> a = np.arange(10)
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> a[np.array([[1, 1], [7, -2]])]
array([[1, 1],
[7, 8]])
上面的例子中,我们对一个一维数组采用了二维索引数组进行索引,得到是一个二维数组。也就是说,使用索引数组时返回的是与索引数组具有相同形状的数组。
4.6 索引多维数组
利用索引数组对多维数组进行索引:
>>> a = np.arange(35).reshape(5, 7)
>>> a
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, 32, 33, 34]])
>>> a[[2, 4, 4], [3, 3, 4]]
array([17, 31, 32])
上面的例子中,我们用两个索引数组对一个二维数组进行了索引:[2, 4, 4]
和 [3, 3, 4]
,这样的方式,即表示我们要索引的元素位置是[2,3], [4,3], [4,4]
这三个,所以结果是[17, 31, 32]
。
我们可以利用广播机制进行索引:
>>> a[[2, 4, 4], 5]
array([19, 33, 33])
这样,我们要索引的元素位置就是[2,5], [4,5], [4,5]
,结果就是[19, 33, 33]
。
当用一维索引数组对二维数组进行索引时,即得到的选中的行组成的新数组:
>>> a = np.arange(10).reshape(2, 5)
>>> a
array([[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9]])
>>> a[[0, 0, 1]]
array([[0, 1, 2, 3, 4],
[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9]])
这个方式也同样适用于多维索引数组比数组维度低的情况。
4.7 布尔索引数组
布尔索引数组的原则是True
就输出, False
就不输出:
>>> a = np.arange(12).reshape(3,4)
>>> a
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
>>> b = a > 5
>>> b
array([[False, False, False, False],
[False, False, True, True],
[ True, True, True, True]])
>>> a[b]
array([ 6, 7, 8, 9, 10, 11])
当布尔索引数组的维度少于数组时,则按照较高维度进行索引:
>>> a = np.arange(35).reshape(5, 7)
>>> a
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, 32, 33, 34]])
>>> a[[True, False, True, False, False]]
array([[ 0, 1, 2, 3, 4, 5, 6],
[14, 15, 16, 17, 18, 19, 20]])
上面的例子中,布尔索引数组是一维的,在对一个二维数组进行索引时得到第一行和第三行组成的二维数组。
再看一个三维数组的例子,通过一个二维布尔索引数组,对一个三维数组进行索引:
>>> a = np.arange(30).reshape(2, 3, 5)
>>> a
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]]])
>>> b = np.array([[True, True, False], [False, True, True]])
>>> a[b]
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[20, 21, 22, 23, 24],
[25, 26, 27, 28, 29]])
4.8 索引数组和切片组合
索引数组里可以使用切片:
>>> a = np.arange(35).reshape(5, 7)
>>> a
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, 32, 33, 34]])
>>> a[np.array([0, 2, 4]), 1:3]
array([[ 1, 2],
[15, 16],
[29, 30]])
4.8 np.newaxis
和 ...
np.newaxis
:可以使用新增一个维度,这种情况下不会有新元素,只是维度增加了:
>>> a=np.arange(5)
>>> a
array([0, 1, 2, 3, 4])
>>> a.shape
(5,)
>>> b = a[:,np.newaxis]
>>> b
array([[0],
[1],
[2],
[3],
[4]])
>>> b.shape
(5, 1)
>>> c = a[np.newaxis,:]
>>> c.shape
(1, 5)
>>> c
array([[0, 1, 2, 3, 4]])
...
:表示产生完整索引数组所需要的冒号:
- x[1,2,…] 相当于 x[1,2,:,:,:],
- x[…,3] 等效于 x[:,:,:,:,3]
- x[4,…,5,:] 等效于 x[4,:,:,5,:]。
>>> a = np.arange(15).reshape(5, 3)
>>> a
array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11],
[12, 13, 14]])
>>> a[1,...]
array([3, 4, 5])
>>> a[1]
array([3, 4, 5])
>>> a[...,2]
array([ 2, 5, 8, 11, 14])
4.9 索引后赋值
可以利用广播的特性 或者 给索引数组赋值一个形状一样的值:
>>> a = np.arange(10)
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> a[2:7] = 1
>>> a
array([0, 1, 1, 1, 1, 1, 1, 7, 8, 9])
>>> a[2:7]=np.arange(5)
>>> a
array([0, 1, 0, 1, 2, 3, 4, 7, 8, 9])
>>> a[2:7] += 10
>>> a
array([ 0, 1, 10, 11, 12, 13, 14, 7, 8, 9])
5. 迭代
我们可以用下面方式对数组进行迭代,默认从最高维开始迭代:
>>> a = np.arange(6).reshape(2, 3)
>>> for x in a:
... print(x)
...
[0 1 2]
[3 4 5]
也可以利用array的属性flat
,这个属性会将数组进行降维平铺,然后就可以方便地遍历所有元素:
>>> for x in a.flat:
... print(x)
...
0
1
2
3
4
5
6. 数组的操作
6.1 改变尺寸
可以通过reshape
得到一个对应尺寸的数组:
>>> a = np.arange(6)
>>> a
array([0, 1, 2, 3, 4, 5])
>>> a.reshape(2,3)
array([[0, 1, 2],
[3, 4, 5]])
>>> a
array([0, 1, 2, 3, 4, 5])
也可以通过resize
改变数组实际的尺寸:
>>> a.resize(2,3)
>>> a
array([[0, 1, 2],
[3, 4, 5]])
需要注意的是,reshape
只是返回一个对应尺寸的新数组,原数组的尺寸不会改变;而resize
则会改变数组本身的尺寸。
可以对.shape
直接赋值以改变尺寸:
>>> a = np.arange(6)
>>> a.shape = (3,2)
>>> a
array([[0, 1],
[2, 3],
[4, 5]])
对于reshape()
和.shape
,我们可以将size指定为-1,这样就会自动计算其大小(注:resize()
不支持):
>>> a = np.arange(6)
>>> a.shape = 3, -1
>>> a
array([[0, 1],
[2, 3],
[4, 5]])
>>> a.reshape(2, -1)
array([[0, 1, 2],
[3, 4, 5]])
>>> a.reshape(-1, 2)
array([[0, 1],
[2, 3],
[4, 5]])
6.2 平铺
通过函数 flatten()
和 ravel()
都可以将多维数组平铺成一维数组,但二者的区别是修改 flatten()
得到平铺数组不会应该原来的数组,而修改 ravel()
得到的数组会影响原来的数组:
>>> a = np.arange(6).reshape(2, 3)
>>> a
array([[0, 1, 2],
[3, 4, 5]])
>>> b = a.flatten()
>>> b
array([0, 1, 2, 3, 4, 5])
# 修改这个平铺数组不会影响原数组
>>> b[0] = 100
>>> a
array([[0, 1, 2],
[3, 4, 5]])
>>> c = a.ravel()
>>> c
array([0, 1, 2, 3, 4, 5])
# 修改这个平铺数组会影响原数组
>>> c[0] = 100
>>> a
array([[100, 1, 2],
[ 3, 4, 5]])
6.3 排序
通过sort()
可以对数组进行排序,可以指定维度进行排序,默认是按照最低维度排序的:
>>> a = np.array([[8, 0, 1], [5, 3, 4], [2, 6, 7]])
>>> a
array([[8, 0, 1],
[5, 3, 4],
[2, 6, 7]])
>>> a.sort()
>>> a
array([[0, 1, 8],
[3, 4, 5],
[2, 6, 7]])
>>> a.sort(axis=0)
>>> a
array([[0, 1, 5],
[2, 4, 7],
[3, 6, 8]])
函数sort()
默认是升序排列的,如过需要降序可以通过负数的方式:-np.sort(-a)
。
6.4 转置
通过属性 .T
或者函数 transponse()
可以完成对数组进行转置:
>>> a = np.arange(6).reshape(2, 3)
>>> a
array([[0, 1, 2],
[3, 4, 5]])
>>> a.T
array([[0, 3],
[1, 4],
[2, 5]])
>>> a.transpose()
array([[0, 3],
[1, 4],
[2, 5]])
上面转置的方法不会改变数组本身,只会返回一个新数组。
6.5 合并
通过 np.vstack()
和 np.hstack()
可以对数组进行合并:
>>> a = np.arange(4).reshape(2, 2)
>>> a
array([[0, 1],
[2, 3]])
>>> b = np.arange(start=4, stop=8).reshape(2, 2)
>>> b
array([[4, 5],
[6, 7]])
>>> np.vstack((a,b))
array([[0, 1],
[2, 3],
[4, 5],
[6, 7]])
>>> np.hstack((a,b))
array([[0, 1, 4, 5],
[2, 3, 6, 7]])
上面是固定的垂直合并和水平合并,也可以通过 np.concatenate
指定轴进行合并(axis
越小代表维度越高):
>>> np.concatenate((a,b), axis=0)
array([[0, 1],
[2, 3],
[4, 5],
[6, 7]])
>>> np.concatenate((a,b), axis=1)
array([[0, 1, 4, 5],
[2, 3, 6, 7]])
6.6 分割
通过 np.hsplit
可以将数组沿水平轴进行分割。可以进行等量分割或不等量分割:
>>> a = np.arange(9).reshape(3, 3)
>>> a
array([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
# 等量分割成3列
>>> np.hsplit(a, 3)
[array([[0],
[3],
[6]]), array([[1],
[4],
[7]]), array([[2],
[5],
[8]])]
# 不等量分割,1份为1列,1份为2列
>>> np.hsplit(a, (1,2))
[array([[0],
[3],
[6]]), array([[1],
[4],
[7]]), array([[2],
[5],
[8]])]
通过np.vsplit
可以沿垂直轴进行分割,也可以通过 np.array_split
指定轴进行分割。
6.7 拷贝
Numpy中的数组分为不拷贝、浅拷贝和深拷贝。如果直接赋值,则不会复制数组对象和其数据:
>>> a = np.arange(12)
>>> b = a
>>> id(a)
2258092791568
>>> id(b)
2258092791568
>>> b is a
True
通过函数view()
可以创建一个视图,即浅拷贝。表现为不同的数组对象共享相同的数据,改变一个会影响另一个:
>>> a = np.arange(12)
>>>
>>> b = a.view()
>>> id(a)
2257817754800
>>> id(b)
2258092792144
>>> a[0] = 130
>>> b
array([130, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
通过函数 copy()
可以生成一个数组和数据的完成副本,即深拷贝,二者互不影响:
>>> a = np.arange(12)
>>> b = a.copy()
>>> id(a)
2258092791664
>>> id(b)
2258092791568
>>> a[0] = 130
>>> b
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
7. 数组运算
NumPy中的运算效率高且拥有矢量化的特点,非常容易理解。基本数学运算就是指基于数学符号的运算,包含下面几种(按对应元素进行运算):
序号 | 运算 | 描述 |
---|---|---|
1 | + , - , * , / , // , % | 基本数学运算 |
2 | ** | 指数运算 |
3 | += , -= , *= , /= , //= , %= , **= | 复合赋值运算 |
4 | > , >= , < , <= , == | 比较运算 |
7.1 +, -, *, /, //, %
NumPy的数组,支持和数字的直接运算,:
>>> a = np.array([1,2,3])
>>> a+10 # 加
array([11, 12, 13])
>>> a-10 # 减
array([-9, -8, -7])
>>> a*10 # 乘
array([10, 20, 30])
>>> a/10 # 除
array([0.1, 0.2, 0.3])
>>> a // 10 # 取整除
array([0, 0, 0], dtype=int32)
>>> a%10 # 取余
array([1, 2, 3], dtype=int32)
也支持数组和数组之间的运算:
>>> a = np.array([1,2,3])
>>> b = np.array([4,5,6])
>>> a+b
array([5, 7, 9])
>>> a-b
array([-3, -3, -3])
>>> a*b
array([ 4, 10, 18])
>>> a/b
array([0.25, 0.4 , 0.5 ])
>>> a//b
array([0, 0, 0])
>>> a%b
array([1, 2, 3])
因为数组间的运算是对相同位置的元素进行逐个运算,所以参与运算的数组必须拥有相同的shape
,否组无法进行运算。
7.2 **
通过**N
的形式可以进行N次幂的运算,支持和数字 或 数组间的方式:
>>> a = np.array([1,2,3])
>>> b = np.array([4,5,6])
>>> a**2 # 数组a的2次方
array([1, 4, 9])
>>> a**b # 数组a的b次方
array([1, 32, 729])
7.3 +=, -=, *=, /=, //=, %=, **=
NumPy的复合赋值运算和大多数编程语言作用类似,但它和前面两小节的基本数学运算和指数运算的区别是:基本数学运算和指数运算会生成一个新数组,不会改变数组元素的值;而复合赋值运算则会作用于数组元素本身。
>>> import numpy as np
>>> a = np.array([1,2,3])
>>> a += 1
>>> a
array([2, 3, 4])
>>> a -= 1
>>> a
array([1, 2, 3])
>>> a *= 2
>>> a
array([2, 4, 6])
>>> a **= 2
>>> a
array([4, 16, 36])
上面的例子中我们可以看到其实是改变了数组a
本身。
另外,例子中没有使用 /=
,因为/=
比较特殊,需要单独说,理论上我们定义了数组a
之后,应该直接a /= 2
就可以,但我们这样写的话会报错:
>>> a /= 2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
numpy.core._exceptions._UFuncOutputCastingError: Cannot cast ufunc 'divide' output from dtype('float64') to dtype('int32') with casting rule 'same_kind'
也就是说/=
的作用数组需要是浮点类型,而我们定义成了整形,所以我们有两种方式,即一开始我们就把数组定义成浮点型,或者 使用符号//=
处理整数相除:
>>> a = np.array([1, 2, 3], dtype=np.float32)
>>> a /= 2
>>> a
array([0.5, 1. , 1.5], dtype=float32)
>>> b = np.array([1, 2, 3])
>>> b //= 2
>>> b
array([0, 1, 1])
7.4 >, >=, <, <=, ==
通过比较运算,我们可以得到一个布尔型的数组:
>>> a = np.array([1, 2, 3])
>>> a > 1
array([False, True, True])
>>> a >= 1
array([ True, True, True])
>>> a < 1
array([False, False, False])
>>> a <= 1
array([ True, False, False])
>>> a == 1
array([ True, False, False])
8. 广播
NumPy里的广播是个挺有意思的概念,其主要的目的就是让形状或维度不同的数组可以进行运算并有一套自己的规则。
广播兼容的条件是,两个数组从末尾的维度推导,
- 他们的尺寸是一样,或者
- 其中一个是1
则他们是广播兼容的,正如下面两个满足广播兼容的例子:
A (3d array): 15 x 3 x 5
B (2d array): 3 x 5
Result (3d array): 15 x 3 x 5
A (4d array): 8 x 1 x 6 x 1
B (3d array): 7 x 1 x 5
Result (4d array): 8 x 7 x 6 x 5
如果不满足上面的条件,就是广播不兼容的,运算时会抛出 ValueError: operands could not be broadcast together
异常:
A (1d array): 3
B (1d array): 4 # trailing dimensions do not match
A (2d array): 2 x 1
B (3d array): 8 x 4 x 3 # second from last dimensions mismatched
了解广播主要需要了解生成数组的尺寸规则,如果这点明白了,具体的运算就比较清晰了:
# 一维数组的广播
>>> a = np.arange(4)
>>> a + 10
array([10, 11, 12, 13])
# 二维数组的广播
>>> b = a.reshape(4, 1)
>>> b
array([[0],
[1],
[2],
[3]])
>>> c = np.ones(5)
>>> b + c
array([[1., 1., 1., 1., 1.],
[2., 2., 2., 2., 2.],
[3., 3., 3., 3., 3.],
[4., 4., 4., 4., 4.]])
前文我们介绍过 np.newaxis
,我们可以利用它将数组升维,然后进行广播计算:
>>> a = np.array([0.0, 10.0, 20.0, 30.0])
>>> b = np.array([1.0, 2.0, 3.0])
>>> a[:, np.newaxis] + b
array([[ 1., 2., 3.],
[ 11., 12., 13.],
[ 21., 22., 23.],
[ 31., 32., 33.]])
9. 通函数
NumPy除了直接使用数学符号外,还提供了一些数学函数,这些被称为“通函数”(ufunc
)。这些通函数在数组上按元素进行运算,并生成一个新数组。
9.1 一元函数
函数 | 描述 |
---|---|
np.abs | 绝对值 |
np.sqrt | 开方(负数开方结果为NAN) |
np.square | 平方 |
np.exp | 计算指数(e^x) |
np.log,np.log10,np.log2,np.log1p | 求以e为底,以10为底,以2为底,以(1+x为底的对数 |
np.sign | 将数组中的值标签化,大于0的变成1,等于0的变成0,小于0的变成-1 |
np.ceil | 获取数组中每个元素的上限整数 |
np.floor | 获取数组中每个元素的下限整数 |
np.clop | 设置数组中的上下限制 |
np.rint,np.round | 返回四舍五入后的值 |
np.modf | 将整数和小数分割开来形成两个数组 |
np.isnan | 判断是否是nan |
np.isinf | 判断是否是inf |
np.cos,np.cosh,np.sinh,np.tan,np.tanh | 三角函数 |
np.arccos,np.arcsin,np.arctan | 反三角函数 |
np.nonzero | 找出非0的元素和其坐标 |
np.cumsum | 累加 |
np.diff | 累差 |
9.2 二元函数
函数 | 描述 |
---|---|
np.add | 加法运算 |
np.subtract | 减法运算 |
np.negative | 复数运算 |
np.multiply | 乘法运算 |
np.divide | 除法运算 |
np.floor_divide | 取整运算,相当于// |
np.mod | 取余运算 |
np.dot | 内积(点乘) |
greater,greater_equal,less,less_equal,equal,not_equal | >,>=,<,<=,=,!=的函数表达式 |
logical_and | 且运算符函数表达式 |
logical_or | 或运算符函数表达式 |
9.3 聚合函数
函数 | 描述 |
---|---|
np.sum | 计算元素的和 |
np.prod | 计算元素的积 |
np.mean | 计算元素的平均值 |
np.std | 计算元素的标准差 |
np.var | 计算元素的方差 |
np.min | 计算元素的最小值 |
np.max | 计算元素的最大值 |
np.argmin | 找出最小值的索引 |
np.argmax | 找出最大值的索引 |
np.median | 计算元素的中位数 |
np.average | 计算元素的加权平均数 |
9.4 布尔数组函数
函数 | 描述 |
---|---|
np.all() | 判断所有元素或者指定轴的元素中,是否全为True |
np.any() | 判断所有元素或者指定轴的元素中,是否有True |
10. API函数
NumPy中的API的类型很丰富,而且非常多,这一部分可参照官方API文档。