Что такое массивы NumPy и зачем нужна индексация
NumPy (Numerical Python) — это фундаментальная библиотека для научных вычислений в Python. Она предоставляет мощный объект многомерного массива, а также инструменты для работы с этими массивами. Одним из важнейших аспектов работы с массивами NumPy является индексация, которая позволяет получать доступ к отдельным элементам, срезам и даже сложным подмножествам данных. Без эффективной индексации работа с большими объемами данных в NumPy была бы крайне затруднительной. В контексте интернет-маркетинга, например, индексация массивов NumPy может использоваться для быстрого анализа больших наборов данных о рекламных кампаниях, выявления трендов и оптимизации бюджета.
Основные понятия: индексы, срезы и маски
- Индекс — это целое число, указывающее на позицию элемента в массиве. Индексы начинаются с 0.
- Срез — это подмножество массива, выбранное с использованием диапазона индексов. Например, можно выбрать все элементы с 2-го по 5-й.
- Маска (булева индексация) — это логический массив (массив булевых значений), который используется для выбора элементов, удовлетворяющих определенному условию.
Базовая индексация: доступ к отдельным элементам
Индексация одномерных массивов
Доступ к элементам одномерного массива осуществляется по их индексу, начиная с 0.
import numpy as np
# Создаем одномерный массив.
arr: np.ndarray = np.array([10, 20, 30, 40, 50])
# Получаем доступ к элементу с индексом 0.
first_element: int = arr[0] # 10
# Получаем доступ к элементу с индексом 3.
fourth_element: int = arr[3] # 40
print(f'{first_element=}')
print(f'{fourth_element=}')
Индексация многомерных массивов
В многомерных массивах для доступа к элементу необходимо указать индексы по каждому измерению.
import numpy as np
# Создаем двумерный массив.
arr: np.ndarray = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
# Получаем доступ к элементу в строке 0, столбце 2.
element: int = arr[0, 2] # 3
# Получаем доступ к элементу в строке 2, столбце 1.
other_element: int = arr[2, 1] # 8
print(f'{element=}')
print(f'{other_element=}')
Использование отрицательных индексов
Отрицательные индексы позволяют обращаться к элементам с конца массива. -1 — последний элемент, -2 — предпоследний и т.д.
import numpy as np
# Создаем одномерный массив.
arr: np.ndarray = np.array([10, 20, 30, 40, 50])
# Получаем доступ к последнему элементу.
last_element: int = arr[-1] # 50
# Получаем доступ к предпоследнему элементу.
second_last_element: int = arr[-2] # 40
print(f'{last_element=}')
print(f'{second_last_element=}')
Срезы массивов NumPy: выбор подмассивов
Создание срезов одномерных массивов
Срезы позволяют выбирать подмножества элементов из массива. Синтаксис: arr[start:stop:step], где start — индекс начала среза (включительно), stop — индекс конца среза (исключительно), step — шаг.
import numpy as np
# Создаем одномерный массив.
arr: np.ndarray = np.array([10, 20, 30, 40, 50, 60, 70])
# Выбираем элементы с индекса 1 по 4 (не включая 4).
slice_arr: np.ndarray = arr[1:4] # [20, 30, 40]
# Выбираем элементы с начала массива до индекса 3 (не включая 3).
slice_arr_start: np.ndarray = arr[:3] # [10, 20, 30]
# Выбираем элементы с индекса 4 до конца массива.
slice_arr_end: np.ndarray = arr[4:] # [50, 60, 70]
print(f'{slice_arr=}')
print(f'{slice_arr_start=}')
print(f'{slice_arr_end=}')
Создание срезов многомерных массивов
В многомерных массивах срезы создаются по каждому измерению.
import numpy as np
# Создаем двумерный массив.
arr: np.ndarray = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
# Выбираем первую строку.
first_row: np.ndarray = arr[0, :] # [1, 2, 3]
# Выбираем второй столбец.
second_column: np.ndarray = arr[:, 1] # [2, 5, 8]
# Выбираем подмассив 2x2.
sub_arr: np.ndarray = arr[:2, :2] # [[1, 2], [4, 5]]
print(f'{first_row=}')
print(f'{second_column=}')
print(f'{sub_arr=}')
Использование шага при создании срезов
Шаг позволяет выбирать элементы с определенным интервалом.
import numpy as np
# Создаем одномерный массив.
arr: np.ndarray = np.array([10, 20, 30, 40, 50, 60, 70])
# Выбираем каждый второй элемент.
every_second: np.ndarray = arr[::2] # [10, 30, 50, 70]
# Выбираем элементы в обратном порядке.
in_reverse: np.ndarray = arr[::-1] # [70, 60, 50, 40, 30, 20, 10]
print(f'{every_second=}')
print(f'{in_reverse=}')
Копирование и представление (view) срезов
Важно понимать, что срезы NumPy по умолчанию создают представление (view) исходного массива, а не его копию. Это означает, что изменение среза отразится и на исходном массиве. Чтобы создать копию среза, необходимо использовать метод .copy().
import numpy as np
# Создаем одномерный массив.
arr: np.ndarray = np.array([10, 20, 30, 40, 50])
# Создаем представление (view).
slice_view: np.ndarray = arr[1:4]
# Создаем копию.
slice_copy: np.ndarray = arr[1:4].copy()
# Изменяем элемент в представлении.
slice_view[0] = 100
print(f'{arr=}') # arr=[ 10 100 30 40 50]
print(f'{slice_view=}') # slice_view=[100 30 40]
# Изменяем элемент в копии.
slice_copy[1] = 200
print(f'{arr=}') # arr=[ 10 100 30 40 50]
print(f'{slice_copy=}') # slice_copy=[100 200 40]
Булева индексация (маскирование): выбор элементов по условию
Создание булевых масок
Булева маска — это массив булевых значений (True/False), который имеет ту же форму, что и исходный массив. Маска создается на основе некоторого условия.
import numpy as np
# Создаем одномерный массив.
arr: np.ndarray = np.array([10, 20, 30, 40, 50])
# Создаем маску: элементы больше 25.
mask: np.ndarray = arr > 25 # [False, False, True, True, True]
print(f'{mask=}')
Применение булевых масок для выбора элементов
Маска применяется к массиву для выбора только тех элементов, для которых соответствующее значение в маске равно True.
import numpy as np
# Создаем одномерный массив.
arr: np.ndarray = np.array([10, 20, 30, 40, 50])
# Создаем маску: элементы больше 25.
mask: np.ndarray = arr > 25
# Применяем маску.
filtered_arr: np.ndarray = arr[mask] # [30, 40, 50]
print(f'{filtered_arr=}')
Использование логических операторов с масками (AND, OR, NOT)
Для создания более сложных условий можно использовать логические операторы & (AND), | (OR), ~ (NOT).
import numpy as np
# Создаем одномерный массив.
arr: np.ndarray = np.array([10, 20, 30, 40, 50])
# Создаем маску: элементы больше 20 И меньше 40.
mask: np.ndarray = (arr > 20) & (arr < 40)
# Применяем маску.
filtered_arr: np.ndarray = arr[mask] # [30]
print(f'{filtered_arr=}')
Метод np.where(): получение индексов элементов, удовлетворяющих условию
Использование np.where() с одним условием
Функция np.where() возвращает индексы элементов массива, удовлетворяющих заданному условию.
import numpy as np
# Создаем одномерный массив.
arr: np.ndarray = np.array([10, 20, 30, 40, 50])
# Получаем индексы элементов больше 25.
indices: tuple[np.ndarray] = np.where(arr > 25)
print(f'{indices=}') # indices=(array([2, 3, 4]),)
Использование np.where() с несколькими условиями
np.where() также можно использовать с несколькими условиями, комбинируя их с помощью логических операторов.
import numpy as np
# Создаем одномерный массив.
arr: np.ndarray = np.array([10, 20, 30, 40, 50])
# Получаем индексы элементов больше 20 И меньше 40.
indices: tuple[np.ndarray] = np.where((arr > 20) & (arr < 40))
print(f'{indices=}') # indices=(array([2]),)
Замена значений на основе условия с помощью np.where()
np.where() можно использовать для замены значений в массиве на основе условия. В этом случае функция принимает три аргумента: условие, значение для элементов, удовлетворяющих условию, и значение для элементов, не удовлетворяющих условию.
import numpy as np
# Создаем одномерный массив.
arr: np.ndarray = np.array([10, 20, 30, 40, 50])
# Заменяем элементы больше 25 на 100, а остальные на 0.
new_arr: np.ndarray = np.where(arr > 25, 100, 0)
print(f'{new_arr=}') # new_arr=[ 0 0 100 100 100]
Функция np.nonzero(): получение индексов ненулевых элементов
Применение np.nonzero() к одномерным массивам
Функция np.nonzero() возвращает индексы ненулевых элементов массива.
import numpy as np
# Создаем одномерный массив.
arr: np.ndarray = np.array([0, 10, 0, 20, 30, 0])
# Получаем индексы ненулевых элементов.
indices: tuple[np.ndarray] = np.nonzero(arr)
print(f'{indices=}') # indices=(array([1, 3, 4]),)
Применение np.nonzero() к многомерным массивам
Для многомерных массивов np.nonzero() возвращает кортеж массивов, где каждый массив содержит индексы ненулевых элементов по соответствующему измерению.
import numpy as np
# Создаем двумерный массив.
arr: np.ndarray = np.array([[0, 1, 0], [2, 0, 3], [0, 0, 4]])
# Получаем индексы ненулевых элементов.
indices: tuple[np.ndarray, np.ndarray] = np.nonzero(arr)
print(f'{indices=}') # indices=(array([0, 1, 1, 2]), array([1, 0, 2, 2]))
# Это означает, что ненулевые элементы находятся в позициях (0, 1), (1, 0), (1, 2) и (2, 2).
Разница между np.where() и np.nonzero()
np.where(condition) возвращает индексы элементов, для которых condition истинно. np.nonzero(arr) эквивалентно np.where(arr != 0). Выбор между ними зависит от конкретной задачи и читаемости кода.
Индексация с использованием целочисленных массивов (Fancy indexing)
Выбор элементов по списку индексов
Fancy indexing позволяет выбирать элементы массива с использованием другого массива целых чисел в качестве индексов.
import numpy as np
# Создаем одномерный массив.
arr: np.ndarray = np.array([10, 20, 30, 40, 50])
# Создаем массив индексов.
indices: np.ndarray = np.array([0, 2, 4])
# Выбираем элементы по индексам.
selected_arr: np.ndarray = arr[indices] # [10, 30, 50]
print(f'{selected_arr=}')
Использование fancy indexing для изменения элементов массива
Fancy indexing также можно использовать для изменения значений элементов массива.
import numpy as np
# Создаем одномерный массив.
arr: np.ndarray = np.array([10, 20, 30, 40, 50])
# Создаем массив индексов.
indices: np.ndarray = np.array([0, 2, 4])
# Изменяем элементы по индексам.
arr[indices] = [100, 300, 500]
print(f'{arr=}') # [100 20 300 40 500]
Сочетание fancy indexing и срезов
Fancy indexing можно комбинировать со срезами для более сложной выборки элементов.
Функция np.argwhere(): Поиск индексов элементов в массиве NumPy
Пример использования np.argwhere для поиска индексов
np.argwhere() находит индексы элементов массива, которые не равны нулю, сгруппированные по элементам. Это похоже на np.where(), но возвращает индексы в другом формате, более удобном для некоторых задач.
import numpy as np
# Создаем массив NumPy.
arr: np.ndarray = np.array([[0, 1, 2], [3, 0, 5], [6, 7, 0]])
# Используем np.argwhere для поиска индексов ненулевых элементов.
indices: np.ndarray = np.argwhere(arr)
print(f'{indices=}')
# Результат:
# indices=[[0, 1],
# [0, 2],
# [1, 0],
# [1, 2],
# [2, 0],
# [2, 1]]
Альтернативные способы получения индексов
Использование np.ndenumerate() для итерации по массиву с индексами
np.ndenumerate() позволяет итерироваться по массиву, получая одновременно индекс и значение каждого элемента.
import numpy as np
# Создаем двумерный массив.
arr: np.ndarray = np.array([[1, 2], [3, 4]])
# Итерируемся по массиву с индексами.
for index, value in np.ndenumerate(arr):
print(f'Index: {index}, Value: {value}')
Применение np.ravel_multi_index() для преобразования многомерных индексов в плоские
np.ravel_multi_index() преобразует набор индексов из многомерного массива в плоский индекс, как если бы массив был развернут в одномерный. Это полезно, когда нужно представить многомерный индекс в виде одного числа.
import numpy as np
# Определяем размеры массива.
shape: tuple[int, int] = (3, 4)
# Задаем многомерные индексы.
multi_index: tuple[np.ndarray, np.ndarray] = (np.array([0, 1, 2]), np.array([0, 2, 3]))
# Преобразуем многомерные индексы в плоские.
flat_index: np.ndarray = np.ravel_multi_index(multi_index, shape)
print(f'{flat_index=}')
Продвинутая индексация: broadcasting и другие техники
Понимание broadcast правил при индексации
При индексации массивов NumPy важно понимать правила broadcasting, которые определяют, как массивы разных размеров могут быть использованы в операциях. Broadcasting может быть полезен для создания сложных выборок элементов.
Использование np.ix_() для создания открытых сеток индексов
np.ix_() используется для создания открытой сетки индексов. Это позволяет комбинировать различные массивы индексов для выборки сложных подмножеств данных.
Оптимизация индексации для повышения производительности
Избегайте ненужного копирования данных
По возможности избегайте создания копий массивов при индексации. Используйте представления (views), если это допустимо для вашей задачи. Копирование данных может значительно замедлить выполнение кода.
Используйте векторизованные операции вместо циклов
NumPy разработан для выполнения векторизованных операций, которые намного быстрее, чем циклы Python. Поэтому старайтесь использовать встроенные функции NumPy и избегать явных циклов при индексации и обработке массивов.
Заключение
Краткое повторение рассмотренных методов индексации
В этой статье мы рассмотрели различные методы индексации массивов NumPy, включая:
- Базовую индексацию для доступа к отдельным элементам.
- Срезы для выбора подмассивов.
- Булеву индексацию (маскирование) для выбора элементов по условию.
- Функции
np.where()иnp.nonzero()для получения индексов элементов, удовлетворяющих условиям. - Fancy indexing для выбора элементов по списку индексов.
- Альтернативные способы, такие как
np.ndenumerate()иnp.ravel_multi_index().
Рекомендации по выбору оптимального метода индексации в зависимости от задачи
Выбор оптимального метода индексации зависит от конкретной задачи. Для простого доступа к элементам используйте базовую индексацию. Для выбора подмножеств данных используйте срезы. Для выбора элементов по условию используйте булеву индексацию или np.where(). Для сложной выборки элементов используйте fancy indexing. Помните о производительности и избегайте ненужного копирования данных. В контексте задач интернет-маркетинга и анализа данных, правильный выбор метода индексации может значительно ускорить обработку больших наборов данных и оптимизацию рекламных кампаний.