Расширенное индексирование массивов NumPy, стало проще

Понять, что означает [:: 2, [0,3,4],…, 2: 5]

Одним из самых больших преимуществ NumPy является его чрезвычайно быстрая индексация, но она может очень быстро усложняться. Например, учитывая случайно сгенерированный массив целых чисел формы 5 × 6 × 3 × 5, что будет выполнять следующая операция и какой формы будет полученный срез?

array[np.round(array)==array].reshape((5,6,3,5))[::2, [0,3,4], ..., 2:5]

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

Одномерные массивы

Рассмотрим конструкцию массива array = np.array([1, 2, 3, 4, 5]), которая создает массив чисел от 1 до 5.

Чтобы получить доступ к первому элементу, мы вызываем array[0]. Аналогичным образом, чтобы получить доступ ко второму элементу, мы вызываем array[1].

Для доступа к последнему элементу мы используем отрицательные индексы (array[-1]). Аналогично, ко второму последнему элементу можно получить доступ с помощью array[-2].

Чтобы получить доступ к диапазону элементов, укажите начальный индекс и конечный индекс. Результат будет включать все элементы до индекса остановки минус один (подумайте об этом как о том, что 7 не включен в range(7)). Например, захват первых трех элементов array может быть выполнен с помощью array[0:3], хотя он может быть переписан как array[:3], который игнорирует 0, но возвращает тот же результат.

Диапазон элементов также поддерживает назначение. Например, array[2:5] = np.arange(3) сделает третий, четвертый и пятый элементы (второй, третий и четвертый индексы) равными 0, 1 и 2 соответственно.

С другой стороны, чтобы получить доступ к нескольким значениям в настраиваемых индексах, передайте список или массив в скобки индексации. Например, array[[1, 3]] возвращает array([2, 4]), потому что он запрашивает второе и четвертое значения массива. Использование индексированных массивов для индексации других массивов дает большую свободу и часто является хорошим способом достижения идеальной индексации, если функции или санкционированные методы не работают. Индексы в этих списках тоже могут быть отрицательными!

Чтобы получить доступ к диапазону элементов с размером шага, необходимо указать начальный индекс, конечный индекс и размер шага. Например, array[:4:2] возвращает [1, 3]. Во-первых, array[:4] возвращает первые четыре элемента, равные [1, 2, 3, 4]. Начиная с первого элемента и меняя размер шага, получается массив [1, 3].

Использование array[:] - один из самых быстрых и эффективных способов копирования массива.

Индексация массивов может показаться недоступной из-за сокращенной записи, используемой для избежания ввода нулей или концов: например, array[::2] возвращает [1, 3, 5]. Три основных параметра индексации - начальный индекс, конечный индекс и размер шага - указываются их положением относительно двоеточия (:). Когда набирается [::2], это означает, что мы намеренно пренебрегаем начальным и конечным индексами и хотим предоставить информацию только о желаемом размере шага. Как следствие, NumPy вернет весь массив с указанным размером шага.

Массивы NumPy также поддерживают условное индексирование. Рассмотрим десятиэлементный массив случайно сгенерированных целых чисел от -5 до 5 включительно:

rand = np.random.randint(low=-5, high=5, size=10)
array([-2, -1,  2, -2,  4,  3, -1, -5, -2,  2])

Чтобы найти все положительные значения, мы можем вызвать rand[rand>0], который возвращает array([2, 4, 3, 2]). Это работает, потому что rand>0 или любое другое аналогичным образом написанное условие называется логической маской, значение которой равно array([False, False, True, False, True, True, False, False, False,
True])
. NumPy просто возвращает значения, соответствующие значения маски которых равны True. При использовании этой логики, пока условие возвращает допустимую логическую маску, допустима такая операция, как rand[rand**3<=2*rand].

Условные операторы работают с массивами всех измерений.

Двумерные массивы

Рассмотрим следующий двумерный массив:

array = np.array([[1, 2, 3, 4, 5],
                  [6, 7, 8, 9, 10],
                  [11, 12, 13, 14, 15]])

Использование одномерного индексирования двумерных данных требует сдвига в представлении о данных не как о табличных, а как о списках. С этой точки зрения array - это массив из трех элементов, где каждый элемент представляет собой другой массив из пяти элементов.

Следовательно, array[:2], который извлекает первые два элемента, должен вернуть:

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

Проверьте себя - что должно array[:2:2] вернуть?

Поскольку array - это просто массив, элементы которого являются массивами, мы должны рассматривать его как таковой. [:2:2] выделяет первые два элемента (нулевой начальный индекс и два конечного индекса) с размером шага два. Поскольку начальная индексация диапазона ([:2]) возвращает только два значения, а размер шага равен двум, результатом будет только первый элемент. В нашем двумерном массиве это другой массив:

