note.nkmk.me

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

Date: 2019-06-16 / tags: Python, NumPy

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

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

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

  • 配列ndarrayの要素や部分配列(行・列など)の選択の基本
  • 整数値(インデックス)で指定
  • スライスで指定
  • bool値のリストで指定(ブーリアンインデックス)
  • 整数値のリストで指定(ファンシーインデックス)
  • 異なる指定方法の組み合わせ
  • 選択範囲に新たな値・配列を代入
  • ビュー(参照)とコピー

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

スポンサーリンク

配列ndarrayの要素や部分配列(行・列など)の選択の基本

NumPy配列ndarrayの要素や部分配列(行・列など)は[○, ○, ○, ...]のように各次元の位置をカンマ区切りで指定する。

import numpy as np

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

print(a_1d[3])
# 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[0, 2])
# 2

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

各次元の位置は、以下のように様々な形式で指定できる。

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

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

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

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

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

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

すべての次元を整数intで指定した場合、要素の値が返される。その型は元のndarrayのデータ型dtypeによる。

後ろの次元を省略した場合は配列ndarrayが返される。

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

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

スライスで指定

スライス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]]

スライスx:x+1で幅1の範囲(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,)

上述のように、すべての次元を整数値で指定すると配列ndarrayではなくスカラー値が返される。

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(type(a_2d[1, 2]))
# <class 'numpy.int64'>

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

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

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

print(a_3d[0, 0])
# [0 1 2 3]

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

print(a_3d[0:1, 0:1, 0:1])
# [[[0]]]

print(a_3d[0:1, 0:1])
# [[[0 1 2 3]]]

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

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

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

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

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の範囲(1行や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(type(a_2d > 5))
# <class 'numpy.ndarray'>

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でもOK。

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

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

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

print(a_1d[np.array([0, 3, 2, 1, 2, -1, -2])])
# [0 3 2 1 2 3 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[[0, 2]])
# [[ 0  1  2  3]
#  [ 8  9 10 11]]

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

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

print(a_2d[[0, 2], [0, 2]])
# [ 0 10]

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

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

print(a_2d[np.ix_([0, 2, 1, 1, -1, -1], [0, 2, 1, 3])])
# [[ 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,)

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

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

二次元配列の例。

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

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

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

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

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

三次元以上の多次元配列の場合、ブール値のリストまたは整数値のリストを適用するのがひとつの次元だけだと特に問題ない。

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[:, :2, [True, False, True, False]])
# [[[ 0  2]
#   [ 4  6]]
# 
#  [[12 14]
#   [16 18]]]

print(a_3d[:, :2, [0, 2]])
# [[[ 0  2]
#   [ 4  6]]
# 
#  [[12 14]
#   [16 18]]]

ブール値のリストまたは整数値のリストを複数次元に適用する場合、[]にそのまま指定してもnp.ix_()を使ってもうまくいかない。

np.ix_()の中で使えるのは一次元のリストやndarrayのみで、スライスが使えない。

print(a_3d[:, [0, 2], [0, 2]])
# [[ 0 10]
#  [12 22]]

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

スライスの代わりにrange()を使う方法がある。全体を選択する場合はshape属性を使うとサイズによらずに指定可能。

print(a_3d[np.ix_(range(2), [0, 2], [0, 2])])
# [[[ 0  2]
#   [ 8 10]]
# 
#  [[12 14]
#   [20 22]]]

print(a_3d[np.ix_(range(a_3d.shape[0]), [0, 2], [0, 2])])
# [[[ 0  2]
#   [ 8 10]]
# 
#  [[12 14]
#   [20 22]]]

np.ix_()の中ではスカラー値も使えない。要素数1のリストやndarrayとして指定する必要がある。この場合、結果の次元数は元のndarrayと同じ。

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

print(a_3d[np.ix_([0], [0, 2], [0, 2])])
# [[[ 0  2]
#   [ 8 10]]]

もしかするともっといい方法があるかもしれない

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

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

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

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

a_2d[0] = 100
print(a_2d)
# [[100 100 100 100]
#  [  4   5   6   7]
#  [  8   9  10  11]]

a_2d[np.ix_([False, True, True], [1, 3])] = 200
print(a_2d)
# [[100 100 100 100]
#  [  4 200   6 200]
#  [  8 200  10 200]]

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

左辺の選択範囲と形状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, :3])
# [[ 0  1  2]
#  [ 8  9 10]]

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

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

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

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

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

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

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

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

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

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

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

ビュー(参照)とコピー

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

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

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

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

a_slice = a_2d[:2, :2]
print(a_slice)
# [[0 1]
#  [4 5]]

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

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

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

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

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

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

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

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

print(np.shares_memory(a_2d, a_fancy_index))
# False

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

a_fancy_index[0, 0] = 100
print(a_fancy_index)
# [[100   1   2   3]
#  [  4   5   6   7]]

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

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

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

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

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

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

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

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

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

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

a_scalar_slice = a_2d[1, :3]
print(a_scalar_slice)
# [4 5 6]

print(np.shares_memory(a_2d, a_scalar_slice))
# True
スポンサーリンク
シェア
このエントリーをはてなブックマークに追加

関連カテゴリー

関連記事