ndarrayオブジェクトではPythonのリスト(シーケンス)のようにインデックスやスライスを用いた要素の選択(取り出しや代入)が可能です。インデックスやスライスは配列に角かっこ「[]」を付加し、その中に記述します。ただし、NumPyでは複数の次元を持つ配列については「配列[インデックス0, インデックス1, ……]」のように記述するのが一般的です。
Pythonのリストではこれを「リスト[インデックス0][インデックス1]」のように記述します。NumPyでもインデックスを指定する場合には「配列[インデックス0][インデックス1]」のように記述しても、その結果は「配列[インデックス0, インデックス1]」と記述したときと変わりませんが、効率が少し悪くなるようです。
以下に例を示します。
a = np.arange(0, 6).reshape((2, -1))
print(a)
# 出力結果:
#[[0 1 2]
# [3 4 5]]
x = a[0] # 第0行の値(一次元配列)を選択
print(x) # [0, 1, 2]
x = a[1] # 第1行の値(一次元配列)を選択
print(x) # [3, 4, 5]
x = a[0, 0] # 第0行第0列の値(この場合は整数値)を選択
print(x) # 0
x = a[1, 2] # 第1行第2列の値(この場合は整数値)を選択
print(x) # 5
x = a[-1] # Pythonのシーケンスと同様に-1は最後の要素を表す
print(x) # [3, 4, 5]
x = a[-1, -1] # 末尾行の末尾列の値(この場合は整数値)を選択
print(x) # 5
記述が少し異なる点を除けば、特に説明の必要はないでしょう。なお、複数の次元を持つ配列に対してインデックスの指定を行う際に、その次元数よりも少ない数のインデックスを指定したときには、元の配列の次元数が削減されたものが返されることには注意してください(といっても、これはPythonのリストでも同様ですね)。例えば、2次元配列に対してインデックスを1つだけ指定すると、返されるのは2次元配列から1つ次元が削減された1次元配列となります。
a = np.arange(0, 12).reshape(3, 4) # 2次元配列
print(a)
# 出力結果:
#[[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
tmp = a[1] # 変数tmpは第0行の1次元配列を参照する
print(tmp) # [4 5 6 7]
対して、2次元配列に対して、インデックスを2つ指定するとスカラー(単体の値)が得られます(2つ上のコード例を参照)。
このときに取得できるのは、元の配列のビューです。ビューというのは、元の配列に格納されている個々の要素を参照できるオブジェクトです。つまり、ビューに格納されている値を変更すると、元の配列の値も変更されます。
先ほどのコード例では変数tmpに代入されたのはビューです。そこで、その要素を変更してから、元の配列の値を表示してみましょう。
tmp[0] = 40 # ビューの先頭要素の値を変更
print(tmp) # [40 5 6 7]
print(a) # 変更が元の配列に反映される
# 出力結果:
#[[ 0 1 2 3]
# [40 5 6 7]
# [ 8 9 10 11]]
上でも行っているように、ndarrayオブジェクトの要素を変更するには、変更したい要素をインデックス指定して、そこに代入するだけです。
a[0, 0] = 100
print(a)
# 出力結果:
#[[100 1 2 3]
# [ 40 5 6 7]
# [ 8 9 10 11]]
ところで、ndarrayオブジェクトではその要素の型は全て同じでした(これはPythonのリストとは違うところです)。そのため、代入しようとした値がndarrayオブジェクトの要素の型(dtype属性で調べられるのは前回にも紹介した通りです)と違っているときには、その値を要素の型に変換できるのであれば代入が行われ、変換できないときには例外が発生します。
print(a.dtype) # int64
a[0, 0] = 123.4 # 浮動小数点数値は整数値に変換される
print(a)
# 出力結果:
#[[123 1 2 3]
# [ 40 5 6 7]
# [ 8 9 10 11]]
a[0, 0] = 'c' # ValueError:文字'c'を代入することはできない
a[0, 0] = 1+1j # TypeError:複素数を整数には変換できない
この例では、配列aの要素の型(dtype属性)はint64になっています。そして、第0行第0列に浮動小数点数値である123.4を代入しようとしています。このとき、浮動小数点数値は整数値に型変換が可能であるため、値が丸められて123が代入されています。
その一方、文字列を表す'c'や複素数である1+1jを代入しようとすると、それらは整数値には変換できないために例外が発生するというわけです。
スライスによる要素の選択も基本的にはPythonのリスト(シーケンス)と同様です。ただし、角かっこ「[]」にスライスをカンマ区切りで書く点はインデックスと同様です。
以下はスライスによる要素選択の例です。分かりやすいようにまずは1次元配列を作成して、その要素をスライスで選択してみましょう。
a = np.arange(0, 10) # 1次元配列を作成
print(a) # [0 1 2 ... 8 9]
# 先頭要素から第5要素(の1つ手前の要素)までを選択
print(a[0:5]) # [0 1 2 3 4]
print(a[:5]) # [0 1 2 3 4]
# 第1要素から第6要素(の1つ手前の要素)までを選択
print(a[1:6]) # [1 2 3 4 5]
# 末尾の要素から先頭の要素までを逆順に選択
print(a[-1::-1]) # [9 8 7 6 5 4 3 2 1 0]
# 先頭要素から末尾要素までの要素を1つとびに選択
print(a[0::2]) # [0 2 4 6 8]
print(a[::2]) # [0 2 4 6 8]
# 全要素を選択
print(a[:]) # [0 1 2 3 4 5 6 7 8 9]
print(a[::-1]) # [9 8 7 6 5 4 3 2 1 0]
これらはPythonのシーケンスをスライスで指定するのと同様なので、説明の必要はないでしょう。
次に2次元配列に対してスライスを指定する例です。
a = np.arange(0, 12).reshape((3, 4))
print(a)
# 出力結果:
#[[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
# 2次元配列で1次元のスライスを指定すると対応する行を要素とする配列が返される
s = a[0:2] # 第0行と第1行を要素とする配列を選択
print(s.shape) # (2, 4):2行4列の2次元配列
print(s)
# 出力結果:
#[[0 1 2 3]
# [4 5 6 7]]
ここでは3行4列の配列を作成して、「a[0:2]」とスライスを指定しています。その結果、第0行と第1行の2つの行を含む配列が返されます。
ここで注意したいのは、2次元配列に対して1つの「インデックス」を指定すると、その次元が1つ減り、1次元の配列が返されていたのに、2次元配列に対して1つの「スライス」を指定しても、次元は削減されずに2次元の配列が返される点です。
# インデックスとスライスで挙動の違い
s = a[0:1] # 第0行を要素とする配列
print(s.shape) # (1, 4)
print(s) # [[0 1 2 3]]
s = a[0] # 第0行を選択
print(s.shape) # (4,)
print(s) # [0 1 2 3]
# この挙動はリストでも同じ
l = [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]]
print(l[0:1]) # [[0, 1, 2, 3]]
print(l[0]) # [0, 1, 2, 3]
とはいえ、上のコード例に示したように、これはPythonのリスト(シーケンス)に対するインデックス指定とスライス指定の振る舞いの差と同様なので慣れている人にとっては珍しいことではないかもしれませんね。
それよりも、角かっこ内にスライスを2つ指定したときと、角かっこを連続させたときとで振る舞いが異なることの方が注意が必要かもしれません。
# インデックスの指定では角かっこにインデックスを複数指定するのと
# 角かっこを連続するのと挙動は変わらない
i = a[0, 3]
print(i) # 3
i = a[0][3]
print(i) # 3
# スライスの指定では挙動が異なる
s = a[0:1, 0:2]
print(s) # [[0 1]]
s = a[0:1][0:2]
print(s) # [[0 1 2 3]]
上のコードの「a[0:1, 0:2]」というのは、2次元配列に対するスライス指定で最初の「0:1」は第0行のみを選択範囲として、次の「0:2」はその選択範囲の中で第0要素から第1要素までを選択することになります。この結果、「[[0 1]]」という2次元配列が得られます(最初のスライス指定が「0:2」であれば2行分の要素が選択されることを考えれば、これが2次元配列となることには納得できるでしょう)。
これに対して、上のコードの「a[0:1][0:2]」を簡単に説明すると、スライス指定で角かっこを連続させる場合、「a[0:1]」で第0行を要素とする2次元配列「[[0 1 2 3]]」が取り出され、次の「[0:2]」によりその第0要素から第1要素(を含む2次元配列)が選択されるので、結果として「[[0 1 2 3]]」が返されるということです。
とはいえ、2次元配列から特定の範囲(スライス)を選択したければ、角かっこ内にスライスをカンマ区切りで並べればよいと覚えれば、それほど難しくはないかもしれません。
小難しい話をしてしまいましたが、以下はもっと単純なスライス指定の例です。
# 各行各列の全容を選択
print(a[:, :])
# 出力結果:
#[[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
# 各行各列の要素を1つ飛ばしで選択(0行目と2行目の0列目と2列目)
print(a[::2, ::2])
# 出力結果:
#[[ 0 2]
# [ 8 10]]
次にスライスで指定された範囲への代入ですが、このときには代入元と代入先ではその要素数が同じか、ブロードキャストにより形状を揃えられなければ代入はできません。またしても、分かりやすいように1次元配列から話を始めましょう。
a = np.arange(6)
print(a) # [0 1 2 3 4 5]
print(a[:2]) # [0 1]
a[:2] = np.array([10, 11]) # 2つの要素のスライスに2つの要素の配列を代入
print(a) # [10 11 2 3 4 5]
a[:2] = 100 # 2つの要素のスライスに単一の要素を代入(ブロードキャストが働く)
print(a) # [100 100 2 3 4 5]
a[:2] = [100, 200, 300] # ValueError:形状が合わない
ここでは6要素の1次元配列を生成しています。そして、「a[:2] = np.array([10, 11])」として、スライスで選択された2要素に同じく2要素の配列を代入しています。これは要素数が同じなので代入可能です。次の「a[:2] = 100」はスカラーである100がブロードキャストされるので、これもまた代入できます。
しかし、同じく2要素の「a[:2]」に3つの要素からなるリストを代入しようとすると形状が異なり、ブロードキャストもできないので例外が発生します(形状が合っていれば、numpy.array関数でndarrayオブジェクトを作成しなくても、リストの要素をそのまま代入可能です)。
このことは2次元以上の配列についても同様です。説明は省略しますが、以下にコード例を示します。
a = np.arange(12).reshape(3, 4)
print(a)
# 出力結果:
#[[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
b = np.array([[0, 20], [80, 100]])
a[::2, ::2] = b # 0行目と2行目の0列目と2列目に代入
print(a)
# 出力結果:
#[[ 0 1 20 3]
# [ 4 5 6 7]
# [ 80 9 100 11]]
b = np.array([0, 2]) # 形状が異なるがブロードキャストできるので代入可能
a[::2, ::2] = b
print(a)
# 出力結果:
#[[ 0 1 2 3]
# [ 4 5 6 7]
# [ 0 9 2 11]]
b = np.array([[0, 2], [8, 10], [12, 14]])
a[::2, ::2] = b # 形状が合うようにブロードキャストできないので代入できない
とここまで、ndarrayオブジェクトの形状や四則演算、インデックスとスライスによる要素の選択と代入の基本について見てきました。少々長くなったので、今回はここまでとして、次回はもう少し高度なndarrayオブジェクトの要素の操作について紹介します。
Copyright© Digital Advantage Corp. All Rights Reserved.