array([[1, 2, 3, 4, 5]])

Использование методов одномерного индексирования для двумерных массивов очень ограничено. По этой причине двумерное индексирование принимает форму array[a:b:c, d:e:f]. a:b:c и d:e:f представляют тройные значения для начального индекса, конечного индекса и индекса шага. Однако a:b:c в левой части запятой применяет преобразования по строкам, а d:e:f применяет их «по столбцам» или к следующему измерению.

Рассмотрим, например, следующую команду: array[:2,3:]. Полезно разбить эту индексацию на несколько последовательных частей.

1 | [:2] принимает первые два элемента array. Поскольку array двумерный, это первые две строки:

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

2 | [3:] указывает начальный индекс (3), но не конечный индекс, что означает, что в массиве из пяти элементов он начнется с четвертого элемента (индекс 3) и продолжится до конца массива. Поскольку он применяется по столбцам, результат будет следующим:

array([[ 4,  5],
       [ 9, 10]])

Как и при одномерном индексировании, двумерные массивы также можно индексировать с помощью списков для доступа к значениям настраиваемого индекса. Однако двумерные массивы также должны индексироваться двумерными списками или массивами.

Индексирование многомерных массивов

Многомерные массивы работают аналогично двум массивам в более низких измерениях. Рассмотрим трехмерный массив с именем array и формой (3, 2, 3):

array([[[ 0,  1,  2],
        [ 3,  4,  5]],

       [[ 6,  7,  8],
        [ 9, 10, 11]],

       [[12, 13, 14],
        [15, 16, 17]]])

Как и двухмерные массивы, измерения можно индексировать с помощью трехзначных групп, с отдельной индексацией для измерений, разделенных запятой. Например, array[:3:2,:,1:3] выполняет следующие операции:

1 | [:3:2] означает «проиндексировать все элементы до третьего индекса (четвертого элемента) с размером шага два». В списке массивов с тремя элементами это означает, что первый и третий элементы сохраняются.

array([[[ 0,  1,  2],
        [ 3,  4,  5]],

       [[12, 13, 14],
        [15, 16, 17]]])

2 | [:] означает «ничего не делать». Поскольку информация не предоставляется, NumPy не изменяет массив, но он должен быть там, чтобы указать, что со вторым измерением ничего не должно произойти.

3 | [1:3] означает «индексировать все элементы, начиная с первого индекса (второй элемент) и заканчивая третьим индексом (четвертый элемент). Этот разрез применяется к третьему и последнему измерению.

array([[[ 1,  2],
        [ 4,  5]],

       [[13, 14],
        [16, 17]]])

Эллипсы () могут использоваться для обозначения нескольких двоеточий и запятых, когда количество измерений велико. Рассмотрим четырехмерный массив z:

z = np.arange(81).reshape(3,3,3,3)

В этом случае z[1, :, :, 0] то же самое, что писать z[1, …, 0]. Кроме того, мы могли бы записать z[:, :, :, 0] as z[…, 0] и z[1, :, 2, 0] как z[1, …, 2, 0] (хотя в этом конкретном случае это не обязательно). Для каждого индекса поддерживается только одно многоточие.

При использовании множества элементов для индексации массива важно понимать разницу между списками и кортежами. Рассмотрим массив 3 на 3 z:

array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])

Команда z[[0,0]] остается в первом построчном измерении, потому что в основных квадратных скобках нет запятых. Результат индексации [0, 0] состоит в том, что первая строка (индекс 0) повторяется дважды:

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

С другой стороны, набор z[(0, 1)] - это еще один метод набора z[0, 1], поскольку он вернет значение в первой строке (индекс 0) и во втором столбце (индекс 1). Перефразированные кортежи могут использоваться для пересечения измерений, но, поскольку их присутствие относительно произвольно, рекомендуется вообще не писать кросс-размерную индексацию в круглых скобках.

Советы по интерпретации и написанию сложной индексации

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

  • Запишите, какую часть каждого измерения вы хотите проиндексировать на английском языке, а затем переведите это измерение за измерением (разделяя их запятыми).
  • Как правило, старайтесь согласовывать типы данных, которые вы используете для индексации в одной команде. Например, использование обозначений a:b и [a, b, c, d], если это не является абсолютно необходимым, может привести к непредсказуемости результата.
  • Не сжимайте все в одну команду. Или попробуйте проиндексировать размер за измерением, используя то же пошаговое сокращение данных, а затем перепишите цепочку команд как одну команду, как только убедитесь, что она работает.
  • Разбейте все на a:b:c групп.