Numpy介绍
NumPy是Python中科学计算的基本包。它是一个Python库,提供了一个多维数组对象,各种派生对象(如屏蔽数组(masked arrays)和矩阵),以及用于对数组进行快速操作的各种例程,包括数学,逻辑,形状操作,排序,选择,I/O,离散傅里叶变换,基本线性代数,基本统计运算,随机模拟等等。
NumPy包的核心是ndarray对象。它封装了同构数据类型的n维数组,其中许多操作在编译的代码中执行以提高性能。NumPy数组和标准Python序列之间有几个重要的区别:
- NumPy数组在创建时具有固定大小,这与Python列表(可以动态增长)不同。更改ndarray的大小会创建一个新数组并删除原始数组。
- NumPy数组中的元素都需要具有相同的数据类型,因此内存大小相同。例外情况是:可以有(Python,包括NumPy)对象的数组,从而允许不同大小的元素的数组。
- NumPy数组有助于对大量数据进行高级数学和其他类型的操作。通常,与使用Python的内置序列相比,此类操作的执行效率更高,代码更少。
- 越来越多的基于Python的科学和数学包正在使用NumPy数组;虽然这些通常支持Python序列输入,但它们在处理之前将此类输入转换为NumPy数组,并且它们通常输出NumPy数组。换句话说,为了有效地使用当今许多(甚至可能是大多数)基于Python的科学/数学软件,仅仅知道如何使用Python的内置序列类型是不够的——还需要知道如何使用NumPy数组。
序列大小和速度在科学计算中尤为重要。作为一个简单的示例,考虑将一维序列中的每个元素与另一个长度相同的序列中的相应元素相乘的情况。如果数据存储在两个 Python 列表中,a 和 b,我们可以遍历每个元素:
c = []
for i in range(len(a)):
c.append(a[i]*b[i])
这是正确的答案,但如果 a 和 b 都包含数百万个数字,我们将为 Python 中循环的低效率付出代价。 我们可以在 C 中通过编写来更快地完成相同的任务(为了清楚起见,忽略变量声明和初始化、内存分配等):
for (i=0; i<rows; i++):{
c[i] = a[i] * b[i];
}
这节省了解释Python代码和操作Python对象所涉及的所有开销,但代价是牺牲了在Python中编码所获得的好处。此外,所需的编码工作随着我们数据的维度而增加。例如,在 2-D 数组的情况下,C 代码(像以前一样删节)扩展为:
for (i=0; i<rows; i++):{
for (i=0; j<colcumns; j++):{
c[i][j] = a[i][j] * b[i][j];
}
}
NumPy为我们提供了两全其美的优势:当涉及ndarray时,逐个元素的操作是"默认模式",但逐个元素的操作是由预编译的C代码快速执行的。在 NumPy 中:
c = a * b
以接近C的速度做了前面的例子,但是由于代码的简单性,我们期望从基于Python的东西中得到。事实上,NumPy甚至更简单!最后一个例子说明了NumPy的两个功能,它们是其大部分功能的基础:矢量化和广播。
为什么NumPy很快?
矢量化描述了代码中没有任何显式循环,索引等 - 当然,这些事情只是在优化的,预编译的C代码中的"幕后"发生。矢量化代码具有许多优点,其中包括:
- 矢量化代码更简洁,更易于阅读
- 更少的代码行通常意味着更少的错误
- 代码更接近于标准数学符号(通常更容易正确编码数学构造)
- 矢量化导致更多的"Pythonic"代码。如果没有矢量化,我们的代码将充斥着低效且难以读取的for循环。
广播(Broadcasting)是用于描述操作的隐式逐个元素行为的术语;一般来说,在NumPy中,所有操作,不仅仅是算术运算,包括逻辑,位,函数等,都以这种隐式逐个元素的方式运行,即它们广播。此外,在上面的示例中,可以是相同形状的多维数组,也可以是标量和数组,甚至是两个具有不同形状的数组,前提是较小的数组可以扩展为较大的形状,从而使生成的广播是明确的。
NumPy快速入门
先决条件
需要了解一些Python。有关学习,可以参阅Python 3.10教程。
要处理这些下面这些示例,除了NumPy之外,还需要安装matplotlib。
对学习者
这是NumPy中数组的快速概述。它演示了 n 维 (n>=2) 数组的表示方式和操作方式。特别是,如果您不知道如何将常用函数应用于 n 维数组(不使用 for 循环),或者如果您想了解 n 维数组的轴和形状属性,本文可能会有所帮助。
学习目标
阅读本文后,应该能够:
- 了解NumPy中一维,二维和n维数组之间的区别;
- 了解如何在不使用 for 循环的情况下将一些线性代数运算应用于 n 维数组;
- 了解 n 维数组的轴和形状属性。
基础知识
NumPy的主要对象是同构多维数组。它是一个元素(通常是数字)表,所有元素都具有相同的类型,由非负整数元组索引。在NumPy中,维度称为轴。
例如,用于 3D 空间中点的坐标的数组[1, 2, 1]具有一个轴。该轴中有3个元素,因此我们说它的长度为3。在下面所示的示例中,数组有 2 个轴。第一个轴的长度为2,第二个轴的长度为3:
[[1. , 0. , 0.],
[0. , 1. , 2.]]
NumPy 的数组类称为 ndarray,它的别名也叫数组。 请注意,numpy.array 与标准 Python 库类 array.array 不同,后者只处理一维数组并且提供的功能较少。 ndarray 对象更重要的属性是:
ndarray.ndim
数组的轴数(尺寸)。
ndarray.shape
数组的维度。 这是一个整数元组,表示每个维度中数组的大小。 对于具有 n 行和 m 列的矩阵,形状将为 (n,m)。 因此,元组的长度就是轴数 ndim。
ndarray.size
数组的元素总数。这等于ndarray.shape的元素的乘积。
ndarray.dtype
描述数组中元素类型的对象。可以使用标准Python类型创建或指定dtype。此外,NumPy还提供了自己的类型,numpy.int32、numpy.int16 和 numpy.float64 就是一些例子。
ndarray.itemsize
数组中每个元素的大小(以byte为单位)。 例如,float64 类型的元素数组的项目大小为 8 (=64/8),而 complex32 类型的一个元素的项目大小为 4 (=32/8)。 它相当于 ndarray.dtype.itemsize。
ndarray.data
包含数组的实际元素的buffer。通常我们不需要使用此属性,因为我们将使用索引工具访问数组中的元素。
示例
>>> import numpy as np
>>> a = np.arange(15).reshape(3, 5)
>>> a
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
>>> a.shape
(3, 5)
>>> a.ndim
2
>>> a.dtype.name
'int64'
>>> a.itemsize
8
>>> a.size
15
>>> type(a)
<class 'numpy.ndarray'>
>>> b = np.array([6, 7, 8])
>>> b
array([6, 7, 8])
>>> type(b)
<class 'numpy.ndarray'>
创建Array
有几种方法可以创建数组。
例如,可以使用array函数从常规 Python 列表或元组创建数组。生成的数组的类型是从序列中元素的类型推导出来的。
>>> import numpy as np
>>> a = np.array([2, 3, 4])
>>> a
array([2, 3, 4])
>>> a.dtype
dtype('int64')
>>> b = np.array([1.2, 3.5, 5.1])
>>> b.dtype
dtype('float64')
常见的错误包括使用多个参数进行调用array,而不是提供单个序列作为参数。
>>> b = np.array(1, 2, 3, 4) # WRONG
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: array() takes from 1 to 2 positional arguments but 4 were given
>>> a = np.array([1, 2, 3, 4]) # RIGHT
array将序列序列转换为二维数组,将序列序列序列转换为三维数组,等等。
>>> b = np.array([(1.5, 2, 3), (4, 5, 6)])
>>> b
array([[1.5, 2. , 3. ],
[4. , 5. , 6. ]])
数组的类型也可以在创建时显式指定:
>>> c = np.array([[1, 2], [3, 4]], dtype=complex)
>>> c
array([[1.+0.j, 2.+0.j],
[3.+0.j, 4.+0.j]])
通常,数组的元素最初是未知的,但其大小是已知的。因此,NumPy提供了几个函数来创建具有初始占位符内容的数组。这最大限度地减少了不断增长的阵列的必要性,这是一项昂贵的操作。
zeros函数创建一个充满零的数组,ones函数创建一个充满 1 的数组,empty函数创建一个数组,其初始内容是随机的,并且取决于内存的状态。默认情况下,所创建数组的 dtype为float64 ,但它可以通过关键字参数dtype指定。
>>> np.zeros((3,4))
array([[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]])
>>> np.ones((2,3,4), dtype=np.int16)
array([[[1, 1, 1, 1],
[1, 1, 1, 1],
[1, 1, 1, 1]],
[[1, 1, 1, 1],
[1, 1, 1, 1],
[1, 1, 1, 1]]], dtype=int16)
>>> np.empty((2,3))
array([[1.5, 2. , 3. ],
[4. , 5. , 6. ]])
为了创建数字序列,NumPy提供了类似于Python内置函数range的方法:arange,但返回一个数组。
>>> np.arange(10, 30, 5)
array([10, 15, 20, 25])
>>> np.arange(0, 2, 0.3) # 它接受float参数
array([0. , 0.3, 0.6, 0.9, 1.2, 1.5, 1.8])
当 arange 与浮点参数一起使用时,由于浮点精度有限,通常无法预测获得的元素数量。 出于这个原因,通常最好使用函数 linspace 接收我们想要的元素数量作为参数,而不是step:
>>> from numpy import pi
>>> np.linspace(0, 3, 9) # # 从0到3的9个数字
array([0. , 0.375, 0.75 , 1.125, 1.5 , 1.875, 2.25 , 2.625, 3. ])
>>> x = np.linspace(0, 2*pi, 100) # 在很多点评估函数很有用
>>> f = np.sin(x)
>>> f
array([ 0.00000000e+00, 6.34239197e-02, 1.26592454e-01, 1.89251244e-01,
2.51147987e-01, 3.12033446e-01, 3.71662456e-01, 4.29794912e-01,
4.86196736e-01, 5.40640817e-01, 5.92907929e-01, 6.42787610e-01,
6.90079011e-01, 7.34591709e-01, 7.76146464e-01, 8.14575952e-01,
8.49725430e-01, 8.81453363e-01, 9.09631995e-01, 9.34147860e-01,
9.54902241e-01, 9.71811568e-01, 9.84807753e-01, 9.93838464e-01,
9.98867339e-01, 9.99874128e-01, 9.96854776e-01, 9.89821442e-01,
9.78802446e-01, 9.63842159e-01, 9.45000819e-01, 9.22354294e-01,
8.95993774e-01, 8.66025404e-01, 8.32569855e-01, 7.95761841e-01,
7.55749574e-01, 7.12694171e-01, 6.66769001e-01, 6.18158986e-01,
5.67059864e-01, 5.13677392e-01, 4.58226522e-01, 4.00930535e-01,
3.42020143e-01, 2.81732557e-01, 2.20310533e-01, 1.58001396e-01,
9.50560433e-02, 3.17279335e-02, -3.17279335e-02, -9.50560433e-02,
-1.58001396e-01, -2.20310533e-01, -2.81732557e-01, -3.42020143e-01,
-4.00930535e-01, -4.58226522e-01, -5.13677392e-01, -5.67059864e-01,
-6.18158986e-01, -6.66769001e-01, -7.12694171e-01, -7.55749574e-01,
-7.95761841e-01, -8.32569855e-01, -8.66025404e-01, -8.95993774e-01,
-9.22354294e-01, -9.45000819e-01, -9.63842159e-01, -9.78802446e-01,
-9.89821442e-01, -9.96854776e-01, -9.99874128e-01, -9.98867339e-01,
-9.93838464e-01, -9.84807753e-01, -9.71811568e-01, -9.54902241e-01,
-9.34147860e-01, -9.09631995e-01, -8.81453363e-01, -8.49725430e-01,
-8.14575952e-01, -7.76146464e-01, -7.34591709e-01, -6.90079011e-01,
-6.42787610e-01, -5.92907929e-01, -5.40640817e-01, -4.86196736e-01,
-4.29794912e-01, -3.71662456e-01, -3.12033446e-01, -2.51147987e-01,
-1.89251244e-01, -1.26592454e-01, -6.34239197e-02, -2.44929360e-16])
打印Arrays
打印数组时,NumPy 以类似于嵌套列表的方式显示它,但具有以下布局:
最后一个轴从左到右打印,
倒数第二个从上到下打印,
其余部分也从上到下打印,每个切片与下一个切片之间用空行隔开。
然后将一维数组打印为行,将二维数组打印为矩阵,将三维数组打印为矩阵列表。
>>> a = np.arange(6) # 一维
>>> print(a)
[0 1 2 3 4 5]
>>>
>>> b = np.arange(12).reshape(4, 3) # 二维
>>> print(b)
[[ 0 1 2]
[ 3 4 5]
[ 6 7 8]
[ 9 10 11]]
>>>
>>> c = np.arange(24).reshape(2, 3, 4) # 三维
>>> print(c)
[[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
[[12 13 14 15]
[16 17 18 19]
[20 21 22 23]]]
有关reshape的更多详细信息,请参阅下文。
如果数组太大而无法打印,NumPy 会自动跳过数组的中心部分,只打印角:
>>> print(np.arange(10000))
[ 0 1 2 ... 9997 9998 9999]
>>>
>>> print(np.arange(10000).reshape(100, 100))
[[ 0 1 2 ... 97 98 99]
[ 100 101 102 ... 197 198 199]
[ 200 201 202 ... 297 298 299]
...
[9700 9701 9702 ... 9797 9798 9799]
[9800 9801 9802 ... 9897 9898 9899]
[9900 9901 9902 ... 9997 9998 9999]]
要禁用此行为并强制 NumPy 打印整个阵列,可以使用set_printoptions更改打印选项。
>>> import sys
>>> import numpy as np
>>> print(sys.maxsize)
9223372036854775807
>>> np.set_printoptions(threshold=sys.maxsize)
基本操作
数组上的算术运算符按元素逐个应用,创建一个新数组并用结果填充。
>>> a = np.array([20, 30, 40, 50])
>>> b = np.arange(4)
>>> b
array([0, 1, 2, 3])
>>> c = a - b
>>> c
array([20, 29, 38, 47])
>>> b**2
array([0, 1, 4, 9])
>>> 10 * np.sin(a)
array([ 9.12945251, -9.88031624, 7.4511316 , -2.62374854])
>>> a < 35
array([ True, True, False, False])
与许多矩阵语言不同,乘积运算符 * 在 NumPy 数组中按元素进行操作。 矩阵乘积可以使用 @ 运算符(在 python >=3.5 中)或 dot 函数或方法来执行:
>>> A = np.array([[1, 1], [0, 1]])
>>> B = np.array([[2, 0], [3, 4]])
>>> A * B # 元素乘积
array([[2, 0],
[0, 4]])
>>> A @ B # 矩阵积
array([[5, 4],
[3, 4]])
>>> A.dot(B) # 矩阵积
array([[5, 4],
[3, 4]])
某些操作,例如 += 和 *=,会在适当的位置修改现有数组,而不是创建新数组。
>>> rg = np.random.default_rng(1) # 创建默认随机数生成器的实例
>>> a = np.ones((2, 3), dtype=int)
>>> b = rg.random((2, 3))
>>> a *= 3
>>> a
array([[3, 3, 3],
[3, 3, 3]])
>>> b += a
>>> b
array([[3.51182162, 3.9504637 , 3.14415961],
[3.94864945, 3.31183145, 3.42332645]])
>>> a += b # b 不会自动转换为整数类型
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
numpy.core._exceptions.UFuncTypeError: Cannot cast ufunc 'add' output from dtype('float64') to dtype('int32') with casting rule 'same_kind'
当处理不同类型的数组时,生成的数组的类型对应于更通用或更精确的数组类型(这种行为称为 upcasting,向上转型)
>>> import math
>>> a = np.ones(3, dtype=np.int32)
>>> b = np.linspace(0, math.pi, 3)
>>> b.dtype.name
'float64'
>>> c = a + b
>>> c
array([1. , 2.57079633, 4.14159265])
>>> c.dtype.name
'float64'
>>> d = np.exp(c * 1j)
>>> d
array([ 0.54030231+0.84147098j, -0.84147098+0.54030231j,
-0.54030231-0.84147098j])
>>> d.dtype.name
'complex128'
许多一元运算,例如计算数组中所有元素的总和,都是作为 ndarray 类的方法实现的。
>>> a = rg.random((2, 3))
>>> a
array([[0.82770259, 0.40919914, 0.54959369],
[0.02755911, 0.75351311, 0.53814331]])
>>> a.sum()
3.1057109529998157
>>> a.min()
0.027559113243068367
>>> a.max()
0.8277025938204418
默认情况下,这些操作适用于数组,就好像它是一个数字列表一样,无论其形状如何。 但是,通过指定轴参数,您可以沿数组的指定轴应用操作:
>>> b = np.arange(12).reshape(3, 4)
>>> b
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
>>> b.sum(axis=0) # 每列的总和
array([12, 15, 18, 21])
>>>
>>> b.min(axis=1) # 每行的最小值
array([0, 4, 8])
>>> b.cumsum(axis=1) # 沿每一行的累积总和
array([[ 0, 1, 3, 6],
[ 4, 9, 15, 22],
[ 8, 17, 27, 38]])
通用函数
NumPy 提供了熟悉的数学函数,例如 sin、cos 和 exp。 在 NumPy 中,这些被称为“通用函数”(ufunc)。 在 NumPy 中,这些函数对数组进行元素操作,生成一个数组作为输出。
>>> B = np.arange(3)
>>> B
array([0, 1, 2])
>>> np.exp(B) # e的矩阵元素次幂
array([1. , 2.71828183, 7.3890561 ])
>>> np.sqrt(B) # 矩阵元素开平方根
array([0. , 1. , 1.41421356])
>>> C = np.array([2., -1., 4.])
>>> np.add(B, C) # 矩阵元素求和
array([2., 0., 6.])
通用函数包括:
exp | sqrt | add | all | any |
apply_along_axis | argmax | argmin | argsort | average |
bincount | ceil | clip | conj | corrcoef |
cov | cross | cumprod | cumsum | diff |
dot | floor | inner | invert | lexsort |
max | maximum | mean | median | min |
minimum | nonzero | outer | prod | re |
round | sort | std | sum | trace |
transpose | var | vdot | vectorize | where |
索引、切片和迭代
一维数组可以被索引,切片和迭代,就像列表和其他Python序列一样。
>>> a = np.arange(10)**3
>>> a
array([ 0, 1, 8, 27, 64, 125, 216, 343, 512, 729], dtype=int32)
>>> a[2]
8
>>> a[2:5]
array([ 8, 27, 64], dtype=int32)
>>># 下面切片相当于a[0:6:2]
>>># 从0索引到6索引,每第2个元素为1000
>>> a[:6:2] = 1000
>>> a
array([1000, 1, 1000, 27, 1000, 125, 216, 343, 512, 729],
dtype=int32)
>>> a[::-1] # a翻转
array([ 729, 512, 343, 216, 125, 1000, 27, 1000, 1, 1000],
dtype=int32)
>>> for i in a:
... print(i**(1 / 3.))
...
9.999999999999998
1.0
9.999999999999998
3.0
9.999999999999998
5.0
5.999999999999999
6.999999999999999
7.999999999999999
8.999999999999998