NumPy配列ndarrayの要素・行・列を取得(抽出)、代入

Modified: | Tags: Python, NumPy

NumPy配列ndarrayの要素の値や行・列などの部分配列を取得(抽出)したり、選択範囲に新たな値・配列を代入する方法について説明する。

公式ドキュメントの該当部分は以下。

要素・部分配列(行・列など)の削除、連結、追加については以下の記事を参照。

本記事のサンプルコードのNumPyのバージョンは以下の通り。バージョンによって仕様が異なる可能性があるので注意。

import numpy as np

print(np.__version__)
# 1.26.1

配列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]]

print(a_2d[1, 2])
# 6

print(a_2d[1])
# [4 5 6 7]

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

上の例のように、二次元配列に対しては[i]i番目の行、[:, i]i番目の列を抽出できる(i0始まり)。詳細は後述。

各次元の位置は整数値のほかリストやスライスなど様々な形式で指定でき、任意の部分配列を選択できる。

a_3d = np.arange(24).reshape(2, 3, 4)
print(a_3d)
# [[[ 0  1  2  3]
#   [ 4  5  6  7]
#   [ 8  9 10 11]]
# 
#  [[12 13 14 15]
#   [16 17 18 19]
#   [20 21 22 23]]]

print(a_3d[1, [True, False, True], ::2])
# [[12 14]
#  [20 22]]

以下、位置・範囲の指定方法についての詳細を説明する。

整数値(インデックス)で指定

単独の位置(インデックス)は整数intのスカラー値で指定する。

インデックスは0始まりで、負の値を使うと末尾から指定できる(-1が一番最後)。存在しない位置を指定するとエラーとなる。

a_1d = np.arange(10)
print(a_1d)
# [0 1 2 3 4 5 6 7 8 9]

print(a_1d[0])
# 0

print(a_1d[4])
# 4

print(a_1d[-1])
# 9

print(a_1d[-4])
# 6

# print(a_1d[100])
# IndexError: index 100 is out of bounds for axis 0 with size 10

# print(a_1d[-100])
# IndexError: index -100 is out of bounds for axis 0 with size 10

多次元配列の場合も同様。次元ごとにそれぞれ位置を指定する。

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[1, -1])
# 7

後ろの次元の指定は省略可能。例えば二次元配列の場合は行のみを指定して、その行を一次元配列として取得できる。

これは次に説明するように末尾のスライス:が省略されている状態([i][i, :]と等価)。列を選択したい場合は[:, i]とする。

print(a_2d[1])
# [4 5 6 7]

print(a_2d[1, :])
# [4 5 6 7]

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

スライスで指定

スライスstart:end:stepで範囲を選択できる。詳細は以下の記事も参照。

一次元配列の例。

a_1d = np.arange(10)
print(a_1d)
# [0 1 2 3 4 5 6 7 8 9]

print(a_1d[2:7])
# [2 3 4 5 6]

print(a_1d[:7])
# [0 1 2 3 4 5 6]

print(a_1d[2:])
# [2 3 4 5 6 7 8 9]

print(a_1d[2:7:2])
# [2 4 6]

二次元配列の例。末尾の:は省略可能。

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, 1:3])
# [[1 2]
#  [5 6]]

print(a_2d[:2, :])
# [[0 1 2 3]
#  [4 5 6 7]]

print(a_2d[:2])
# [[0 1 2 3]
#  [4 5 6 7]]

スライスi:i+1で幅1の範囲(一行や一列)を選択できるが、整数intのスカラー値で指定した場合とは結果の形状shape、次元数ndimが異なる。

print(a_2d[1:2])
# [[4 5 6 7]]

print(a_2d[1:2].shape)
# (1, 4)

print(a_2d[1])
# [4 5 6 7]

print(a_2d[1].shape)
# (4,)
print(a_2d[:, 2:3])
# [[ 2]
#  [ 6]
#  [10]]

