150

I'm struggling to select the specific columns per row of a NumPy matrix.

Suppose I have the following matrix which I would call X:

[1, 2, 3]
[4, 5, 6]
[7, 8, 9]

I also have a list of column indexes per every row which I would call Y:

[1, 0, 2]

I need to get the values:

[2]
[4]
[9]

Instead of a list with indexes Y, I can also produce a matrix with the same shape as X where every column is a bool / int in the range 0-1 value, indicating whether this is the required column.

[0, 1, 0]
[1, 0, 0]
[0, 0, 1]

I know this can be done with iterating over the array and selecting the column values I need. However, this will be executed frequently on big arrays of data and that's why it has to run as fast as it can.

I was thus wondering if there is a better solution?

Georgy
  • 12,464
  • 7
  • 65
  • 73
Zee
  • 2,240
  • 4
  • 18
  • 18

7 Answers7

155

If you've got a boolean array you can do direct selection based on that like so:

>>> a = np.array([True, True, True, False, False])
>>> b = np.array([1,2,3,4,5])
>>> b[a]
array([1, 2, 3])

To go along with your initial example you could do the following:

>>> a = np.array([[1,2,3], [4,5,6], [7,8,9]])
>>> b = np.array([[False,True,False],[True,False,False],[False,False,True]])
>>> a[b]
array([2, 4, 9])

You can also add in an arange and do direct selection on that, though depending on how you're generating your boolean array and what your code looks like YMMV.

>>> a = np.array([[1,2,3], [4,5,6], [7,8,9]])
>>> a[np.arange(len(a)), [1,0,2]]
array([2, 4, 9])
wjandrea
  • 28,235
  • 9
  • 60
  • 81
Slater Victoroff
  • 21,376
  • 21
  • 85
  • 144
  • 28
    +1 for the example using `arange` . This was particularly useful to me for retrieving different blocks from multiple matrices (so basically the 3D case of this example) – Gerrit-K Jan 14 '16 at 10:44
  • 2
    Hi, could you explain why we have to use `arange` instead of `:`? I know your way works and mine doesn't, but I would like to understand why. – marcotama Jun 19 '16 at 03:18
  • 1
    @tamzord because it's a numpy array and not a vanilla python list, so the `:` syntax doesn't work the same way. – Slater Victoroff Jun 19 '16 at 19:07
  • 3
    @SlaterTyranus, thanks for responding. My understanding, after some reading, is that mixing `:` with advanced indexing means: "for every sub-space along `:`, apply the given advanced indexing". Is my understanding correct? – marcotama Jun 20 '16 at 00:09
  • @tamzord explain what you mean by "sub-space" – Slater Victoroff Jun 20 '16 at 18:32
  • @SlaterTyranus I mean the mathematical definition: when you have a n-dimensional space and you fix one coordinate you obtain a sub-space; that is, a (n-1)-dimensional space. The documentation uses that term and I think it is also using the same definition: (ctrl-F "subspace") http://docs.scipy.org/doc/numpy/reference/arrays.indexing.html I hope it makes sense – marcotama Jun 21 '16 at 06:07
56

You can do something like this:

In [7]: a = np.array([[1, 2, 3],
   ...: [4, 5, 6],
   ...: [7, 8, 9]])

In [8]: lst = [1, 0, 2]

In [9]: a[np.arange(len(a)), lst]
Out[9]: array([2, 4, 9])

More on indexing multi-dimensional arrays: http://docs.scipy.org/doc/numpy/user/basics.indexing.html#indexing-multi-dimensional-arrays

Ashwini Chaudhary
  • 244,495
  • 58
  • 464
  • 504
39

Recent numpy versions have added a take_along_axis (and put_along_axis) that does this indexing cleanly.

In [101]: a = np.arange(1,10).reshape(3,3)                                                             
In [102]: b = np.array([1,0,2])                                                                        
In [103]: np.take_along_axis(a, b[:,None], axis=1)                                                     
Out[103]: 
array([[2],
       [4],
       [9]])

It operates in the same way as:

In [104]: a[np.arange(3), b]                                                                           
Out[104]: array([2, 4, 9])

