Python♪NumPyのa[[0], 0, 0:1]は何次元の配列になる?

NumPyの配列の要素はスライスやリスト(配列)を使って部分的に要素を抜き出せますが、抜き出した配列の次元がどうなるのか混乱しませんか?しかし、実は抜き出し後の配列の次元数は機械的に判断できます。最初に知っておくと頭に入りやすくなります。

1.要素を整数で指定する場合

まず、最初に要素のインデックス番号を整数で指定する場合の規則性を説明します。何次元の配列になるのかについてはコード01の15~23行目の右側にコメント文を追加しました。

要素を整数で直接指定する場合の規則性として、要素を指定した数だけ出力の次元が減ります。

16行目のa[:, :, :]は各要素をスライスで指定しており、配列aの内容がそのまま出力されますが、17行目以降のコードでは、要素を直接指定した分だけ出力される配列の次元が減っていることがわかります。

#コード01
import numpy as np
a = np.arange(24).reshape(2, 3, 4)
'''
a =
[[[ 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)  #3次元配列を出力
print(a[:, :, :])  #3次元配列を出力
print(a[0, :, :])  #2次元配列を出力
print(a[:, 1, :])  #2次元配列を出力
print(a[:, :, 2])  #2次元配列を出力
print(a[:, 1, 2])  #1次元配列を出力
print(a[0, :, 2])  #1次元配列を出力 
print(a[0, 1, :])  #1次元配列を出力 
print(a[0, 1, 2])  #値(スカラー)を出力

以下をクリックすると、コード01の出力が表示されます。参考資料としてご使用ください。まずは出力配列が何次元になるかだけ分かればよいと思いますので、開かずに次に進んでいただいてもかまいません。

出力01の表示・非表示の切り替え

※ブラウザによっては最初から表示されてしまいます。(Google Chrome推奨)

#出力01
※読みやすいように実際の出力にコメント文を追加しています。
# a =
[[[ 0  1  2  3]
  [ 4  5  6  7]
  [ 8  9 10 11]]

 [[12 13 14 15]
  [16 17 18 19]
  [20 21 22 23]]]
# a[:, :, :] =
[[[ 0  1  2  3]
  [ 4  5  6  7]
  [ 8  9 10 11]]

 [[12 13 14 15]
  [16 17 18 19]
  [20 21 22 23]]]
# a[0, :, :] =
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
# a[:, 1, :] =
[[ 4  5  6  7]
 [16 17 18 19]]
# a[:, :, 2] =
[[ 2  6 10]
 [14 18 22]]
# a[:, 1, 2] =
[ 6 18]
# a[0, :, 2] =
[ 2  6 10]
# a[0, 1, :] =
[4 5 6 7]
# a[0, 1, 2] =
6

なお、少し説明が横道にそれますが、スライス「:」が後ろ側にあるばあいだけスライスを省略できます。一方、a[:, :, 2]をa[, , 2]のようにスライスが前方にある場合は省略することができません。ついでに覚えておきましょう。

#コード02
import numpy as np
a = np.arange(24).reshape(2, 3, 4)

print(a[0, 1, 2])
print(a[0, 1])    #a[0, 1, :]の省略形
print(a[0])       #a[0, :, :]の省略形
print(a)          #a[:, :, :]の省略形

#print(a[, , 2])  #エラー

2.スライスで要素を部分的に指定する場合

NumPy配列ではa[1:3]のようにスライスで要素を部分的に指定することができます。この場合は出力される配列の次元は変化しません。

ここで注意しなければならないのは、a[0:1]のような場合です。a[0:1]はインデックス番号0の要素だけを指定するスライスですが、次元は減りません。a[0]の場合は次元が減りますから、混同しないようにしてください。

コード03の15~19行目は全て出力結果は3次元のままです。

#コード03
import numpy as np
a = np.arange(24).reshape(2, 3, 4)

print(a[:, :, :])  #3次元配列を出力
print(a[:, 0:2, :])  #3次元配列を出力
print(a[:, 0:1, :])  #3次元配列を出力
print(a[0:1, :, 0:1])  #3次元配列を出力
print(a[0:1, 0:1, 0:1])  #3次元配列を出力
出力03の表示・非表示の切り替え

※ブラウザによっては最初から表示されてしまいます。(Google Chrome推奨)

#出力03
# a[:, :, :] =
[[[ 0  1  2  3]
  [ 4  5  6  7]
  [ 8  9 10 11]]

 [[12 13 14 15]
  [16 17 18 19]
  [20 21 22 23]]]
# a[:, 0:2, :] =
[[[ 0  1  2  3]
  [ 4  5  6  7]]

 [[12 13 14 15]
  [16 17 18 19]]]
# a[:, 0:1, :] =
[[[ 0  1  2  3]]

 [[12 13 14 15]]]
# a[0:1, :, 0:1] =
[[[0]
  [4]
  [8]]]
# a[0:1, 0:1, 0:1]) =
[[[0]]]

コード04をみると、要素を整数で直接指定した場合とスライスで指定した場合の違いがよくわかります。5行目のa[0, 0, 0]の出力は0ですが、6行目のa[0:1, 0:1, 0:1]の出力は[[[0]]]です。

#コード04
import numpy as np
a = np.arange(24).reshape(2, 3, 4)

print(a[0, 0, 0]) #1次元配列を出力
print(a[0:1, 0:1, 0:1])  #3次元配列を出力
#出力04
0
[[[0]]]

3.listやNumPy配列で要素を指定する場合

listやNumPy配列を利用したインデックスはファンシーインデックスとも呼ばれます。ファンシーインデックスを使うことで、柔軟な要素指定が可能になります。

ファンシーインデックスについては以下の記事を参考にしてください。

この次のファンシーインデックスとスライスを混在させた指定方法を理解するためにも、ファンシーインデックスのしくみを完全に理解しておいてください。

NumPy♪ファンシーインデックスが苦手だと感じたら

4.配列とスライスで指定する場合の配列部の扱い

では、ファンシーインデックス(list、NumPy配列)とスライスを混在させて要素を指定する場合、ファンシーインデックスの部分はどのように扱われるのでしょうか。

コード05の11行目のd3[:, [0, 1, 0], [0]].shapeは(2, 3)です。配列の形状(2, 3)の2は、d3[:, [0, 1, 0], [0]].shapeの0番目の次元の「:」です。

一方、d3[:, [0, 1, 0], [0]].shapeの1番目の次元の[0, 1, 0]と2番目の次元の[0]をブロードキャストによって形状をそろえると[0, 1, 0][0, 0, 0]になり、その形状は(3, )です。この3が、d3[:, [0, 1, 0], [0]].shapeの形状(2, 3)の3の部分です。

なお、11行目の[0, 1, 0], [0]、13行目の[0, 1, 0], [0]、15行目の[0, 1, 0], [0, 0, 0]はいずれもブロードキャストによって形状をそろえると[0, 1, 0]と[0, 0, 0]になるため、全く同じ結果になります。

つまり、ファンシーインデックスとスライスを混在させた場合、ファンシーインデックスの部分はブロードキャストによって形状がそろえられます。

この説明がわかりにくい方は、やはり以下の記事を先に読んでください。
NumPy♪ファンシーインデックスが苦手だと感じたら

#コード05
import numpy as np
d3 = np.array([[[  0.,   1.,   2.,   3.],
                [ 10.,  11.,  12.,  13.],
                [ 20.,  21.,  22.,  23.]],
               [[100., 101., 102., 103.],
                [110., 111., 112., 113.],
                [120., 121., 122., 123.]]])
print(d3.shape)  #(2, 3, 4)
print(d3)
print(d3[:, [0, 1, 0], 0].shape)  #(2, 3)
print(d3[:, [0, 1, 0], 0])
print(d3[:, [0, 1, 0], [0]].shape)  #(2, 3)
print(d3[:, [0, 1, 0], [0]])
print(d3[:, [0, 1, 0], [0, 0, 0]].shape)  #(2, 3)
print(d3[:, [0, 1, 0], [0, 0, 0]])
#コード05
(2, 3, 4)
[[[  0.   1.   2.   3.]
  [ 10.  11.  12.  13.]
  [ 20.  21.  22.  23.]]

 [[100. 101. 102. 103.]
  [110. 111. 112. 113.]
  [120. 121. 122. 123.]]]
(2, 3)
[[  0.  10.   0.]
 [100. 110. 100.]]
(2, 3)
[[  0.  10.   0.]
 [100. 110. 100.]]
(2, 3)
[[  0.  10.   0.]
 [100. 110. 100.]]

では、ファンシーインデックスの部分を少しだけ変えてみます。

コード06の9行目のd3[:, [0], [0]].shapeの形状(2, 1)の2の部分は0番目の次元の「:」です。1の部分は、1番目の次元[0]と2番目の次元[0]の形状(1, )の1です。

11行目のd3[:, [[0, 1, 0]], [0, 0, 0]].shapeでは、1番目の次元の[[0, 1, 0]]と2番目の次元の[0, 0, 0]をブロードキャストで形状をそろえると[[0, 1, 0]][[0, 0, 0]]となり、形状は(1, 3)になります。この1, 3がd3[:, [[0, 1, 0]], [0, 0, 0]].shapeの形状(2, 1, 3)の1, 3の部分です。

#コード06
import numpy as np
d3 = np.array([[[  0.,   1.,   2.,   3.],
                [ 10.,  11.,  12.,  13.],
                [ 20.,  21.,  22.,  23.]],
               [[100., 101., 102., 103.],
                [110., 111., 112., 113.],
                [120., 121., 122., 123.]]])
print(d3[:, [0], [0]].shape)  #(2, 1)
print(d3[:, [0], [0]])
print(d3[:, [[0, 1, 0]], [0, 0, 0]].shape)  #(2, 1, 3)
print(d3[:, [[0, 1, 0]], [0, 0, 0]])
#出力06
(2, 1)
[[  0.]
 [100.]]
(2, 1, 3)
[[[  0.  10.   0.]]

 [[100. 110. 100.]]]

このようにスライス以外の部分は、ブロードキャストによって形状がそろえられるということを覚えておいてください。従って、コード07では出力07のようにbroadcastのエラーが発生します。

#コード07
import numpy as np
d3 = np.array([[[  0.,   1.,   2.,   3.],
                [ 10.,  11.,  12.,  13.],
                [ 20.,  21.,  22.,  23.]],
               [[100., 101., 102., 103.],
                [110., 111., 112., 113.],
                [120., 121., 122., 123.]]])

print(d3[:, [0, 1, 0], [0, 0, 0, 0]].shape)
#出力07
IndexError: shape mismatch: indexing arrays could not be broadcast together with shapes (3,) (4,) 

5.配列とスライスで指定する場合の配列の形状

ファンシーインデックス(list、NumPy配列)とスライスを混在させて要素を指定する場合、配列の各次元の長さはどのような順番になるのでしょうか。

コード08は、NumPy配列とスライスを混在させた例であり、各行の#の後に出力結果を示します。np2の形状(2, )がどの位置になるか注目してください。

16行目のd5[:,:,np2,np2,:].shapeの出力結果は(3, 4, 2, 7)です。np2が連続している場合は(3, 4, 2, 7)のようにnp2の形状が途中に入ります。なお、(3, 4, 2, 7)の3, 4, 7はスライス「:」部分の次元の長さです。

一方、17行目のd5[:,np2,:,np2,:].shapeの出力結果は(2, 3, 5, 7)です。np2がスライス「:」によって分断されている場合は(2, 3, 5, 7)のようにnp2の形状が先頭になります

#コード08
import numpy as np
d5 = np.arange(2520).reshape((3, 4, 5, 6, 7))
np2=np.array([0, 1])

print(d5[:,:,:,:,np2].shape)  #(3, 4, 5, 6, 2)
print(d5[:,:,:,np2,:].shape)  #(3, 4, 5, 2, 7)
print(d5[:,:,np2,:,:].shape)  #(3, 4, 2, 6, 7)
print(d5[:,np2,:,:,:].shape)  #(3, 2, 5, 6, 7)
print(d5[np2,:,:,:,:].shape)  #(2, 4, 5, 6, 7)

print(d5[:,:,:,np2,np2].shape)  #(3, 4, 5, 2)
print(d5[:,:,np2,:,np2].shape)  #(2, 3, 4, 6)
print(d5[:,np2,:,:,np2].shape)  #(2, 3, 5, 6)
print(d5[np2,:,:,:,np2].shape)  #(2, 4, 5, 6)
print(d5[:,:,np2,np2,:].shape)  #(3, 4, 2, 7)
print(d5[:,np2,:,np2,:].shape)  #(2, 3, 5, 7)
print(d5[np2,:,:,np2,:].shape)  #(2, 4, 5, 7)
print(d5[:,np2,np2,:,:].shape)  #(3, 2, 6, 7)
print(d5[np2,:,np2,:,:].shape)  #(2, 4, 6, 7)
print(d5[np2,np2,:,:,:].shape)  #(2, 5, 6, 7)

print(d5[:,:,np2,np2,np2].shape)  #(3, 4, 2)
print(d5[:,np2,:,np2,np2].shape)  #(2, 3, 5)
print(d5[:,np2,np2,:,np2].shape)  #(2, 3, 6)
print(d5[:,np2,np2,np2,:].shape)  #(3, 2, 7)
print(d5[np2,:,:,np2,np2].shape)  #(2, 4, 5)
print(d5[np2,:,np2,:,np2].shape)  #(2, 4, 6)
print(d5[np2,:,np2,np2,:].shape)  #(2, 4, 7)
print(d5[np2,np2,:,:,np2].shape)  #(2, 5, 6)
print(d5[np2,np2,:,np2,:].shape)  #(2, 5, 7)
print(d5[np2,np2,np2,:,:].shape)  #(2, 6, 7)

print(d5[:,np2,np2,np2,np2].shape)  #(3, 2)
print(d5[np2,:,np2,np2,np2].shape)  #(2, 4)
print(d5[np2,np2,:,np2,np2].shape)  #(2, 5)
print(d5[np2,np2,np2,:,np2].shape)  #(2, 6)
print(d5[np2,np2,np2,np2,:].shape)  #(2, 7)

練習のために1行2列のNumPy配列[[0, 1]]を使用した場合の例もコード09に示します。

こんどは、np21の形状(1, 2)がどの位置になるか注目してください。例えば6行目のd5[:,:,:,:,np12].shapeの形状(3, 4, 5, 6, 1, 2)では一番最後にnp21の形状(1, 2)が配置されています。

コード08の時と同じルールで形状が決定されていることがわかります。

#コード09
import numpy as np
d5 = np.arange(2520).reshape((3, 4, 5, 6, 7))
np12=np.array([[0, 1]])

print(d5[:,:,:,:,np12].shape)  #(3, 4, 5, 6, 1, 2)
print(d5[:,:,:,np12,:].shape)  #(3, 4, 5, 1, 2, 7)
print(d5[:,:,np12,:,:].shape)  #(3, 4, 1, 2, 6, 7)
print(d5[:,np12,:,:,:].shape)  #(3, 1, 2, 5, 6, 7)
print(d5[np12,:,:,:,:].shape)  #(1, 2, 4, 5, 6, 7)

print(d5[:,:,:,np12,np12].shape)  #(3, 4, 5, 1, 2)
print(d5[:,:,np12,:,np12].shape)  #(1, 2, 3, 4, 6)
print(d5[:,np12,:,:,np12].shape)  #(1, 2, 3, 5, 6)
print(d5[np12,:,:,:,np12].shape)  #(1, 2, 4, 5, 6)
print(d5[:,:,np12,np12,:].shape)  #(3, 4, 1, 2, 7)
print(d5[:,np12,:,np12,:].shape)  #(1, 2, 3, 5, 7)
print(d5[np12,:,:,np12,:].shape)  #(1, 2, 4, 5, 7)
print(d5[:,np12,np12,:,:].shape)  #(3, 1, 2, 6, 7)
print(d5[np12,:,np12,:,:].shape)  #(1, 2, 4, 6, 7)
print(d5[np12,np12,:,:,:].shape)  #(1, 2, 5, 6, 7)

6.まとめ

ファンシーインデックスとスライスが混在する場合は、スライス以外の部分をブロードキャストで形状をそろえ、スライスがファンシーインデックスを分断しているかどうかに注目すれば、どんな複雑なものでも対応することができます。

私も最初は、なぜコード10の5行目d4[:,np2,:,np2].shapeの結果が(3, 2, 5)にならないのか悩んだのですが、コード08、コード09のように少し大きな配列を確かめることによって頭を整理することができました。

#コード10
import numpy as np
d4 = np.arange(360).reshape((3, 4, 5, 6))
np2=np.array([0, 1])
print(d4[:,np2,:,np2].shape)  #(2, 3, 5)
print(d4[:,np2,:,np2])

7.参考資料

参考資料として、3次元配列d3、4次元配列d4の検証結果であるコード11についてもご紹介しますのでご一読ください。

#コード11
import numpy as np
d3 = np.arange(60).reshape((3,4,5))
np2=np.array([0,1])

print(d3[:,:,np2].shape)  #(3, 4, 2)
print(d3[:,np2,:].shape)  #(3, 2, 5)
print(d3[np2,:,:].shape)  #(2, 4, 5)

print(d3[:,np2,np2].shape)  #(3, 2)
print(d3[np2,:,np2].shape)  #(2, 4)
print(d3[np2,np2,:].shape)  #(2, 5)

d4 = np.arange(360).reshape((3,4,5,6))
np2=np.array([0,1])

print(d4[:,:,:,np2].shape)  #(3, 4, 5, 2)
print(d4[:,:,np2,:].shape)  #(3, 4, 2, 6)
print(d4[:,np2,:,:].shape)  #(3, 2, 5, 6)
print(d4[np2,:,:,:].shape)  #(2, 4, 5, 6)

print(d4[:,:,np2,np2].shape)  #(3, 4, 2)
print(d4[:,np2,:,np2].shape)  #(2, 3, 5)
print(d4[np2,:,:,np2].shape)  #(2, 4, 5)
print(d4[:,np2,np2,:].shape)  #(3, 2, 6)
print(d4[np2,:,np2,:].shape)  #(2, 4, 6)
print(d4[np2,np2,:,:].shape)  #(2, 5, 6)

print(d4[:,np2,np2,np2].shape)  #(3, 2)
print(d4[np2,:,np2,np2].shape)  #(2, 4)
print(d4[np2,np2,:,np2].shape)  #(2, 5)
print(d4[np2,np2,np2,:].shape)  #(2, 6)

私が実際に購入した教材のご紹介

以下、私が実際に購入したPythonの教材をまとめてみました。 Pythonを学習する上で、少しでもお役に立つことができればうれしいです。

Python♪私が購入したPythonの書籍のレビュー
UdemyのPythonの動画講座を書籍を買う感覚で購入してみた

その他

Twitterへのリンクです。SNSもはじめました♪

液晶ペンタブレットを買いました
 (1) モバイルディスプレイを買うつもりだったのに激安ペンタブレット購入

以下、私が光回線を導入した時の記事一覧です。
 (1) 2020年「光回線は値段で選ぶ」では後悔する ←宅内工事の状況も説明しています。
 (2) NURO光の開通までWiFiルーターを格安レンタルできる
 (3) NURO光の屋外工事の状況をご紹介。その日に開通!
 (4) 光回線開通!実測するとNURO光はやっぱり速かった
 (5) ネット上のNURO光紹介特典は個人情報がもれないの?