print(a_2d[:, 2:3].shape)
# (3, 1)

print(a_2d[:, 2])
# [ 2  6 10]

print(a_2d[:, 2].shape)
# (3,)
print(a_2d[1:2, 2:3])
# [[6]]

print(a_2d[1:2, 2:3].shape)
# (1, 1)

print(a_2d[1, 2])
# 6

print(a_2d[1, 2].shape)
# ()

print(type(a_2d[1, 2]))
# <class 'numpy.int64'>

このように、スライスで選択した場合は元のndarrayの次元数が保持されるが、整数intで選択した場合は次元数が削られる。

同じ範囲を選択しているようでも、形状shapeや次元数ndimが異なると配列同士の連結などの処理では結果が変わったりエラーになったりする場合があるので注意。

bool値のリストで指定(ブーリアンインデックス)

各次元のサイズ(要素数)と同じサイズのbool値(True, False)のリストやndarrayを指定すると、Trueの位置のみが選択・抽出される。マスク処理のようなイメージ。

一次元配列の例。リストでもndarrayでも指定可能。サイズが異なるとエラーになる。

a_1d = np.arange(4)
print(a_1d)
# [0 1 2 3]

print(a_1d[[True, False, True, False]])
# [0 2]

print(a_1d[np.array([True, False, True, False])])
# [0 2]

# print(a_1d[[True, False]])
# IndexError: boolean index did not match indexed array along dimension 0; dimension is 4 but corresponding boolean dimension is 2

二次元配列の例。全体を示すスライス:と合わせて任意の行・列を抽出できる。

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[[True, False, True]])
# [[ 0  1  2  3]
#  [ 8  9 10 11]]

print(a_2d[:, [True, False, True, False]])
# [[ 0  2]
#  [ 4  6]
#  [ 8 10]]

複数の次元に同時にboolのリストを指定すると想定通りの結果にならないので注意。np.ix_()を使う必要がある。

print(a_2d[[True, False, True], [True, False, True, False]])
# [ 0 10]

print(a_2d[np.ix_([True, False, True], [True, False, True, False])])
# [[ 0  2]
#  [ 8 10]]

1の範囲(一行や一列)を選択した場合、スライスと同様、元のndarrayの次元数が保持される。

print(a_2d[:, [True, False, False, False]])
# [[0]
#  [4]
#  [8]]

なお、ndarrayの比較演算の結果はbool値を要素とするndarrayとなる。このような元の配列と同じ形状shapebool型のndarray[]に指定すると、Trueの要素のみが選択され、一次元に平坦化された配列が返される。

print(a_2d > 5)
# [[False False False False]
#  [False False  True  True]
#  [ True  True  True  True]]

print(a_2d[a_2d > 5])
# [ 6  7  8  9 10 11]

複数条件を指定することも可能。括弧()で囲み、&(かつ)、|(または)を使う。否定~も使える。and, or, notだとエラーになるので注意。

print((a_2d > 5) & (a_2d < 10))
# [[False False False False]
#  [False False  True  True]
#  [ True  True False False]]

print(a_2d[(a_2d > 5) & (a_2d < 10)])
# [6 7 8 9]

ブーリアンインデックスを利用して条件を満たす行・列を抽出する方法については以下の記事を参照。

整数値のリストで指定(ファンシーインデックス)

整数値のリストやndarrayで範囲を選択することも可能。詳細は以下の記事も参照。

一次元配列の例。

順番が前後したり重複したりしてもよい。負の値も使用可能。選択というよりも、元の配列の位置を選んで新たな配列を再構成するイメージ。リストでもndarrayでもよい。

a_1d = np.arange(4)
print(a_1d)
# [0 1 2 3]

print(a_1d[[0, 2]])
# [0 2]

print(a_1d[[3, 3, 3, -3, 0]])
# [3 3 3 1 0]

print(a_1d[np.array([3, 3, 3, -3, 0])])
# [3 3 3 1 0]

