Библиотека Numpy помогает придерживаться двух принципов:
- Упрощение разработки и поддержки готового кода
- Оптимизация вычислений
Для этого разработчики библиотеки использовали следующие подходы и механизмы:
- Одинаковый интерфейс методов для выполнения операций над массивами разной размерности и типа
- Укладывание и векторизация для замены циклов
В этом уроке мы рассмотрим примеры векторизованных функций и методов — причем и встроенных в Numpy, и реализованных самостоятельно.
Арифметические операции
Начнем с самой простой арифметической операции — сложения:
import numpy as np
# Сумма элементов вектора
test_vector = np.array([1,5,9])
print(np.sum(test_vector))
# => 15
# Сумма элементов матрицы
test_matrix = np.array(
[
[2, 4, 6],
[1, 2, 8],
]
)
print(np.sum(test_matrix))
# => 23
Интерфейс функции np.sum()
позволяет применять ее к массивам разной размерности без изменения синтаксиса. Реализация без циклов делается в одну строчку кода как для векторов, так и для матриц.
В случае одномерной структуры (вектора) сложение элементов можно производить только по одному направлению. В случае матрицы все немного по-другому — их элементы можно складывать по двум осям:
- По строкам
- По столбцам
Для суммирования по каждой из осей достаточно параметризовать функцию np.sum()
таким образом:
# Сумма элементов матрицы по столбцам
print(np.sum(test_matrix, axis=0))
# => [ 3 6 14]
# Сумма элементов матрицы по строкам
print(np.sum(test_matrix, axis=1))
# => [12 11]
Интерфейс многих функций библиотеки Numpy сделан по аналогии с np.sum()
. Например, чтобы найти средние значения в матрице по строкам и столбцам, можно воспользоваться функцией np.mean()
с теми же параметрами:
# Среднее элементов матрицы по столбцам
print(np.mean(test_matrix, axis=0))
# => [1.5 3. 7. ]
# Среднее элементов матрицы по строкам
print(np.mean(test_matrix, axis=1))
# => [4. 3.67]
Методы массивов также реализованы с учетом возможности действия по разным осевым направлением. Синтаксических отличий от функций нет. Рассмотрим на примере метода для нахождения минимального значения.
# Минимальный элемент матрицы по столбцам
print(test_matrix.min(axis=0))
# => [1 2 6]
# Минимальный элемент матрицы по строкам
print(test_matrix.min(axis=1))
# => [2 1]
Векторизация не только присутствует в библиотеке Numpy. В более широком смысле это один из подходов к параллельным вычислениям, которые реализуются на уровне процессоров и видеокарт.
При этом важно понимать, что векторизация в Numpy не всегда помогает оптимизировать скорость выполнения функций. Иногда этого просто нельзя достичь.
Кроме ускорения, есть еще одна цель. Это унификация интерфейса написанных функций — она повышает читаемость кода. Это важно в разработке любых программ, ведь небольшое количество строк и единообразие кода снижают вероятность возникновения ошибок и упрощают поддержку.
Создание векторизованных функций
Чтобы создать собственную векторизованную функцию, можно использовать функцию np.vectorize()
.
Рассмотрим ее применение на примере функции fill_over_bound()
. Она находит значения, выходящие за пределы некоторой границы, а затем меняет их на значение этой границы:
# Исходная функция
def fill_over_bound(a, b):
'''Limit elements from array a by value b'''
returned_value = a
if a > b:
returned_value = b
return returned_value
# Векторизованный вариант
vectorized_fill_over_bound = np.vectorize(fill_over_bound)
Векторизованный вариант исходной функции можно применять к массивам разной размерности:
vector_array = [1,2,3,4,5,6]
matrix_array = [[1,6],[14,15]]
bound_value = 4
print(vectorized_fill_over_bound(vector_array, bound_value))
# => [1 2 3 4 4 4]
print(vectorized_fill_over_bound(matrix_array, bound_value))
# => [[1 4]
# [4 4]]
При этом использование np.vectorize()
необходимо. Попытка использовать не векторизованный вариант функции приведет к ошибке:
fill_over_bound(matrix_array, bound_value)
# => TypeError: '>' not supported between instances of 'list' and 'int'
Представим, что нам нужно подготовить данные о продажах сети магазинов за неделю.
Предположим, что для оценки качества работы магазинов мы решили рассматривать только типичные значения продаж. Типичным будем называть только те значения, которые не отклоняются от среднего показателя на величину более одного стандартного отклонения.
Перед аналитиком стоит такая задача — преобразовать все значения к типичному диапазону, заменив выбивающиеся значения на значения ближайшей к нему границы. Так выглядит список продаж:
# Список продаж
orders = [
[7, 1, 7, 8],
[4, 2, 4, 5],
[3, 5, 2, 3],
[8, 12, 8, 7],
[15, 11, 13, 9],
[21, 18, 17, 21],
[25, 16, 25, 94]
]
orders = np.array(orders)
А типичный диапазон к нему будет выглядеть вот так:
# Среднее
mean_val = orders.mean()
# Стандартное отклонение
sigma_val = orders.std()
# Построение типичного диапазона
print(f'({mean_val:.2f} - {sigma_val:.2f}, {mean_val:.2f} + {sigma_val:.2f})')
# => (13.25 - 17.02, 13.25 + 17.02)
# Типичный диапазон
print(f'({mean_val - sigma_val:.2f}, {mean_val + sigma_val:.2f})')
# => (-3.77, 30.27)
Теперь посмотрим на векторизованную функцию, которая заменяет нетипичные значения на ближайшую границу диапазона:
def fill_sigma_values(value, mean_val, sigma_val):
prepared_value = value
low_bound = mean_val - sigma_val
upper_bound = mean_val + sigma_val
if value < low_bound:
prepared_value = low_bound
elif value > upper_bound:
prepared_value = upper_bound
return prepared_value
fill_sigma_values = np.vectorize(fill_sigma_values)
Чтобы получить типичный массив продаж, остается только применить эту функцию к исходным данным:
print(fill_sigma_values(orders, mean_val, sigma_val))
# => [[ 7 1 7 8]
# [ 4 2 4 5]
# [ 3 5 2 3]
# [ 8 12 8 7]
# [15 11 13 9]
# [21 18 17 21]
# [25 16 25 30]]
Обратите внимание, что в примере выше функция не поменяла тип исходного массива. В итоге мы получили целочисленный массив типичных продаж.
Выводы
В этом уроке вы узнали больше о векторизации. Этот подход позволяет сократить время выполнения функций и упростить интерфейс для их использования. Теперь у вас есть необходимые знания, которые помогут создавать свои векторизированные функции и применять встроенные в библиотеку Numpy.
Самостоятельная работа
Нажмите, чтобы увидеть тестовые данные
clicks_values = [
[319, 265, 319, 328],
[292, 274, 292, 301],
[283, 301, 274, 283],
[328, 364, 328, 284],
[391, 355, 373, 337],
[445, 418, 409, 445],
[481, 400, 481, 409],
[388, 267, 333, 344],
[531, 278, 300, 311],
[289, 311, 278, 289],
[344, 388, 344, 333],
[421, 377, 399, 355],
[487, 454, 443, 487],
[531, 432, 531, 443],
[312, 264, 312, 320],
[288, 300, 288, 296],
[280, 296, 272, 280],
[320, 352, 320, 312],
[376, 344, 360, 328],
[424, 400, 392, 424],
[456, 384, 456, 392],
[347, 269, 347, 360],
[308, 282, 308, 321],
[295, 321, 282, 392],
[360, 412, 360, 347],
[443, 399, 425, 373],
[529, 490, 477, 529],
[581, 464, 581, 477]
]
clicks_values = np.array(clicks_values)
clicks_values
Представим такую ситуацию. В отделе аналитики магазина новый тимлид, из-за чего существенно изменился подход к написанию кода. Теперь предпочтение отдается только Numpy-подобным конструкциям. Команда аналитиков должна переписать решение двух задач по обработке данных кликов со страниц сайта. Данные не поменялись — как и раньше, это массив (матрица кликов). Строки матрицы — дни наблюдений, столбцы — страницы сайта.
Шаг 1. Напишите функцию get_int_columns_mean(clicks_values: np.ndarray)
, которая возвращает целочисленный массив средних значений кликов для каждой страницы сайта. Напоминаем, что циклы использовать не стоит.
Нажмите, чтобы увидеть ответ
def test(columns_mean):
expected_value = np.array([387, 352, 367, 360])
# shape
assert np.array_equal(columns_mean,expected_value)
def get_int_columns_mean(clicks_values):
return np.mean(clicks_values, axis=0).astype(int)
columns_mean = get_int_columns_mean(clicks_values)
test(columns_mean)
Шаг 2. Давно обнаружилось, что кликов на страницу сайта всегда четное количество. Однако в систему аналитики по каким-то причинам попадают нечетные значения. Напишите функцию get_even_clicks(clicks_values: np.ndarray)
, которая пройдет по всем элементам массива и поправит нечетные значения, увеличив их на 1.
Нажмите, чтобы увидеть ответ
def test(even_clicks):
# shape
assert even_clicks.shape == clicks_values.shape
# even
assert np.all(even_clicks % 2 + 1)
def get_even_clicks(a):
return a + 1 if a % 2 else a
get_even_clicks = np.vectorize(get_even_clicks)
even_clicks = get_even_clicks(clicks_values)
test(even_clicks)
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.