すこしふしぎ.

VR/HI系院生による技術ブログ.まったりいきましょ.(友人ズとブログリレー中.さぼったら焼肉おごらなきゃいけない)

numpy使い方_2_スライスとブールインデックス

前回に引き続きnumpy使い方を勉強していきます. 今回はndarrayからデータを取得する方法についてまとめていきます.

インデクシング & スライシング

ndarrayはpythonのlistの拡張として利用することができます. なので,listで使えるインデックス参照やスライシングは同じように使うことができます.

# ndarray生成
In [1]: a = np.arange(10)

In [2]: a
Out[2]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

# インデクシング
In [3]: a[5]
Out[3]: 5

# スライシング
In [4]: a[5:8]
Out[4]: array([5, 6, 7])

# スライシングに対する代入(ブロードキャスト)
In [5]: a[5:8] = 12

In [6]: a
Out[6]: array([ 0,  1,  2,  3,  4, 12, 12, 12,  8,  9])

ちなみにnumpyなしのpythonだとこうなります.

>>> a = range(10)
>>> a
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> a[5] #indexing
5
>>> a[5:8] #slicing
[5, 6, 7]
>>> a[5:8] = 12
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only assign an iterable
>>> a[5:8] = [12,12,12]
>>> a
[0, 1, 2, 3, 4, 12, 12, 12, 8, 9]

numpyではa[5:8] = 12のところでブロードキャストが行われているおかげでエラーが出ないんですね.. 通常のlistに同じことをやろうとするとエラーになってしまいました.

listとndarrayの違いとして,ndarrayのスライスは新しいコピーではなく,元のndarrayそのものを参照しているということが挙げられます. そのため,ndarrayのスライスを変更すると,元のndarrayも変更されます.見てみましょう.

# ndarray生成
In [7]: a = np.arange(10)

In [8]: a
Out[8]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

# スライシング
In [9]: b = a[5:8]

In [10]: b
Out[10]: array([5, 6, 7])

# インデックス参照で値変更
In [11]: b[0] = 100

In [12]: b
Out[12]: array([100,   6,   7])

In [13]: a
Out[13]: array([  0,   1,   2,   3,   4, 100,   6,   7,   8,   9]) #オリジナルも変更されている

# スライシングで値変更
In [14]: b[:] = 12345

In [15]: b
Out[15]: array([12345, 12345, 12345])

In [16]: a
Out[16]: array([    0,     1,     2,     3,     4, 12345, 12345, 12345,     8,     9]) #同じくオリジナルも変更されている

# 逆もまた然り
In [17]: a[5]=10

In [18]: a
Out[18]: array([    0,     1,     2,     3,     4,    10, 12345, 12345,     8,     9])

In [19]: b
Out[19]: array([   10, 12345, 12345]) #オリジナル変更でスライスも変更される

一方通常のlistでは,

# list生成
>>> a = range(10)
# スライシング
>>> b = a[5:8]
>>> a
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> b
[5, 6, 7]
# スライスを変更
>>> b[0] = 10
>>> b
[10, 6, 7]
>>> a
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] #オリジナルは不変

というように,オリジナルのlistは変更されません.

これは,listのスライスがオリジナルの一部コピーとして返されるのに対し,ndarrayではオリジナルをそのまま参照しているためです. このスライスをオリジナルのビューと言うそうです.

スライスを生成するたびにデータのコピーを行うとメモリを食うため,このような仕様になっているのでしょう. numpyで扱うデータは膨大なモノが想定されますから,パフォーマンス的にも納得です.

その他多次元配列になった際のスライス仕様はlistと同様です. ブロードキャストによるスカラ代入も,上と同様に可能です.

ブールインデックス

ndarrayにおけるデータ参照に,ブールインデックスという手段があります. あるndarrayに対し,同じサイズのブール値を格納したndarrayを与えることで,trueの要素についてのみ処理を行うことができたりします. いわばマスク処理みたいな者ですね. 実際にみてみましょう.

まずはターゲットとマスクとなるndarrayを生成します.

# 5*4のndarray
In [1]: a = np.arange(20).reshape(5,4)

In [2]: a
Out[2]:
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11],
       [12, 13, 14, 15],
       [16, 17, 18, 19]])

# maskとなるndarray
In [4]: t = True

In [5]: f = False

In [6]: mask = [[t,t,t,f],[t,f,f,t],[f,f,t,t],[t,f,f,f],[t,f,f,t]]

