Pythonのデータサイエンスにおいて、NumPyは欠かすことのできない基礎ライブラリです。
NumPyの多次元配列(ndarray)を操作する際、特定の値を取得したり変更したりする方法として「ファンシーインデックス(Fancy Indexing)」という非常に強力な機能が存在します。
通常のインデックス参照やスライシングよりも柔軟かつ高度なデータ抽出が可能になるため、効率的なコードを書く上で必須の知識と言えます。
この記事では、ファンシーインデックスの仕組みから実践的な活用方法までを具体例を交えて詳しく紹介します。
NumPyファンシーインデックスの基本概念
ファンシーインデックスとは、インデックスの配列を渡すことで、複数の要素を一度に指定して操作する手法を指します。
通常のスライシングでは「開始:終了:ステップ」といった連続的な範囲指定しかできませんが、ファンシーインデックスを使えば不連続な位置にある要素も自由自在に抽出できます。
スライシングとの違い
スライシングは配列の「ビュー(参照)」を返しますが、ファンシーインデックスは常にデータのコピー(新しい配列)を作成するという大きな違いがあります。
この特性を理解していないと、大規模なデータを扱う際に意図しないメモリ消費が発生したり、元の配列を書き換えようとして失敗したりすることがあるため注意が必要です。
1次元配列での基本的な使い方
まずは、最もシンプルな1次元配列におけるファンシーインデックスの使い方を見ていきましょう。
リストや別のndarrayを用いて、抽出したい要素のインデックスを直接指定します。
import numpy as np
# サンプル配列の作成
x = np.array([10, 20, 30, 40, 50, 60, 70, 80, 90, 100])
# 抽出したいインデックスをリストで指定
indices = [1, 3, 4, 8]
# ファンシーインデックスによる抽出
result = x[indices]
print(result)
[20 40 50 90]
このように、indicesに指定した順番通りに新しい配列が作成されます。
同じインデックスを重複して指定することも可能であり、その場合は同じ要素が複数回抽出されます。
多次元配列でのファンシーインデックス
多次元配列においてファンシーインデックスを利用すると、特定の行や列、あるいは特定の座標にある値をピンポイントで取得できます。
特定の行を抽出する
2次元配列に対して1つのインデックス配列を渡すと、それは「行」の指定として扱われます。
# 3x4の行列を作成
matrix = np.arange(12).reshape(3, 4)
print("元の行列:")
print(matrix)
# 0行目と2行目を抽出
rows = [0, 2]
selected_rows = matrix[rows]
print("\n抽出結果:")
print(selected_rows)
元の行列:
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
抽出結果:
[[ 0 1 2 3]
[ 8 9 10 11]]
特定の座標(要素)を抽出する
行と列の両方にインデックス配列を渡すと、それらがペアとして扱われ、特定の座標にある要素が抽出されます。
# (0, 1), (1, 2), (2, 0) の要素を抽出したい場合
row_indices = [0, 1, 2]
col_indices = [1, 2, 0]
# 座標指定による抽出
elements = matrix[row_indices, col_indices]
print(elements)
[1 6 8]
この結果は、1次元配列として返される点に注意してください。
結果の形状を調整したい場合は、インデックス配列自体の形状を工夫する必要があります。
実践的な活用例
ファンシーインデックスは、単なるデータ抽出だけでなく、データの加工や並び替えにおいて非常に高いパフォーマンスを発揮します。
データのランダムなシャッフル
データセットを学習用とテスト用に分割する際など、行単位でデータをランダムに入れ替えたい場合に役立ちます。
data = np.arange(10).reshape(5, 2)
print("元のデータ:")
print(data)
# 行数に基づいたランダムなインデックスを生成
indices = np.random.permutation(data.shape[0])
print(f"\nシャッフル用インデックス: {indices}")
# ファンシーインデックスで並び替え
shuffled_data = data[indices]
print("\nシャッフル後のデータ:")
print(shuffled_data)
元のデータ:
[[0 1]
[2 3]
[4 5]
[6 7]
[8 9]]
シャッフル用インデックス: [3 0 4 1 2]
シャッフル後のデータ:
[[6 7]
[0 1]
[8 9]
[2 3]
[4 5]]
条件に基づいた値の代入
ファンシーインデックスを用いることで、特定の条件を満たす箇所を一括で書き換えるといった操作も容易に行えます。
x = np.zeros(10)
indices = [1, 3, 5]
# 特定のインデックスに対して一括代入
x[indices] = 99
print(x)
[ 0. 99. 0. 99. 0. 99. 0. 0. 0. 0.]
ファンシーインデックス使用時の注意点
ファンシーインデックスを使用する際には、いくつか把握しておくべき重要な挙動があります。
代入時の挙動(重複インデックス)
同じインデックスに対して複数回代入を行おうとした場合、プログラム上の期待とは異なる結果になることがあります。
x = np.zeros(5)
# インデックス0に1を足す操作を3回繰り返すつもり
indices = [0, 0, 0]
x[indices] += 1
print(x)
[1. 0. 0. 0. 0.]
この例では、期待される [3, 0, 0, 0, 0] ではなく [1, 0, 0, 0, 0] となっています。
これは x[indices] += 1 が内部的に x[indices] = x[indices] + 1 として評価され、同じ場所への代入が繰り返されているだけだからです。
このような累積的な操作を行いたい場合は、np.add.at 関数を使用するのが適切です。
ブロードキャストとの組み合わせ
ファンシーインデックスとブロードキャストを組み合わせることで、非常に複雑な部分配列の操作が可能になります。
例えば、特定の行を選びつつ、その中から特定の列を抜き出すといった操作が1行で記述できます。
| 手法 | 返されるデータ型 | 元の配列への影響 |
|---|---|---|
| スライシング | ビュー(参照) | 変更すると元データも変わる |
| ファンシーインデックス | コピー(実体) | 変更しても元データは変わらない |
まとめ
NumPyのファンシーインデックスは、配列操作の自由度を劇的に高めてくれる便利な機能です。
特定の要素を抽出するだけでなく、データの並び替えや一括更新など、多くの場面で活用されています。
スライシングとは異なり、抽出されたデータは「コピー」であるという点を意識することが、バグの少ない効率的なプログラムを書く鍵となります。
大量のデータを扱うデータ分析や機械学習の実装において、今回紹介したテクニックをぜひ活用してみてください。
