8. Индексация, срезы, итерирование

Одномерные массивы очень похожи на простые списки Python:

>>> import numpy as np
>>> a = np.arange(12)**2
>>> a
array([  0,   1,   4,   9,  16,  25,  36,  49,  64,  81, 100, 121])
>>>
>>> #  Обратиться к элементу можно по его индексу
... 
>>> a[0]
0
>>> a[7]
49
>>> a[-1]
121
>>>
>>> #  Извлечение срезов по двум индексам массива
... 
>>> a[4:8]
array([16, 25, 36, 49])
>>>
>>> a[:6], a[6:]
(array([ 0,  1,  4,  9, 16, 25]), array([ 36,  49,  64,  81, 100, 121]))
>>>
>>> #  Извлечение элементов с определенным шагом
... 
>>> a[0:9:2]
array([ 0,  4, 16, 36, 64])
>>> a[::3]
array([ 0,  9, 36, 81])
>>>
>>> a[::-1]    #  элементы массива в обратном порядке
array([121, 100,  81,  64,  49,  36,  25,  16,   9,   4,   1,   0])
>>> 
>>> #  Каждому второму элементу можно присвоить значение
... 
>>> a[0:12:2] = -1
>>> a
array([ -1,   1,  -1,   9,  -1,  25,  -1,  49,  -1,  81,  -1, 121])
>>>
>>> #  К массивам можно применять оператор in
>>> for i in a:
...     print(i**(1/2), end = ', ')
... 
nan, 1.0, nan, 3.0, nan, 5.0, nan, 7.0, nan, 9.0, nan, 11.0,

Многомерные массивы имеют один индекс на одну ось, поэтому обращение к элементам и извлечение срезов производится не по одному а по нескольким индексам:

>>> a = np.arange(27).reshape(3, 9)
>>> a
array([[ 0,  1,  2,  3,  4,  5,  6,  7,  8],
       [ 9, 10, 11, 12, 13, 14, 15, 16, 17],
       [18, 19, 20, 21, 22, 23, 24, 25, 26]])
>>>
>>> a[0, 0]
0
>>> a[1, 4]
13
>>> a[2, 8]
26
>>>
>>> #  В качестве срезов можно извлекать как столбцы так и строки
... 
>>> a[0:3, 0]    #  Извлекаем столбец
array([ 0,  9, 18])
>>>
>>> a[0:3, 1]
array([ 1, 10, 19])
>>>
>>> a[1:3, 2]    #  Можно извлечь часть столбца
array([11, 20])
>>>
>>> a[0, :]    #  Извлекаем строку
array([0, 1, 2, 3, 4, 5, 6, 7, 8])
>>>
>>> a[0, 2:7]    #  Можно извлечь часть строки
array([2, 3, 4, 5, 6])
>>>
>>> a[1, 3:6]
array([12, 13, 14])
>>>
>>> a[2, 4:5]
array([22])
>>> 
>>> a[0:2, 0:9]    #  Извлекаем определенные строки
array([[ 0,  1,  2,  3,  4,  5,  6,  7,  8],
       [ 9, 10, 11, 12, 13, 14, 15, 16, 17]])

Те же правила действуют и для многомерных массивов. Результаты могут показаться немного необычными, но это связано с тем, что нам непривычно работать с массивами у которых больше трех осей. После некоторой практики это быстро проходит:

>>> a = np.arange(27).reshape(3, 3, 3)
>>> a
array([[[ 0,  1,  2],
        [ 3,  4,  5],
        [ 6,  7,  8]],

       [[ 9, 10, 11],
        [12, 13, 14],
        [15, 16, 17]],

       [[18, 19, 20],
        [21, 22, 23],
        [24, 25, 26]]])
>>>
>>> a[0, 0, 0]
0
>>> a[0:3, 1, 1]
array([ 4, 13, 22])
>>>
>>> a[1, 0:3, 1]
array([10, 13, 16])
>>>
>>> a[1, 1, 0:3]
array([12, 13, 14])

Если указать количество индексов меньше количества осей, то недостающие индексы считаются полными фрагментами как если бы вместо них было указано : - т.е. отсутствующие индексы считаются полными срезами.

