Индексация NumPy, её особенности и применение

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

Подпишись на группу Вконтакте и Телеграм-канал. Там еще больше полезного контента для программистов.
А на YouTube-канале ты найдешь обучающие видео по программированию. Подписывайся!

Сравнение с Python

Конечно же, для начала импортируем исследуемый модуль numpy.

import numpy as np

По традиции, мы импортируем numpy под псевдонимом np.

Теперь создадим вектор (в Python — одномерный список) целых чисел от 0 до 9, используя list.

numbers = list(range(10))
numbers

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

У списков Python есть только две возможности индексирования по умолчанию (имеется в виду без дополнений пользователя с помощью наследования) — передать туда целое число либо объект типа slicePython он имеет литеральное обозначение start:end:step).

numbers[5], numbers[9:5:-1]

(5, [9, 8, 7, 6])

Теперь же создадим массив NumPy с такими же элементами.

arr = np.arange(0, 10, 1)
arr

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

Опробуем тот же метод индексации.

arr[5], arr[9:5:-1]

(5, array([9, 8, 7, 6]))

Отлично, мы убедились в том, что массивы NumPy поддерживают те же инструменты индексации, что и объекты Python (написано именно так, потому что не только списки в Python поддерживают индексацию ).

Индексирование с помощью массивов

Также массивы NumPy можно индексировать с помощью обычных итерируемых объектов, содержащих индексы.

arr[[5, 3, 6, 2]]

array([5, 3, 6, 2])

Стоит заметить, что порядок элементов сохраняется в соответствии индексов, указанных в массиве.

Также возможна индексация с помощью булевых массивов (такие массивы называются маской). Их форма должна совпадать с формой индексируемого массива. К примеру :

mask = [True, False, True, False, True] * 2
arr[mask]

array([0, 2, 4, 5, 7, 9])

Особо удобным этот инструмент кажется из-за того, что все операции, которые должны возвращать булево значение, по отношению к массивам NumPy выполняются поэлементно, и соответственно возвращают булев массив такой же формы. Наглядный пример — чтобы выбрать все элементы массива, больше 5, достаточно передать в квадратные скобки конструкцию arr > 5, которая возвращает array([False, False, False, False, False, False, True, True, True, True]).

arr[arr > 5]

array([6, 7, 8, 9])

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

Перейдем к созданию двумерных массивов и выполнению операций над ними. Создадим матрицу вида

    \[A = \begin{pmatrix}1 & 2 & 3\\4 & 5 & 6\\7 & 8 & 9\\\end{pmatrix}\]

matrix = np.array([range(1, 10)]).reshape(3, 3)
matrix

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

Теперь перед нами раскрывается ещё больше возможностей, так как теперь у массива появилось две строки индексации (в нашем случае axis=0 это строки матрицы, а axis=1 — столбцы).

Это не значит, что двумерный массив не поддерживает обыкновенную индексацию массивом чисел:

matrix[[0, 1]]

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

Но теперь же мы можем передать в квадратные скобки второй массив, который произведет индексацию уже по второй оси.

matrix[[0, 1], 2] 

array([3, 6])

Также мы можем совмещать срезы и индексы, таким способом делая наш инструмент всё более гибким.

matrix[:, [0, 2]]

array([[1, 3], 
       [4, 6], 
       [7, 9]])

Конструкция [:, ] означает полный перебор по соответствующей оси.

Также никто не запрещает нам использовать два среза по обеим осям. В следующем примере продемонстрирована интересная работа, которая поясняет, что конструкции [0] и [:1] не совсем одинаковые вещи, какими кажутся на первый взгляд.

matrix[:, 0], matrix[:, :1]

(array([1, 4, 7]), 
 array([[1], 
        [4], 
        [7]]))

Также с помощью срезов и операций сравнения можно выполнять интересные вещи. К примеру, выберем из матрицы все столбцы, второй элемент которых (имеется в виду действительно второй элемент, а не элемент с индексом 2) больше 3.

matrix[:, matrix[:, 1] > 3]

array([[2, 3], 
       [5, 6], 
       [8, 9]])

Срез — копирование или нет?

Ещё один вопрос заключается в следующем: при срезе элементы копируются или возвращается ссылка на них? Рассмотрим поведение подобного случая по отношению к спискам Python.

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

matrix_slice = matrix_list[0:1]
matrix_slice[0] = 5

matrix_list

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

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

matrix = np.arange(0, 9, 1).reshape(3, 3)

matrix_slice = matrix[0:1]
matrix_slice[0] = 5

matrix

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

Ха! Индексация NumPy срез не копирует, а потому его изменение прямо влияет на исходный объект. Будьте с этим предельно осторожны.

Заключение

Сегодня мы рассмотрели, что же такое индексация массивов NumPy. Указание индекса, маски и срезы — всё это сегодня было. Также мы обсудили проблему копирования среза, таким способом выявив значительную разницу между NumPy и Python.

Если вы используете Python в изучении физики, советую прочитать статью Бросок тела с высоты, моделируем на Python.

А также подписывайтесь на группу ВКонтакте, Telegram и YouTube-канал. Там еще больше полезного и интересного для программистов.