Чтобы проанализировать данные, можно провести множество разных операций:
- Сравнить значения
- Поискать минимальные и максимальные значения
- Найти суммы и произведения элементов
И это далеко не полный список всех доступных преобразований. В некоторых случаях вычисления над элементами требуют использования более сложных математических операций и функций. Именно под это заточена библиотека Numpy, которая позволяет не только готовить данные к обработке, но и проводить необходимые вычисления. В этом уроке мы разберемся, как эти вычисления работают и как применять их на практике.
Поэлементные преобразования и укладывание
Numpy помогает ускорить операции и упростить синтаксис — так происходит благодаря поэлементным преобразованиям. Он позволяет оперировать с данными разной размерности. Такой подход называется укладыванием.
Чтобы погрузиться в эту тему глубже, познакомимся с распространенными задачами с арифметическими операциями над данными и выясним, как работает укладывание элементов одного массива данных в другой.
Чтобы выполнять арифметические операции со стандартными структурами данных в Python, нужно использовать циклы. Их количество и вложенность зависит от размерности. Numpy работает по-другому — логика и синтаксические конструкции в операциях над массивами остается одинаковой для структур разной размерности. Для оптимизации и повышения качества кода циклы скрыты от пользователя.
Посмотрим на пример ниже. В нем показан ряд операций над одномерным массивом данных и числовым значением, которое поэлементно применяется ко всему массиву:
import numpy as np
# Исходный массив
arr1 = np.array([0, 1, 2, 3, 4, 5, 6, 7])
# Значение для изменения элементов массива
change_array_value = 5
print(arr1 + change_array_value)
# => [ 5 6 7 8 9 10 11 12]
print(arr1 - change_array_value)
# => [-5 -4 -3 -2 -1 0 1 2]
print(arr1 * change_array_value)
# => [ 0 5 10 15 20 25 30 35]
print(arr1 / change_array_value)
# => [0. 0.2 0.4 0.6 0.8 1. 1.2 1.4]
Циклы в примере выше отсутствуют. Как мы уже говорили, в Numpy это называется укладыванием. Укладывание элемента в массив было разобрано на примере вектора и числа. Однако укладывать можно не только один элемент, а любой массив подходящего размера — при условии, если структура большей размерности. Посмотрим на пример прибавления элементов вектора построчно к матрице:
# Добавление вектора к матрице
matrix_array = np.array([[5, 8], [8, 9]])
vector_array = np.array([1, 2])
print(matrix_array + vector_array)
# => [[ 6 10]
# [ 9 11]]
Чтобы выполнить те же операции над двумя массивами, также не используются циклы. Все синтаксические конструкции остаются без изменений:
# Массив для изменения значений исходного
arr2 = np.array([2, 2, 2, 2, -1, -1, -1, -1])
print(arr1 + arr2)
# => [2 3 4 5 3 4 5 6]
print(arr1 - arr2)
# => [-2 -1 0 1 5 6 7 8]
print(arr1 * arr2)
# => [ 0 2 4 6 -4 -5 -6 -7]
print(arr1 / arr2)
# => [ 0. 0.5 1. 1.5 -4. -5. -6. -7. ]
Для сравнения посмотрим, как выполняются аналогичные задачи над стандартными списками. Без циклов и генератора zip()
в этом случае не обойтись:
arr1 = [0, 1, 2, 3, 4, 5, 6, 7]
change_array_value = 5
arr2 = [2, 2, 2, 2, -1, -1, -1, -1]
print([arr1_val + change_array_value for arr1_val in arr1])
# => [5, 6, 7, 8, 9, 10, 11, 12]
print([arr1_val + arr2_val for arr1_val, arr2_val in zip(arr1, arr2)])
# => [2, 3, 4, 5, 3, 4, 5, 6]
Создатели Numpy целенаправленно разработали библиотеку, в которой выполнение функционала не зависит от размерности данных. В качестве примера приведены поэлементные операции над матрицами:
arr1 = np.array([[5, 8], [8, 9]])
arr2 = np.array([[3, 1], [7, 2]])
change_array_value = 3
print(arr1 * arr2)
# => [[15 8]
# [56 18]]
print(arr1 / change_array_value)
# => [[1.66666667 2.66666667]
# [2.66666667 3. ]]
Во всех примерах выше операции с массивами Numpy производились по одному шаблону. Размерность данных не влияла на синтаксис — мы использовали одинаковые математические операторы, меняя только операнды: числа, вектора, матрицы.
Для сравнения изучим пример операций над матрицами, которые представлены стандартными списками. Здесь необходимо использовать циклы:
# Пример для аналогичных операций над стандартными списками
arr1 = [[5, 8], [8, 9]]
arr2 = [[3, 1], [7, 2]]
change_array_value = 3
for i in range(len(arr1)):
for j in range(len(arr1[0])):
arr1[i][j] *= arr2[i][j]
print(arr1)
# => [[15, 8],
# [56, 18]]
for i in range(len(arr2)):
for j in range(len(arr2[0])):
arr2[i][j] += change_array_value
print(arr2)
# => [[6, 4],
# [10, 5]]
При работе со стандартными списками чем больше размерность, тем больше строк кода. К этому моменту стоит относиться внимательно, ведь длина кода делает его сложнее в поддержке и может приводить к возникновению ошибок.
Как это работает на практике
В качестве практического примера решим задачу, с которой сталкивается аналитик данных в своей работе. Возьмем исторические данные по продажам ноутбуков в сети из четырех магазинов за неделю. Попробуем посмотреть отклонения от средних показателей. Средние показатели могут быть вычислены по-разному в зависимости от среза данных. Так можно смотреть на ситуацию:
- Во всей сети магазинов
- В каждом магазине по отдельности
- С распределением по дням недели
Предположим, что из базы данных сервиса выгрузили продажи в виде списка списков значений, где внешний список объединяет списки продаж по каждому дню недели для четырех магазинов:
# Продажи магазина
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, 17]
]
orders = np.array(orders)
После инициализации данных в виде массива можно перейти к анализу отклонений от среднего по всей сети:
# Среднее значение по всем магазинам по всем дням
mean_orders_value = orders.mean()
print(mean_orders_value)
# => 10.5
print(orders - mean_orders_value)
# => [[-3.5 -9.5 -3.5 -2.5]
# [-6.5 -8.5 -6.5 -5.5]
# [-7.5 -5.5 -8.5 -7.5]
# [-2.5 1.5 -2.5 -3.5]
# [ 4.5 0.5 2.5 -1.5]
# [10.5 7.5 6.5 10.5]
# [14.5 5.5 14.5 6.5]]
Средний показатель для всей сети не всегда подходит для анализа, поскольку у магазинов может быть разный объем продаж. Чтобы лучше понять ситуацию с продажами, найдем среднее по каждому магазину. В примере это среднее значение по столбцам матрицы продаж. Чтобы найти такие средние, используем метод mean()
с параметром axis = 0
:
# Среднее значение по магазинам
mean_by_shop = orders.mean(axis=0)
print(mean_by_shop)
# => [11.85714286 9.28571429 10.85714286 10.]
print(orders - mean_by_shop)
# =>[[-4.85714286 -8.28571429 -3.85714286 -2.]
# [-7.85714286 -7.28571429 -6.85714286 -5.]
# [-8.85714286 -4.28571429 -8.85714286 -7.]
# [-3.85714286 2.71428571 -2.85714286 -3.]
# [ 3.14285714 1.71428571 2.14285714 -1.]
# [ 9.14285714 8.71428571 6.14285714 11.]
# [13.14285714 6.71428571 14.14285714 7.]]
Аналитику также может потребоваться информация о дневных отклонениях. Так, например, можно обнаружить просадку продаж по вине логистов и менеджеров. Для этого необходимо найти средние по дням. К матрице продаж надо применить метод mean()
с параметром axis = 1
:
# Среднее значение по дням
mean_by_day = orders.mean(axis=1)
print(mean_by_day)
# => [ 5.75 3.75 3.25 8.75 12. 19.25 20.75]
# Переформатирование вектора для укладывания по столбцам
mean_by_day = mean_by_day.reshape((7,1))
print(mean_by_day)
# => [[ 5.75]
# [ 3.75]
# [ 3.25]
# [ 8.75]
# [12. ]
# [19.25]
# [20.75]]
print(orders - mean_by_day)
# => [[ 1.25 -4.75 1.25 2.25]
# [ 0.25 -1.75 0.25 1.25]
# [-0.25 1.75 -1.25 -0.25]
# [-0.75 3.25 -0.75 -1.75]
# [ 3. -1. 1. -3. ]
# [ 1.75 -1.25 -2.25 1.75]
# [ 4.25 -4.75 4.25 -3.75]]
В примере выше используется метод reshape()
— он помогает преобразовать исходную строку средних в столбец. Это принципиально необходимо для того, чтобы вектор был уложен в матрицу именно по столбцам.
Выводы
В этом уроке мы узнали, что библиотека Numpy упрощает и оптимизирует вычисления с использованием языка Python. Для этого она применяет подход, который унифицирует интерфейс работы с массивами. Все арифметические операции над массивами производятся без циклов — с использованием только самих символов операций.
Также мы обсудили, что нет различия в синтаксисе для одномерных или двумерных данных. Более того, операции можно производить над массивами разной размерности, укладывая значения одного массива в другой.
Все эти подходы повышают скорость разработки программ и упрощают поддержку готовых решений. Теперь вы знаете часто встречаемые подходы, которые проводят арифметические операции над массивами данных с использованием укладывания.
Самостоятельная работа
Библиотека Numpy часто применяется в алгоритмах обработки изображений и машинного обучения. Но в таких алгоритмах могут применяться данные, преобразованные особым способом. Чтобы лучше понять эту тему, мы рассмотрим ряд задач, с которыми сталкиваются разработчики при подготовке изображений.
Привычные нам картинки — это массивы чисел. Если картинки черно-белая и без сжатия, то число в массиве отражает оттенки серого цвета конкретного пикселя: от 0 (черный) до 255 (белый). Разберемся в этом на примере датасета MNIST, который содержит набор картинок рукописных цифр.
Например, рукописная цифра пять представлена следующим образом:
Изображения в датасете представлены в виде списка значений. Каждое изображение содержит 784 пикселя — это развернутая в один вектор картинка размером 28 на 28 пикселей.
Одно из преобразований изображений перед подачей на алгоритм глубокого обучения — масштабирование значений. Так называют приведение значений пикселей к некоторому интервалу значений или к распределению с нулевым средним. Без масштабирования глубокие нейронные сети могут работать некорректно. Рассмотрим два основных подхода к масштабированию значений пикселей:
Шаг 1. В первом подходе к каждому значению пикселя применяется формула
, где
— минимальное значение пикселя,
— максимальное значение пикселя. Не используя циклы, напишите функцию min_max_scaler()
, которая получает на вход картинку в виде массива значений и возвращает массив исходного размера с примененным преобразованием.
Нажмите, чтобы увидеть ответ
def min_max_scaler(image_mnist):
image = np.array(image_mnist)
min_value = image.min()
max_value = image.max()
image = (image - min_value) / (max_value - min_value)
return image
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.