>>> b = np.arange(27).reshape(3, 9)
>>> b
array([[ 0,  1,  2,  3,  4,  5,  6,  7,  8],
       [ 9, 10, 11, 12, 13, 14, 15, 16, 17],
       [18, 19, 20, 21, 22, 23, 24, 25, 26]])
>>>
>>> b[1]    #  Равносильно b[1, :]
array([ 9, 10, 11, 12, 13, 14, 15, 16, 17])
>>>
>>> c = np.arange(27).reshape(3, 3, 3)
>>> c
array([[[ 0,  1,  2],
        [ 3,  4,  5],
        [ 6,  7,  8]],

       [[ 9, 10, 11],
        [12, 13, 14],
        [15, 16, 17]],

       [[18, 19, 20],
        [21, 22, 23],
        [24, 25, 26]]])
>>>
>>> c[1]    #  Равносильно c[1, :, :]
array([[ 9, 10, 11],
       [12, 13, 14],
       [15, 16, 17]])
>>>
>>> c[1, 1]    #  Равносильно c[1, 1, :]
array([12, 13, 14])

Иногда приходится иметь дело с массивами ранг (размерность) которых больше 3. В таких случаях для обращения к элементам приходится указывать длинные списки индексов:

>>> a = np.arange(81).reshape(3, 3, 3, 3)
a
array([[[[ 0,  1,  2],
         [ 3,  4,  5],
         [ 6,  7,  8]],

        [[ 9, 10, 11],
         [12, 13, 14],
         [15, 16, 17]],

        [[18, 19, 20],
         [21, 22, 23],
         [24, 25, 26]]],


       [[[27, 28, 29],
         [30, 31, 32],
         [33, 34, 35]],

        [[36, 37, 38],
         [39, 40, 41],
         [42, 43, 44]],

        [[45, 46, 47],
         [48, 49, 50],
         [51, 52, 53]]],


       [[[54, 55, 56],
         [57, 58, 59],
         [60, 61, 62]],

        [[63, 64, 65],
         [66, 67, 68],
         [69, 70, 71]],

        [[72, 73, 74],
         [75, 76, 77],
         [78, 79, 80]]]])
>>>
>>> a[1, 1, 2, 2]
44
>>>
>>> a[:, :, 1, 1]
array([[ 4, 13, 22],
       [31, 40, 49],
       [58, 67, 76]])
>>>
>>> a[:, :, :, 0]
array([[[ 0,  3,  6],
        [ 9, 12, 15],
        [18, 21, 24]],

       [[27, 30, 33],
        [36, 39, 42],
        [45, 48, 51]],

       [[54, 57, 60],
        [63, 66, 69],
        [72, 75, 78]]])
>>>
>>> a[:, 0, 0, :]
array([[ 0,  1,  2],
       [27, 28, 29],
       [54, 55, 56]])

Для удобства NumPy позволяет заменять последовательности из : на ...

>>> a[..., 1, 1]    # Эквивалентно a[:, :, 1, 1]
array([[ 4, 13, 22],
       [31, 40, 49],
       [58, 67, 76]])
>>>
>>> a[..., 0]    # Эквивалентно a[:, :, :, 0]
array([[[ 0,  3,  6],
        [ 9, 12, 15],
        [18, 21, 24]],

       [[27, 30, 33],
        [36, 39, 42],
        [45, 48, 51]],

       [[54, 57, 60],
        [63, 66, 69],
        [72, 75, 78]]])
>>>
>>> #  Список индексов может содержать только одно многоточие
...
>>> a[..., 0, 0, ...]    #  Так неправильно
>>> a[...,0,0,:]    #  Так правильно и эквивалентно a[:, 0, 0, :]
array([[ 0,  1,  2],
       [27, 28, 29],
       [54, 55, 56]])

Итерирование многомерных массивов выполняется только по первой оси

>>> a = np.arange(9).reshape(3, 3)
>>> a
array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])
>>> 
>>> for i in a:
...     print(i)
... 
[0 1 2]
[3 4 5]
[6 7 8]

