NumPy 數(shù)組花式索引(Fancy Indexing)的實(shí)現(xiàn)
前面的博客分享了我對于NumPy數(shù)組索引的學(xué)習(xí)心得——如何使用簡單索引(例如 arr[0])、切片(例如 arr[:5])和布爾掩碼(例如 arr[arr > 0])來訪問和修改數(shù)組的部分內(nèi)容。
這里,我們將介紹另一種數(shù)組索引方式,稱為花式或矢量化索引,其中我們用索引數(shù)組代替單個(gè)標(biāo)量。
這種方式可以讓我們非常快速地訪問和修改數(shù)組中復(fù)雜子集的值。
探索花式索引
花式索引在概念上很簡單:它指的是通過傳遞一個(gè)索引數(shù)組來一次性訪問多個(gè)數(shù)組元素。
以下面的數(shù)組為例:
import numpy as np rng = np.random.default_rng(seed=1024) x = rng.integers(100, size=10) print(x)
[55 32 87 25 8 37 74 88 38 62]
假設(shè)我們想要訪問三個(gè)不同的元素。我們可以這樣做:
[x[3], x[9], x[8]]
[np.int64(25), np.int64(62), np.int64(38)]
另外,我們也可以傳遞一個(gè)索引列表或數(shù)組來獲得相同的結(jié)果:
ind = [3, 9, 8] x[ind]
array([25, 62, 38])
當(dāng)使用索引數(shù)組時(shí),結(jié)果的形狀反映的是索引數(shù)組的形狀,而不是被索引數(shù)組的形狀:
ind = np.array([[3, 7],
[4, 5]])
x[ind]
array([[25, 88],
[ 8, 37]])
花式索引同樣適用于多維數(shù)組。請看下面的數(shù)組:
X = np.arange(30).reshape((5, 6)) X
array([[ 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]])
像標(biāo)準(zhǔn)索引一樣,第一個(gè)索引表示行,第二個(gè)索引表示列:
row = np.array([0, 1, 2]) col = np.array([2, 1, 3]) X[row, col]
array([ 2, 7, 15])
注意,結(jié)果中的第一個(gè)值是 X[0, 2],第二個(gè)是 X[1, 1],第三個(gè)是 X[2, 3]。
在花式索引中,索引的配對遵循了數(shù)組的廣播機(jī)制中提到的所有廣播規(guī)則。
因此,例如,如果我們在索引中結(jié)合使用列向量和行向量,就會得到一個(gè)二維的結(jié)果:
X[row[:, np.newaxis], col]
array([[ 2, 1, 3],
[ 8, 7, 9],
[14, 13, 15]])
這里,每個(gè)行索引值都會與每個(gè)列向量進(jìn)行匹配,這與我們在算術(shù)運(yùn)算廣播中看到的方式完全一致。
例如:
row[:, np.newaxis] * col
array([[0, 0, 0],
[2, 1, 3],
[4, 2, 6]])
在使用花式索引時(shí),務(wù)必要記住:返回值的形狀反映的是索引的廣播后形狀,而不是被索引數(shù)組的形狀。
組合索引
為了實(shí)現(xiàn)更強(qiáng)大的操作,花式索引可以與我們之前見過的其他索引方式結(jié)合使用。例如,給定數(shù)組 X:
print(X)
[[ 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]]
我們可以將花式索引與簡單索引結(jié)合使用:
X[2, [2, 0, 1]]
array([14, 12, 13])
我們還可以將花式索引與切片結(jié)合使用:
X[1:, [2, 0, 1]]
array([[ 8, 6, 7],
[14, 12, 13],
[20, 18, 19],
[26, 24, 25]])
我們還可以將花式索引與掩碼(布爾索引)結(jié)合使用:
mask = np.array([True, False, True, False, False, False]) X[row[:, np.newaxis], mask]
array([[ 0, 2],
[ 6, 8],
[12, 14]])
所有這些索引方式的組合,使我們能夠非常靈活、高效地訪問和修改數(shù)組的值。
示例:選擇隨機(jī)點(diǎn)
花式索引的一個(gè)常見用途是從矩陣中選擇部分行的子集。
例如,我們可能有一個(gè) N × D N \times D N×D 的矩陣,表示 N N N 個(gè) D D D 維空間中的點(diǎn),比如下面這些從二維正態(tài)分布中抽取的點(diǎn):
mean = [0, 0]
cov = [[1, 2],
[2, 5]]
X = rng.multivariate_normal(mean, cov, 100)
X.shape
(100, 2)
使用Matplotlib,我們可以將這些點(diǎn)可視化為散點(diǎn)圖(見下圖):
%matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('seaborn-v0_8-whitegrid')
plt.scatter(X[:, 0], X[:, 1]);

