NumPyのファンシーインデックス(リストによる選択と代入)

Posted: | Tags: Python, NumPy

NumPyにはインデックスのリストによってNumPy配列ndarrayの部分配列を選択するファンシーインデックスという仕組みがある。選択した部分配列を抽出したり新たな値を代入したりできる。

例えば二次元配列に対しては任意の位置の行や列を選択できて便利だが、行・列両方を指定したい場合は注意が必要。

以下の内容について説明する。

  • 一次元のNumPy配列ndarrayのファンシーインデックス
  • 多次元のNumPy配列ndarrayのファンシーインデックス
    • 行の選択
    • 列の選択
    • そのほかの部分配列の選択
  • ファンシーインデックスを使った代入
  • ビュー(参照)とコピー
  • スライスとの組み合わせ

ファンシーインデックスを利用した行や列の並べ替えについては以下の記事を参照。

NumPy配列ndarrayの要素や部分配列を選択する方法についてのまとめは以下の記事を参照。

また、条件を満たす行・列を抽出したい場合は以下の記事を参照。

一次元のNumPy配列ndarrayのファンシーインデックス

以下の一次元のNumPy配列ndarrayを例とする。

import numpy as np

a = np.arange(10) * 10
print(a)
# [ 0 10 20 30 40 50 60 70 80 90]

インデックス(0始まりの位置)を指定することで各要素にアクセス可能。

print(a[5])
# 50

print(a[8])
# 80

インデックスのリストや配列を指定すると複数の要素にアクセスできる。このような仕組みをファンシーインデックスという。

print(a[[5, 8]])
# [50 80]

インデックスの並びは昇順である必要はなく、重複していてもOK。

print(a[[5, 4, 8, 0]])
# [50 40 80  0]

print(a[[5, 5, 5, 5]])
# [50 50 50 50]

ファンシーインデックスの結果の形状は、指定するリスト・配列の形状に依存する。

以下のようにnumpy.ndarrayの二次元配列を指定すると対応する要素がその形状に従って返される。

idx = np.array([[5, 4], [8, 0]])
print(idx)
# [[5 4]
#  [8 0]]

print(a[idx])
# [[50 40]
#  [80  0]]

リストのリストで構成された二次元配列の場合エラーとなる。

# print(a[[[5, 4], [8, 0]]])
# IndexError: too many indices for array

[]でさらに囲むとOK。

print(a[[[[5, 4], [8, 0]]]])
# [[50 40]
#  [80  0]]

なぜこうなるかはまだ調べていない。

多次元のNumPy配列ndarrayのファンシーインデックス

以下の二次元のNumPy配列ndarrayを例とする。

a_2d = np.arange(12).reshape((3, 4))
print(a_2d)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

行の選択

インデックス(0始まりの位置)を指定することで各行にアクセス可能。

print(a_2d[0])
# [0 1 2 3]

print(a_2d[2])
# [ 8  9 10 11]

インデックスのリストや配列を指定すると複数の行にアクセスできる。インデックスの並びは昇順である必要はなく、重複していてもOK。

print(a_2d[[2, 0]])
# [[ 8  9 10 11]
#  [ 0  1  2  3]]

print(a_2d[[2, 2, 2]])
# [[ 8  9 10 11]
#  [ 8  9 10 11]
#  [ 8  9 10 11]]

列の選択

列の指定はファンシーインデックスだけでなくスライスと組み合わせて実現する。

NumPy配列ndarrayに対するスライスについては以下の記事を参照。

以下のように各列にアクセス可能。

print(a_2d[:, 1])
# [1 5 9]

print(a_2d[:, 3])
# [ 3  7 11]

なお、形状を保ったまま1列分を抽出したい場合は列の指定にもスライスを使う。

print(a_2d[:, 1:2])
# [[1]
#  [5]
#  [9]]

列をインデックスのリストや配列を指定すると複数の行にアクセスできる。インデックスの並びは昇順である必要はなく、重複していてもOK。

print(a_2d[:, [3, 1]])
# [[ 3  1]
#  [ 7  5]
#  [11  9]]

print(a_2d[:, [3, 3, 3]])
# [[ 3  3  3]
#  [ 7  7  7]
#  [11 11 11]]

そのほかの部分配列の選択

行全体や列全体ではない部分配列をファンシーインデックスを使って指定する方法を説明する。

多次元のNumPy配列ndarrayの要素へのアクセスは各次元のインデックスをカンマ,で区切って指定する。

print(a_2d[0, 1])
# 1

print(a_2d[2, 3])
# 11

各次元のインデックスをリストや配列で指定すると、対応するインデックスの位置の要素が抽出される。

例えば二次元配列の場合に行・列のインデックスをリストで指定しても、その行・列の部分配列が選択されるわけではないので注意。

print(a_2d[[0, 2], [1, 3]])
# [ 1 11]

この例の結果の値はそれぞれ以下のインデックスの値に対応している。

# index
# [[0, 1] [2, 3]]

各次元に指定したリスト・配列がブロードキャスト不可能な組み合わせの場合はエラーIndexErrorとなる。

