标准的 Python 实现是用 C 语言编写的。这意味着每一个 Python 对象都是一个聪明的伪 C 语言结构体,该结构体不仅包含其值,还有其他信息。例如:当我们在 Python 中定义一个整型,例如:x = 10000 时,x 并不是一个 “原生” 整型,而是一个指针,指向一个 C 语言的复合结构体,结构体里包含了一些值。
查看 Python 源代码,可以发现整型(长整型)的定义,如下:
struct _longobject{
long ob_refcnt; //一个引用计数,它帮助 Python 默默地处理内存的分配和回收
PyTypeObject *ob_type; //将变量的类型编码
size_t ob_size; //指定接下来的数据成员的大小
long ob_digit[1]; //包含我们希望 Python 变量表示的实际整型值
};
差异:C 语言整型本质上是对某个内存位置的标签,里面存储的字节会编码成整型。而 Python 的整型其实是一个指针,指向包含这个 Python 对象所有信息的某个内存位置,其中包括可以转换成整型的字节。由于 Python 的整型结构体里面还包含了大量额外的信息,所以 Python 可以自由、动态地编码。
Python 中的标准可变多元素容器是列表。因为 Python 的动态特性,甚至可以创建一个异构的列表:
L = [True,'2',3.0,4]
print([type(item) for item in L])
[<class 'bool'>, <class 'str'>, <class 'float'>, <class 'int'>]
为了获得这些灵活的类型,列表中的每一项必须包含各自的类型信息,引用类型和其他信息;也就是说,每一项都是一个完整的 Python 对象。
来看一个特殊的例子,如果列表中的所有变量都是同一类型的,那么很多信息都会是多余的——将数据存储在固定类型的数组中应该会更高效。动态类型的列表和固定类型的(NumPy 式)数组间的区别就在于此。
在实现层面,数组基本上包含一个指向连续数据块的指针。另一方面,Python 列表包含一个指向指针块的指针,这其中的每一个指针对应一个完整的 Python 对象。列表的优势是灵活,因为列表中的元素可以用任何类型的数据填充。固定类型的 NumPy 式数组缺乏这种灵活性,但是能更有效地存储和操作数据。
Python 提供了几种将数据存储在有效的、固定类型的数据缓存中的选项。内置的数组(array)模块可以用于创建统一类型的密集数组:
import array
L = list(range(10))
A = array.array('i',L) #这里的 'i' 是一个数据类型码,表示数据为整型
array('i', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
Python 的数组对象提供了数组型数据的有效存储,而 NumPy 为该数据加上了高效的操作。
从用 np 别名导入 NumPy 的标准做法:
import numpy as np
首先,可以用 np.array 从 Python 列表创建数组:
np.array([1,4,2,5,3])
[1,4,2,5,3]
NumPy 要求数组必须包含同一类型的数据。如果类型不匹配,NumPy 将会向上转型。如:
np.array([3.14,4,2,3])
[3.14 4. 2. 3. ]
如果希望明确数组的数据类型,可以用 dtype 关键字:
np.array([1,2,3,4],dtype='float32')
[1. 2. 3. 4.]
NumPy 数组可以被指定为多维的。以下是用列表的列表初始化多维数组的一种方法:
#嵌套列表构成的多维数组
np.array([range(i,i+3) for i in [2,4,6]])
[[2 3 4]
[4 5 6]
[6 7 8]]
面对大型数组的时候,用 NumPy 内置的方法从头创建数组是一种更高效的方法。
#创建一个长度为 10 的数组,数组的元素都是 0
np.zeros(10,dtype=int)
[0 0 0 0 0 0 0 0 0 0]
np.ones((3,5),dtype=float)
[[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]]
#创建一个 3x5 的浮点型数组,数组的值都是 3.14
print(np.full((3,5),3.14))
[[3.14 3.14 3.14 3.14 3.14]
[3.14 3.14 3.14 3.14 3.14]
[3.14 3.14 3.14 3.14 3.14]]
#创建一个线性序列数组
#从 0 开始,到 20 结束,步长为 2
#(它和内置的 range() 函数类似)
print(np.arange(0,20,2))
[ 0 2 4 6 8 10 12 14 16 18]
#创建一个 5 个元素的数组,这 5 个数均匀地分布到 0~1
print(np.linspace(0,1,5))
[0. 0.25 0.5 0.75 1. ]
#创建一个 3x3 的、在 0~1 均匀分布的随机数组成的数组
print(np.random.random((3,3)))
[[0.886326 0.93619859 0.59884604]
[0.20913196 0.15508227 0.01782209]
[0.20183124 0.5761326 0.19984512]]
#创建一个 3x3 的、均值为 0、标准差为 1 的正态分布的随机数数组
print(np.random.normal(0,1,(3,3)))
[[ 1.22672264 -1.25538307 -0.13381821]
[ 1.18173765 0.95377135 0.52868231]
[ 0.0503752 0.84407108 -0.06514662]]
#创建一个 3x3 的、[0,10) 区间的随机整型数组
print(np.random.randint(0,10,(3,3)))
[[2 9 6]
[2 1 3]
[2 0 5]]
#创建一个 3x3 的单位矩阵
print(np.eye(3))
[[1. 0. 0.]
[0. 1. 0.]
[0. 0. 1.]]
#创建一个由 3 个整型数组成的未初始化的数组
#数组的值是内存空间中的任意值
print(np.empty(3))
[1. 1. 1.]
定义三个随机数组:一个一维数组、一个二维数组和一个三维数组。我们将用 NumPy 的随机数生成器设置一组种子值,以确保每次程序执行时都可以生成同样的随机数组:
np.random.seed(0) #设置种子随机数
x1 = np.random.randint(10,size=6) #一维数组
x2 = np.random.randint(10,size=(3,4)) #二维数组
x3 = np.random.randint(10,size=(3,4,5)) #三维数组
每个数组都有 ndim(数组的维度)、shape(数组每个维度的大小)和 size(数组的总大小)属性;
print("x3 ndim:",x3.ndim) #数组的维度 x3 ndim: 3
print("x3 shape:",x3.shape) #数组每个维度的大小 x3 shape: (3, 4, 5)
print("x3 size:",x3.size) #数组的总大小 x3 size: 60
另外一个有用的属性是 dtype,它是数组的数据类型:
print("dtype:",x3.dtype) #数组的数据类型 dtype: int32
其他的属性包括每个数组元素字节大小的 itemsize,以及表示数组粽总字节大小的属性 nbytes:
print("itemsize:",x3.itemsize,"bytes") #每个数组元素字节大小 itemsize: 4 bytes
print("nbytes:",x3.nbytes,"bytes") #数组总字节大小 nbytes: 240 bytes
一般来说,可以认为 nbytes = itemsize * size。
在一维数组中,你可以通过中括号指定索引获取第 i 个值(从 0 开始计数):
print(x1) #[5 0 3 3 7 9]
print(x1[0]) #5
print(x1[4]) #7
为了获取数组的末尾索引,可以使用负值索引:
print(x1[-1]) #9
print(x1[-2]) #7
在多维数组中,可以用逗号分隔的索引元组获取元素:
print(x2)
'''
[[3 5 2 4]
[7 6 8 8]
[1 6 7 7]]
'''
print(x2[0,0]) #3
print(x2[2,0]) #1
print(x2[2,-1]) #7
也可以使用上述方式修改元素值:
x2[0,0] = 12
print(x2)
'''
[[12 5 2 4]
[ 7 6 8 8]
[ 1 6 7 7]]
'''
切片符号用冒号(:)表示。为了获取数组 x 的一个切片,可以用以下方式:
x[start:end:step]
如果以上 3 个参数都未被指定,那么它们会被分别设置成默认值 start = 0,end = 维度的大小 和 step = 1。
一维数组
x = np.arange(10)
print(x) #[0 1 2 3 4 5 6 7 8 9]
print(x[:5]) #前五个元素 [0 1 2 3 4]
print(x[5:]) #索引 5 之后的元素 [5 6 7 8 9]
print(x[4:7]) #中间的子数组 [4 5 6]
print(x[::2]) #每隔一个元素 [0 2 4 6 8]
print(x[1::2]) #每隔一个元素,从索引 1 开始 [1 3 5 7 9]
逆序数组的一种方法:
print(x[::-1]) #所有元素,逆序 [9 8 7 6 5 4 3 2 1 0]
print(x[5::-2]) #从索引 5 开始每隔一个元素逆序 [5 3 1]
多维数组
print(x2)
'''
[[12 5 2 4]
[ 7 6 8 8]
[ 1 6 7 7]]
'''
print(x2[:2,:3]) #两行 三列
'''
[[12 5 2]
[ 7 6 8]]
'''
print(x2[:3,::2]) #所有行,每隔一列
'''
[[12 2]
[ 7 8]
[ 1 7]]
'''
同样也可以逆序:
print(x2[::-1,::-1]) #逆序
'''
[[ 7 7 6 1]
[ 8 8 6 7]
[ 4 2 5 12]]
'''
获取数组的行和列
一种常见的需求是获取数组的单行与单列。用一个冒号(:)表示空切片。
print(x2[:,0]) #x2 的第一列 [12 7 1]
print(x2[0,:]) #x2 的第一行 [12 5 2 4]
获取行时,出于语法的简洁考虑,可以省略空的切片:
print(x2[0]) #等于 x2[0,:] [12 5 2 4]
非副本视图的子数组
数组切片返回的是数组数据的视图,而不是数值数据的副本。
print(x2)
'''
[[12 5 2 4]
[ 7 6 8 8]
[ 1 6 7 7]]
'''
x2_sub = x2[:2,:2]
print(x2_sub)
'''
[[12 5]
[ 7 6]]
'''
现在如果修改这个数组,将会看到原始的数组也被修改了。结果如下:
x2_sub[0,0] = 99
print(x2_sub)
'''
[[99 5]
[ 7 6]]
'''
print(x2)
'''
[[99 5 2 4]
[ 7 6 8 8]
[ 1 6 7 7]]
'''
它意味着在处理非常大的数据集时,可以获取或处理这些数据集的片段,而不用复制底层的数据缓存。
创建数组的副本
可以简单地通过 copy() 来实现。
x2_sub_copy = x2[:2,:2].copy()
print(x2_sub_copy)
'''
[[99 5]
[ 7 6]]
'''
如果修改这个子数组,原始的数组不会被改变:
x2_sub_copy[0,0] = 42
print(x2_sub_copy)
'''
[[42 5]
[ 7 6]]
'''
print(x2)
'''
[[99 5 2 4]
[ 7 6 8 8]
[ 1 6 7 7]]
'''
通过 reshape() 函数来实现。
例如:如果你希望将数字 1~9 放入一个 3 × 3 的矩阵中,可以采用如下方法:
grid = np.arange(1,10).reshape((3,3))
print(grid)
'''
[[1 2 3]
[4 5 6]
[7 8 9]]
'''
另外一个常见的变形模式是将一个一维数组转变为二维的行或列的矩阵。你可以通过 reshape 方法来实现,或者更简单地在一个切片操作中利用 newaxis 关键字:
x = np.array([1,2,3])
print(x.reshape((1,3))) #通过变形获得行向量 [[1 2 3]]
#通过 newaxis 获得的行向量
print(x[np.newaxis,:]) #[[1 2 3]]
print(x.reshape((3,1)))
'''
[[1]
[2]
[3]]
'''
print(x[:,np.newaxis])
'''
[[1]
[2]
[3]]
'''
数组的拼接
拼接或连接 NumPy 中的两个数组主要由 np.concatenate、np.vstack、np.hstack 例程实现。
x = np.array([1,2,3])
y = np.array([3,2,1])
print(np.concatenate([x,y])) #[1 2 3 3 2 1]
np.concatenate 也可以用于二维数组的拼接:
grid = np.array([[1,2,3],
[4,5,6]])
#沿着第一个轴拼接
print(np.concatenate([grid,grid]))
'''
[[1 2 3]
[4 5 6]
[1 2 3]
[4 5 6]]
'''
#沿第二个轴拼接
print(np.concatenate([grid,grid],axis=1))
'''
[[1 2 3 1 2 3]
[4 5 6 4 5 6]]
'''
沿着固定维度处理数组时,使用 np.vstack(垂直栈)和 np.hstack(水平栈)函数会更简洁:
x = np.array([1,2,3])
grid = np.array([[9,8,7]
[6,5,4]])
#垂直栈数组
print(np.vstack([x,grid]))
'''
[[1 2 3]
[9 8 7]
[6 5 4]]
'''
#水平栈
y = np.array([[99],
[99]])
print(np.hstack([grid,y]))
'''
[[ 9 8 7 99]
[ 6 5 4 99]]
'''
数组的分裂
分裂可以使用 np.split、np.hsplit 和 np.vsplit 函数来实现。可以向以上函数传递一个索引列表作为参数,索引列表记录的是分裂点位置:
x = [1,2,3,99,99,3,2,1]
x1,x2,x3 = np.split(x,[3,5])
print(x1,x2,x3) #[1 2 3] [99 99] [3 2 1]
注意:N 分裂将会得到 N+1 个子数组。
相关的 np.hsplit 和 np.vsplit 函数用法也是一样:
grid = np.arange(16).reshape((4,4))
print(grid)
'''
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]
[12 13 14 15]]
'''
upper,lower = np.vsplit(grid,[2])
print(upper)
'''
[[0 1 2 3]
[4 5 6 7]]
'''
print(lower)
'''
[[ 8 9 10 11]
[12 13 14 15]]
'''
left,right = np.hsplit(grid,[2])
print(left)
'''
[[ 0 1]
[ 4 5]
[ 8 9]
[12 13]]
'''
print(right)
'''
[[ 2 3]
[ 6 7]
[10 11]
[14 15]]
'''
NumPy 为很多类型的操作提供了非常方便的、静态类型的、可编译程序的接口,也被称作向量操作。
NumPy 中的向量操作的通过通用函数实现的。通用函数的主要目的是对 NumPy 数组中的值执行更快的重复操作。
通用函数有两种存在形式:一元通用函数对单个输入操作,二元通用函数对两个输入操作。
NumPy 通用函数的使用方式非常自然,因为它用到了 Python 原生的算术运算符,标准的加、减、乘、除都可以使用:
x = np.arange(4)
print("x =",x) # x = [0 1 2 3]
print("x + 5 =",x + 5) # x + 5 = [5 6 7 8]
print("x - 5 =",x - 5) # x - 5 = [-5 -4 -3 -2]
print("x * 2 =",x * 2) # x * 2 = [0 2 4 6]
print("x / 2 =",x / 2) # x / 2 = [0. 0.5 1. 1.5]
print("x // 2 =",x // 2) # x // 2 = [0 0 1 1]
还有求负数、** 表示的指数运算和 % 表示的求模运算的一元通用函数:
print("-x =",-x) # -x = [ 0 -1 -2 -3]
print("x ** 2 =",x ** 2) # x ** 2 = [0 1 4 9]
print("x % 2 =",x % 2) # x % 2 = [0 1 0 1]
所有这些算术运算符都是 NumPy 内置函数的简单封装器:
运算符 | 对应的通用函数 | 描述 |
---|---|---|
+ | np.add | 加法运算 |
- | np.subtract | 减法运算 |
- | np.negative | 负数运算 |
* | np.multiply | 乘法运算 |
/ | np.divide | 除法运算 |
// | np.floor_divide | 向下整除运算 |
** | np.power | 指数运算 |
% | np.mod | 模/取余 |
正如 NumPy 能理解 Python 内置的运算操作,NumPy 也可以理解 Python 内置的绝对值函数:
x = np.array([-2,-1,0,1,2])
print(abs(x)) # [2 1 0 1 2]
对应的通用函数是 np.absolute,该函数也可以用别名 np.abs 来访问:
print(np.absolute(x)) # [2 1 0 1 2]
print(np.abs(x)) # [2 1 0 1 2]
这个通用函数也可以用来处理复数。当处理复数时,绝对值返回的是该复数的模:
x = np.array([3-4j,4-3j,2+0j,0+1j])
print(np.abs(x)) #[5. 5. 2. 1.]
定义的角度数组:
theta = np.linspace(0,np.pi,3)
现在可以对这些值进行三角函数的计算:
print("theta =",theta) # theta = [0. 1.57079633 3.14159265]
print("sin(theta) =",np.sin(theta)) # sin(theta) = [0.0000000e+00 1.0000000e+00 1.2246468e-16]
print("cos(theta) =",np.cos(theta)) # cos(theta) = [ 1.000000e+00 6.123234e-17 -1.000000e+00]
print("tan(theta) =",np.tan(theta)) # tan(theta) = [ 0.00000000e+00 1.63312394e+16 -1.22464680e-16]
逆三角函数同样可以使用:
x = [-1,0,1]
print("x =",x) # x = [-1, 0, 1]
print("arcsin(x) =",np.arcsin(x)) # arcsin(x) = [-1.57079633 0. 1.57079633]
print("arccos(x) =",np.arccos(x)) # arccos(x) = [3.14159265 1.57079633 0. ]
print("arctan(x) =",np.arctan(x)) # arctan(x) = [-0.78539816 0. 0.78539816]
指数运算:
x = [-1,0,1]
print("x =",x) # x = [1, 2, 3]
print("arcsin(x) =",np.arcsin(x)) # e^x = [ 2.71828183 7.3890561 20.08553692]
print("arccos(x) =",np.arccos(x)) # 2^x = [2. 4. 8.]
print("arctan(x) =",np.arctan(x)) # 3^x = [ 3 9 27]
对数运算:
x = [1,2,4,10]
print("x =",x) # x = [1, 2, 4, 10]
print("ln(x) =",np.log(x)) # ln(x) = [0. 0.69314718 1.38629436 2.30258509]
print("log2(x) =",np.log2(x)) # log2(x) = [0. 1. 2. 3.32192809]
print("log10(x) =",np.log10(x)) # log10(x) = [0. 0.30103 0.60205999 1. ]
还有一些特殊的版本,对于非常小的输入值可以保持较好的精度:
x = [0,0.001,0.01,0.1]
print("exp(x) - 1 =",np.expm1(x)) # exp(x) - 1 = [0. 0.0010005 0.01005017 0.10517092]
print("log(1+x) =",np.log1p(x)) # log(1+x) = [0. 0.0009995 0.00995033 0.09531018]
所有的通用函数都可以通过 out 参数来指定计算结果的存放位置:
x = np.arange(5)
y = np.empty(5)
np.multiply(x,10,out=y)
print(y) # [ 0. 10. 20. 30. 40.]
这个特性也可以被用作数组视图,例如可以将计算结果写入指定数组的每隔一个元素的位置:
y = np.zeros(10)
np.power(2,x,out=y[::2])
print(y) # [ 1. 0. 2. 0. 4. 0. 8. 0. 16. 0.]
慎重使用 out 可以有效地节约内存。
有些聚合功能可以直接在对象上计算。例如,如果我们希望用一个特定的运算 reduce 一个数组,那么可以用任何通用函数的 reduce 方法。一个 reduce 方法会对给定的元素和操作重复执行,直至得到单个元素。
例如,对 add 通用函数调用 reduce 方法会返回数组中所有元素的和:
x = np.arange(1,6)
print(x) # [1 2 3 4 5]
print(np.add.reduce(x)) # 15
同样,对 multiply 通用函数调用 reduce 方法会返回数组中所有元素的乘积:
print(np.multiply.reduce(x)) # 120
如果需要存储每次计算的中间结果,可以使用 accumulate:
print(np.add.accumulate(x)) # [ 1 3 6 10 15]
print(np.multiply.accumulate(x)) # [ 1 2 6 24 120]
任何通用函数都可以用 outer 方法获得两个不同输入数组所有元素对的函数运算结果。这意味着,你可以用一行代码实现一个乘法表:
x = np.arange(1,6)
print(np.multiply.outer(x,x))
'''
[[ 1 2 3 4 5]
[ 2 4 6 8 10]
[ 3 6 9 12 15]
[ 4 8 12 16 20]
[ 5 10 15 20 25]]
'''
通用函数另外一个非常有用的特性是它能够操作不同大小和形状的数组,一组这样的操作被称作广播。
NumPy 的 sum 函数在编译码中执行操作,所以 NumPy 的操作计算得更快一些。
big_array = np.random.random(1000000)
print(np.sum(big_array)) # 500209.12067471276
print(np.max(big_array)) # 0.9999994392723005
print(np.min(big_array)) # 1.4057692298008462e-06
一种更简洁的语法形式是数组对象直接调用这些方法:
print(big_array.min(),big_array.max(),big_array.sum())
# 1.4057692298008462e-06 0.9999994392723005 500209.12067471276
一种常见的聚合操作是沿着一行或一列聚合。例如,假如你有一些数据存在二维数组上:
M = np.random.random((3,4))
print(M)
'''
[[0.0049466 0.25863997 0.62346477 0.90474173]
[0.71661557 0.699582 0.80401456 0.60471376]
[0.43905815 0.73525983 0.3703232 0.57361603]]
'''
默认情况下,每一个 NumPy 聚合函数将会返回对整个数组的聚合结果:
print(M.sum()) # 6.734976167538792
聚合函数还有一个参数,用于指定沿着哪条轴方向进行聚合。例如,可以通过指定 axis = 0 找到找到每一列的最小值:
print(M.min(axis = 0)) # [0.0049466 0.25863997 0.3703232 0.57361603]
axis 关键字指定的是数组将会被折叠的维度,而不是将要返回的维度。因此指定 axis = 0 意味着第一个轴将要被折叠——对于二维数组,这意味着每一列的值将被聚合。
函数名称 | NaN 安全版本 | 描述 |
---|---|---|
np.sum | np.nansum | 计算元素的和 |
np.prob | np.nanprob | 计算元素的积 |
np.mean | np.nanmmean | 计算元素的平均值 |
np.std | np.nanstd | 计算元素的标准差 |
np.var | np.nanvar | 计算元素的方差 |
np.min | np.nanmin | 找出最小值 |
np.max | np.nanmax | 找出最大值 |
np.argmin | np.nanargmin | 找出最小值的索引 |
np.argmax | np.nanargmax | 找出最大值的索引 |
np.median | np.nanmedia | 计算元素的中位数 |
np.percentile | np.nanpercentile | 计算基于元素排序的统计值 |
np.any | N/A | 验证是否存在元素为真 |
np.all | N/A | 验证所有元素是否为真 |
广播可以简单理解为用于不同大小数组的二元通用函数(加、减、乘等)的一组规则。
广播允许二元运算符可以用于不同大小的数组。例如,可以简单地将一个标量(可以认为是一个零维的数组)和一个数组相加。
a = np.array([0,1,2])
print(a + 5) # [5 6 7]
NumPy 的广播遵循一组严格的规则,设定这组规则是为了决定两个数组间的操作。
通用函数可以避免用户写一些很慢的 Python 循环。数组的归一化进一步扩展了这个功能。假设你有一个有 10 个观测值的数组,每个观测值包含 3 个数值。按照惯例,我们将用一个 10 × 3 的数组存放该数据:
X = np.random.random((10,3))
我们可以计算每个特征的均值,计算方法是利用 mean 函数沿着第一个维度聚合:
Xmean = X.mean(0)
print(Xmean) # [0.51985373 0.29768565 0.38901366]
现在通过从 X 数组的元素中减去这个均值实现归一化(该操作是一个广播操作):
X_centered = X - Xmean
为了进一步核对我们的处理是否正确,可以查看一下归一化的数组的均值是否接近 0:
print(X_centered.mean(0)) # [-1.11022302e-17 3.88578059e-17 0.00000000e+00]
我们希望定义一个函数 z = f(x,y),可以用广播沿着数值区间计算该函数:
# x 和 y 表示 0~50 个步长的序列
x = np.linspace(0,5,50)
y = np.linspace(0,5,50)[:,np.newaxis]
z = np.sin(x) ** 10 + np.cos(10 + y * x) * np.cos(x)
我们将用 Matplotlib 来画这个二维数组:
import matplotlib.pyplot as plt
plt.imshow(z,origin='lower',extent=[0,5,0,5],cmap='viridis')
plt.colorbar()
用掩码开查看和操作 NumPy 数组中的值。当你想基于某些准则来抽取、修改、计数或对一个数组中的值进行其他操作时,掩码就可以派上用场了。
NumPy 不仅实现了 +、-、×、/ 和其他一些运算符的数组逐个元素操作,还实现了如 < 和 > 的逐个元素的比较的通用函数。这些比较运算的结果是一个布尔数据类型的数组。一种有 6 种这种操作:
x = np.array([1,2,3,4,5])
print(x < 3) # [ True True False False False]
print(x > 3) # [False False False True True]
print(x <= 3) # [ True True True False False]
print(x >= 3) # [False False True True True]
print(x != 3) # [ True True False True True]
print(x == 3) # [False False True False False]
运算符 | 对应的通用函数 |
---|---|
== | np.equal |
!= | np.not_equal |
< | np.less |
<= | np.less_equal |
> | np.greater |
>= | np.greater_equal |
和算术运算通用函数一样,这些比较运算通用函数也可以用于任意形状、大小的数组。
首先给定一个二维数组:
x = np.array([[5,0,3,3],
[7,9,3,5],
[2,4,7,6]])
print(x)
如果要统计布尔数组中 True 记录的个数,可以使用 np.count_nonzero 函数:
#统计有多少值小于 6
print(np.count_nonzero(x < 6)) # 8
也可以使用 sum,使用 sum 的好处是可以和其他 NumPy 聚合函数一样,这个求和可以沿着行或列进行:
#每行有多少值小于 6
print(np.sum(x < 6,axis=1)) # [4 2 2]
如要快速检查任意或所有这些值是否为 True,可以用 np.any() 或 np.all() :
#有没有大于 8 的
print(np.any(x > 8)) # True
#是否所有的值都小于 10
print(np.all(x < 10)) # True
运算符 | 对应的通用函数 |
---|---|
& | np.bitwise_and |
| | np.bitwise_or |
^ | np.bitwise_xor |
~ | np.bitwise_not |
通过掩码选择数据的子数据集。例如,我们希望抽取出 x 矩阵中小于 5 的元素:
x = np.array([[5,0,3,3],
[7,9,3,5],
[2,4,7,6]])
如前面的方法,利用比较运算符可以得到一个布尔数组:
print(x < 5)
'''
[[False True True True]
[False False True False]
[ True True False False]]
'''
现在为了将这些值选出,可以进行简单的索引,即掩码操作:
print(x[x < 5]) # [0 3 3 3 2 4]
现在返回的是一个一维数组,它包含了所有满足条件的值。
它意味着传递一个数组来一次性获得多个数组元素。
rand = np.random.RandomState(42)
x = rand.randint(100,size=10)
print(x) # [51 92 14 71 60 20 82 86 74 74]
假设我们希望获得三个不同的元素,可以用下列方式实现:
print([x[3],x[7],x[2]]) # [71, 86, 14]
另一种方法是通过传递索引的单个列表或数组来获得同样的结果:
ind = [3,7,2]
print(x[ind]) # [71 86 14]
利用花哨的索引,结果的形状与索引数组的形状一致,而不是与被索引数组的形状一致:
ind = np.array([[3,7],
[4,5]])
print(x[ind])
'''
[[71 86]
[60 20]]
'''
花哨的索引也对多个维度适用。假设我们有以下数组:
X = np.arange(12).reshape((3,4))
print(X)
'''
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
'''
和标准索引一样,第一个索引指的是行,第二个索引指的是列:
row = np.array([0,1,2])
col = np.array([2,1,3])
print(X[row,col]) # [ 2 5 11]
print(X)
'''
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
'''
可以将花哨的索引与简单的索引相组合使用:
print(X[2,[2,0,1]]) # [10 8 9]
也可以将花哨的索引获和切片组合使用:
print(X[1:,[2,0,1]])
'''
[[ 6 4 5]
[10 8 9]]
'''
更可以将花哨的索引与掩码组合使用:
mask = np.array([1,0,1,0],dtype=bool)
print(X[row[:,np.newaxis],mask])
'''
[[ 0 2]
[ 4 6]
[ 8 10]]
'''
假设我们有一个索引数组,并且希望设置数组中对应的值:
x = np.arange(10)
i = np.array([2,1,8,4])
x[i] = 99
print(x) # [ 0 99 99 3 99 5 6 7 99 9]
可以用任何的赋值操作来实现:
x[i] -= 10
print(x) # [ 0 89 89 3 89 5 6 7 89 9]
不过操作重复的索引可能会产生一些出乎意料的结果:
x = np.zeros(10)
x[[0,0]] = [4,6]
print(x) # [6. 0. 0. 0. 0. 0. 0. 0. 0. 0.] 4 被覆盖了
i = [2,3,3,4,4,4]
x[i] += 1
print(x) # [6. 0. 1. 1. 1. 0. 0. 0. 0. 0.]
没有发生累加。改变的方法是使用 at() 方法:
x = np.zeros(10)
np.add.at(x,i,1)
print(x) # [0. 0. 1. 2. 3. 0. 0. 0. 0. 0.]
默认情况下,np.sort 的排序算法是快速排序。
如果想在不修改原始输入数组的基础上返回一个排好序的数组,可以用 np.sort:
x = np.array([2,1,4,3,5])
print(np.sort(x)) # [1 2 3 4 5]
如果希望用排好序的数组替代原始数组,可以使用数组的 sort 方法:
x.sort()
print(x) # [1 2 3 4 5]
另外一个相关的函数是 argsort,该函数返回的是原始数组排好序的索引值:
x = np.array([2,1,4,3,5])
i = np.argsort(x)
print(i) # [1 0 3 2 4]
这些索引可以被用于(通过花哨的索引)创建有序数组:
print(x[i]) # [1 2 3 4 5]
沿着行或列进行排序
NumPy 排序算法的一个有用的功能是通过 axis 参数,沿着多维数组的行或列进行排序:
rand = np.random.RandomState(42)
X = rand.randint(0,10,(4,6))
print(X)
'''
[[6 3 7 4 6 9]
[2 6 7 4 3 7]
[7 2 5 4 1 7]
[5 1 4 0 9 5]]
'''
对 X 的每一列进行排序:
print(np.sort(X,axis=0))
'''
[[2 1 4 0 1 5]
[5 2 5 4 3 7]
[6 3 7 4 6 7]
[7 6 7 4 9 9]]
'''
对 X 的每一行进行排序:
print(np.sort(X,axis=1))
'''
[[3 4 6 6 7 9]
[2 3 4 6 7 7]
[1 2 4 5 7 7]
[0 1 4 5 5 9]]
'''
有时候我们并不希望对整个数组进行排序,仅仅希望找到数组中第 K 小的值,NumPy 的 np.partition 函数提供了该功能。np.partition 函数输入的数组和数字 K,输出结果是一个新数组,最左边的是第 K 小的值,往右的任意顺序的其他值。
x = np.array([7,2,3,1,6,5,4])
print(np.partition(x,3)) # [2 1 3 4 6 5 7]
注意,结果数组中前三个值是数组中最小的三个值,剩下的位置是原始数组中剩下的值。在这两个分隔区中,元素都是任意排序的。
与排序类似,也可以沿着多维数组任意的轴进行分隔:
print(X)
'''
[[6 3 7 4 6 9]
[2 6 7 4 3 7]
[7 2 5 4 1 7]
[5 1 4 0 9 5]]
'''
print(np.partition(X,2,axis=1))
'''
[[3 4 6 7 6 9]
[2 3 4 7 6 7]
[1 2 4 5 7 7]
[0 1 4 5 9 5]]
'''
假定现在有关一些人的分类数据(如姓名、年龄和体重),现在需要存储这些数据用于 Python 项目。
通过指定复合数据类型,可以构造一个结构化数组:
data = np.zeros(4,dtype={
'names':('name','age','weight'),'formats':('U10','i4','f8')})
print(data.dtype)
# [('name', '
现在生成了一个空的数组容器,可以将列表数据放入数组中:
name = ['Alice','Bob','Cathy','Doug']
age = [25,45,37,19]
weight = [55.0,85.5,68.0,61.5]
data['name'] = name
data['age'] = age
data['weight'] = weight
print(data)
# [('Alice', 25, 55. ) ('Bob', 45, 85.5) ('Cathy', 37, 68. ) ('Doug', 19, 61.5)]
所有的数据都放在了一个内存块中。
结构化数组的方便之处在于,你可以通过索引值或名称查看相应的值:
#获取所有名字
print(data['name']) # ['Alice' 'Bob' 'Cathy' 'Doug']
#获取数据第一行
print(data[0]) # ('Alice', 25, 55.)
利用布尔掩码,还可以做一些更复杂的操作,如按照年龄进行筛选:
print(data[data['age'] < 30]['name']) # ['Alice' 'Doug']
结构化数组的数据类型有多种制定方式。此前我们看到的是采用字典的方法:
print(np.dtype({
'names':('name','age','weight'),
'formats':('U10','i4','f8')}))
# [('name', '
符合类型也可以是元组列表:
print(np.dtype([('name','S10'),('age','i4'),('weight','f8')]))
# [('name', 'S10'), ('age', '
NumPy 数据类型符号 | 描述 |
---|---|
‘b’ | 字节型 |
‘i’ | 有符号整型 |
‘u’ | 无符号整型 |
‘f’ | 浮点数 |
‘c’ | 复数浮点数 |
‘S’、‘a’ | 字符串 |
‘U’ | Unicode 编码字符串 |
‘V’ | 原生数据、raw data(空,void) |
你可以创建一种类型,其中每个元素都包含一个数组或矩阵。
tp = np.dtype([('id','i8'),('mat','f8',(3,3))])
X = np.zeros(1,dtype=tp)
print(X[0]) # (0, [[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]])
print(X['mat'][0])
'''
[[0. 0. 0.]
[0. 0. 0.]
[0. 0. 0.]]
'''
NumPy 还提供了 np.recarry 类。它和前面介绍的结构化数组几乎相同,但是它有一个独特第特征:域可以像属性一样获取,而不是像字典的键那样获取。前面的例子通过以下方式获取年龄:
print(data['age']) # [25 45 37 19]
如果将这些数据当做一个记录数组,我们可以用很少的按键获取这个结果:
data_rec = data.view(np.recarray)
print(data_rec.age) # [25 45 37 19]
记录数组的不好之处在于,即使使用同样的语法,在获取域时也会有一些额外的开销。