讓我們用花式索引選擇 20 個(gè)隨機(jī)點(diǎn)。我們將首先隨機(jī)選擇 20 個(gè)不重復(fù)的索引,然后用這些索引來選取原數(shù)組中的一部分:
indices = np.random.choice(X.shape[0], 20, replace=False) indices
array([87, 77, 55, 54, 34, 76, 30, 12, 61, 90, 29, 94, 8, 91, 81, 97, 74,
5, 99, 20], dtype=int32)
selection = X[indices] # 使用花式索引選擇點(diǎn) selection.shape
(20, 2)
現(xiàn)在,為了查看哪些點(diǎn)被選中了,我們將在被選中的點(diǎn)的位置上疊加大圓圈(見下圖):
plt.scatter(X[:, 0], X[:, 1], alpha=0.3)
plt.scatter(selection[:, 0], selection[:, 1],
facecolor='none', edgecolor='black', s=200);

這種策略常用于快速劃分?jǐn)?shù)據(jù)集,例如在統(tǒng)計(jì)模型驗(yàn)證(如超參數(shù)與模型驗(yàn)證)中進(jìn)行訓(xùn)練/測試集分割,以及在采樣方法中用于解答統(tǒng)計(jì)問題。
用花式索引修改值
正如花式索引可以用來訪問數(shù)組的部分內(nèi)容,它也可以用來修改數(shù)組的部分內(nèi)容。
例如,假設(shè)我們有一個(gè)索引數(shù)組,并希望將數(shù)組中對應(yīng)的元素設(shè)置為某個(gè)值:
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]
我們可以對其使用任何賦值類型的運(yùn)算符。例如:
x[i] -= 10 print(x)
[ 0 89 89 3 89 5 6 7 89 9]
請注意,對于這些操作,如果索引中有重復(fù)項(xiàng),可能會導(dǎo)致一些意想不到的結(jié)果。請看下面的例子:
x = np.zeros(10) x[[0, 0]] = [4, 6] print(x)
[6. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
4 到哪去了?這個(gè)操作首先執(zhí)行 x[0] = 4,然后執(zhí)行 x[0] = 6。
結(jié)果當(dāng)然是 x[0] 的值為 6。
這很合理,但請考慮下面這個(gè)操作:
i = [2, 3, 3, 4, 4, 4] x[i] += 1 x
array([6., 0., 1., 1., 1., 0., 0., 0., 0., 0.])
你可能會期望 x[3] 的值為 2,x[4] 的值為 3,因?yàn)槊總€(gè)索引重復(fù)的次數(shù)就是它們應(yīng)該增加的次數(shù)。為什么實(shí)際不是這樣呢?
從概念上講,這是因?yàn)?x[i] += 1 實(shí)際上等價(jià)于 x[i] = x[i] + 1。x[i] + 1 會先被整體計(jì)算出來,然后再把結(jié)果賦值回 x 的這些索引位置。
這樣一來,實(shí)際上是賦值操作被多次執(zhí)行,而不是累加操作被多次執(zhí)行,這就導(dǎo)致了看起來不太直觀的結(jié)果。
那么如果你想要每個(gè)索引都累加多次該怎么辦?這時(shí)可以使用 ufunc 的 at 方法,如下所示:
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)進(jìn)行原地應(yīng)用。
另一個(gè)在原理上類似的方法是 ufunc 的 reduceat 方法,你可以在 NumPy 官方文檔 中了解更多信息。
示例:數(shù)據(jù)分箱(Binning Data)
你可以利用這些思想高效地對數(shù)據(jù)進(jìn)行自定義分箱計(jì)算。
例如,假設(shè)我們有 100 個(gè)數(shù)值,并希望快速判斷它們分別落在一組分箱(bins)中的哪個(gè)區(qū)間。
我們可以像下面這樣用 ufunc.at 來實(shí)現(xiàn):
rng = np.random.default_rng(seed=1024) x = rng.normal(size=100) # 手工計(jì)算直方圖 bins = np.linspace(-5, 5, 20) counts = np.zeros_like(bins) # 為每個(gè) x 找到合適的分箱 i = np.searchsorted(bins, x) # 將這些索引對應(yīng)的分箱加一 np.add.at(counts, i, 1)
現(xiàn)在,counts 反映了每個(gè)分箱中的點(diǎn)的數(shù)量——換句話說,就是一個(gè)直方圖(見下圖):
# 繪制直方圖 plt.plot(bins, counts, drawstyle='steps');

當(dāng)然,如果每次想要繪制直方圖都要手動(dòng)實(shí)現(xiàn)上述步驟會很不方便。
這也是為什么 Matplotlib 提供了 plt.hist 例程,它可以用一行代碼完成相同的操作:
plt.hist(x, bins, histtype='step');