# print(a_2d[[0, 2, 1], [1, 3]])
# IndexError: shape mismatch: indexing arrays could not be broadcast together with shapes (3,) (2,) 

インデックスに二次元配列を指定すると二次元配列の結果が得られる。この場合はNumPy配列ndarrayではなくリストのリストでもOK。

print(a_2d[[[0, 0], [2, 2]], [[1, 3], [1, 3]]])
# [[ 1  3]
#  [ 9 11]]

この例の結果の値はそれぞれ以下のインデックスの値に対応している。

# index
# [[0, 1] [0, 3]
#  [2, 1] [2, 3]]

各次元に指定したインデックスはブロードキャストして処理されるので以下のように省略できる。

print(a_2d[[[0], [2]], [1, 3]])
# [[ 1  3]
#  [ 9 11]]

このようなインデックスを生成するのに便利なnumpy.ix_()関数がある。

引数に各次元のインデックスをそれぞれ一次元のリスト・配列で指定する。

idxs = np.ix_([0, 2], [1, 3])
print(idxs)
# (array([[0],
#        [2]]), array([[1, 3]]))

結果はnumpy.ndarrayを要素とするタプル。各要素が各次元に合わせた形状に変換されたインデックスとなる。

print(type(idxs))
# <class 'tuple'>

print(type(idxs[0]))
# <class 'numpy.ndarray'>

print(idxs[0])
# [[0]
#  [2]]

print(idxs[1])
# [[1 3]]

numpy.ix_()の結果をそのままインデックスとして指定すると部分配列が選択される。インデックスの並びは昇順である必要はなく、重複していてもOK。

print(a_2d[np.ix_([0, 2], [1, 3])])
# [[ 1  3]
#  [ 9 11]]

print(a_2d[np.ix_([2, 0], [3, 3, 3])])
# [[11 11 11]
#  [ 3  3  3]]

以下の書き方もできる。行を選択してから列を選択する。

print(a_2d[[0, 2]][:, [1, 3]])
# [[ 1  3]
#  [ 9 11]]

この書き方の場合、後述のように値の代入ができないので注意。

ファンシーインデックスを使った代入

ファンシーインデックスでは値を抽出するだけでなく、選択した部分配列に値を代入することも可能。

右辺の値がブロードキャストされて代入される。

a_2d = np.arange(12).reshape((3, 4))
print(a_2d)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

a_2d[np.ix_([0, 2], [1, 3])] = 100
print(a_2d)
# [[  0 100   2 100]
#  [  4   5   6   7]
#  [  8 100  10 100]]

a_2d[np.ix_([0, 2], [1, 3])] = [100, 200]
print(a_2d)
# [[  0 100   2 200]
#  [  4   5   6   7]
#  [  8 100  10 200]]

a_2d[np.ix_([0, 2], [1, 3])] = [[100, 200], [300, 400]]
print(a_2d)
# [[  0 100   2 200]
#  [  4   5   6   7]
#  [  8 300  10 400]]

ファンシーインデックスで行を選択してからさらに列を選択する書き方の場合、値の代入はできない。

print(a_2d[[0, 2]][:, [1, 3]])
# [[100 200]
#  [300 400]]

a_2d[[0, 2]][:, [1, 3]] = 0
print(a_2d)
# [[  0 100   2 200]
#  [  4   5   6   7]
#  [  8 300  10 400]]

インデックスの並びが昇順でなくても対応した位置に代入される。

a_2d = np.arange(12).reshape((3, 4))
print(a_2d)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

a_2d[[2, 0]] = [[100, 200, 300, 400], [500, 600, 700, 800]]
print(a_2d)
# [[500 600 700 800]
#  [  4   5   6   7]
#  [100 200 300 400]]

インデックスが重複している場合は上書きされ、結果として最後の値が代入される。

a_2d[[2, 2]] = [[-1, -2, -3, -4], [-5, -6, -7, -8]]
print(a_2d)
# [[500 600 700 800]
#  [  4   5   6   7]
#  [ -5  -6  -7  -8]]

ビュー(参照)とコピー

ファンシーインデックスはビュー(参照)ではなくコピーを返す。

ファンシーインデックスの結果を変数に代入してその要素を変更しても、元の配列の要素は変更されない。

a_2d = np.arange(12).reshape((3, 4))
print(a_2d)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

a_fancy = a_2d[np.ix_([0, 2], [1, 3])]
print(a_fancy)
# [[ 1  3]
#  [ 9 11]]

a_fancy[0, 0] = 100
print(a_fancy)
# [[100   3]
#  [  9  11]]

print(a_2d)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

スライスとの組み合わせ

NumPyの部分配列の選択方法としてスライスがある。

各次元のインデックスをそれぞれファンシーインデックスとスライスで指定できる。

a_2d = np.arange(12).reshape((3, 4))
print(a_2d)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

print(a_2d[[2, 0], ::-1])
# [[11 10  9  8]
#  [ 3  2  1  0]]

print(a_2d[::2, [3, 0, 1]])
# [[ 3  0  1]
#  [11  8  9]]

スライスはビュー(参照)を返すが、ファンシーインデックスでの指定が含まれている場合はコピーとなる。

関連カテゴリー

関連記事