Если необходимо выполнить итерирование по каждому объекту, то можно воспользоваться атрибутом flat.

>>> for elem in a.flat:
...     print(elem)
... 
0
1
2
3
4
5
6
7
8

8.1. Дополнительные возможности индексирования

Работая с последовательностями: строками, списками или кортежами, мы привыкли только к одному единственному способу индексирования, который предлагает нам Python. Неужели это можно сделать как-то по другому? Оказывается можно, и, иногда это оказывается гораздо удобнее чем может показаться. Помимо индексирования целыми числами и срезами, к которому мы так сильно привыкли, NumPy предлагает еще два необычных метода индексирования. Первый - это индексация с помощью массивов целых чисел, а второй - это индексация с помощью массивов булевых значений.

8.1.1. Индексирование массивами целых чисел

Что такое массив индексов? Массив индексов - это обычный массив целых чисел, причем каждое число соответствует некоторому индексу определенного элемента в другом массиве, например:

>>> a = np.arange(10, 20)
>>> a
array([10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
>>>
>>> i = np.array([1, 1, 1, 9, 8, 5])
>>>
>>> b = a[i]
>>> b
array([11, 11, 11, 19, 18, 15])

Немного необычно, но зато очень удобно. В самом деле, не будь у нас такого способа индексирования, то для составления подобных массивов, каждый раз пришлось бы писать что-то подобное:

>>> a = np.arange(10, 20)
>>> a
array([10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
>>>
>>> b = np.array([a[1], a[1], a[1], a[9], a[8], a[5]])
>>> b
array([11, 11, 11, 19, 18, 15])

Как видно, в таких случаях индексация с помощью массивов оказывается гораздо удобнее. Причем индексы массивов могут содержать любое количество осей и элементов по этим осям:

>>> a = np.arange(5, 15, 2)
>>> a
array([ 5,  7,  9, 11, 13])
>>>
>>> i = np.array([2, 1, 3, 0, 4])
>>> a[i]
array([ 9,  7, 11,  5, 13])
>>>
>>> j = np.array([[4, 0], [3, 1], [2, 4]])
>>> a[j]
array([[13,  5],
       [11,  7],
       [ 9, 13]])
>>>
>>> k = np.array([[[0, 3], [1, 4]], [[0, 2], [4, 3]], [[3, 0], [2, 4]]])
>>> a[k]
array([[[ 5, 11],
        [ 7, 13]],

       [[ 5,  9],
        [13, 11]],

       [[11,  5],
        [ 9, 13]]])

Если в некотором массиве есть вложенные массивы, то их так же можно индексировать с помощью массивов целых чисел:

>>> a = np.arange(6).reshape(3, 2)
>>> a
array([[0, 1],
       [2, 3],
       [4, 5]])
>>>
>>> i = np.array([2, 2, 0])
>>> a[i]
array([[4, 5],
       [4, 5],
       [0, 1]])
>>>
>>> j = np.array([[1, 1], [2, 0], [1, 0]])
>>> a[j]
array([[[2, 3],
        [2, 3]],

       [[4, 5],
        [0, 1]],

       [[2, 3],
        [0, 1]]])

Если нам нужно добраться до элементов, которые находятся в его вложенных массивах, т.е. задавать индексы для нескольких измерений, то мы так же можем воспользоваться индексацией с помощью нескольких массивов целых чисел. Но в таком случае появляется дополнительное требование - массивы индексов должны иметь одинаковые размеры.

>>> a = np.arange(25).reshape(5, 5)
>>> a
array([[ 0,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14],
       [15, 16, 17, 18, 19],
       [20, 21, 22, 23, 24]])
>>>
>>> i = np.array([1, 2, 3])
>>>
>>> a[i]    #  Так мы вытащим 3 средние строки из массива "а"
array([[ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14],
       [15, 16, 17, 18, 19]])
>>>
>>> j = np.array([0, 2, 4])    #  Теперь из каждой вытащенной строки вытаскиваем
>>> a[i, j]                     #  по одному элементу с указанным индексом
array([ 5, 12, 19])
>>>
>>> m = np.array([[1, 2, 0], [2, 3, 1], [3, 4, 2]])
>>> m
array([[1, 2, 0],
       [2, 3, 1],
       [3, 4, 2]])
>>> 
>>> a[m]     #  Формируем три элемента из строк массива "а" с индексами из "m"
array([[[ 5,  6,  7,  8,  9],
        [10, 11, 12, 13, 14],
        [ 0,  1,  2,  3,  4]],

       [[10, 11, 12, 13, 14],
        [15, 16, 17, 18, 19],
        [ 5,  6,  7,  8,  9]],

       [[15, 16, 17, 18, 19],
        [20, 21, 22, 23, 24],
        [10, 11, 12, 13, 14]]])
>>>
>>> k = np.array([[2, 3, 3], [0, 2, 4], [0, 4, 0]])
>>> k
array([[2, 3, 3],
       [0, 2, 4],
       [0, 4, 0]])
>>>
>>> a[m, k]    #  Из каждой строки вытаскиваем по 1-му элементу с индексом из "k"
array([[ 7, 13,  3],
       [10, 17,  9],
       [15, 24, 10]])

Более того, при индексации мы можем указывать не только числа, массивы и даже срезы, но и их комбинации:

>>> a = np.arange(1, 33, 2).reshape(4, 4)
>>> a
array([[ 1,  3,  5,  7],
       [ 9, 11, 13, 15],
       [17, 19, 21, 23],
       [25, 27, 29, 31]])
>>>
>>> i = np.array([[2, 1], [0, 2]])
>>> i
array([[2, 1],
       [0, 2]])
>>>
>>> a[i]
array([[[17, 19, 21, 23],
        [ 9, 11, 13, 15]],

       [[ 1,  3,  5,  7],
        [17, 19, 21, 23]]])
>>>
>>> #  Вытащим из каждой строчки элемент с индексом 1
...
>>> a[i, 1]  
array([[19, 11],
       [ 3, 19]])
>>>
>>> #  из 1-й строки "а" формируем массив с индексами из "i"
...
>>> a[1, i]   
array([[13, 11],
       [ 9, 13]])
>>>
>>> #  из каждой строки "а" формируем массив с индексами из "i"
...
>>> a[:, i]   
array([[[ 5,  3],
        [ 1,  5]],

       [[13, 11],
        [ 9, 13]],

       [[21, 19],
        [17, 21]],

       [[29, 27],
        [25, 29]]])
>>>
>>> #  из каждой строки "а" формируем массив с индексами из "i[0]"
...
>>> a[:, i[0]]   
array([[ 5,  3],
       [13, 11],
       [21, 19],
       [29, 27]])

Если мы используем несколько массивов для индексирования, то эти массивы можно объединить в последовательность:

>>> a = np.arange(16).reshape(4, 4)
>>> a
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11],
       [12, 13, 14, 15]])
>>>
>>> i = np.array([[1, 2], [0, 3]])
>>> a[i]
array([[[ 4,  5,  6,  7],
        [ 8,  9, 10, 11]],

       [[ 0,  1,  2,  3],
        [12, 13, 14, 15]]])
>>>
>>> j = np.array([[0, 3], [0, 2]])
>>> a[j]
array([[[ 0,  1,  2,  3],
        [12, 13, 14, 15]],

       [[ 0,  1,  2,  3],
        [ 8,  9, 10, 11]]])
>>>
>>> a[i, j]
array([[ 4, 11],
       [ 0, 14]])
>>>
>>> k = [i, j]
>>>
>>> a[k]
array([[ 4, 11],
       [ 0, 14]])

Но если мы просто упакуем эти массивы в новый массив NumPy, то это вполне может сработать, но не всегда, потому что новый массив индексов интерпретируется как индексирование самой первой оси массива и если ее размер меньше, чем наибольшее число в индексе массивов, то это приведет к ошибке.

>>> a = np.arange(16).reshape(4, 4)
>>> a
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11],
       [12, 13, 14, 15]])
