《利用python进行数据分析》第4章学习笔记(2)

本文详细介绍了NumPy中ndarray的索引和切片,包括一维和高维数组的基本操作,布尔型索引和花式索引的应用,以及数组的转置和轴对换。此外,还讲解了通用函数(ufunc)的概念,如sqrt、exp和maximum函数,这些函数能对数组元素执行快速的元素级运算。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第4章 NumPy基础:数组和矢量计算(Part2)

目录

第4章 NumPy基础:数组和矢量计算(Part2)

NumPy的ndarray:一种多维数组对象

基本的索引和切片

一维数组

高维数组的元素索引

高维数组的切片索引

布尔型索引

花式索引

np.ix_函数

数组转置和轴对换

transpose方法

.T

通用函数:快速的元素级数组函数

sqrt函数

exp函数

maximum函数


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]

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值