第4章 NumPy基础:数组和矢量计算(Part2)
目录
NumPy的ndarray:一种多维数组对象
基本的索引和切片
一维数组
一维数组的索引和切片方式跟Python列表的差不多:
#code
data = np.arange(10)
print(data[4:7])
#output
[4 5 6]
比较有趣的是数组的赋值操作。当你将一个标量值赋值给一个切片时(如data[4 : 7] = 10),该值会自动传播到整个选区,这也是后面板块会讲到的广播:
#code
data = np.arange(10)
data[4:7] = 10
print(data)
#output
[ 0 1 2 3 10 10 10 7 8 9]
这是数组跟列表很重要的区别:数组切片是原始数组的视图。这意味着数据不会被复制,视图上的任何修改都会直接反映到源数组上(学到这里我已经震惊了:是我见识太少了没错,Python的功能强大程度远超我的想象)。这么做的原因是:NumPy的设计目的是处理大数据,假如NumPy坚持要将数据复制来复制去的话会产生何等的性能和内存问题。
当然,如果你想要得到的是ndarray切片的副本而非视图,就需要显式地进行复制操作:
#code
data = np.arange(10)
ans = data[4:7].copy()
ans[:] = 10
print(data)
#output
[0 1 2 3 4 5 6 7 8 9]
高维数组的元素索引
这里就拿二维数组来举例。在一个二维数组中,各索引位置上的元素不再是标量而是一维数组:
#code
arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(arr2d[1])
#output
[4 5 6]
想要选取单个元素,有以下两种等价的方式:
#code
print(arr2d[0, 2])
#output
3
#code
print(arr2d[0][2])
#output
3
标量值和数组都可以被赋值给选取出来的元素:
#code
arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
old_values = arr2d[0].copy()
arr2d[0] = 10
print(arr2d)
#output
[[10 10 10]
[ 4 5 6]
[ 7 8 9]]
#code
arr2d[0] = old_values
print(arr2d)
#output
[[1 2 3]
[4 5 6]
[7 8 9]]
高维数组的切片索引
高维度对象的花样更多,你可以在一个或多个轴上进行切片,也可以跟整数索引混合用。以上面那个二维数组arr2d为例:
#code
arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(arr2d[:2])
#output
[[1 2 3]
[4 5 6]]
可以看出,它是沿着第一个轴切片的。你可以一次传入多个切片,就像传入多个索引一样:
#code
print(arr2d[:2, 1:])
#output
[[2 3]
[5 6]]
通过将整数索引和切片混合,可以得到低维度的切片:
#code
print(arr2d[1, 1:])
#output
[5 6]
同样,':'表示选取整个轴,因此可以如下操作对高维轴进行切片:
#code
print(arr2d[:, 1:])
#output
[[2 3]
[5 6]
[8 9]]
自然,对切片表达式的赋值操作也会被扩散到整个选区:
#code
arr2d[:2, 1:] = 0
print(arr2d)
#output
[[1 0 0]
[4 0 0]
[7 8 9]]
布尔型索引
假设我们有一个用于储存数据的二维数组data以及一个储存姓名的一维数组names(含有重复项):
names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
data = np.random.randn(7, 4)
In [26]: data
Out[26]:
array([[ 0.95767999, 1.97872162, -0.44376553, 0.26202612],
[-0.23594696, -0.50043924, -0.67361123, 0.82789564],
[ 0.07433574, 0.15784162, 0.45202428, 0.32563969],
[-0.10919343, -0.32956531, 0.6756846 , -2.07484414],
[ 1.6630715 , 0.67667198, 0.0328173 , -0.11540049],
[ 0.03126675, -1.21538997, -1.77649457, 1.0338667 ],
[-0.26550671, 1.21940716, -1.61525318, 1.35356281]])
这里data是用numpy.random中的randn函数生成的一些正态分布的随机数据。假设每个名字都对应data数组中的一行,而我们想要选出对应于名字“Bob”的所有行。跟算数运算一样,数组的比较运算(如==)也是矢量化的。因此,对names和字符串“Bob”的比较运算将会产生一个布尔型数组:
#code
print(names == 'Bob')
#output
[ True False False True False False False]
这个布尔数组可以用于数组索引(很奇妙吧):
#code
print(data[names == 'Bob'])
#output
[[ 0.95767999 1.97872162 -0.44376553 0.26202612]
[-0.10919343 -0.32956531 0.6756846 -2.07484414]]
得到的这个二维数组,就是data中布尔数组里面为True的索引对应的行组成的。布尔型数组的长度必须跟被索引的轴长度一致。此外,还可以将布尔型数组跟切片、整数(或整数序列,稍后将对此进行详细讲解)混合使用:
#code
print(data[names == 'Bob', :2])
#output
[[ 0.95767999 1.97872162]
[-0.10919343 -0.32956531]]
选取这三个名字中的两个需要组合应用多个布尔条件,使用&(和)、|(或)之类的布尔算数运算符即可(注意,Python关键字and和or在布尔型数组中无效):
#code
mask = (names == 'Bob') | (names == 'Will')
print(data[mask])
#output
[[ 0.95767999 1.97872162 -0.44376553 0.26202612]
[ 0.07433574 0.15784162 0.45202428 0.32563969]
[-0.10919343 -0.32956531 0.6756846 -2.07484414]
[ 1.6630715 0.67667198 0.0328173 -0.11540049]]
通过布尔型索引选取数组中的数据,将总是创建数据的副本,即使返回一模一样的数组也是如此(注意与之前的索引和切片区别):
#code
names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
data = np.random.randn(7, 4)
ans = data[names == 'Bob']
ans[:, :] = 0
print(data)
#output
[[ 0.80367776 0.0955801 -0.8991827 1.63985326]
[ 0.25019002 -1.83505021 0.90092997 -0.45308332]
[-1.56889639 -1.29360906 0.66410176 0.12017974]
[-0.26567149 0.43198936 -1.55376408 -0.51943541]
[ 0.55282885 -0.76275888 0.85140759 -3.32069988]
[ 1.71659377 -1.89047417 -1.08932519 -0.09156839]
[ 0.56066904 -2.77500668 0.83531159 -0.97622234]]
通过布尔型数组设置值是一种经常用到的手段。为了将data中的所有负值都设置为0,我们只需:
#code
data[data < 0] = 0
print(data)
#output
[[0. 0.42708043 0.90044892 0. ]
[0.25668067 0.34328295 0.25563744 0.29277493]
[0. 0. 1.27998902 1.12231444]
[0. 0.63800488 0. 0. ]
[0.7463506 0. 0. 0.0793762 ]
[0. 0.65730168 0. 0. ]
[0. 0. 0. 0.19658698]]
也可以通过一维布尔数组设置行或列的值:
#code
data[names != 'Joe'] = 7
print(data)
#output
[[ 7. 7. 7. 7. ]
[-0.78648465 1.33055945 0.45142084 0.34568422]
[ 7. 7. 7. 7. ]
[ 7. 7. 7. 7. ]
[ 7. 7. 7. 7. ]
[ 1.18827119 0.9305965 0.86195534 -0.45259296]
[ 1.31648937 0.84809778 -0.37054247 1.30075392]]
花式索引
花式索引(Fancy indexing)是一个NumPy术语,它指的是利用整数数组进行索引。我们有一8 x 4数组:
arr = np.empty((8, 4))
for i in range(8):
arr[i] = i
现在要以特定顺序选取行子集,只需传入一个用于指定顺序的整数列表或ndarray即可:
#code
print(arr[[4, 3, 0, 2]])
#output
[[4. 4. 4. 4.]
[3. 3. 3. 3.]
[0. 0. 0. 0.]
[2. 2. 2. 2.]]
而使用负数索引将会从末尾开始选取行:
#code
print(arr[[-4, -1, -2]])
#output
[[4. 4. 4. 4.]
[7. 7. 7. 7.]
[6. 6. 6. 6.]]
一次传入多个索引组会有点特别:
#code
arr = np.arange(32).reshape((8, 4))
print(arr)
print()
print(arr[[3, 5, 6, 1], [3, 2, 0, 1]])
#output
[[ 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]]
[15 22 24 5]
我们发现它返回的是一个一维数组,其中的元素对应各个索引元组:15,22,24,5分别位于原二维数组中的(3,3),(5,2),(6,0),(1,1)。
这样的结果和我们的预期其实并不太一样,最开始这样写是想要选取矩阵的行列子集才对,答案应该是矩阵区域的形式才对。那我们应该怎么操作呢?如下:
#code
print(arr[[3, 5, 6, 1]][:, [2, 1, 0]])
#output
[[14 13 12]
[22 21 20]
[26 25 24]
[ 6 5 4]]
这里的第一维很好理解:我要取3,5,6,1这四行。第二维稍微有些困难:逗号前的‘:’表示对于第一维选取的所有行,逗号之后的数组表示让这些行内部按照2,1,0列的顺序排列。
而如果我这样写:
print(arr[[3, 5, 6, 1]][[2, 1, 0]])
得到的结果是arr[[3, 5, 6, 1]]这个数组的2,1,0行组成的数组:
#output
[[24 25 26 27]
[20 21 22 23]
[12 13 14 15]]
np.ix_函数
另一种选取矩阵行列子集的办法是使用np.ix_函数,它可以将两个一维整数数组转换为一个用于选取方形区域的索引器:
#code
print(arr[np.ix_([3, 5, 6, 1], [2, 1, 0])])
#output
[[14 13 12]
[22 21 20]
[26 25 24]
[ 6 5 4]]
注意,花式索引跟切片不同,它总是将数据复制到新数组中。我们甚至不可以通过花式索引对区域进行赋值,而布尔型索引可以。
数组转置和轴对换
transpose方法
转置(transpose)是重塑的一种特殊形式,他返回的是源数据的视图(不会进行任何复制操作)。对于高维数组,transpose需要得到一个由轴编号组成的元组才能对这些轴进行转置。假设我们有如下2 x 2 x 4的数组:
#code
arr = np.arange(16).reshape((2, 2, 4))
print(arr)
#output
[[[ 0 1 2 3]
[ 4 5 6 7]]
[[ 8 9 10 11]
[12 13 14 15]]]
我们对其进行转置,交换第一维和第三维:
#code
print(arr.transpose((2, 1, 0)))
#output
[[[ 0 8]
[ 4 12]]
[[ 1 9]
[ 5 13]]
[[ 2 10]
[ 6 14]]
[[ 3 11]
[ 7 15]]]
如果真的想弄明白transpose是如何变换的,建议画个三维的图来模拟一下。《总之就是非常抽象》
.T
T属性是transpose方法中的一个特殊属性:
In [97]: arr = np.arange(6).reshape((2,3))
In [98]: arr
Out[98]:
array([[0, 1, 2],
[3, 4, 5]])
In [99]: arr.T
Out[99]:
array([[0, 3],
[1, 4],
[2, 5]])
作者说这个属性可以用于计算矩阵内积。但由于我并不知道什么是矩阵内积,所以这一部分我就不装腔作势地记笔记了。
通用函数:快速的元素级数组函数
通用函数(即ufunc)是一种对ndarray中的数据执行元素级运算的函数。你可以将其看作简单函数(接受一个或多个标量值,并产生一个或多个标量值)的矢量化包装器。通俗一点来说,以前的简单函数是对一个或几个元素进行操作,而现在的ufunc可以对一个或几个数组内的元素进行相同的操作(矢量化)。
接受一个数组的为一元(unary)function,接受两个数组的被称为二元(binary)ufunc。这里列出了一些一元ufunc:
和一些二元ufunc:
sqrt函数
计算数组内每个元素的算术平方根
#code
arr = np.arange(5)
print(np.sqrt(arr))
#output
[0. 1. 1.41421356 1.73205081 2. ]
exp函数
返回对应的e的幂次方组成的数组
#code
arr = np.arange(5)
print(np.exp(arr))
#output
[ 1. 2.71828183 7.3890561 20.08553692 54.59815003]
maximum函数
返回两个数组中对应位置上较大值组成的数组
#code
x = np.random.randn(8)
y = np.random.randn(8)
print(x)
print(y)
print(np.maximum(x, y))
#output
[ 0.8542218 0.15467269 -0.25069745 -0.02257362 -0.43170026 -0.37563768 -0.31284188 -0.17468371]
[ 1.46811131 0.99628472 0.31656146 1.09026832 -0.88662433 0.30242216 0.66771456 0.07684679]
[ 1.46811131 0.99628472 0.31656146 1.09026832 -0.43170026 0.30242216 0.66771456 0.07684679]