>>>
>>> i = np.array([[1, 2], [0, 3]])
>>> j = np.array([[0, 3], [0, 2]])
>>>
>>> a[i, j]
array([[ 4, 11],
       [ 0, 14]])
>>>
>>> s = np.array([i, j])
>>> s
array([[[1, 2],
        [0, 3]],

       [[0, 3],
        [0, 2]]])
>>>
>>> a[s]     #  Это сработает
array([[[[ 4,  5,  6,  7],
         [ 8,  9, 10, 11]],

        [[ 0,  1,  2,  3],
         [12, 13, 14, 15]]],


       [[[ 0,  1,  2,  3],
         [12, 13, 14, 15]],

        [[ 0,  1,  2,  3],
         [ 8,  9, 10, 11]]]])
>>>
>>> a = np.arange(12).reshape(3, 4)
>>> a     #  Количество элементов по первой оси стало равно 3
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
>>>
>>> i = np.array([[1, 2], [0, 2]])
>>> a[i]
array([[[ 4,  5,  6,  7],
        [ 8,  9, 10, 11]],

       [[ 0,  1,  2,  3],
        [ 8,  9, 10, 11]]])
>>>
>>> j = np.array([[0, 3], [0, 2]])
>>>
>>> a[i, j]
array([[ 4, 11],
       [ 0, 10]])