二次元配列の例。全体を示すスライス:と合わせて任意の行・列を抽出可能。

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[[0, 2]])
# [[ 0  1  2  3]
#  [ 8  9 10 11]]

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

上述のブーリアンインデックスと同様に、複数の次元に同時にリストを指定すると想定通りの結果にならない。np.ix_()を使う。

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

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

一次元の例と同様、順番が前後したり重複したりしてもよい。負の値も使用可能。

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

要素数1のリストで選択した場合、スライスやブーリアンインデックスと同様、元のndarrayの次元数が保持される。整数のスカラー値で指定した場合とは結果が異なるので注意。

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

print(a_2d[:, [1]].shape)
# (3, 1)

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

print(a_2d[:, 1].shape)
# (3,)

異なる指定方法の組み合わせ

これまでの例にも出てきているが、次元ごとに異なる形式で指定してもよい。

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[1, 1:3])
# [5 6]

print(a_2d[1, [False, True, True, False]])
# [5 6]

print(a_2d[1, [1, 2]])
# [5 6]

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

print(a_2d[1:, [False, True, True, False]])
# [[ 5  6]
#  [ 9 10]]

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

ブール値のリスト(ブーリアンインデックス)と整数値のリスト(ファンシーインデックス)の組み合わせはnp.ix_()を使う必要がある。

print(a_2d[[False, True, True], [1, 2]])
# [ 5 10]

print(a_2d[np.ix_([False, True, True], [1, 2])])
# [[ 5  6]
#  [ 9 10]]

np.ix_()には一次元のリストや配列しか指定できないので注意。

例えば、三次元以上の多次元配列で複数のリストを指定する場合はnp.ix_()を使う必要があるが、スカラー値やスライスと同時に使えない。

a_3d = np.arange(24).reshape(2, 3, 4)
print(a_3d)
# [[[ 0  1  2  3]
#   [ 4  5  6  7]
#   [ 8  9 10 11]]
# 
#  [[12 13 14 15]
#   [16 17 18 19]
#   [20 21 22 23]]]

# print(a_3d[np.ix_(0, [True, True, False], [0, 2])])
# ValueError: Cross index must be 1 dimensional

# print(a_3d[np.ix_(:1, [True, True, False], [0, 2])])
# SyntaxError: invalid syntax

スカラー値は要素数1のリストやndarrayとして指定する。この場合、結果の次元数は元のndarrayと同じ。

print(a_3d[np.ix_([0], [True, True, False], [0, 2])])
# [[[0 2]
#   [4 6]]]

スライスの代わりにはrange()を使う方法がある。range(a.shape[n])[::]のように、shape属性で対象の次元のサイズを取得しrange()に渡してからスライスを適用する。もっといい方法があるかもしれない。

print(a_3d[np.ix_(range(a_3d.shape[0])[:1], [True, True, False], [0, 2])])
# [[[0 2]
#   [4 6]]]

print(list(range(5)[::2]))
# [0, 2, 4]

print(list(range(5)[1:3]))
# [1, 2]

選択範囲に新たな値・配列を代入

これまでの例では範囲を選択して要素や部分配列の値を取得していたが、選択範囲に新たな値や配列を代入することも可能。代入する場合は次に説明するビューとコピーの違いに注意。

右辺にスカラー値を指定すると、左辺の選択範囲のすべての要素にその値が代入される。

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[0, 0] = 10
print(a_2d)
# [[10  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

a_2d[1] = 100
print(a_2d)
# [[ 10   1   2   3]
#  [100 100 100 100]
#  [  8   9  10  11]]

a_2d[np.ix_([False, True, True], [1, 3])] = 1000
print(a_2d)
# [[  10    1    2    3]
#  [ 100 1000  100 1000]
#  [   8 1000   10 1000]]

右辺には配列も指定できる。