In [7]: b = np.array(mask)

In [8]: b
Out[8]:
array([[ True,  True,  True, False],
       [ True, False, False,  True],
       [False, False,  True,  True],
       [ True, False, False, False],
       [ True, False, False,  True]], dtype=bool)

生成したマスクndarrayをターゲットのインデックスに与えると, マスクがTrueとなっている部分だけを抜き出すことができます.

# 対象にマスクを与えると...?
In [9]: a[b]
Out[9]: array([ 0,  1,  2,  4,  7, 10, 11, 12, 16, 19]) # マスクのTrue部だけが抜き出されている

このようなブール値ndarrayによる要素参照を,ブールインデックス参照と言います. なお,ブールインデックス参照でかえってくるndarrayはもとのndarrayのコピーなので注意が必要です. 別変数に格納した場合,その変数を介してもとのndarrayを変更することはありません. 先ほどのスライスとは別の仕様ですね.

単純にマスクして必要な部分だけを取得する以外に, マスクの部分だけに新しい値を代入することもできます.

# 値を与えると...?
In [10]: a[b] = 100

In [11]: a
Out[11]:
array([[100, 100, 100,   3],
       [100,   5,   6, 100],
       [  8,   9, 100, 100],
       [100,  13,  14,  15],
       [100,  17,  18, 100]]) 

bによるマスク部のみが100に変わっていますね.

ちょい応用すると

ndarrayに対し比較演算子を与えると,ブロードキャストにより全要素に対して比較ができます. その結果,元のndarrayと同じサイズのブール値を格納したndarrayが取得できます. これを元のndarrayに与えることで,条件を満たす要素に対してのみ操作ということが可能になります.

# 正規分布に基づくランダム.5*4のndarray
In [1]: a = np.random.randn(20).reshape(5,4)

In [2]: a
Out[2]:
array([[ 2.58057751, -0.46774102,  0.52755024, -0.40947069],
       [ 1.07003347, -0.50359847,  0.31234041,  1.67661682],
       [-0.62534411, -1.21105982,  0.79340921, -0.41574284],
       [-0.32260056,  1.02902607, -1.43930132,  0.42790323],
       [-0.2446355 ,  0.69058512, -0.83595337,  0.95035227]])

# aに比較演算子を適用
In [3]: a > 0
Out[3]:
array([[ True, False,  True, False],
       [ True, False,  True,  True],
       [False, False,  True, False],
       [False,  True, False,  True],
       [False,  True, False,  True]], dtype=bool)

In [4]: mask = a > 0

In [5]: mask
Out[5]:
array([[ True, False,  True, False],
       [ True, False,  True,  True],
       [False, False,  True, False],
       [False,  True, False,  True],
       [False,  True, False,  True]], dtype=bool)

# aにマスクを適用
In [6]: a[mask]
Out[6]:
array([ 2.58057751,  0.52755024,  1.07003347,  0.31234041,  1.67661682,
        0.79340921,  1.02902607,  0.42790323,  0.69058512,  0.95035227])

In [7]: a[mask] = 10

In [8]: a
Out[8]:
array([[ 10.        ,  -0.46774102,  10.        ,  -0.40947069],
       [ 10.        ,  -0.50359847,  10.        ,  10.        ],
       [ -0.62534411,  -1.21105982,  10.        ,  -0.41574284],
       [ -0.32260056,  10.        ,  -1.43930132,  10.        ],
       [ -0.2446355 ,  10.        ,  -0.83595337,  10.        ]])


# 無理矢理こんなこともできる
In [13]: a[1:3][ a[1:3] > 0 ]
Out[13]: array([ 10.,  10.,  10.,  10.])

In [14]: a[1:3][ a[1:3] > 0 ] = 100

In [15]: a
Out[15]:
array([[  10.        ,   -0.46774102,   10.        ,   -0.40947069],
       [ 100.        ,   -0.50359847,  100.        ,  100.        ],
       [  -0.62534411,   -1.21105982,  100.        ,   -0.41574284],
       [  -0.32260056,   10.        ,   -1.43930132,   10.        ],
       [  -0.2446355 ,   10.        ,   -0.83595337,   10.        ]])

# 単純に書くとこんな感じ
In [16]: a[a < 0] = -100 #負の要素を-100に書き換える

