Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Быстрые поэлементные операции Python: Numpy-массивы

Numpy

Чтобы проанализировать данные, можно провести множество разных операций:

  • Сравнить значения
  • Поискать минимальные и максимальные значения
  • Найти суммы и произведения элементов

И это далеко не полный список всех доступных преобразований. В некоторых случаях вычисления над элементами требуют использования более сложных математических операций и функций. Именно под это заточена библиотека 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. Для этого она применяет подход, который унифицирует интерфейс работы с массивами. Все арифметические операции над массивами производятся без циклов — с использованием только самих символов операций.

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

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


Самостоятельная работа

Нажмите, чтобы увидеть тестовые данные
image_mnist = [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 17, 17, 17, 17, 81, 180, 180, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 139, 253, 253, 253, 253, 253, 253, 253, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 60, 228, 253, 253, 253, 253, 253, 253, 253, 207, 197, 46, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 213, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 223, 52, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 66, 231, 253, 253, 253, 108, 40, 40, 115, 244, 253, 253, 134, 3, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 63, 114, 114, 114, 37, 0, 0, 0, 205, 253, 253, 253, 15, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 57, 253, 253, 253, 15, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 253, 253, 253, 15, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 95, 253, 253, 253, 15, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 205, 253, 253, 253, 15, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 61, 99, 96, 0, 0, 45, 224, 253, 253, 195, 10, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 11, 25, 105, 83, 189, 189, 228, 253, 251, 189, 189, 218, 253, 253, 210, 27, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 42, 116, 173, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 221, 116, 7, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 118, 253, 253, 253, 253, 245, 212, 222, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 160, 15, 0, 0, 0, 0,
    0, 0, 0, 0, 254, 253, 253, 253, 189, 99, 0, 32, 202, 253, 253, 253, 240, 122, 122, 190, 253, 253, 253, 174, 0, 0, 0, 0,
    0, 0, 0, 0, 255, 253, 253, 253, 238, 222, 222, 222, 241, 253, 253, 230, 70, 0, 0, 17, 175, 229, 253, 253, 0, 0, 0, 0,
    0, 0, 0, 0, 158, 253, 253, 253, 253, 253, 253, 253, 253, 205, 106, 65, 0, 0, 0, 0, 0, 62, 244, 157, 0, 0, 0, 0,
    0, 0, 0, 0, 6, 26, 179, 179, 179, 179, 179, 30, 15, 10, 0, 0, 0, 0, 0, 0, 0, 0, 14, 6, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]

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

Привычные нам картинки — это массивы чисел. Если картинки черно-белая и без сжатия, то число в массиве отражает оттенки серого цвета конкретного пикселя: от 0 (черный) до 255 (белый). Разберемся в этом на примере датасета MNIST, который содержит набор картинок рукописных цифр.

Например, рукописная цифра пять представлена следующим образом:

400

Изображения в датасете представлены в виде списка значений. Каждое изображение содержит 784 пикселя — это развернутая в один вектор картинка размером 28 на 28 пикселей.

Одно из преобразований изображений перед подачей на алгоритм глубокого обучения — масштабирование значений. Так называют приведение значений пикселей к некоторому интервалу значений или к распределению с нулевым средним. Без масштабирования глубокие нейронные сети могут работать некорректно. Рассмотрим два основных подхода к масштабированию значений пикселей:

Шаг 1. В первом подходе к каждому значению пикселя применяется формула , где — минимальное значение пикселя, — максимальное значение пикселя. Не используя циклы, напишите функцию min_max_scaler(), которая получает на вход картинку в виде массива значений и возвращает массив исходного размера с примененным преобразованием.

Нажмите, чтобы увидеть ответ
    def test(image):
        # shape
        assert image.shape[0] == 784
        # min
        assert image.min() == 0.
        # max
        assert image.max() == 1.
        # reverse
        assert np.array_equal(
            image * 255,
            np.array(image_mnist, dtype=float)
        )

    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

    image = min_max_scaler(image_mnist)
    test(image)

Шаг 2. Во втором подходе к каждому значению пикселя применяется формула $(value - mean)/std$, где $mean$ - среднее значение всех пикселей, $std$ - стандартное отклонение значений от среднего. Не используя циклы, напишите функцию standard_scaler(), которая получает на вход картинку в виде массива значений и возвращает массив исходного размера с примененным преобразованием.

Нажмите, чтобы увидеть ответ
    def test(image):
        # shape
        assert image.shape[0] == 784
        # mean
        assert np.isclose(image.mean(), 0.)
        # std
        assert np.isclose(image.std(), 1.)

    def standard_scaler(image_mnist):
        image = np.array(image_mnist)
        mean_value = image.mean()
        std_value = image.std()
        image = (image - mean_value) / std_value
        return image

    image = standard_scaler(image_mnist)
    test(image)

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Об обучении на Хекслете

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 5 025 ₽ в месяц
новый
Сбор, анализ и интерпретация данных
9 месяцев
с нуля
Старт 2 мая

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»
Изображение Тото

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