>>>
>>> s = np.array([i, j])
array([[[1, 2],
        [0, 2]],

       [[0, 3],
        [0, 2]]])
>>>
>>> a[s]    #  Приведет к ошибке
Traceback (most recent call last):
  File "", line 1, in 
IndexError: index 3 is out of bounds for axis 0 with size 3
>>>
>>> #  Ошибка произошла потому что размер нулевой оси равен 3,
... #  а максимальный индекс в "s" равен 3
... 
>>> a[list(s)]    #  Эквивалентно a[i, j]
array([[ 4, 11],
       [ 0, 10]])
>>>
>>> a[tuple(s)]    #  так же аналогично a[i, j]
array([[ 4, 11],
       [ 0, 10]])

Индексирование с помощью массивов можно применять для изменения элементов исходного массива:

>>> a = np.arange(10)
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>>
>>> a[[1,3,4,8]] = 0
>>> a
array([0, 0, 2, 0, 0, 5, 6, 7, 0, 9])
>>>
>>> #  Это возможно не только с одномерными массивами
...
>>> b = np.array([[0, 1], [2, 3], [4, 5]])
>>> b
array([[0, 1],
       [2, 3],
       [4, 5]])
>>>
>>> b[[0, 2]] = [[1, 0], [5, 4]]
>>> b
array([[1, 0],
       [2, 3],
       [5, 4]])
>>>
>>> b[[0, 2], 1] = 10
>>> b
array([[ 1, 10],
       [ 2,  3],
       [ 5, 10]])

Если массив индексов содержит повторы, то изменение одного элемента происходит несколько раз, пока не будет присвоено последнее значение:

>>> a = np.arange(10)
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>>
>>> a[[0, 0, 0, 1, 3, 4]] = [7, 8, 9, 20, 30, 50]
>>> a
array([ 9, 20,  2, 30, 50,  5,  6,  7,  8,  9])

В данном примере элемент a[0] менялся три раза, сначала ему было присвоено значение 7, потом 8 и наконец 9.

Над элементами, которые указаны в массиве индексов можно выполнять математические операции:

>>> a = np.arange(7)
>>> a
array([0, 1, 2, 3, 4, 5, 6])
>>>
>>> a[[1, 3, 6]] = a[[1, 3, 6]] + 10
>>> a
array([ 0, 11,  2, 13,  4,  5, 16])
>>>
>>> a[[2, 4, 5]] = a[[2, 4, 5]]**3
>>> a
array([  0,  11,   8,  13,  64, 125,  16])
>>>
>>> a[[3, 4, 6]] = a[[3, 4, 6]]*5
>>> a
array([  0,  11,   8,  65, 320, 125,  80])
>>>
>>> a[[3, 4, 6]] = a[[3, 4, 6]]*5
>>> a
array([   0,   11,    8,  325, 1600,  125,  400])
>>>
>>> a[[3,4,6]] = a[[3,4,6]]%19
>>> a
array([  0,  11,   8,   2,   4, 125,   1])