In [17]: a
Out[17]:
array([[  10., -100.,   10., -100.],
       [ 100., -100.,  100.,  100.],
       [-100., -100.,  100., -100.],
       [-100.,   10., -100.,   10.],
       [-100.,   10., -100.,   10.]])

さらに,行だけ or 列だけの指定も可能です.

# ターゲットndarray
In [1]: a = np.random.randn(20).reshape(5,4)

# 行マスク
In [2]: c = array([True,False,False,True,False])

In [3]: a
Out[3]:
array([[ 0.11346123, -0.46532497,  0.0814962 ,  1.24235993],
       [ 0.74865781, -0.44638719, -1.73922234, -0.30353582],
       [ 0.27460557, -0.41929013, -0.48273503,  1.10366707],
       [ 0.76218305, -1.26225842,  0.18340329,  1.5690767 ],
       [-1.13976385, -0.59062488,  0.2713594 , -0.17257197]])

In [4]: c
Out[4]: array([ True, False, False,  True, False], dtype=bool)

# 行マスクをターゲットに与えると..
In [5]: a[c]
Out[5]:
array([[ 0.11346123, -0.46532497,  0.0814962 ,  1.24235993], # 1行目
       [ 0.76218305, -1.26225842,  0.18340329,  1.5690767 ]]) # 4行目

# 代入すると...
In [6]: a[c] = 0

In [7]: a
Out[7]:
array([[ 0.        ,  0.        ,  0.        ,  0.        ], # ブロードキャストされてる
       [ 0.74865781, -0.44638719, -1.73922234, -0.30353582],
       [ 0.27460557, -0.41929013, -0.48273503,  1.10366707],
       [ 0.        ,  0.        ,  0.        ,  0.        ], # ここも
       [-1.13976385, -0.59062488,  0.2713594 , -0.17257197]]) 

列マスクを利用する際には,ちょっとテクニックが必要です. ブールインデックスと,先ほどのスライスを併用することで実現します.

# 列index
In [13]: d = array([True,False,False,True])

# そのまま与えると
In [14]: a[d]
Out[14]:
array([[ 0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.]]) # 行インデックスと見なされる.(5行目ないけどエラーにならない?)

# 行成分をスライス(全範囲指定),列成分をブールインデックスで与える
In [15]: a[:,d]
Out[16]:
array([[ 0.        ,  0.        ],
       [ 0.74865781, -0.30353582],
       [ 0.27460557,  1.10366707],
       [ 0.        ,  0.        ],
       [-1.13976385, -0.17257197]]) # 1,4列目のみ取得できた

じゃー行列両方ブールで指定するのも簡単だね! ...と思ったのですが,どうも予期しない結果になってしまいました.

# ターゲット
In [17]: a
Out[17]:
array([[ 0.        ,  0.        ,  0.        ,  0.        ],
       [ 0.74865781, -0.44638719, -1.73922234, -0.30353582],
       [ 0.27460557, -0.41929013, -0.48273503,  1.10366707],
       [ 0.        ,  0.        ,  0.        ,  0.        ],
       [-1.13976385, -0.59062488,  0.2713594 , -0.17257197]])

# 改めて行マスク
In [18]: e = array([True,False,False,False,True])

# マスクndarray
In [19]: e
Out[19]: array([ True, False, False, False,  True], dtype=bool)

In [20]: d
Out[20]: array([ True, False, False,  True], dtype=bool)

# 行マスクと列マスクを与える.
# 期待:1,5行目と1,4列目の成分からなる2*2行列がでてくる!

In [21]: a[e,d]
Out[21]: array([ 0.        , -0.17257197])  # あれ?(1,4)(5,4)のみ?

# 無理矢理やってみる
In [22]: a[e][:,d]
Out[22]:
array([[ 0.        ,  0.        ],
       [-1.13976385, -0.17257197]])

なんとか期待すべき形にはなっていますが,ちょっとエレガントではないですね.. 行列両方のマスクをする際は,あらかじめターゲットndarrayと同じサイズのマスクを作成した方が無難ですね.

ブールインデックスは最初イメージが取っ付きにくいですが,うまく使いこなすとかなり便利な気がします.まだ具体的シーンはよくわからないけれどw

まとめ

numpyにおけるスライシングに軽くふれ,ブーインデックスというインデックス参照についてまとめました.思った以上に長くなってしまったので今日はこの辺で. 次回はファンシーインデックスについて書きますね.