NumPy基础
提纲
- 概述
- 创建数组
- 数组的运算
- 数组的索引
- 数组的变形操作
- numpy随机数模块
一、NumPy概述
1.1 NumPy简介
智能算法经常用到数组和矩阵(向量)运算。
虽然Python内置数据结构中的列表可以当作数组来使用,但是一个列表可以存储多种类型的元素,导致数值运算效率较低。
虽然Python也提供了内置的array(数组)模块,但它仅仅支持一维数组,不支持多维数组,也没有各种运算函数,因此并不适合数值运算。
为了弥补Python数值计算的不足,吉姆·弗贾宁(Jim Hugunin)、特拉维斯·奥利芬特(Travis Oliphant)等人联合开发了NumPy项目。
NumPy是Python语言的一个基于C语言的扩展程序库,支持多维度的数组(即N维数组对象ndarray)与矩阵运算,并对数组运算提供了大量的数学函数库。NumPy功能非常强大,支持广播、线性代数运算、傅里叶变换、随机数生成等功能,对很多第三方库(如SciPy
、Pandas
、matplotlib
等)提供了底层支持。
1.2 导入NumPy
NumPy是Python的第三方库,使用前需要安装。
# pip install numpy
在使用时,NumPy还是需要显式导入的。为了方便,我们常会为NumPy起一个别名,通常这个别名为np
。
#导入numpy并指定别名
import numpy as np
#输出numpy别名
print(np.__version__)
1.20.1
二、创建NumPy数组
NumPy最重要的一个特点就是支持N维数组对象ndarray
。
NumPy数组中的元素只能是同一种数据类型。
2.1 利用序列生成——array()方法
生成NumPy数组最简单的方式,莫过于利用array()
方法。array()
方法可以接收任意数据类型(如列表、元组等)作为数据源。
array()
方法的签名为:numpy.array(object, dtype=None, copy=True, order='K',subok=False, ndmin=0)
array()
方法的返回值为ndarray
对象。
arr1 = np.array([1,2,3])
arr1
array([1, 2, 3])
(1)精度转换
1)自动转换
如果构造NumPy数组的数据源类型精度不统一,且这些数据类型可以相互转换,那么NumPy会遵循 “就高不就低”(upcast) 的规则进行类型转换,比如说列表中的数据有整数,也有浮点数,NumPy会把所有数据都转换为浮点数,这是因为浮点数的精度更高。
2)手动转换
我们也可以用astype()
方法显式指定被转换数组的数据类型。
(2)适配高维数组
如果数据序列是嵌套的,且嵌套序列是等长的,则通过array()
方法可以把嵌套的序列转为与嵌套级别适配的高维数组。
2.2 利用特定函数生成
(1)arange()
函数
函数作用:arange()
根据start
与stop
指定的范围及step
设定的步长,生成一个ndarray
对象。
函数格式:arange(start, stop, step, dtype)
参数说明:
start
为起始值,默认为0
。
stop
为终止值。取值区间是左闭右开的,即stop
这个终止值是不包括在内的。
step
为步长,如果不指定,默认值为1
。
dtype
指明返回ndarray
的数据类型,如果没有提供,则会使用输入数据的类型。
arr1 = np.arange(10)
arr1
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
arange()
方法的使用与Python的内置函数range()
十分类似。两者都能均匀地(evenly)等分区间,但是range()
无法将步长设置为浮点数,而np.arange()
可以将步长设置为任意实数。
arr1 = np.arange(0,10,0.5)
arr1
array([0. , 0.5, 1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5, 5. , 5.5, 6. ,
6.5, 7. , 7.5, 8. , 8.5, 9. , 9.5])
(2)linspace()
函数
函数作用:linspace()
在指定区间内生成指定个数的元素的数组
函数格式:linspace(start, stop, num)
参数说明:前两个参数分别指明生成元素的左右区间边界,第三个参数确定上下限之间均匀等分的数据个数。
需要注意的是,arange()
中数据区间是左闭右开的(即区间的最后一个数值是取不到的),而linspace()
生成的数据区间为闭区间。当然我们也可以在该函数中指定endpoint=False
,使生成数据区间变为左闭右开区间。
案例:在[1,10]中生成20个等间隔的数据
arr1 = np.linspace(1,10,20)
arr1
array([ 1. , 1.47368421, 1.94736842, 2.42105263, 2.89473684,
3.36842105, 3.84210526, 4.31578947, 4.78947368, 5.26315789,
5.73684211, 6.21052632, 6.68421053, 7.15789474, 7.63157895,
8.10526316, 8.57894737, 9.05263158, 9.52631579, 10. ])
(3)zeros()
和ones()
函数
np.zeros()
、np.ones()
等函数可以生成指定维度和填充固定数值的数组,它们通常用来对某些变量进行初始化。其中,
np.zeros()
函数生成的数组由0
来填充,
np.ones()
生成的数组由1
来填充。
arr1 = np.zeros((3,4))
arr1
array([[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]])
np.zeros((3,4))
等价于np.zeros(shape=(3,4))
,shape
参数处需要通过一个元组或列表来指明生成数组的尺寸。
类似地,我们可以用np.ones()
生成指定尺寸、元素全为1
的数组,dtype
参数设置元素的类型。
arr1 = np.ones((3,4),dtype=float)
arr1
array([[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]])
(3)zeros_like()
和ones_like()
还有一种生成全0
数组的方法是np.zeros_like()
。该方法的核心思想可概括为“借壳上市”,它会借用某个给定数组的类型、尺寸(即维度信息),但其中的所有元素都被置换为0
,这也是“zeros_like”名称的来源。
arr1 = np.array([[1,2,3],[4,5,6]])
arr1
array([[1, 2, 3],
[4, 5, 6]])
arr2 = np.zeros_like(arr1)
arr2
array([[0, 0, 0],
[0, 0, 0]])
和np.zero_like()
非常相似的一个操作是ones_like()
。它的功能是将数组中的元素都填充为1
,数组的尺寸信息和数据类型来自一个给定数组。
和zeros_like()
、ones_like()
功能类似的方法还有以下两个。
empty_like()
:产生和给定数组尺寸和类型相同的数组,但该数组中的元素没有被初始化(uninitialized),你可以认为它是一个“万事俱备,只欠数据”的数组。
full_like()
:产生和给定数组尺寸和类型相同的数组,该数组中的元素都被初始化为某个给定值。
2.3 多维数组的属性
属性 | 说明 |
---|---|
ndim | 返回 int。表示数组的维数 |
shape | 返回 tuple。表示数组的尺寸,对于 n 行 m 列的矩阵,形状为(n,m) |
size | 返回 int。表示数组的元素总数,等于数组形状的乘积 |
dtype | 返回 data-type。描述数组中元素的类型 |
itemsize | 返回 int。表示数组的每个元素的大小(以字节为单位) |
arr1 = np.array([[1,2,3],[4,5,6]])
arr1.ndim, arr1.shape, arr1.size, arr1.dtype
(2, (2, 3), 6, dtype('int32'))
但NumPy数组形状并不是一成不变的,可以通过reshape()
方法将原有数组进行**“重构”(变形)**。
通常返回的是非拷贝副本,即改变返回后数组的元素,原数组对应元素的值也会改变。
arr1 = np.arange(10)
arr1
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
arr1.reshape(2,5)
array([[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9]])
里需要注意的是,NumPy表示三维数组维度信息的方式和我们通常的认知稍有不同。
比如,我们想创建两个3行5列的数组,它的形状参数为(2, 3, 5)
,而不是(3, 5, 2)
。
arr1 = np.arange(30).reshape(2,3,5)
arr1
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]]])
三、NumPy数组中的运算
3.1 向量运算
案例:数组向量加法
arr1 = np.array([1,2,3])
arr2 = np.array([4,5,6])
arr3 = arr1 + arr2
arr3
array([5, 7, 9])
类似于加法,基于NumPy数组的减法、乘法、除法等各种数学运算都可以这么高效地完成。
3.2 算术运算
NumPy吸纳了Fortran或MATLAB等语言的优点,只要操作数组的形状(维度)一致,我们就可以很方便地对它们**逐元素(element-wise)**实施加、减、乘、除、取余、指数运算等操作。这些操作特别适合大规模的并行计算。
NumPy中还有很多好用的统计函数,如sum()
、min()
、max()
、median()
、mean()
、average()
、std()
、var()
分别用于求和、最小值、最大值、中位数、平均数、加权平均数、标准差、方差。
NumPy中还有很多常用的数学函数,如三角函数sin()
、cos()
和tan()
等,这也使得导入了NumPy后,Python宛若一个功能强大的科学计算器。
arr1 = np.array([1,2,3,3,4,5])
np.min(arr1)
1
np.mean(arr1)
3.0
3.3 NumPy中的广播
在NumPy中,如果对两个数组实施加、减、乘、除等运算,参与运算的两个数组需形状相同。但实际上,NumPy具有“智能填充”功能,当两个数组的形状不相同时,可扩充较小数组中的元素来匹配较大数组的形状,这种机制叫作广播(broadcasting)。
这种广播机制,也称为张量自动扩展,它是一种轻量级的张量复制手段。需要说明的是,对于大部分场景,广播机制仅仅在逻辑上改变了张量的尺寸,只待实际需要时才真正实现张量的赋值和扩展。这种优化流程节省了大量计算资源,并由计算框架(如NumPy)隐式完成,用户无须关心实现细节。
NumPy的广播规则:
- 扩展维度:如果两个张量的尺寸不同,则NumPy的广播机制会为尺寸较小的张量添加一个轴(广播轴),使其维度信息与较大张量的相同。
- 复制数据:尺寸较小的张量沿着新添加的轴不断重复之前的元素,直至尺寸与较大的张量相同。
- 低维有1:如果两个张量的尺寸在任何维度上都不匹配,则需将某维度中尺寸为1的张量拉伸,以匹配另一个较大张量的尺寸。
如果两个张量在任何维度上尺寸都不一致,且两者均没有任何一个维度为1,则会出现广播错误,即广播不会发生。也就是说,为了让广播操作能够顺利进行,广播操作的两个对象,它们某个维度上的尺寸,要么相等,要么为1。
3.4 NumPy数组中的轴方向
在NumPy的多维数组中,常有“约减”(Reduce,亦有文献译作“规约”)的提法。它表示将众多数据按照某种规则合并成一个或几个数据。“约减”之后,数据的个数在总量上是减少的。在这里,“约减”的“减”并非减法之意,而是元素的减少。比如说,数组的加法操作就是一种“约减”操作,因为它对众多元素按照加法指令实施操作,最后合并为少数的一个或几个值。
求N维数组的均值(mean)、最大值(max)和最小值(min)等,这些操作都属于约减操作。但有时,我们会有这样的需求,对指定维度方向的值进行统计,如统计某一行(或列)的和、均值、最大值、最小值等。这个时候,就需要给“约减”指令指定方向。
诸如sum()
、min()
、max()
,mean()
等函数,它们都有一个名为操作轴(axis
)的参数。
其默认值为None
,也就是不指定约减方向,它将所有数据都“约减”为一个元素。
如果axis
的值为0
,可简单地理解为从垂直方向进行“约减”。
如果axis
的值为1
,则可以简单理解为从水平方向进行“约减”。
四、NumPy数组的索引
4.1 通过索引访问数组元素
索引(index)是指数组元素所在的位置编号,有点类似于邮编之于地区。我们可以通过NumPy数组的索引来获取、设置数组元素的值。如果希望访问数组中的值,像访问列表元素一样,给出数组的下标即可。
相应地,访问二维数组时,需要通过两个索引来执行相应操作。访问二维数组的方式有两种:
- 第一种是类似于C、C++一样,使用两个方括号,每个方括号对应一个维度信息。
- 另一种更为简便的访问方式,把两个方括号合并,在一个方括号内分别给出两个维度信息,不同维度信息间用逗号
(,)
隔开。
arr1 = np.array([[1,2,3],[4,5,6]])
arr1
array([[1, 2, 3],
[4, 5, 6]])
arr1[0][2]
3
arr1[0,2]
3
通过这种方法同样可以修改二维数组中的值。
arr1[0,2]=100
arr1[0,2]
100
4.2 NumPy中的切片访问
与Python中列表的操作类似,除了通过索引访问数组元素,在NumPy中还可以通过切片操作来访问和修改数组数据。
切片操作的核心是从原始数组中,按照给定规则提取出一个新的数组,对原始数组没有任何影响。
切片规则通常是这样的:数组名[start:end:step]
。其中start
表示起始索引(从0
开始),end
表示结束索引(至-1
结束),step
表示步长,步长为正时表示从左向右取值,步长为负时则反向取值。
切片的步长step
可取负值。当step=−1
时,start: end: −1
表示从start
开始逆序读取至end
结束(不包含end
)。考虑最特殊的一种例子,当切片方式为:: -1
时就完成了逆序读取。
arr1=np.arange(10)
arr1[2:7:2]
array([2, 4, 6])
4.3 花式索引:列表作为索引
前面的索引方法有一个特点:索引要么是一个值,要么是一片值(即切片访问)。
如果索引只是一个值,那么自然只能访问一个数组元素。
如果索引基于切片方法,那么被访问的数组元素或连续分布,或通过设置步长有规律地间隔分布。
如果我们想一次性访问数组中的多个元素,而它们又没什么规律可循,该怎么呢?“花式”索引(FancyIndexing)就是用来解决这个问题的。
“花式”索引是指,将多个需要访问元素的索引汇集起来,构成一个整型数组,然后把这个内含索引的数组,整体作为目标数组的索引,这样就能一次性地读取多个“杂乱无序”甚至重复的数组元素。由于这种读取数组元素的方式有些花哨,故称“花式”索引,又因为索引都是整数,亦有文献称之为整数索引。
arr1 = np.array([1,2,3,3,4,5])
arr1[[1,3]]
array([2, 3])
对于二维数组,如果要读取某一个元素,必须指定行和列两个参数。如果没有显式指定二维索引,就无法正确读取具体的元素。
如果访问一个二维数组,但只给出一维坐标,那么这个一维坐标指的是行索引坐标,数组的内层括号[0,2,1,0]
表示的是行号,它也是一种“花式”索引,表示要读取第0行、第2行、第1行和第0行(第2次访问)的数据。
4.4 布尔索引:比较表达式作为索引
a = np.array([1,2,3,3,4,5])
a[a>3]
array([4, 5])
方括号内的a > 3
意义并不简单。我们知道,a
是一个数组对象,而5
是一个标量,二者之所以能比较,是因为NumPy悄悄地使用了前面章节提到的广播技术,它把3
广播(复制)成与数组a
尺寸一模一样的数组,数组内的元素都是3
。然后数组a
中个每个元素都与3
做比较,逐个判断该元素是否大于3
,因此返回的是一个形状与数组a
相同的布尔数组
这个尺寸(维度信息)与原始数组相同的数组,叫作布尔数组。布尔数组可以整体作为索引,形成一个布尔索引,然后NumPy会依据逐元素(element-wise)规则,返回对应位置布尔值为True
的元素。因此,a[a>3]
的含义,就是返回数组中大于3
的元素。
五、数组的变形操作
5.1 二维数组的转置与展平
(1)二维数组转置
我们可以通过transpose()
方法将二维数组转置,这种转置仅仅得到原有二维数组的视图,原始数组并没有发生变化。
(2)二维数组展平
1)ravel()
我们可以利用ravel()
方法来完成将多维数组降维成一维数组。同样地,ravel()
返回的仅仅是原始数组的视图而已,原始数组本身并没有发生变化。
2)flatten()
flatten()
函数同样可以完成将多维数组展平成一维数组的操作。不同于ravel()
返回的是原始数组的视图,flatten()
会重新分配内存,完成一次从原始数据到新内存空间的深拷贝,但原始数组并没有发生任何变化。
5.2 数组的堆叠操作
有时,我们需要将不同的NumPy数组,通过堆叠(stack)操作,拼接为一个新的较大的数组。堆叠方式大致分为水平方向堆叠(horizontal stack)、垂直方向堆叠(vertical stack)、深度方向堆叠(depth-wise stack)等。、
上图所示的这三种排列方式,分别体现了hstack()
、vstack()
和dstack()
方法在拼接数组时的特点。除此之外,堆叠函数还包括concatenate()
、column_stack()
、row_stack()
等。很多实现堆叠功能的不同函数有着“异曲同工”之妙,通过配置不同的参数,可达到相同的数组拼接目的。
(1)水平方向堆叠hstack()
hstack的首字母“h”来自英文单词“horizontal”(水平),表示所操作的数组是在水平方向堆叠的,其实就是按列顺序堆叠起来。
arr1 = np.zeros((2,2))
arr1
array([[0., 0.],
[0., 0.]])
arr2 = np.ones((2,3))
arr2
array([[1., 1., 1.],
[1., 1., 1.]])
arr3 = np.hstack((arr1,arr2))
arr3
array([[0., 0., 1., 1., 1.],
[0., 0., 1., 1., 1.]])
hstack()
实际上表示的是不同数组在水平方向上的堆叠。我们利用concatenate()
函数,并设置水平轴方向(axis=1
)的连接,就可以达到相同的堆叠效果。
为了完成堆叠,hstack()
要求参与堆叠操作的两个数组在垂直(即行)方向的尺寸是相同的。
(2)垂直方向堆叠vstack()
类似地,vstack()
实现的是轴0
方向(即垂直方向)的数组堆叠。vstack
一词的首字母v表示的是vertical(垂直)的意思。vstack()
的函数原型为vstack(tup)
,其中参数tup
表示元组,元组内的元素可以是元组、列表或NumPy数组等,返回结果为NumPy数组。
arr1 = np.zeros((3,3))
arr1
array([[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.]])
arr2 = np.ones((2,3))
arr2
array([[1., 1., 1.],
[1., 1., 1.]])
arr3 = np.vstack((arr1,arr2))
arr3
array([[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.],
[1., 1., 1.],
[1., 1., 1.]])
类似于hstack()
,为了完成数组堆叠操作,vstack()
要求除第一个轴(参与堆叠)方向的维度尺寸不一样以外,其他维度的尺寸必须保证一致。
5.3 数组分割操作
NumPy也提供了数组的分割操作。和堆叠类似,分割也包括水平方向分割、垂直方向分割和深度方向分割,分别用hsplit()
、vsplit()
和dsplit()
实现。类似于concatenate()
方法可通过设置轴方向,既实现水平方向堆叠,又实现垂直方向堆叠,split()
也可以通过设置分割方向,分别实现hsplit()
、vsplit()
和dsplit()
的功能。
六、NumPy中的随机数模块
NumPy中也含有随机数模块,即numpy.random
模块。
numpy.random
模块中提供了大量与随机数相关的函数。
python内置的random
模块用于生成随机数,numpy.random
用于生成随机数矩阵。
随机数是由随机种子根据一定的规则计算出来的数值,所以,只要计算方法一定,随机种子一定,那么产生的随机数就不会变。若不设定随机种子,随机数生成器会将系统时间作为随机种子来生成随机数。
# 在区间[0,1)中生成服从均匀分布的数组,每个维度占据一个参数位置,rand()支持对不确定参数赋值,二维数组就有两个参数,以此类推。
# rand(d0, d1, ..., dn)
np.random.rand(2,5)
array([[0.84492252, 0.98072095, 0.63809183, 0.74701318, 0.74079855],
[0.23183099, 0.87364001, 0.57100839, 0.70213023, 0.70982771]])
# 在区间[0,1)中生成服从均匀分布的数组,参数即结果数组的形状元组
# random(size=None)
np.random.random((2,5))
array([[0.74867025, 0.11237504, 0.81028004, 0.54099473, 0.67045688],
[0.31118362, 0.55093925, 0.48897572, 0.73137661, 0.50420562]])
# 在区间[0,1)中生成服从标准正态分布的数组,参数即结果数组的形状
# randn(d0, d1, ..., dn)
np.random.randn(2,5)
array([[-0.29269022, 0.85976458, -1.21948284, -0.85250544, 1.62391503],
[ 0.30559764, 0.17003518, 1.89516677, 0.59479242, 0.13385514]])
# 在1-20之间生成形状为2,5的整数数组
# randint(low, high=None, size=None, dtype=int)
np.random.randint(1,20,(2,5))
array([[15, 4, 13, 14, 9],
[ 3, 15, 8, 1, 1]])
# 从序列a中按概率p随机输出size个元素的一维数组
# choice(a, size=None, replace=True, p=None)
np.random.choice(['a','b','c'])
'c'
# p参数中概率和必须为1
np.random.choice(['a', 'b', 'c'], 2,p=[0.5, 0.3, 0.2])
array(['b', 'b'], dtype='<U1')
参考资料
《Python极简讲义:一本书入门数据分析与机器学习》
https://www.numpy.org.cn
https://www.runoob.com/numpy/numpy-tutorial.html
习题
https://zhuanlan.zhihu.com/p/58576235
https://github.com/rougier/numpy-100/blob/master/100_Numpy_exercises_with_hints_with_solutions.md