В примерах выше, было бы гораздо удобнее использовать вместо выражения a[[1, 3, 6]] = a[[1, 3, 6]] + 10 его сокращенный вариант a[[1,3,6]] += 10 и это действительно возможно. Но оба варианта выражений делают не совсем то, что можно было бы ожидать. Можно предположить, что если в массиве индексов содержатся повторы, то и математические операции так же будут повторяться, но этого не происходит:

>>> a
array([  0,  11,   8,   2,   4, 125,   1])
>>>
>>> a[[0, 0, 1]] = a[[0, 0, 1]] + 1
>>> a     #  Можно подумать, что a[0] будет равно 2, но...
array([  1,  12,   8,   2,   4, 125,   1])
>>>
>>> a[[0, 0, 1]] -= 1
>>> a     #  Ожидаем, что a[0] будет равно -1
array([  0,  11,   8,   2,   4, 125,   1])

Казалось бы, что в первом случае к a[0] единица прибавится два раза и два раза будет вычтена во втором случае, но вместо двух раз это произошло всего лишь один раз. Такое поведение связано с тем, что по правилам Python запись a += 1 должна быть эквивалентна a = а + 1. То есть, сколько бы раз мы не обращались к a[0] и меняли его, при следующем обращении к a[0] мы будем иметь дело с элементом исходного массива, а не его измененным значением. Даже если индексы указаны во так: a[[0, 0, 1, 0]], то любая математическая операция над a[0] будет выполнена всего один раз.


8.1.2. Индексация массивами булевых значений

Булевых значений всего два, это False и True. Возникает весьма интересный вопрос, как с помощью таких массивов выполняется индексация, ведь они состоят из одних лишь логических значиний. И снова, оказывается не только возможно, но и крайне удобно в некоторых случаях:

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

Выглядит не совсем убедительно. Хорошо, вот другой пример:

>>> a = np.arange(18).reshape(3, 6)
>>> a
array([[ 0,  1,  2,  3,  4,  5],
       [ 6,  7,  8,  9, 10, 11],
       [12, 13, 14, 15, 16, 17]])
>>>
>>> a % 2 == 0    # какие элементы являются четными?
array([[ True, False,  True, False,  True, False],
       [ True, False,  True, False,  True, False],
       [ True, False,  True, False,  True, False]], dtype=bool)
>>>
>>> b = a % 2 == 0    #  Создадим булев массив
>>> a[b]
array([ 0,  2,  4,  6,  8, 10, 12, 14, 16])
>>>
>>> a[b] = -1     #  Заменим все четные числа на -1
>>> a
array([[-1,  1, -1,  3, -1,  5],
       [-1,  7, -1,  9, -1, 11],
       [-1, 13, -1, 15, -1, 17]])

Скорее всего, именно при выполнении логических операций над массивами такой метод индексирования окажется наиболее полезным. Тем более, что всегда можно воспользоваться его коротким вариантом:

>>> a = np.arange(24).reshape(3, 8)
>>> a
array([[ 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[a % 2 == 0]    #  Вместо b = a % 2 == 0; 
>>>
>>>a[b]
array([ 0,  2,  4,  6,  8, 10, 12, 14, 16, 18, 20, 22])
>>>
>>> a[a > 19]
array([20, 21, 22, 23])
>>>
>>> a[10 < a < 20]    #  Можно подумать, что и это сработает но
Traceback (most recent call last):
  File "", line 1, in 
ValueError: The truth value of an array with more than one
element is ambiguous. Use a.any() or a.all()
>>>
>>> #  Однако, такое сравнение неоднозначно,
... #  допустим, a[0] действительно меньше 20, т.е. True
... #  и в то же время a[0] не больше 10 т.е False.
... #  Лучше поступить вот так:
... 
>>> b = a[a>10]
>>> b
array([11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23])
>>>
>>> b[b < 20]    #  получим то что ожидалось от a[10 < a < 20]
array([11, 12, 13, 14, 15, 16, 17, 18, 19])

Мы уже видели, что индексация с помощью массива позволяет использовать в качестве его элементов числа, срезы, а так же другие массивы индексов. Теперь к таким элементам добавляются еще и булевы массивы. Вот только, нужно помнить, что если мы хотим выбрать какието элементы на определенной оси индексируемого массива, то все нужные и ненужные элементы должны быть перечисленны в одномерном булевом массиве. Понятно, что количество элементов в таком булевом массиве, должно быть равно количеству элементов по необходимой оси.

>>> a = np.arange(21).reshape(3, 7)
>>> a
array([[ 0,  1,  2,  3,  4,  5,  6],
       [ 7,  8,  9, 10, 11, 12, 13],
       [14, 15, 16, 17, 18, 19, 20]])
>>>
>>> i = np.array([True, False, True])
>>> j = np.array([True, False, False, True, False, False, True])
>>>
>>> a[i]    #  Выбираем 0-ю и 2-ю строку из "a"
array([[ 0,  1,  2,  3,  4,  5,  6],
       [14, 15, 16, 17, 18, 19, 20]])
>>>
>>> a[0, j]    #  Выбираем 0-ю строку и в ней 0-й, 3-й и 6-й элемент
array([0, 3, 6])
>>>
>>> a[:, j]    #  В каждой строке выбираем 0-й, 3-й и 6-й элемент
array([[ 0,  3,  6],
       [ 7, 10, 13],
       [14, 17, 20]])
>>>
>>> a[i, j]    #  0-й, 3-й и 6-й элемент из 0-й и 2-й строки
Traceback (most recent call last):
  File "", line 1, in 
IndexError: shape mismatch: indexing arrays could not be
broadcast together with shapes (2,) (3,) 
>>> # Но ничего не работает... 
...
>>> b = a[i]
>>> b
array([[ 0,  1,  2,  3,  4,  5,  6],
       [14, 15, 16, 17, 18, 19, 20]])
>>>
>>> b[:, j]    #  Ведь это работает! Почему не работает a[i,j]
array([[ 0,  3,  6],
       [14, 17, 20]])
>>>
>>>
>>> #  Если j = np.array([[True, False, False, True, False, False, True],
... #                     [True, False, False, True, False, False, True]])
... #  то благодаря механизму транслирования сработает индексация вида a[i][j]
... 
>>> j = np.array([[True, False, False, True, False, False, True],
... [True, False, False, True, False, False, True]])
>>> 
>>> a[i][j]
array([ 0,  3,  6, 14, 17, 20])

Здесь можно долго ломать голову, но оказывается, что если мы собрались использовать массивы i и j в одном массиве индексов [i, j] (а не [i][j]), то количество элементов True в массивах i и j должно совпадать. Причем получается не совсем то, что можно было бы ожидать:

>>> a
array([[ 0,  1,  2,  3,  4,  5,  6],
       [ 7,  8,  9, 10, 11, 12, 13],
       [14, 15, 16, 17, 18, 19, 20]])
>>>
>>> i
array([ True, False,  True], dtype=bool)
>>>
>>> j
array([ True, False, False,  True, False, False,  True], dtype=bool)
>>>
>>>  #  Сравняем количество элементов True в массивах "i" и "j"
...
>>> j = np.array([True, False, False, False, False, False, True])
>>> a[i,j]
array([ 0, 20])

Все равно, не слишком то понятно как это работает. Давайте попробуем разобраться:

>>> a = np.arange(49).reshape(7, 7)
>>> a
array([[ 0,  1,  2,  3,  4,  5,  6],
       [ 7,  8,  9, 10, 11, 12, 13],
       [14, 15, 16, 17, 18, 19, 20],
       [21, 22, 23, 24, 25, 26, 27],
       [28, 29, 30, 31, 32, 33, 34],
       [35, 36, 37, 38, 39, 40, 41],
       [42, 43, 44, 45, 46, 47, 48]])
>>>
>>> i = np.array([ True, False,  True, False,  True, False,  True])
>>> j = np.array([ True, False,  True, False,  True, False,  True])
>>>
>>> a[i]    #  отлично подойдет что бы извлекать строки
array([[ 0,  1,  2,  3,  4,  5,  6],
       [14, 15, 16, 17, 18, 19, 20],
       [28, 29, 30, 31, 32, 33, 34],
       [42, 43, 44, 45, 46, 47, 48]])
>>>
>>> a[:, j]    #  отлично подойдет что бы извлекать столбцы
array([[ 0,  2,  4,  6],
       [ 7,  9, 11, 13],
       [14, 16, 18, 20],
       [21, 23, 25, 27],
       [28, 30, 32, 34],
       [35, 37, 39, 41],
       [42, 44, 46, 48]])
>>>
>>> b = a[i]
>>> b[:, j]    #  Извлекаем пересечения строк и столбцов
array([[ 0,  2,  4,  6],
       [14, 16, 18, 20],
       [28, 30, 32, 34],
       [42, 44, 46, 48]])
>>>
>>> a[i, j]    
array([ 0, 16, 32, 48])
>>>
>>> #  Заменим False и True буквами F и T
... #  теперь оформим таблицу
... #          T   F   T   F   T   F   T
... #      T [ 0,  1,  2,  3,  4,  5,  6]
... #      F [ 7,  8,  9, 10, 11, 12, 13]
... #      T [14, 15, 16, 17, 18, 19, 20]
... #      F [21, 22, 23, 24, 25, 26, 27]
... #      T [28, 29, 30, 31, 32, 33, 34]
... #      F [35, 36, 37, 38, 39, 40, 41]
... #      T [42, 43, 44, 45, 46, 47, 48]

В данном случае извлекаются не все элементы на пересечении букв "Т", а только по одному из каждой строки. Соответствующие элементы из i и j образуют что-то вроде координат:

i = np.array([ True, False,  False, True,  False, False,  True])
>>> j = np.array([ True, False,  False, True,  False, False,  True])
>>>
>>> a[i, j]
array([ 0, 24, 48])
>>>
... #          T   F   F   T   F   F   T
... #      T [ 0,  1,  2,  3,  4,  5,  6]
... #      F [ 7,  8,  9, 10, 11, 12, 13]
... #      F [14, 15, 16, 17, 18, 19, 20]
... #      T [21, 22, 23, 24, 25, 26, 27]
... #      F [28, 29, 30, 31, 32, 33, 34]
... #      F [35, 36, 37, 38, 39, 40, 41]
... #      T [42, 43, 44, 45, 46, 47, 48]
>>>
>>> i = np.array([ True, True,  False, False,  True, True,  False])
>>> j = np.array([ True, True,  False, False,  True, True,  False])
>>>
>>> a[i, j]
array([ 0,  8, 32, 40])
>>>
... #          T   T   F   F   T   T   F
... #      T [ 0,  1,  2,  3,  4,  5,  6]
... #      T [ 7,  8,  9, 10, 11, 12, 13]
... #      F [14, 15, 16, 17, 18, 19, 20]
... #      F [21, 22, 23, 24, 25, 26, 27]
... #      T [28, 29, 30, 31, 32, 33, 34]
... #      T [35, 36, 37, 38, 39, 40, 41]
... #      F [42, 43, 44, 45, 46, 47, 48]
>>>
>>> j = np.array([ True, True,  False, False,  True, True,  False])
>>> j = np.array([ False, True,  True, False,  False, True,  True])
>>>
>>> a[i, j]
>>>
array([ 1,  9, 33, 41])
... #          F   T   T   F   F   T   T
... #      T [ 0,  1,  2,  3,  4,  5,  6]
... #      T [ 7,  8,  9, 10, 11, 12, 13]
... #      F [14, 15, 16, 17, 18, 19, 20]
... #      F [21, 22, 23, 24, 25, 26, 27]
... #      T [28, 29, 30, 31, 32, 33, 34]
... #      T [35, 36, 37, 38, 39, 40, 41]
... #      F [42, 43, 44, 45, 46, 47, 48]

Наверняка существуют задачи где это может очень пригодиться, тем более такая индексация выполняется на языке C т. е. гораздо быстрее чем аналогичная на Python.