but with different axis handling. It's especially aimed at applying the results of argsort and argmax.

hpaulj
  • 221,503
  • 14
  • 230
  • 353
7

A simple way might look like:

In [1]: a = np.array([[1, 2, 3],
   ...: [4, 5, 6],
   ...: [7, 8, 9]])

In [2]: y = [1, 0, 2]  #list of indices we want to select from matrix 'a'

range(a.shape[0]) will return array([0, 1, 2])

In [3]: a[range(a.shape[0]), y] #we're selecting y indices from every row
Out[3]: array([2, 4, 9])
mbpaulus
  • 7,301
  • 3
  • 29
  • 40
3

You can do it by using iterator. Like this:

np.fromiter((row[index] for row, index in zip(X, Y)), dtype=int)

Time:

N = 1000
X = np.zeros(shape=(N, N))
Y = np.arange(N)

#@Aशwini चhaudhary
%timeit X[np.arange(len(X)), Y]
10000 loops, best of 3: 30.7 us per loop

#mine
%timeit np.fromiter((row[index] for row, index in zip(X, Y)), dtype=int)
1000 loops, best of 3: 1.15 ms per loop

#mine
%timeit np.diag(X.T[Y])
10 loops, best of 3: 20.8 ms per loop
Kei Minagawa
  • 4,395
  • 3
  • 25
  • 43
  • 1
    OP mentioned it should run fast on *large* arrays, so your benchmarks are not very representative. I'm curious how your last method performs for (much) larger arrays! –  May 03 '14 at 20:02
  • @moarningsun: Updated. `np.diag(X.T[Y])` is so slow... But `np.diag(X.T)` is so fast(10us). I don't know why. – Kei Minagawa May 05 '14 at 02:52
1

The answer from hpaulj using take_along_axis should be the accepted one.

Here is a derived version with an N-dim index array:

>>> arr = np.arange(20).reshape((2,2,5))
>>> idx = np.array([[1,0],[2,4]])
>>> np.take_along_axis(arr, idx[...,None], axis=-1)
array([[[ 1],
        [ 5]],

       [[12],
        [19]]])

Note that the selection operation is ignorant about the shapes. I used this to refine a possibly vector-valued argmax result from histogram by fitting parabolas:

def interpol(arr):
    i = np.argmax(arr, axis=-1)
    a = lambda Δ: np.squeeze(np.take_along_axis(arr, i[...,None]+Δ, axis=-1), axis=-1)
    frac = .5*(a(1) - a(-1)) / (2*a(0) - a(-1) - a(1)) # |frac| < 0.5
    return i + frac

Note the squeeze to remove the dimension of size 1 resulting in the same shape of i and frac, the integer and fractional part of the peak position.

I'm quite sure that it is possible to avoid the lambda, but would the interpolation formula still look nice?

Rainald62
  • 706
  • 12
  • 19
0

Another clever way is to first transpose the array and index it thereafter. Finally, take the diagonal, its always the right answer.

X = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]])
Y = np.array([1, 0, 2, 2])

np.diag(X.T[Y])

Step by step:

Original arrays:

>>> X
array([[ 1,  2,  3],
       [ 4,  5,  6],
       [ 7,  8,  9],
       [10, 11, 12]])

>>> Y
array([1, 0, 2, 2])

Transpose to make it possible to index it right.

>>> X.T
array([[ 1,  4,  7, 10],
       [ 2,  5,  8, 11],
       [ 3,  6,  9, 12]])

Get rows in the Y order.

>>> X.T[Y]
array([[ 2,  5,  8, 11],
       [ 1,  4,  7, 10],
       [ 3,  6,  9, 12],
       [ 3,  6,  9, 12]])

The diagonal should now become clear.

>>> np.diag(X.T[Y])
array([ 2,  4,  9, 12]
Thomas Devoogdt
  • 816
  • 11
  • 16
  • 2
    This technically works and looks very elegant. However, I find that this approach completely explodes when you’re dealing with large arrays. In my case, NumPy swallowed 30GB of swap and filled my SSD. I recommend using the advanced indexing approach instead. – 5nefarious Jan 30 '20 at 19:08