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

Фильтрация значений и вырезание Python: Numpy-массивы

Numpy

При работе с данными часто возникает необходимость найти элементы по определенному условию. С точки зрения поиска ошибок в данных критерии поиска могут быть разными, например:

  • Отрицательные значения в графе «Количество продаж»
  • Буквы вместо цифр в графе «Номер телефона»
  • Нереалистично большие числа в графе «Сумма сделки»

В стандартном Python искать и фильтровать значения можно с помощью функции filter(). В Numpy есть схожая функциональность, которую мы рассмотрим в этом уроке. Вы узнаете, как получить элементы по заданному условию при работе с массивами numpy.ndarray.

Как создать булеву маску

Для фильтрации значений массива numpy.ndarray по определенному условию используют булевы маски — массивы значений True и False. Каждый элемент проходит фильтрацию через булеву маску и распределяется в зависимости от значения маски:

  • Если на той же позиции в маске стоит значение True, элемент добавляется в итоговый массив
  • Если на позиции стоит значение False, то элемент не будет включен в итоговый массив

Существует три способа работы с булевой маской:

  • Создать массив значений True и False вручную
  • Использовать операторы сравнения над элементами исходного массива
  • Применить логическое отрицание к текущей маске — поменять True на False и наоборот

Так все три способа выглядят в коде:

import numpy as np

# Исходный массив
base_array = np.array([0, 1, 2, 3, 4, 5, 6, 7,])
print(base_array)
# => [0 1 2 3 4 5 6 7]

# Ручное создание маски
handmade_mask = [True, True, True, False, False, False, False, False,]
print(handmade_mask)
# => [True, True, True, False, False, False, False, False]

# Создание маски по условию
compare_mask = base_array < 3
print(compare_mask)
# => [ True  True  True False False False False False]

# Создание маски по логическому отрицанию условия
opposite_compare_mask = ~(base_array >= 3)
print(opposite_compare_mask)
# => [ True  True  True False False False False False]

Первый способ на практике встречается редко, потому что на ручное создание маски уходит слишком много времени, особенно при большом размере массива:

Как применять маску

Чтобы применить булеву маску к исходному массиву, достаточно подставить ее в качестве индекса:

print(base_array[handmade_mask])
# => [0 1 2]
print(base_array[compare_mask])
# => [0 1 2]
print(base_array[opposite_compare_mask])
# => [0 1 2]

Как мы говорили выше, в Python реализована функция filter(), которая применяется для итеративной фильтрации значений списка по условию:

# Фильтрация значений с использованием filter
filtered_list = list(
    filter(
        lambda x: x < 3,
        [0, 1, 2, 3, 4, 5, 6, 7,]
    )
)
print(filtered_list)
# => [0 1 2]

Для итеративной фильтрации элементов массива numpy.ndarray используется другой способ — метод numpy.fromiter():

# Итеративное создание нового массива с использованием fromiter, тип массива задается аргументом dtype
filtered_array = np.fromiter(
    (base_array_element for base_array_element in base_array if base_array_element < 3),
    dtype = base_array.dtype
)
print(filtered_array)
# => [0 1 2]

На практике часто требуется не только убирать значения из исходного массива, но и заменять их. Для этого используется метод numpy.where():

# Заменяем отфильтрованные элементы на 0
print(np.where(base_array < 3, base_array, 0))
# => [0 1 2 0 0 0 0 0]

Если для фильтрации мы используем составное условие, лучше инициализировать маску отдельно. Это упрощает поддержку и делает код более читабельным:

# Заменяем отфильтрованные элементы на 0 с использованием маски
compare_masks = (base_array > 5) | (base_array < 3)
print(np.where(compare_masks, base_array, 0))
# => [0 1 2 0 0 0 6 7]

В реальных данных регулярно возникают пропущенные значения. Это может происходить из-за человеческого фактора, сбоя в работе сервисов или ошибки при записи в базу данных. Для таких случаев в Numpy существует отдельный тип данных numpy.nan (not a number):

# Массив с пропущенными значениями
raw_array = np.array([0, 1, None, 3, 4, None, 6, 7,], dtype=np.float64)
print(raw_array)
# => [ 0.  1. nan  3.  4. nan  6.  7.]

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

Рассмотрим на примере. Сначала нужно обнаружить пропуски:

# Маска для поиска пропусков
nan_mask = np.isnan(raw_array)
print(nan_mask)
# => [False False  True False False  True False False]

Затем чистим данные от пропусков:

# Маска для фильтрации пропущенных значений
not_nan_mask = ~nan_mask
print(raw_array[not_nan_mask])
# => [0. 1. 3. 4. 6. 7.]

Теперь заменяем пропуски на некоторое значение:

# Заменяем пропуски на 0
print(np.where(nan_mask, 0, raw_array))
# => [0. 1. 0. 3. 4. 0. 6. 7.]

Как применять маску с двумерными массивами

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

  • Сначала обнаружим выбросы двух типов — значения выше 200 и отрицательные значения. Число 200 выбрано потому, что именно столько ноутбуков хранится на складе. Менеджер магазина знает, что в день не бывает более 200 продаж
  • Затем заменим выбросы и пропуски на среднее значение продаж

Мы проводим эти операции, чтобы подготовить сырые данные к более глубокому анализу с применением методов статистики и машинного обучения:

# Создаем список списков продаж четырех магазинов
orders_values =  [
    [7, 1, -7, None],
    [1000, 2, 4, None],
    [3, None, 503, 3],
    [8, 12, 8, 7],
    [15, 11, None, 9],
    [None, 18, 17, -21],
    [252, 16, 25, 17]
]