這個(gè)函數(shù)會生成一個(gè)與剛才幾乎相同的圖像。
在計(jì)算分箱時(shí),Matplotlib 實(shí)際上調(diào)用了 np.histogram 函數(shù),其實(shí)現(xiàn)方式與我們手動(dòng)實(shí)現(xiàn)的非常類似。我們可以在這里對比兩者:
print(f"NumPy 直方圖 ({len(x)} 點(diǎn)):")
%timeit counts, edges = np.histogram(x, bins)
print(f"自定義直方圖 ({len(x)} 點(diǎn)):")
%timeit np.add.at(counts, np.searchsorted(bins, x), 1)
NumPy 直方圖 (100 點(diǎn)): 6.43 μs ± 68.1 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each) 自定義直方圖 (100 點(diǎn)): 5.53 μs ± 86.7 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
我們自己的一行算法居然比 NumPy 中的優(yōu)化算法快!這是怎么回事?如果你深入查看 np.histogram 的源碼(在 IPython 中輸入 np.histogram?? 即可),你會發(fā)現(xiàn)它比我們簡單的“查找并計(jì)數(shù)”要復(fù)雜得多;這是因?yàn)?NumPy 的算法更加靈活,尤其是在數(shù)據(jù)點(diǎn)數(shù)量很大時(shí),專門針對更好的性能進(jìn)行了設(shè)計(jì):
x = rng.normal(size=1000000)
print(f"NumPy 直方圖 ({len(x)} 點(diǎn)):")
%timeit counts, edges = np.histogram(x, bins)
print(f"自定義直方圖 ({len(x)} 點(diǎn)):")
%timeit np.add.at(counts, np.searchsorted(bins, x), 1)
NumPy 直方圖 (1000000 點(diǎn)): 5.75 ms ± 107 μs per loop (mean ± std. dev. of 7 runs, 100 loops each) 自定義直方圖 (1000000 點(diǎn)): 48.7 ms ± 1.19 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
這個(gè)對比說明,算法效率幾乎從來不是一個(gè)簡單的問題。適用于大數(shù)據(jù)集的高效算法,并不總是小數(shù)據(jù)集下的最佳選擇,反之亦然。
但自己編寫算法的優(yōu)勢在于:只要理解了這些基礎(chǔ)方法,你就擁有了無限的可能性——不再受限于內(nèi)置例程,可以創(chuàng)造屬于自己的數(shù)據(jù)探索方式。
高效使用 Python 進(jìn)行數(shù)據(jù)密集型應(yīng)用的關(guān)鍵,不僅在于了解像 np.histogram 這樣的通用便捷函數(shù)及其適用場景,還在于當(dāng)你需要更靈活的行為時(shí),能夠利用底層功能實(shí)現(xiàn)自定義操作。
內(nèi)容總結(jié)
本章介紹了 NumPy 的花式索引(Fancy Indexing)及其強(qiáng)大用法。主要內(nèi)容包括:
- 花式索引允許通過整數(shù)數(shù)組或列表一次性訪問或修改多個(gè)數(shù)組元素,支持一維和多維數(shù)組,并遵循廣播機(jī)制。
- 花式索引可以與切片、布爾索引等其他索引方式靈活組合,實(shí)現(xiàn)復(fù)雜的數(shù)據(jù)選取和操作。
- 通過實(shí)際案例,展示了如何用花式索引高效地選擇、可視化和修改數(shù)據(jù)子集。
- 講解了花式索引賦值時(shí)的“非累加”特性,以及如何用
np.add.at實(shí)現(xiàn)真正的逐元素累加。 - 以自定義直方圖為例,說明了花式索引和 ufunc 的結(jié)合在數(shù)據(jù)分箱等統(tǒng)計(jì)計(jì)算中的高效應(yīng)用。
- 最后強(qiáng)調(diào),理解底層索引和廣播機(jī)制,有助于靈活高效地處理大規(guī)模數(shù)據(jù),突破內(nèi)置函數(shù)的限制,提升數(shù)據(jù)分析能力。
到此這篇關(guān)于NumPy 數(shù)組花式索引(Fancy Indexing)的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)NumPy 數(shù)組花式索引內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
python pygame實(shí)現(xiàn)擋板彈球游戲
這篇文章主要為大家詳細(xì)介紹了python pygame實(shí)現(xiàn)擋板彈球游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-11-11
Python的Pandas庫中使用DataFrame篩選和刪除含特定值的行與列
Pandas是一個(gè)強(qiáng)大的數(shù)據(jù)處理庫,提供了各種功能來操作和處理數(shù)據(jù),這篇文章主要給大家介紹了關(guān)于Python的Pandas庫中使用DataFrame篩選和刪除含特定值的行與列的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-05-05
Python 讀取xml數(shù)據(jù),cv2裁剪圖片實(shí)例
這篇文章主要介紹了Python 讀取xml數(shù)據(jù),cv2裁剪圖片實(shí)例,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-03-03
Python+OpenCV 實(shí)現(xiàn)圖片無損旋轉(zhuǎn)90°且無黑邊
今天小編就為大家分享一篇Python+OpenCV 實(shí)現(xiàn)圖片無損旋轉(zhuǎn)90°且無黑邊,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-12-12

