2.7 花哨的索引
在前面的小节中,我们看到了如何利用简单的索引值(如
arr[0]
)、
切片(如
arr[:5]
)和布尔掩码(如
arr[arr > 0]
)获得并修改部分
数组。在这一节中,我们将介绍另外一种数组索引,也称作花哨的索引
(
fancy indexing
)。花哨的索引和前面那些简单的索引非常类似,但是
传递的是索引数组,而不是单个标量。花哨的索引让我们能够快速获得
并修改复杂的数组值的子数据集。
2.7.1 探索花哨的索引
花哨的索引在概念上非常简单,它意味着传递一个索引数组来一次性获
得多个数组元素。例如以下数组:
![](https://i-blog.csdnimg.cn/blog_migrate/563fdeec5128d601e1a67031b04c07ca.png)
假设我们希望获得三个不同的元素,可以用以下方式实现:
另外一种方法是通过传递索引的单个列表或数组来获得同样的结果:
利用花哨的索引,结果的形状与索引数组的形状一致,而不是与被索引
数组的形状一致:
![](https://i-blog.csdnimg.cn/blog_migrate/4127e1ae5622c0d5ad3d3ef0b417a163.png)
花哨的索引也对多个维度适用。假设我们有以下数组:
和标准的索引方式一样,第一个索引指的是行,第二个索引指的是列:
这里需要注意,结果的第一个值是
X[0, 2]
,第二个值是
X[1, 1]
,第
三个值是
X[2, 3]
。在花哨的索引中,索引值的配对遵循
2.5
节介绍过
的广播的规则。因此当我们将一个列向量和一个行向量组合在一个索引
中时,会得到一个二维的结果:
X[row[:, np.newaxis], col]
array([[ 2, 1, 3],
[ 6, 5, 7],
[10, 9, 11]])
这里,每一行的值都与每一列的向量配对,正如我们看到的广播的算术
运算:
row[:, np.newaxis] * col
array([[0, 0, 0],
[2, 1, 3],
[4, 2, 6]])
这里特别需要记住的是,花哨的索引返回的值反映的是广播后的索引数
组的形状,而不是被索引的数组的形状。
2.7.2 组合索引
花哨的索引可以和其他索引方案结合起来形成更强大的索引操作:
print(X)
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
可以将花哨的索引和简单的索引组合使用:
X[2, [2, 0, 1]]
array([10, 8, 9])
也可以将花哨的索引和切片组合使用:
X[1:, [2, 0, 1]]
array([[ 6, 4, 5],
[10, 8, 9]])
更可以将花哨的索引和掩码组合使用:
![](https://i-blog.csdnimg.cn/blog_migrate/3bd91a80f3a4fffd946fe762ceb28689.png)
索引选项的组合可以实现非常灵活的获取和修改数组元素的操作。
2.7.3 示例:选择随机点
花哨的索引的一个常见用途是从一个矩阵中选择行的子集。例如我们有
一个
N
×
D
的矩阵,表示在
D
个维度的
N
个点。以下是一个二维正态分
布的点组成的数组:
![](https://i-blog.csdnimg.cn/blog_migrate/20ecd5487abee21f5be2601953ded0df.png)
可以用散点图将这些点可视化(如图2-7
所示):
图
2-7
:正态分布的点
我们将利用花哨的索引随机选取 20 个点——选择 20 个随机的、不重复
的索引值,并利用这些索引值选取到原始数组对应的值:
indices = np.random.choice(X.shape[0], 20, replace=False)
indices
array([93, 45, 73, 81, 50, 10, 98, 94, 4, 64, 65, 89, 47, 84, 82, 80, 25, 90, 63, 20])
selection = X[indices] # 花哨的索引
selection.shape
(20, 2)
现在来看哪些点被选中了,将选中的点在图上用大圆圈标示出来(如图
2-8
所示):
![](https://i-blog.csdnimg.cn/blog_migrate/4cd1496743074715850f709bc9f36f47.png)
图
2-8
:随机选择的点
2.7.4 用花哨的索引修改值
正如花哨的索引可以被用于获取部分数组,它也可以被用于修改部分数
组。例如,假设我们有一个索引数组,并且希望设置数组中对应的值:
![](https://i-blog.csdnimg.cn/blog_migrate/dab49b751434765b79bf6e300b99eecd.png)
可以用任何的赋值操作来实现,例如:
不过需要注意,操作中重复的索引会导致一些出乎意料的结果产生,如
以下例子所示:
x = np.zeros(10)
x[[0, 0]] = [4, 6]
print(x)
[ 6. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
4
去哪里了呢?这个操作首先赋值
x[0] = 4
,然后赋值
x[0] = 6
,因
此当然
x[0]
的值为
6
。
以上还算合理,但是设想以下操作:
你可能期望
x[3]
的值为
2
,
x[4]
的值为
3
,因为这是这些索引值重复的次数。但是为什么结果不同于我们的预想呢?从概念的角度理解,这
是因为
x[i] += 1
是
x[i] = x[i] + 1
的简写。
x[i] + 1
计算后,这个结果被赋值给了
x
相应的索引值。记住这个原理后,我们却发现数
组并没有发生多次累加,
而是发生了赋值,显然这不是我们希望的结
果。
因此,如果你希望累加,该怎么做呢?你可以借助通用函数中的
at()
方法(在
NumPy 1.8
以后的版本中可以使用)来实现。进行如下操作:
x = np.zeros(10)
np.add.at(x, i, 1)
print(x)
[ 0. 0. 1. 2. 3. 0. 0. 0. 0. 0.]
at()
函数在这里对给定的操作、给定的索引(这里是
i
)以及给定的
值(这里是
1
)执行的是就地操作。另一个可以实现该功能的类似方法
是通用函数中的
reduceat()
函数,你可以在
NumPy
文档中找到关于
该函数的更多信息。
2.7.5 示例:数据区间划分
你可以用这些方法有效地将数据进行区间划分并手动创建直方图。例
如,假定我们有
1000
个值,希望快速统计分布在每个区间中的数据频
次,可以用
ufunc.at
来计算:
np.random.seed(42)
x = np.random.randn(100) # 手动计算直方图
bins = np.linspace(-5, 5, 20)
counts = np.zeros_like(bins) # 为每个x找到合适的区间
i = np.searchsorted(bins, x) # 为每个区间加上1
np.add.at(counts, i, 1)
# 画出结果
plt.plot(bins, counts, linestyle='steps');
import warnings
warnings.simplefilter("error")
图
2-9
:手动计算的直方图
当然,如果每次需要画直方图你都这么做的话,也是很不明智的。这就
是为什么
Matplotlib
提供了
plt.hist()
方法,该方法仅用一行代码就
实现了上述功能:
![](https://i-blog.csdnimg.cn/blog_migrate/b754fedbb9930f5f8b04680b49fe9ce1.png)
这个函数将生成一个和图
2-9
几乎一模一样的图。为了计算区间,
Matplotlib
将使用
np.histogram
函数,该函数的计算功能也和上面执
行的计算类似。接下来比较一下这两种方法:
![](https://i-blog.csdnimg.cn/blog_migrate/c2390efa002df4e6efcecc3f94087d13.png)
可以看到,我们一行代码的算法比
NumPy
优化过的算法快好几倍!这
是如何做到的呢?如果你深入
np.histogram
源代码(可以在
IPython
中输入
np.histogram??
查看源代码),就会看到它比我们前面用过的
简单的搜索和计数方法更复杂。这是由于
NumPy
的算法更灵活(需要
适应不同场景),因此在数据点比较大时更能显示出其良好性能:
以上比较表明,算法效率并不是一个简单的问题。一个对大数据集非常
有效的算法并不总是小数据集的最佳选择,反之同理(详情请参见
2.8.3
节)。但是自己编写这个算法的好处是可以理解这些基本方法。
你可以利用这些编写好的模块去扩展,以实现一些有意思的自定义操
作。将
Python
有效地用于数据密集型应用中的关键是,当应用场景合
适时知道使用
np.histogram
这样的现成函数,当需要执行更多指定的
操作时也知道如何利用更低级的功能来实现。