# Конвертируем в Numpy-массив
orders = np.array(orders_values, dtype=np.float64)
print(orders)
# => [[   7.    1.   -7.   nan]
#  [1000.    2.    4.   nan]
#  [   3.   nan  503.    3.]
#  [   8.   12.    8.    7.]
#  [  15.   11.   nan    9.]
#  [  nan   18.   17.  -21.]
#  [ 252.   16.   25.   17.]]

Чтобы отфильтровать значения, нужно создать маски:

# Маска для отрицательных значений
negative_values = orders < 0
print(orders[negative_values])
# => [ -7. -21.]

# Маска для больших значений
big_values = orders > 200
print(orders[big_values])
# => [1000.  503.  252.]

# Маска для пропущенных значений и подсчета их количества
nan_values = np.isnan(orders)
print(sum(sum(nan_values)))
# => 5

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

# Маска стандартных значений
normal_mask = ~negative_values & ~big_values & ~nan_values

# Стандартные значения заказов
normal_values = orders[normal_mask]

# Среднее значение для стандартных заказов
normal_mean = normal_values.mean()
normal_mean = int(normal_mean)
print(normal_mean)
# => 10

Далее остается только заменить нестандартные значения на величину среднего:

# Массив, в котором нестандартные значения заменены на среднее
prepared_orders = np.where(normal_mask, orders, normal_mean)
print(prepared_orders)
# => [[ 7.  1. 10. 10.]
#  [10.  2.  4. 10.]
#  [ 3. 10. 10.  3.]
#  [ 8. 12.  8.  7.]
#  [15. 11. 10.  9.]
#  [10. 18. 17. 10.]
#  [10. 16. 25. 17.]]

Выводы

В этом уроке мы разобрали методы поиска и отбора значений по определенному условию, которые часто применяются при работе с данными. Мы узнали, как искать элементы, неудовлетворяющие некоторому условию, а также отсутствующие элементы и пропуски. Это самые частые случаи при работе с реальными данными, поэтому важно знать, как с ним работать.

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


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

Нажмите, чтобы увидеть тестовые данные
clicks_values =  [
    [319, -265, 319, 328],
    [292, 274, 292, 301],
    [283, 301, 274, 283],
    [328, 364, 328, None],
    [391, 355, 373, 337],
    [445, 418, 409, 445],
    [481, 400, 481, 409],
    [None, 267, 333, 344],
    [300, 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, None, 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, None],
    [360, 412, 360, 347],
    [None, 399, 425, 373],
    [529, 490, 477, 529],
    [581, 464, 581, -477]
]

clicks_values = np.array(clicks_values, dtype=np.float64)
clicks_values

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

Пройдем по всему процессу пошагово:

Шаг 1. Напишите функцию get_negative_values_number(clicks_values: np.ndarray), которая возвращает количество отрицательных значений в данных.

Нажмите, чтобы увидеть ответ
    def test(negative_values_number):
        expected_value = 6
        # shape
        assert negative_values_number == expected_value

    def get_negative_values_number(clicks_values):
        return len(clicks_values[clicks_values < 0])

    negative_values_number = get_negative_values_number(clicks_values)
    test(negative_values_number)

Шаг 2. Напишите функцию get_nan_values_number(clicks_values: np.ndarray), которая возвращает количество пропусков (np.nan) значений в данных.

Нажмите, чтобы увидеть ответ
    def test(negative_values_number):
        expected_value = 5
        # shape
        assert negative_values_number == expected_value

    def get_nan_values_number(clicks_values):
        return len(clicks_values[np.isnan(clicks_values)])

    nan_values_number = get_nan_values_number(clicks_values)
    test(nan_values_number)

Шаг 3. Анализ отрицательных значений выявил баг — он возникает, когда оператор зажимает клавишу Shift. При этом вводимое значение по модулю (то есть без знака) является правильным. Напишите функцию fillneg_values_number(clicks_values: np.ndarray), которая возвращает массив с исправленными отрицательными значениями на абсолютные.

Нажмите, чтобы увидеть ответ
    def test(corr_neg_clicks_values):
        # num negatives
        assert len(corr_neg_clicks_values[corr_neg_clicks_values < 0]) == 0
        # abs values
        assert (corr_neg_clicks_values + clicks_values == 0).sum() == len(clicks_values[clicks_values < 0])

    def fillneg_values_number(clicks_values):
        return np.where(clicks_values < 0, abs(clicks_values), clicks_values)

    corr_neg_clicks_values = fillneg_values_number(clicks_values)
    test(corr_neg_clicks_values)

Шаг 4. С пропущенными значениями баг оказался серьезнее. Команда аналитиков решила, что лучше заполнить пропуски нулевыми значениями. Напишите функцию fillna_values_number(clicks_values: np.ndarray), которая будет возвращать массив, где пропуски заполнены нулями.

Нажмите, чтобы увидеть ответ
    def test(corr_nan_clicks_values):
        # num nan
        assert np.isnan(corr_nan_clicks_values).sum() == 0
        # fils values
        assert len(corr_nan_clicks_values[corr_nan_clicks_values == 0]) == len(clicks_values[np.isnan(clicks_values)])

    def fillna_values_number(clicks_values):
        return np.where(np.isnan(clicks_values), 0, clicks_values)

    corr_nan_clicks_values = fillna_values_number(clicks_values)
    test(corr_nan_clicks_values)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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