左辺の選択範囲と形状shapeが一致しているとそのまま代入される。以下の例のように、飛び飛びの場所でも問題ない。

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, 1:])
# [[ 1  2  3]
#  [ 9 10 11]]

print(np.arange(6).reshape(2, 3) * 100)
# [[  0 100 200]
#  [300 400 500]]

a_2d[::2, 1:] = np.arange(6).reshape(2, 3) * 100
print(a_2d)
# [[  0   0 100 200]
#  [  4   5   6   7]
#  [  8 300 400 500]]

左辺の選択範囲と右辺の配列の形状が一致していない場合、ブロードキャストして代入される。

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, 1:])
# [[ 1  2  3]
#  [ 9 10 11]]

print(np.arange(3) * 100)
# [  0 100 200]

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

ブロードキャストできない形状だとエラー。

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, 1:])
# [[ 1  2  3]
#  [ 9 10 11]]

print(np.arange(2) * 100)
# [  0 100]

# a_2d[::2, 1:] = np.arange(2) * 100
# ValueError: could not broadcast input array from shape (2,) into shape (2,3)

ブロードキャストについては以下の記事を参照。

ビュー(参照)とコピー

部分配列を選択する場合、各次元の位置の指定形式によって、ビュー(参照)を返すものとコピーを返すものがある。

例えば、スライスを使うとビューが返される。

2つのndarrayが同じメモリのデータを参照している(片方がもう片方のビューである)かどうかはnp.shares_memory()で確認できる。

a = np.arange(6).reshape(2, 3)
print(a)
# [[0 1 2]
#  [3 4 5]]

a_slice = a[:, :2]
print(a_slice)
# [[0 1]
#  [3 4]]

print(np.shares_memory(a, a_slice))
# True

ビューの場合、選択した部分配列の要素の値を変更すると元の配列の要素の値も変わる。逆に元の配列の要素の値を変更すると部分配列の要素の値も変わる。

a_slice[0, 0] = 100
print(a_slice)
# [[100   1]
#  [  3   4]]

print(a)
# [[100   1   2]
#  [  3   4   5]]

a[0, 0] = 0
print(a)
# [[0 1 2]
#  [3 4 5]]

print(a_slice)
# [[0 1]
#  [3 4]]

bool値のリストで指定するブーリアンインデックスや、整数値のリストで指定するファンシーインデックスの場合、ビューではなくコピーが返される。

a_boolean_index = a[:, [True, False, True]]
print(a_boolean_index)
# [[0 2]
#  [3 5]]

print(np.shares_memory(a, a_boolean_index))
# False

この場合、選択した部分配列の要素の値を変更しても元の配列はそのまま。逆も同様。

a_boolean_index[0, 0] = 100
print(a_boolean_index)
# [[100   2]
#  [  3   5]]

print(a)
# [[0 1 2]
#  [3 4 5]]

ndarraycopy()メソッドを使うとコピーが生成される。スライスで選択した部分配列を元の配列とは別々に処理したい場合はcopy()を使えばよい。

a_slice_copy = a[:, :2].copy()
print(a_slice_copy)
# [[0 1]
#  [3 4]]

print(np.shares_memory(a, a_slice_copy))
# False

a_slice_copy[0, 0] = 100
print(a_slice_copy)
# [[100   1]
#  [  3   4]]

print(a)
# [[0 1 2]
#  [3 4 5]]

異なる指定方法を組み合わせる場合、ブーリアンインデックスやファンシーインデックスが含まれているとコピーが返される。

a_fancy_index_slice = a[:, [0, 2]]
print(a_fancy_index_slice)
# [[0 2]
#  [3 5]]

print(np.shares_memory(a, a_fancy_index_slice))
# False

整数のスカラー値とスライスの場合はビュー。

a_scalar_slice = a[1, :2]
print(a_scalar_slice)
# [3 4]

print(np.shares_memory(a, a_scalar_slice))
# True

関連カテゴリー

関連記事